app-init
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"mysql-tunnel/internal/logger"
|
||||
)
|
||||
|
||||
// HTTPClient is a generic HTTP client for the tunnel endpoint.
|
||||
// Navicat request/response encoding belongs in package navicat (Phase 2).
|
||||
type HTTPClient struct {
|
||||
base *http.Client
|
||||
insecure bool
|
||||
userAgent string
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Timeout time.Duration
|
||||
TLSInsecureSkipVerify bool
|
||||
}
|
||||
|
||||
func NewHTTPClient(opt Options) *HTTPClient {
|
||||
if opt.Timeout <= 0 {
|
||||
opt.Timeout = 30 * time.Second
|
||||
}
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 8,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
|
||||
}
|
||||
if opt.TLSInsecureSkipVerify {
|
||||
transport.TLSClientConfig.InsecureSkipVerify = true
|
||||
}
|
||||
return &HTTPClient{
|
||||
base: &http.Client{
|
||||
Timeout: opt.Timeout,
|
||||
Transport: transport,
|
||||
},
|
||||
insecure: opt.TLSInsecureSkipVerify,
|
||||
userAgent: "mysql-tunnel/0.1",
|
||||
}
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
StatusCode int
|
||||
ContentType string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (c *HTTPClient) Do(ctx context.Context, method, rawURL string, contentType string, body io.Reader) (*Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, rawURL, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build request: %w", redactErr(err))
|
||||
}
|
||||
req.Header.Set("User-Agent", c.userAgent)
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
resp, err := c.base.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("http %s %s: %w", method, logger.RedactURL(rawURL), redactErr(err))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, 64<<20))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read response from %s: %w", logger.RedactURL(rawURL), err)
|
||||
}
|
||||
return &Response{
|
||||
StatusCode: resp.StatusCode,
|
||||
ContentType: resp.Header.Get("Content-Type"),
|
||||
Body: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *HTTPClient) InsecureTLS() bool {
|
||||
return c.insecure
|
||||
}
|
||||
|
||||
func redactErr(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s", logger.RedactError(err))
|
||||
}
|
||||
|
||||
func LooksLikeNavicatHeader(body []byte) bool {
|
||||
if len(body) < 16 {
|
||||
return false
|
||||
}
|
||||
magic := uint32(body[0])<<24 | uint32(body[1])<<16 | uint32(body[2])<<8 | uint32(body[3])
|
||||
return magic == 1111
|
||||
}
|
||||
|
||||
func HeaderErrno(body []byte) (uint32, bool) {
|
||||
if len(body) < 10 {
|
||||
return 0, false
|
||||
}
|
||||
errno := uint32(body[6])<<24 | uint32(body[7])<<16 | uint32(body[8])<<8 | uint32(body[9])
|
||||
return errno, true
|
||||
}
|
||||
|
||||
func IsProbablyHTML(body []byte) bool {
|
||||
s := strings.TrimSpace(string(body))
|
||||
if len(s) < 5 {
|
||||
return false
|
||||
}
|
||||
ls := strings.ToLower(s[:min(64, len(s))])
|
||||
return strings.HasPrefix(ls, "<!doctype") || strings.HasPrefix(ls, "<html")
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLooksLikeNavicatHeader(t *testing.T) {
|
||||
body := make([]byte, 16)
|
||||
body[2] = 0x04
|
||||
body[3] = 0x57 // 1111
|
||||
body[5] = 206
|
||||
if !LooksLikeNavicatHeader(body) {
|
||||
t.Fatal("expected magic 1111")
|
||||
}
|
||||
body[3] = 0x00
|
||||
if LooksLikeNavicatHeader(body) {
|
||||
t.Fatal("false positive")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderErrno(t *testing.T) {
|
||||
body := make([]byte, 16)
|
||||
body[9] = 202
|
||||
errno, ok := HeaderErrno(body)
|
||||
if !ok || errno != 202 {
|
||||
t.Fatalf("errno=%d ok=%v", errno, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPClientPOST(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("method %s", r.Method)
|
||||
}
|
||||
if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
|
||||
t.Errorf("ct %s", r.Header.Get("Content-Type"))
|
||||
}
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
if strings.Contains(string(b), "actn=C") == false {
|
||||
t.Errorf("body %s", b)
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain; charset=x-user-defined")
|
||||
hdr := make([]byte, 16)
|
||||
hdr[2] = 0x04
|
||||
hdr[3] = 0x57
|
||||
hdr[5] = 206
|
||||
hdr[9] = 202
|
||||
_, _ = w.Write(hdr)
|
||||
_, _ = w.Write([]byte{0x12})
|
||||
_, _ = w.Write([]byte("invalid parameters"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewHTTPClient(Options{Timeout: 5 * time.Second})
|
||||
resp, err := c.Do(context.Background(), http.MethodPost, srv.URL, "application/x-www-form-urlencoded", strings.NewReader("actn=C&host=mysql&port=3306&login=root"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("status %d", resp.StatusCode)
|
||||
}
|
||||
if !LooksLikeNavicatHeader(resp.Body) {
|
||||
t.Fatal("header")
|
||||
}
|
||||
errno, _ := HeaderErrno(resp.Body)
|
||||
if errno != 202 {
|
||||
t.Fatalf("errno %d", errno)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeout(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := NewHTTPClient(Options{Timeout: 50 * time.Millisecond})
|
||||
_, err := c.Do(context.Background(), http.MethodGet, srv.URL, "", nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected timeout")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user