diff --git a/backend/sftp/sftp.go b/backend/sftp/sftp.go index 3becf7b5c..eed3f378a 100644 --- a/backend/sftp/sftp.go +++ b/backend/sftp/sftp.go @@ -10,6 +10,7 @@ import ( "fmt" "io" iofs "io/fs" + "net/url" "os" "path" "regexp" @@ -482,6 +483,14 @@ Example: myUser:myPass@localhost:9005 `, Advanced: true, + }, { + Name: "http_proxy", + Default: "", + Help: `URL for HTTP CONNECT proxy + +Set this to a URL for an HTTP proxy which supports the HTTP CONNECT verb. +`, + Advanced: true, }, { Name: "copy_is_hardlink", Default: false, @@ -545,6 +554,7 @@ type Options struct { HostKeyAlgorithms fs.SpaceSepList `config:"host_key_algorithms"` SSH fs.SpaceSepList `config:"ssh"` SocksProxy string `config:"socks_proxy"` + HTTPProxy string `config:"http_proxy"` CopyIsHardlink bool `config:"copy_is_hardlink"` } @@ -570,6 +580,7 @@ type Fs struct { savedpswd string sessions atomic.Int32 // count in use sessions tokens *pacer.TokenDispenser + proxyURL *url.URL // address of HTTP proxy read from environment } // Object is a remote SFTP file that has been stat'd (so it exists, but is not necessarily open for reading) @@ -867,6 +878,15 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e opt.Port = "22" } + // get proxy URL if set + if opt.HTTPProxy != "" { + proxyURL, err := url.Parse(opt.HTTPProxy) + if err != nil { + return nil, fmt.Errorf("failed to parse HTTP Proxy URL: %w", err) + } + f.proxyURL = proxyURL + } + sshConfig := &ssh.ClientConfig{ User: opt.User, Auth: []ssh.AuthMethod{}, diff --git a/backend/sftp/ssh_internal.go b/backend/sftp/ssh_internal.go index 8911cc13c..2508bbf31 100644 --- a/backend/sftp/ssh_internal.go +++ b/backend/sftp/ssh_internal.go @@ -31,6 +31,8 @@ func (f *Fs) newSSHClientInternal(ctx context.Context, network, addr string, ssh ) if f.opt.SocksProxy != "" { conn, err = proxy.SOCKS5Dial(network, addr, f.opt.SocksProxy, baseDialer) + } else if f.proxyURL != nil { + conn, err = proxy.HTTPConnectDial(network, addr, f.proxyURL, baseDialer) } else { conn, err = baseDialer.Dial(network, addr) } diff --git a/lib/proxy/http.go b/lib/proxy/http.go new file mode 100644 index 000000000..616d2c4c1 --- /dev/null +++ b/lib/proxy/http.go @@ -0,0 +1,75 @@ +package proxy + +import ( + "bufio" + "crypto/tls" + "fmt" + "net" + "net/http" + "net/url" + "strings" + + "golang.org/x/net/proxy" +) + +// HTTPConnectDial connects using HTTP CONNECT via proxyDialer +// +// It will read the HTTP proxy address from the environment in the +// standard way. +// +// It optionally takes a proxyDialer to dial the HTTP proxy server. +// If nil is passed, it will use the default net.Dialer. +func HTTPConnectDial(network, addr string, proxyURL *url.URL, proxyDialer proxy.Dialer) (net.Conn, error) { + if proxyDialer == nil { + proxyDialer = &net.Dialer{} + } + if proxyURL == nil { + return proxyDialer.Dial(network, addr) + } + + // prepare proxy host with default ports + host := proxyURL.Host + if !strings.Contains(host, ":") { + if strings.EqualFold(proxyURL.Scheme, "https") { + host += ":443" + } else { + host += ":80" + } + } + + // connect to proxy + conn, err := proxyDialer.Dial(network, host) + if err != nil { + return nil, fmt.Errorf("HTTP CONNECT proxy failed to Dial: %q", err) + } + + // wrap TLS if HTTPS proxy + if strings.EqualFold(proxyURL.Scheme, "https") { + tlsConfig := &tls.Config{ServerName: proxyURL.Hostname()} + tlsConn := tls.Client(conn, tlsConfig) + if err := tlsConn.Handshake(); err != nil { + _ = conn.Close() + return nil, fmt.Errorf("HTTP CONNECT proxy failed to make TLS connection: %q", err) + } + conn = tlsConn + } + + // send CONNECT + _, err = fmt.Fprintf(conn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", addr, addr) + if err != nil { + _ = conn.Close() + return nil, fmt.Errorf("HTTP CONNECT proxy failed to send CONNECT: %q", err) + } + br := bufio.NewReader(conn) + req := &http.Request{URL: &url.URL{Scheme: "http", Host: addr}} + resp, err := http.ReadResponse(br, req) + if err != nil { + _ = conn.Close() + return nil, fmt.Errorf("HTTP CONNECT proxy failed to read response: %q", err) + } + if resp.StatusCode != http.StatusOK { + _ = conn.Close() + return nil, fmt.Errorf("HTTP CONNECT proxy failed: %s", resp.Status) + } + return conn, nil +}