Skip to content

HTTP proxy support #63

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

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ _testmain.go
*.prof

coverage.txt

# Editors
.idea/
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
+--------+ +----------+ +-----------+
Expand All @@ -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
Expand Down
80 changes: 77 additions & 3 deletions easyssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"sync"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down
37 changes: 37 additions & 0 deletions example/http_proxy/http_proxy.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
18 changes: 16 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
112 changes: 112 additions & 0 deletions http_proxy.go
Original file line number Diff line number Diff line change
@@ -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
}