132 lines
3.0 KiB
Go
132 lines
3.0 KiB
Go
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
|
|
}
|