| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- package tlsfingerprint
- import (
- "bufio"
- "context"
- "crypto/tls"
- "crypto/x509"
- "encoding/base64"
- "fmt"
- "net"
- "net/http"
- "net/url"
- "strings"
- "time"
- "golang.org/x/net/proxy"
- )
- // TransportOptions configures transport plumbing, not the account fingerprint.
- // Nil certificate pools use the system trust store. Certificate verification
- // cannot be disabled; local tests may supply a test CA without weakening TLS.
- type TransportOptions struct {
- DialContext func(context.Context, string, string) (net.Conn, error)
- RootCAs *x509.CertPool
- ProxyRootCAs *x509.CertPool
- HandshakeTimeout time.Duration
- }
- // NewHTTPTransport returns a native HTTP/1.1 transport that also supports a
- // WebSocket 101 upgrade. No response-body wrapper is used. Plain HTTP and
- // unsupported proxy/protocol combinations fail instead of bypassing TLS.
- func NewHTTPTransport(profile *Profile, proxyURL *url.URL, opts TransportOptions) (*http.Transport, error) {
- t := &http.Transport{MaxIdleConns: 100, MaxIdleConnsPerHost: 10, IdleConnTimeout: 90 * time.Second}
- if err := ConfigureTransport(t, profile, proxyURL, opts); err != nil {
- return nil, err
- }
- return t, nil
- }
- // ConfigureTransport installs the same verified uTLS path for HTTP and WSS.
- // Pool sizing and response timeouts already set on t are retained.
- func ConfigureTransport(t *http.Transport, profile *Profile, proxyURL *url.URL, opts TransportOptions) error {
- if t == nil || profile == nil {
- return fmt.Errorf("TLS fingerprint transport requires a transport and profile")
- }
- for _, protocol := range profile.ALPNProtocols {
- if protocol != "http/1.1" {
- return fmt.Errorf("TLS fingerprint transport does not support ALPN %q", protocol)
- }
- }
- dial, err := configuredTLSDialer(profile, proxyURL, opts)
- if err != nil {
- return err
- }
- // net/http cannot infer negotiated HTTP/2 from a *utls.UConn. Advertising
- // h2 and then writing HTTP/1.1 would corrupt the connection.
- t.ForceAttemptHTTP2 = false
- t.TLSNextProto = make(map[string]func(string, *tls.Conn) http.RoundTripper)
- t.Proxy = nil // CONNECT is performed by the dialer, including HTTPS proxies.
- t.DialTLS = nil
- t.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12, NextProtos: []string{"http/1.1"}}
- t.DialContext = func(context.Context, string, string) (net.Conn, error) {
- return nil, fmt.Errorf("TLS fingerprint transport requires HTTPS or WSS")
- }
- t.DialTLSContext = dial
- return nil
- }
- func configuredTLSDialer(profile *Profile, proxyURL *url.URL, opts TransportOptions) (func(context.Context, string, string) (net.Conn, error), error) {
- p := profile.Clone()
- if opts.HandshakeTimeout <= 0 {
- opts.HandshakeTimeout = 10 * time.Second
- }
- if opts.DialContext == nil {
- opts.DialContext = (&net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}).DialContext
- }
- if opts.RootCAs != nil {
- opts.RootCAs = opts.RootCAs.Clone()
- }
- if opts.ProxyRootCAs != nil {
- opts.ProxyRootCAs = opts.ProxyRootCAs.Clone()
- }
- var proxyCopy *url.URL
- if proxyURL != nil {
- purl := *proxyURL
- purl.Scheme = strings.ToLower(purl.Scheme)
- if purl.Hostname() == "" {
- return nil, fmt.Errorf("TLS fingerprint proxy host is missing")
- }
- switch purl.Scheme {
- case "http", "https", "socks5", "socks5h":
- default:
- return nil, fmt.Errorf("unsupported TLS fingerprint proxy scheme %q", purl.Scheme)
- }
- proxyCopy = &purl
- }
- return func(ctx context.Context, network, addr string) (net.Conn, error) {
- ctx, cancel := context.WithTimeout(ctx, opts.HandshakeTimeout)
- defer cancel()
- conn, err := openTunnel(ctx, network, addr, proxyCopy, opts)
- if err != nil {
- return nil, err
- }
- return performTLSHandshakeWithRoots(ctx, conn, p, addr, opts.RootCAs)
- }, nil
- }
- type contextProxyDialer struct {
- ctx context.Context
- dial func(context.Context, string, string) (net.Conn, error)
- }
- func (d contextProxyDialer) Dial(network, addr string) (net.Conn, error) {
- return d.dial(d.ctx, network, addr)
- }
- func (d contextProxyDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
- return d.dial(ctx, network, addr)
- }
- func openTunnel(ctx context.Context, network, addr string, proxyURL *url.URL, opts TransportOptions) (net.Conn, error) {
- if proxyURL == nil {
- return opts.DialContext(ctx, network, addr)
- }
- proxyAddr := proxyURL.Host
- if proxyURL.Port() == "" {
- port := "1080"
- if proxyURL.Scheme == "http" {
- port = "80"
- } else if proxyURL.Scheme == "https" {
- port = "443"
- }
- proxyAddr = net.JoinHostPort(proxyURL.Hostname(), port)
- }
- if proxyURL.Scheme == "socks5" || proxyURL.Scheme == "socks5h" {
- var auth *proxy.Auth
- if proxyURL.User != nil {
- password, _ := proxyURL.User.Password()
- auth = &proxy.Auth{User: proxyURL.User.Username(), Password: password}
- }
- dialer, err := proxy.SOCKS5(network, proxyAddr, auth, contextProxyDialer{ctx: ctx, dial: opts.DialContext})
- if err != nil {
- return nil, fmt.Errorf("create SOCKS proxy: %w", err)
- }
- contextDialer, ok := dialer.(proxy.ContextDialer)
- if !ok {
- return nil, fmt.Errorf("SOCKS proxy does not support context cancellation")
- }
- return contextDialer.DialContext(ctx, network, addr)
- }
- conn, err := opts.DialContext(ctx, network, proxyAddr)
- if err != nil {
- return nil, fmt.Errorf("connect to proxy: %w", err)
- }
- complete := false
- defer func() {
- if !complete {
- _ = conn.Close()
- }
- }()
- // CONNECT reads/writes must obey cancellation even when no deadline was
- // supplied by the request. configuredTLSDialer always supplies a timeout.
- rawConn := conn
- stop := context.AfterFunc(ctx, func() { _ = rawConn.Close() })
- defer stop()
- if proxyURL.Scheme == "https" {
- outer := tls.Client(conn, &tls.Config{
- ServerName: proxyURL.Hostname(), RootCAs: opts.ProxyRootCAs,
- MinVersion: tls.VersionTLS12, NextProtos: []string{"http/1.1"},
- })
- if err := outer.HandshakeContext(ctx); err != nil {
- return nil, fmt.Errorf("HTTPS proxy TLS handshake: %w", err)
- }
- conn = outer
- }
- req := &http.Request{Method: http.MethodConnect, URL: &url.URL{Opaque: addr}, Host: addr, Header: make(http.Header)}
- if proxyURL.User != nil {
- password, _ := proxyURL.User.Password()
- value := base64.StdEncoding.EncodeToString([]byte(proxyURL.User.Username() + ":" + password))
- req.Header.Set("Proxy-Authorization", "Basic "+value)
- }
- if err := req.Write(conn); err != nil {
- return nil, fmt.Errorf("write proxy CONNECT: %w", err)
- }
- reader := bufio.NewReader(conn)
- resp, err := http.ReadResponse(reader, req)
- if err != nil {
- return nil, fmt.Errorf("read proxy CONNECT: %w", err)
- }
- if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("proxy CONNECT returned HTTP %d", resp.StatusCode)
- }
- // A successful CONNECT transfers ownership of the socket to the tunnel.
- // Closing its response body would close the tunnel before TLS starts.
- if reader.Buffered() > 0 {
- conn = &bufferedTunnelConn{Conn: conn, reader: reader}
- }
- complete = true
- return conn, nil
- }
- type bufferedTunnelConn struct {
- net.Conn
- reader *bufio.Reader
- }
- func (c *bufferedTunnelConn) Read(p []byte) (int, error) { return c.reader.Read(p) }
|