Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements missing HTTPS proxy functionality #978

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 58 additions & 9 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,20 @@ func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufS
type Dialer struct {
// NetDial specifies the dial function for creating TCP connections. If
// NetDial is nil, net.Dialer DialContext is used.
// If "Proxy" field is also set, this function dials the proxy.
NetDial func(network, addr string) (net.Conn, error)

// NetDialContext specifies the dial function for creating TCP connections. If
// NetDialContext is nil, NetDial is used.
// If "Proxy" field is also set, this function dials the proxy.
NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error)

// NetDialTLSContext specifies the dial function for creating TLS/TCP connections. If
// NetDialTLSContext is nil, NetDialContext is used.
// If NetDialTLSContext is set, Dial assumes the TLS handshake is done there and
// TLSClientConfig is ignored.
// If "Proxy" field is also set, this function dials the proxy (and performs
// the TLS handshake with the proxy, ignoring TLSClientConfig).
NetDialTLSContext func(ctx context.Context, network, addr string) (net.Conn, error)

// Proxy specifies a function to return a proxy for a given
Expand All @@ -73,7 +77,7 @@ type Dialer struct {

// TLSClientConfig specifies the TLS configuration to use with tls.Client.
// If nil, the default configuration is used.
// If either NetDialTLS or NetDialTLSContext are set, Dial assumes the TLS handshake
// If NetDialTLSContext is set, Dial assumes the TLS handshake
// is done there and TLSClientConfig is ignored.
TLSClientConfig *tls.Config

Expand Down Expand Up @@ -275,18 +279,13 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h
}
}

// If needed, wrap the dial function to connect through a proxy.
// If needed, generate a dial function to connect to the proxy, possibly
// wrapping the previously computed "netDial" function.
if d.Proxy != nil {
proxyURL, err := d.Proxy(req)
netDial, err = d.proxyNetDial(req, netDial)
if err != nil {
return nil, nil, err
}
if proxyURL != nil {
netDial, err = proxyFromURL(proxyURL, netDial)
if err != nil {
return nil, nil, err
}
}
}

hostPort, hostNoPort := hostPortNoPort(u)
Expand Down Expand Up @@ -415,6 +414,56 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h
return conn, resp, nil
}

// Returns an updated proxy dial function, wrapping the passed
// "netDial" function, or an error if one occurred. If the proxy
// is HTTPS, then TLS handshake will occur in the returned dial function.
func (d *Dialer) proxyNetDial(req *http.Request, netDial netDialerFunc) (netDialerFunc, error) {
if d.Proxy == nil {
return nil, errors.New("proxy url function is nil")
}
// Generate the proxy URL and validate.
proxyURL, err := d.Proxy(req)
if err != nil {
return nil, err
}
// Request should *not* be proxied; use originial dial function.
if proxyURL == nil {
return netDial, nil
}
if proxyURL.Scheme == "http" && d.NetDialTLSContext != nil {
return nil, errors.New("dial TLS function set for dialing non-HTTPS proxy")
}
Comment on lines +433 to +435
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not match the logic in

	var netDial netDialerFunc
	switch {
	case u.Scheme == "https" && d.NetDialTLSContext != nil:
		netDial = d.NetDialTLSContext
	case d.NetDialContext != nil:
		netDial = d.NetDialContext
	case d.NetDial != nil:
		netDial = func(ctx context.Context, net, addr string) (net.Conn, error) {
			return d.NetDial(net, addr)
		}
	default:
		netDial = (&net.Dialer{}).DialContext
	}

that seems to indicate that NetDialTLSContext is ONLY used if the scheme is https , but ignored otherwise

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems we can remove this check or just shortcut and

Suggested change
if proxyURL.Scheme == "http" && d.NetDialTLSContext != nil {
return nil, errors.New("dial TLS function set for dialing non-HTTPS proxy")
}
if proxyURL.Scheme == "http" {
return proxyFromURL(proxyURL, netDial)
}

// For HTTPS proxy, either use TLS dial function set in Dialer (which performs
// the TLS handshake within the function), or create a new dial function by wrapping
// the passed "netDial" for the connection and using TLSClientConfig for the handshake.
if proxyURL.Scheme == "https" {
if d.NetDialTLSContext != nil {
netDial = d.NetDialTLSContext
Comment on lines +440 to +441
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note for self:

  • url is http and proxy-url is https, we replace the dialer ... users can specify a custom tls dialer just for the proxy.
  • url is https and proxy-url is https, the same tls dialer is used for both

Copy link
Author

@seans3 seans3 Feb 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • url is https and proxy-url is https, the same tls dialer is used for both

I do not think this is correct. There is only one "dial". It is either to the "proxy" or to the "upstream" server. If there is a proxy, the dial is to the "proxy" (and the TLS handshake must occur in the dial). The "proxy" server is the place that dials the "upstream" in this case. The second TLS handshake (not dial) to the "upstream" occurs over the previously established connection, and it MUST use the TLSClientConfig. So the NetDialTLSContext is only used for the proxy connection if it is set and the Proxy function returns non-nil, HTTPS proxy URL.

// Ensures later TLS handshake occurs to backend over proxied connection.
d.NetDialTLSContext = nil
} else if d.TLSClientConfig == nil {
return nil, errors.New("HTTPS proxy requires TLS dial function or TLS client config")
} else {
proxyDial := netDial
netDial = func(ctx context.Context, unused, addr string) (net.Conn, error) {
// Creates TCP connection to addr using previously computed "netDial"
conn, err := proxyDial(ctx, "tcp", addr)
cfg := cloneTLSConfig(d.TLSClientConfig)
if cfg.ServerName == "" {
_, hostNoPort := hostPortNoPort(proxyURL)
cfg.ServerName = hostNoPort
}
tlsConn := tls.Client(conn, cfg)
// Do the TLS handshake using TLSConfig over the wrapped connection.
err = doHandshake(ctx, tlsConn, cfg)
return tlsConn, err
}
}
}
// Wrap dial function to implement proxy CONNECT method.
return proxyFromURL(proxyURL, netDial)
}

func cloneTLSConfig(cfg *tls.Config) *tls.Config {
if cfg == nil {
return &tls.Config{}
Expand Down
Loading