1
0
mirror of https://github.com/rclone/rclone.git synced 2025-08-10 06:09:44 +02:00

sftp: add --sftp-http-proxy to connect via HTTP CONNECT proxy

This commit is contained in:
Nick Craig-Wood
2025-04-23 15:08:52 +01:00
parent 9f0e237931
commit 2404831725
3 changed files with 97 additions and 0 deletions

View File

@@ -10,6 +10,7 @@ import (
"fmt" "fmt"
"io" "io"
iofs "io/fs" iofs "io/fs"
"net/url"
"os" "os"
"path" "path"
"regexp" "regexp"
@@ -480,6 +481,14 @@ Supports the format user:pass@host:port, user@host:port, host:port.
Example: Example:
myUser:myPass@localhost:9005 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, Advanced: true,
}, { }, {
@@ -545,6 +554,7 @@ type Options struct {
HostKeyAlgorithms fs.SpaceSepList `config:"host_key_algorithms"` HostKeyAlgorithms fs.SpaceSepList `config:"host_key_algorithms"`
SSH fs.SpaceSepList `config:"ssh"` SSH fs.SpaceSepList `config:"ssh"`
SocksProxy string `config:"socks_proxy"` SocksProxy string `config:"socks_proxy"`
HTTPProxy string `config:"http_proxy"`
CopyIsHardlink bool `config:"copy_is_hardlink"` CopyIsHardlink bool `config:"copy_is_hardlink"`
} }
@@ -570,6 +580,7 @@ type Fs struct {
savedpswd string savedpswd string
sessions atomic.Int32 // count in use sessions sessions atomic.Int32 // count in use sessions
tokens *pacer.TokenDispenser 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) // 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" 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{ sshConfig := &ssh.ClientConfig{
User: opt.User, User: opt.User,
Auth: []ssh.AuthMethod{}, Auth: []ssh.AuthMethod{},

View File

@@ -31,6 +31,8 @@ func (f *Fs) newSSHClientInternal(ctx context.Context, network, addr string, ssh
) )
if f.opt.SocksProxy != "" { if f.opt.SocksProxy != "" {
conn, err = proxy.SOCKS5Dial(network, addr, f.opt.SocksProxy, baseDialer) 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 { } else {
conn, err = baseDialer.Dial(network, addr) conn, err = baseDialer.Dial(network, addr)
} }

75
lib/proxy/http.go Normal file
View File

@@ -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
}