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, "