From 8c250aa76f6f884f5ec2469eaaed6490b819fcc1 Mon Sep 17 00:00:00 2001 From: Vladislav Yarmak Date: Thu, 9 Feb 2023 01:17:19 +0200 Subject: [PATCH] switch to bundled http proxy connector --- go.mod | 3 +- go.sum | 2 - upstream.go | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++ utils.go | 5 +- 4 files changed, 169 insertions(+), 7 deletions(-) create mode 100644 upstream.go diff --git a/go.mod b/go.mod index 9cd5d8d..731e29f 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,7 @@ module github.com/Snawoot/dumbproxy go 1.13 require ( - github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b // indirect github.com/tg123/go-htpasswd v1.2.0 golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 - golang.org/x/net v0.5.0 // indirect + golang.org/x/net v0.5.0 ) diff --git a/go.sum b/go.sum index 4540178..bc6caf2 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,6 @@ github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaa github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b h1:xZ59n7Frzh8CwyfAapUZLSg+gXH5m63YEaFCMpDHhpI= -github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b/go.mod h1:uDd4sYVYsqcxAB8j+Q7uhL6IJCs/r1kxib1HV4bgOMg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/upstream.go b/upstream.go new file mode 100644 index 0000000..d223ab5 --- /dev/null +++ b/upstream.go @@ -0,0 +1,166 @@ +package main + +import ( + "bufio" + "bytes" + "context" + "crypto/tls" + "encoding/base64" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strings" + "sync" + + xproxy "golang.org/x/net/proxy" +) + +type HTTPProxyDialer struct { + address string + tls bool + userinfo *url.Userinfo + next ContextDialer +} + +func NewHTTPProxyDialer(address string, tls bool, userinfo *url.Userinfo, next Dialer) *HTTPProxyDialer { + return &HTTPProxyDialer{ + address: address, + tls: tls, + next: maybeWrapWithContextDialer(next), + userinfo: userinfo, + } +} + +func HTTPProxyDialerFromURL(u *url.URL, next xproxy.Dialer) (xproxy.Dialer, error) { + host := u.Hostname() + port := u.Port() + tls := false + + switch strings.ToLower(u.Scheme) { + case "http": + if port == "" { + port = "80" + } + case "https": + tls = true + if port == "" { + port = "443" + } + default: + return nil, errors.New("unsupported proxy type") + } + + address := net.JoinHostPort(host, port) + + return NewHTTPProxyDialer(address, tls, u.User, next), nil +} + +func (d *HTTPProxyDialer) Dial(network, address string) (net.Conn, error) { + return d.DialContext(context.Background(), network, address) +} + +func (d *HTTPProxyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + switch network { + case "tcp", "tcp4", "tcp6": + default: + return nil, errors.New("only \"tcp\" network is supported") + } + conn, err := d.next.DialContext(ctx, "tcp", d.address) + if err != nil { + return nil, fmt.Errorf("proxy dialer is unable to make connection: %w", err) + } + if d.tls { + hostname, _, err := net.SplitHostPort(d.address) + if err != nil { + hostname = address + } + conn = tls.Client(conn, &tls.Config{ + ServerName: hostname, + }) + } + + stopGuardEvent := make(chan struct{}) + guardErr := make(chan error, 1) + go func() { + select { + case <-stopGuardEvent: + close(guardErr) + case <-ctx.Done(): + conn.Close() + guardErr <- ctx.Err() + } + }() + var stopGuardOnce sync.Once + stopGuard := func() { + stopGuardOnce.Do(func() { + close(stopGuardEvent) + }) + } + defer stopGuard() + + var reqBuf bytes.Buffer + fmt.Fprintf(&reqBuf, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n", address, address) + if d.userinfo != nil { + fmt.Fprintf(&reqBuf, "Proxy-Authorization: %s\r\n", basicAuthHeader(d.userinfo)) + } + fmt.Fprintf(&reqBuf, "User-Agent: dumbproxy/%s\r\n\r\n", version) + _, err = io.Copy(conn, &reqBuf) + if err != nil { + conn.Close() + return nil, fmt.Errorf("unable to write proxy request for remote connection: %w", err) + } + + resp, err := readResponse(conn) + if err != nil { + conn.Close() + return nil, fmt.Errorf("reading proxy response failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + conn.Close() + return nil, fmt.Errorf("bad status code from proxy: %d", resp.StatusCode) + } + + stopGuard() + if err := <-guardErr; err != nil { + return nil, fmt.Errorf("context error: %w", err) + } + return conn, nil +} + +var ( + responseTerminator = []byte("\r\n\r\n") +) + +func readResponse(r io.Reader) (*http.Response, error) { + var respBuf bytes.Buffer + b := make([]byte, 1) + for !bytes.HasSuffix(respBuf.Bytes(), responseTerminator) { + n, err := r.Read(b) + if err != nil { + return nil, fmt.Errorf("unable to read HTTP response: %w", err) + } + if n == 0 { + continue + } + _, err = respBuf.Write(b) + if err != nil { + return nil, fmt.Errorf("unable to store byte into buffer: %w", err) + } + } + resp, err := http.ReadResponse(bufio.NewReader(&respBuf), nil) + if err != nil { + return nil, fmt.Errorf("unable to decode proxy response: %w", err) + } + return resp, nil +} + +func basicAuthHeader(userinfo *url.Userinfo) string { + username := userinfo.Username() + password, _ := userinfo.Password() + return "Basic " + base64.StdEncoding.EncodeToString( + []byte(username+":"+password)) +} diff --git a/utils.go b/utils.go index 70a9c95..4e65559 100644 --- a/utils.go +++ b/utils.go @@ -18,7 +18,6 @@ import ( "sync" "time" - "github.com/magisterquis/connectproxy" "golang.org/x/crypto/bcrypt" "golang.org/x/crypto/ssh/terminal" xproxy "golang.org/x/net/proxy" @@ -311,8 +310,8 @@ var registerDialerTypesOnce sync.Once func proxyDialerFromURL(proxyURL string, forward Dialer) (Dialer, error) { registerDialerTypesOnce.Do(func() { - xproxy.RegisterDialerType("http", connectproxy.New) - xproxy.RegisterDialerType("https", connectproxy.New) + xproxy.RegisterDialerType("http", HTTPProxyDialerFromURL) + xproxy.RegisterDialerType("https", HTTPProxyDialerFromURL) }) parsedURL, err := url.Parse(proxyURL) if err != nil {