-
Notifications
You must be signed in to change notification settings - Fork 59
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
feat: HTTP/1.1 CONNECT client #350
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the contribution. I have a few comments.
x/httpproxy/connect_client.go
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please move the code to a new package x/httpconnect?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, done!
x/httpproxy/connect_client.go
Outdated
return conn, nil | ||
} | ||
|
||
func (c *connectClient) sendConnectRequest(ctx context.Context, remoteAddr string, conn transport.StreamConn) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was hoping to use the http.Client
to avoid replicating logic, provide extra functionality, and keep it less tied to HTTP/1.1.
Does https://pkg.go.dev/net/http#Client.Do not work with a CONNECT method?
I think you may need https://pkg.go.dev/net/http#NewResponseController to enable full duplex and hijack the connection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A great insight!
http.Client
does work with a CONNECT
request.
A ResponseController
can take over a server-side connection, but not a client-side one. However, I have found a way to achieve this with an http.Client
using an io.Pipe
.
x/httpproxy/connect_client.go
Outdated
|
||
// connectClient is a [transport.StreamDialer] implementation that sends a CONNECT request | ||
type connectClient struct { | ||
endpoint transport.StreamEndpoint |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to know the proxy address to pass to connect.
The connection RemoteAddress has the connected IP, it loses the domain information.
I suggest you pass the address and a dialer instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A good idea, done
x/httpproxy/connect_client.go
Outdated
endpoint transport.StreamEndpoint | ||
|
||
// proxyAuth is the Proxy-Authorization header value. If empty, the header is not sent | ||
proxyAuth string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps have a http.Header object here instead that gets merged into the request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, it is better to merge. Done
x/httpproxy/connect_client_test.go
Outdated
connClient, err := NewConnectClient(endpoint, WithProxyAuthorization("Basic "+creds)) | ||
require.NoError(t, err, "NewConnectClient") | ||
|
||
streamConn, err := connClient.DialStream(context.Background(), host) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you write "Request" to the connection and expect the response to be "Response"? That will exercise the exchange as well, which has been an issue before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure what you are referring to. Could you, please, provide a small example?
If what we read is not a valid response, we should perform an early return with a non-nil error, should we not?
Upd. I have added a testcase for that unhappy path.
9869d62
to
2e46ac4
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a couple of small things
x/httpconnect/connect_client.go
Outdated
// WithHeaders appends the given [headers] to the CONNECT request | ||
func WithHeaders(headers http.Header) ConnectClientOption { | ||
return func(c *connectClient) { | ||
c.headers = headers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you copy the headers into the internal one instead? That is safer and lets the caller reuse the input headers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A good point, done
x/httpconnect/connect_client.go
Outdated
|
||
pr, pw := io.Pipe() | ||
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodConnect, "http://"+remoteAddr, pr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may want to specify whether we want to use HTTP or HTTPS. Typically TLS has to be handled by the HTTP Client. But for now I think it's ok.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. I have added a TODO on that line
x/httpconnect/pipe_conn.go
Outdated
) | ||
|
||
// PipeConn is a [transport.StreamConn] that overrides [Read], [Write] (and corresponding [Close]) functions with the given [reader] and [writer] | ||
type PipeConn struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep this private for now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, done
x/httpconnect/connect_client_test.go
Outdated
targetURL, err := url.Parse(targetSrv.URL) | ||
require.NoError(t, err) | ||
|
||
proxySrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could use https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/x@v0.0.0-20250110201030-80a3f247a159/httpproxy#NewConnectHandler instead of duplicating the logic. Though we don't have support for authorization there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestion! I have replaced the duplicating logic with a ConnectHandler
8768cb9
to
b9e4f2d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. Thank you for the contribution!
Hi,
The merge request adds an HTTP/1.1 CONNECT client, as described in #319
As for HTTP/2 support, I'm getting to know the HTTP/2 implementation in Go and will share an implementation later.
Sincerely,
Nikolai