diff --git a/.gitignore b/.gitignore index db171bc..90b1753 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ _testmain.go *.prof coverage.txt + +# Editors +.idea/ diff --git a/README.md b/README.md index b608c94..7ac02be 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ This project is forked from [easyssh](https://github.com/hypersleep/easyssh) but * [x] Support key path of user private key. * [x] Support Timeout for the TCP connection to establish. * [x] Support SSH ProxyCommand. +* [x] Support HTTP Proxy traversal. ```bash +--------+ +----------+ +-----------+ @@ -28,6 +29,15 @@ This project is forked from [easyssh](https://github.com/hypersleep/easyssh) but | Laptop | <--> | Firewall | <--> | FooServer | +--------+ +----------+ +-----------+ 192.168.1.5 121.1.2.3 10.10.29.68 + + + OR + + +--------+ +-----------------+ +----------+ +-----------+ + | Laptop | <--> | Corporate Proxy | <--> | Jumphost | <--> | FooServer | + +--------+ +-----------------+ +----------+ +-----------+ + 192.168.1.5 192.168.1.1:8080 121.1.2.3 10.10.29.68 + ``` ## Usage diff --git a/easyssh.go b/easyssh.go index bdfa589..b91acf6 100644 --- a/easyssh.go +++ b/easyssh.go @@ -11,6 +11,8 @@ import ( "io/ioutil" "log" "net" + "net/http" + "net/url" "os" "path/filepath" "sync" @@ -45,6 +47,9 @@ type ( KeyExchanges []string Fingerprint string + // HTTP Proxy support + ProxyInfo func(req *http.Request) (*url.URL, error) + // Enable the use of insecure ciphers and key exchange methods. // This enables the use of the the following insecure ciphers and key exchange methods: // - aes128-cbc @@ -203,7 +208,25 @@ func (ssh_conf *MakeConfig) Connect() (*ssh.Session, *ssh.Client, error) { defer closer.Close() } - // Enable proxy command + // HTTP proxy support + var proxyAddr string + if ssh_conf.ProxyInfo != nil { + req, _ := http.NewRequest("CONNECT", "https://"+ssh_conf.Server, nil) + proxyInfo, err := ssh_conf.ProxyInfo(req) + if proxyInfo == nil { // Try http:// as well + req, _ = http.NewRequest("CONNECT", "http://"+ssh_conf.Server, nil) + proxyInfo, err = ssh_conf.ProxyInfo(req) + } + if err == nil && proxyInfo != nil { + proxyAddr = proxyInfo.Host + if proxyInfo.User != nil { + password, _ := proxyInfo.User.Password() + proxyAddr = proxyInfo.User.Username() + ":" + password + "@" + proxyAddr + } + } + } + + // Use bastion server if ssh_conf.Proxy.Server != "" { proxyConfig, closer := getSSHConfig(DefaultConfig{ User: ssh_conf.Proxy.User, @@ -220,8 +243,35 @@ func (ssh_conf *MakeConfig) Connect() (*ssh.Session, *ssh.Client, error) { if closer != nil { defer closer.Close() } + var err error + var proxyClient *ssh.Client + var direct directDialer + + if proxyAddr != "" { + var pConn net.Conn + var bConn ssh.Conn + var bChans <-chan ssh.NewChannel + var bReq <-chan *ssh.Request - proxyClient, err := ssh.Dial("tcp", net.JoinHostPort(ssh_conf.Proxy.Server, ssh_conf.Proxy.Port), proxyConfig) + bAddr := net.JoinHostPort(ssh_conf.Proxy.Server, ssh_conf.Proxy.Port) + direct = directDialer{} + + registerDialerType() + pConn, err = newHTTPProxyConn(direct, proxyAddr, bAddr) + + if err != nil { + return nil, nil, fmt.Errorf("Error connecting to proxy: %s", err) + } + + bConn, bChans, bReq, err = ssh.NewClientConn(pConn, bAddr, proxyConfig) + + if err != nil { + return nil, nil, fmt.Errorf("Error creating new client connection via proxy bastion: %s", err) + } + proxyClient = ssh.NewClient(bConn, bChans, bReq) + } else { + proxyClient, err = ssh.Dial("tcp", net.JoinHostPort(ssh_conf.Proxy.Server, ssh_conf.Proxy.Port), proxyConfig) + } if err != nil { return nil, nil, err } @@ -238,7 +288,31 @@ func (ssh_conf *MakeConfig) Connect() (*ssh.Session, *ssh.Client, error) { client = ssh.NewClient(ncc, chans, reqs) } else { - client, err = ssh.Dial("tcp", net.JoinHostPort(ssh_conf.Server, ssh_conf.Port), targetConfig) + if proxyAddr != "" { + var pConn net.Conn + var bConn ssh.Conn + var bChans <-chan ssh.NewChannel + var bReq <-chan *ssh.Request + + bAddr := net.JoinHostPort(ssh_conf.Server, ssh_conf.Port) + direct := directDialer{} + + registerDialerType() + pConn, err = newHTTPProxyConn(direct, proxyAddr, bAddr) + + if err != nil { + return nil, nil, fmt.Errorf("Error connecting to proxy: %s", err) + } + + bConn, bChans, bReq, err = ssh.NewClientConn(pConn, bAddr, targetConfig) + + if err != nil { + return nil, nil, fmt.Errorf("Error creating new client connection via proxy: %s", err) + } + client = ssh.NewClient(bConn, bChans, bReq) + } else { + client, err = ssh.Dial("tcp", net.JoinHostPort(ssh_conf.Server, ssh_conf.Port), targetConfig) + } if err != nil { return nil, nil, err } diff --git a/example/http_proxy/http_proxy.go b/example/http_proxy/http_proxy.go new file mode 100644 index 0000000..72ef8f2 --- /dev/null +++ b/example/http_proxy/http_proxy.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" + "net/http" + + "github.com/appleboy/easyssh-proxy" +) + +func main() { + // Create MakeConfig instance with remote username, server address and path to private key. + // Use a HTTP proxy listening on 127.0.0.1:8888 to connect to Proxy/Bastion + ssh := &easyssh.MakeConfig{ + User: "drone-scp", + Server: "localhost", + Port: "22", + KeyPath: "./tests/.ssh/id_rsa", + ProxyInfo: http.ProxyFromEnvironment, + Proxy: easyssh.DefaultConfig{ + User: "drone-scp", + Server: "localhost", + Port: "22", + KeyPath: "./tests/.ssh/id_rsa", + }, + } + + // Call Scp method with file you want to upload to remote server. + // Please make sure the `tmp` floder exists. + err := ssh.Scp("/root/source.csv", "/tmp/target.csv") + + // Handle errors + if err != nil { + panic("Can't run remote command: " + err.Error()) + } else { + fmt.Println("success") + } +} diff --git a/go.mod b/go.mod index f70cdf3..634f7c0 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,10 @@ go 1.15 require ( github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5 + github.com/kr/pretty v0.1.0 // indirect github.com/stretchr/testify v1.6.1 - golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 + golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 + golang.org/x/net v0.0.0-20201021035429-f5854403a974 + golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/go.sum b/go.sum index 92b3ccc..a08b9ee 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,11 @@ 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/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU= github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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= @@ -12,19 +17,28 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9 h1:sYNJzB4J8toYPQTM6pAkcmBRgw9SnQKP9oXCHfgy604= +golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c h1:jceGD5YNJGgGMkJz79agzOln1K9TaZUjv5ird16qniQ= golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/http_proxy.go b/http_proxy.go new file mode 100644 index 0000000..ec6365d --- /dev/null +++ b/http_proxy.go @@ -0,0 +1,112 @@ +package easyssh + +import ( + "bufio" + "fmt" + "net" + "net/http" + "net/url" + + "golang.org/x/net/proxy" +) + +type directDialer struct{} + +func (directDialer) Dial(network, addr string) (net.Conn, error) { + return net.Dial(network, addr) +} + +type connectProxyDialer struct { + host string + forward proxy.Dialer + auth bool + username string + password string +} + +func newConnectProxyDialer(u *url.URL, forward proxy.Dialer) (proxy.Dialer, error) { + host := u.Host + p := &connectProxyDialer{ + host: host, + forward: forward, + } + + if u.User != nil { + p.auth = true + p.username = u.User.Username() + p.password, _ = u.User.Password() + } + + return p, nil +} + +func registerDialerType() { + proxy.RegisterDialerType("http", newConnectProxyDialer) + proxy.RegisterDialerType("https", newConnectProxyDialer) +} + +func newHTTPProxyConn(d directDialer, proxyAddr, targetAddr string) (net.Conn, error) { + proxyURL, err := url.Parse("http://" + proxyAddr) + if err != nil { + return nil, err + } + + proxyDialer, err := proxy.FromURL(proxyURL, d) + if err != nil { + return nil, err + } + + proxyConn, err := proxyDialer.Dial("tcp", targetAddr) + if err != nil { + return nil, err + } + + return proxyConn, err +} + +func (p *connectProxyDialer) Dial(_, addr string) (net.Conn, error) { + c, err := p.forward.Dial("tcp", p.host) + if err != nil { + return nil, err + } + + reqURL, err := url.Parse("http://" + addr) + if err != nil { + _ = c.Close() + return nil, err + } + + req, err := http.NewRequest("CONNECT", reqURL.String(), nil) + if err != nil { + _ = c.Close() + return nil, err + } + + if p.auth { + req.SetBasicAuth(p.username, p.password) + } + + req.Close = false + + err = req.Write(c) + if err != nil { + _ = c.Close() + return nil, err + } + + res, err := http.ReadResponse(bufio.NewReader(c), req) + if err != nil { + res.Body.Close() + _ = c.Close() + return nil, err + } + + res.Body.Close() + + if res.StatusCode != http.StatusOK { + _ = c.Close() + return nil, fmt.Errorf("Connection Error: StatusCode: %d", res.StatusCode) + } + + return c, nil +}