package libbox import ( "bytes" "context" "crypto/sha256" "crypto/tls" "crypto/x509" "encoding/hex" "errors" "fmt" "io" "math/rand" "net" "net/http" "net/url" "os" "strconv" "sync" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/protocol/socks" "github.com/sagernet/sing/protocol/socks/socks5" ) type HTTPClient interface { RestrictedTLS() ModernTLS() PinnedTLS12() PinnedSHA256(sumHex string) TrySocks5(port int32) KeepAlive() NewRequest() HTTPRequest Close() } type HTTPRequest interface { SetURL(link string) error SetMethod(method string) SetHeader(key string, value string) SetContent(content []byte) SetContentString(content string) RandomUserAgent() SetUserAgent(userAgent string) Execute() (HTTPResponse, error) } type HTTPResponse interface { GetContent() ([]byte, error) GetContentString() (string, error) WriteTo(path string) error } var ( _ HTTPClient = (*httpClient)(nil) _ HTTPRequest = (*httpRequest)(nil) _ HTTPResponse = (*httpResponse)(nil) ) type httpClient struct { tls tls.Config client http.Client transport http.Transport } func NewHTTPClient() HTTPClient { client := new(httpClient) client.client.Timeout = C.TCPTimeout client.client.Transport = &client.transport client.transport.TLSClientConfig = &client.tls client.transport.DisableKeepAlives = true return client } func (c *httpClient) ModernTLS() { c.tls.MinVersion = tls.VersionTLS12 c.tls.CipherSuites = common.Map(tls.CipherSuites(), func(it *tls.CipherSuite) uint16 { return it.ID }) } func (c *httpClient) RestrictedTLS() { c.tls.MinVersion = tls.VersionTLS13 c.tls.CipherSuites = common.Map(common.Filter(tls.CipherSuites(), func(it *tls.CipherSuite) bool { return common.Contains(it.SupportedVersions, uint16(tls.VersionTLS13)) }), func(it *tls.CipherSuite) uint16 { return it.ID }) } func (c *httpClient) PinnedTLS12() { c.tls.MinVersion = tls.VersionTLS12 c.tls.MaxVersion = tls.VersionTLS12 } func (c *httpClient) PinnedSHA256(sumHex string) { c.tls.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { for _, rawCert := range rawCerts { certSum := sha256.Sum256(rawCert) if sumHex == hex.EncodeToString(certSum[:]) { return nil } } return E.New("pinned sha256 sum mismatch") } } func (c *httpClient) TrySocks5(port int32) { dialer := new(net.Dialer) c.transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { for { socksConn, err := dialer.DialContext(ctx, "tcp", "127.0.0.1:"+strconv.Itoa(int(port))) if err != nil { break } _, err = socks.ClientHandshake5(socksConn, socks5.CommandConnect, M.ParseSocksaddr(addr), "", "") if err != nil { break } //nolint:staticcheck return socksConn, err } return dialer.DialContext(ctx, network, addr) } } func (c *httpClient) KeepAlive() { c.transport.ForceAttemptHTTP2 = true c.transport.DisableKeepAlives = false } func (c *httpClient) NewRequest() HTTPRequest { req := &httpRequest{httpClient: c} req.request = http.Request{ Method: "GET", Header: http.Header{}, } return req } func (c *httpClient) Close() { c.transport.CloseIdleConnections() } type httpRequest struct { *httpClient request http.Request } func (r *httpRequest) SetURL(link string) (err error) { r.request.URL, err = url.Parse(link) if err != nil { return } if r.request.URL.User != nil { user := r.request.URL.User.Username() password, _ := r.request.URL.User.Password() r.request.SetBasicAuth(user, password) } return } func (r *httpRequest) SetMethod(method string) { r.request.Method = method } func (r *httpRequest) SetHeader(key string, value string) { r.request.Header.Set(key, value) } func (r *httpRequest) RandomUserAgent() { r.request.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", rand.Int()%54, rand.Int()%2)) } func (r *httpRequest) SetUserAgent(userAgent string) { r.request.Header.Set("User-Agent", userAgent) } func (r *httpRequest) SetContent(content []byte) { buffer := bytes.Buffer{} buffer.Write(content) r.request.Body = io.NopCloser(bytes.NewReader(buffer.Bytes())) r.request.ContentLength = int64(len(content)) } func (r *httpRequest) SetContentString(content string) { r.SetContent([]byte(content)) } func (r *httpRequest) Execute() (HTTPResponse, error) { response, err := r.client.Do(&r.request) if err != nil { return nil, err } httpResp := &httpResponse{Response: response} if response.StatusCode != http.StatusOK { return nil, errors.New(httpResp.errorString()) } return httpResp, nil } type httpResponse struct { *http.Response getContentOnce sync.Once content []byte contentError error } func (h *httpResponse) errorString() string { content, err := h.GetContentString() if err != nil { return fmt.Sprint("HTTP ", h.Status) } return fmt.Sprint("HTTP ", h.Status, ": ", content) } func (h *httpResponse) GetContent() ([]byte, error) { h.getContentOnce.Do(func() { defer h.Body.Close() h.content, h.contentError = io.ReadAll(h.Body) }) return h.content, h.contentError } func (h *httpResponse) GetContentString() (string, error) { content, err := h.GetContent() if err != nil { return "", err } return string(content), nil } func (h *httpResponse) WriteTo(path string) error { defer h.Body.Close() file, err := os.Create(path) if err != nil { return err } defer file.Close() return common.Error(bufio.Copy(file, h.Body)) }