Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ You can find the docs at [go docs](https://pkg.go.dev/github.com/melbahja/goph).
- Supports adding new hosts to **known_hosts file**.
- Supports **file system operations** like: `Open, Create, Chmod...`
- Supports **context.Context** for command cancellation.
- Supports **TCP proxy** connections (SOCKS5 and HTTP CONNECT).

## 📄  Usage

Expand Down Expand Up @@ -105,6 +106,7 @@ if err != nil {
client, err := goph.New("root", "192.1.1.3", auth)
```


#### ⤴️ Upload Local File to Remote:
```go
err := client.Upload("/path/to/local/file", "/path/to/remote/file")
Expand Down
194 changes: 193 additions & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ package goph

import (
"context"
"encoding/base64"
"fmt"
"io"
"net"
"os"
"strings"
"time"

"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
"golang.org/x/net/proxy"
)

// Client represents Goph client.
Expand All @@ -30,8 +33,30 @@ type Config struct {
Timeout time.Duration
Callback ssh.HostKeyCallback
BannerCallback ssh.BannerCallback
Proxy *ProxyConfig
}

// ProxyConfig holds proxy configuration.
type ProxyConfig struct {
Type ProxyType
Addr string
Port uint
User string
Password string
}

// ProxyType represents the type of proxy.
type ProxyType int

const (
// ProxyTypeNone indicates no proxy
ProxyTypeNone ProxyType = iota
// ProxyTypeSOCKS5 indicates SOCKS5 proxy
ProxyTypeSOCKS5
// ProxyTypeHTTP indicates HTTP CONNECT proxy
ProxyTypeHTTP
)

// DefaultTimeout is the timeout of ssh client connection.
var DefaultTimeout = 20 * time.Second

Expand Down Expand Up @@ -81,15 +106,182 @@ func NewConn(config *Config) (c *Client, err error) {
return
}

// NewProxyConn returns new client with proxy configuration and error if any.
func NewProxyConn(config *Config, proxyType ProxyType, proxyAddr string, proxyPort uint, proxyUser, proxyPass string) (*Client, error) {
config.Proxy = &ProxyConfig{
Type: proxyType,
Addr: proxyAddr,
Port: proxyPort,
User: proxyUser,
Password: proxyPass,
}
return NewConn(config)
}

// NewSOCKS5ProxyConn returns new client with SOCKS5 proxy configuration.
func NewSOCKS5ProxyConn(user, addr string, auth Auth, proxyAddr string, proxyPort uint, proxyUser, proxyPass string) (*Client, error) {
callback, _ := DefaultKnownHosts()
return NewProxyConn(&Config{
User: user,
Addr: addr,
Port: 22,
Auth: auth,
Timeout: DefaultTimeout,
Callback: callback,
}, ProxyTypeSOCKS5, proxyAddr, proxyPort, proxyUser, proxyPass)
}

// NewHTTPProxyConn returns new client with HTTP CONNECT proxy configuration.
func NewHTTPProxyConn(user, addr string, auth Auth, proxyAddr string, proxyPort uint, proxyUser, proxyPass string) (*Client, error) {
callback, _ := DefaultKnownHosts()
return NewProxyConn(&Config{
User: user,
Addr: addr,
Port: 22,
Auth: auth,
Timeout: DefaultTimeout,
Callback: callback,
}, ProxyTypeHTTP, proxyAddr, proxyPort, proxyUser, proxyPass)
}

// NewSOCKS5ProxyUnknown returns new client with SOCKS5 proxy and insecure host key checking.
func NewSOCKS5ProxyUnknown(user, addr string, auth Auth, proxyAddr string, proxyPort uint, proxyUser, proxyPass string) (*Client, error) {
return NewProxyConn(&Config{
User: user,
Addr: addr,
Port: 22,
Auth: auth,
Timeout: DefaultTimeout,
Callback: ssh.InsecureIgnoreHostKey(),
}, ProxyTypeSOCKS5, proxyAddr, proxyPort, proxyUser, proxyPass)
}

// NewHTTPProxyUnknown returns new client with HTTP CONNECT proxy and insecure host key checking.
func NewHTTPProxyUnknown(user, addr string, auth Auth, proxyAddr string, proxyPort uint, proxyUser, proxyPass string) (*Client, error) {
return NewProxyConn(&Config{
User: user,
Addr: addr,
Port: 22,
Auth: auth,
Timeout: DefaultTimeout,
Callback: ssh.InsecureIgnoreHostKey(),
}, ProxyTypeHTTP, proxyAddr, proxyPort, proxyUser, proxyPass)
}

// Dial starts a client connection to SSH server based on config.
func Dial(proto string, c *Config) (*ssh.Client, error) {
return ssh.Dial(proto, net.JoinHostPort(c.Addr, fmt.Sprint(c.Port)), &ssh.ClientConfig{
var conn net.Conn
var err error

if c.Proxy != nil && c.Proxy.Type != ProxyTypeNone {
conn, err = dialThroughProxy(c)
if err != nil {
return nil, err
}
} else {
conn, err = net.DialTimeout(proto, net.JoinHostPort(c.Addr, fmt.Sprint(c.Port)), c.Timeout)
if err != nil {
return nil, err
}
}

sshConn, chans, reqs, err := ssh.NewClientConn(conn, net.JoinHostPort(c.Addr, fmt.Sprint(c.Port)), &ssh.ClientConfig{
User: c.User,
Auth: c.Auth,
Timeout: c.Timeout,
HostKeyCallback: c.Callback,
BannerCallback: c.BannerCallback,
})
if err != nil {
conn.Close()
return nil, err
}

return ssh.NewClient(sshConn, chans, reqs), nil
}

// dialThroughProxy establishes a connection through the configured proxy.
func dialThroughProxy(c *Config) (net.Conn, error) {
proxyAddr := net.JoinHostPort(c.Proxy.Addr, fmt.Sprint(c.Proxy.Port))

switch c.Proxy.Type {
case ProxyTypeSOCKS5:
return dialSOCKS5Proxy(proxyAddr, c.Proxy.User, c.Proxy.Password, c.Addr, c.Port, c.Timeout)
case ProxyTypeHTTP:
return dialHTTPProxy(proxyAddr, c.Proxy.User, c.Proxy.Password, c.Addr, c.Port, c.Timeout)
default:
return nil, fmt.Errorf("unsupported proxy type: %v", c.Proxy.Type)
}
}

// dialSOCKS5Proxy establishes a connection through a SOCKS5 proxy.
func dialSOCKS5Proxy(proxyAddr, proxyUser, proxyPass, targetAddr string, targetPort uint, timeout time.Duration) (net.Conn, error) {
var auth *proxy.Auth
if proxyUser != "" {
auth = &proxy.Auth{
User: proxyUser,
Password: proxyPass,
}
}

dialer, err := proxy.SOCKS5("tcp", proxyAddr, auth, proxy.Direct)
if err != nil {
return nil, fmt.Errorf("failed to create SOCKS5 dialer: %w", err)
}

target := net.JoinHostPort(targetAddr, fmt.Sprint(targetPort))

// Set timeout on the dialer if it's supported
if timeoutDialer, ok := dialer.(interface{ SetTimeout(time.Duration) }); ok {
timeoutDialer.SetTimeout(timeout)
}

return dialer.Dial("tcp", target)
}

// dialHTTPProxy establishes a connection through an HTTP CONNECT proxy.
func dialHTTPProxy(proxyAddr, proxyUser, proxyPass, targetAddr string, targetPort uint, timeout time.Duration) (net.Conn, error) {
// First connect to the proxy
conn, err := net.DialTimeout("tcp", proxyAddr, timeout)
if err != nil {
return nil, fmt.Errorf("failed to connect to HTTP proxy: %w", err)
}

target := net.JoinHostPort(targetAddr, fmt.Sprint(targetPort))

// Send CONNECT request
connectReq := fmt.Sprintf("CONNECT %s HTTP/1.1\r\n", target)
connectReq += fmt.Sprintf("Host: %s\r\n", target)

if proxyUser != "" {
// Basic auth for proxy
auth := proxyUser + ":" + proxyPass
encodedAuth := base64.StdEncoding.EncodeToString([]byte(auth))
connectReq += fmt.Sprintf("Proxy-Authorization: Basic %s\r\n", encodedAuth)
}

connectReq += "\r\n"

if _, err := conn.Write([]byte(connectReq)); err != nil {
conn.Close()
return nil, fmt.Errorf("failed to send CONNECT request: %w", err)
}

// Read response
buffer := make([]byte, 1024)
n, err := conn.Read(buffer)
if err != nil {
conn.Close()
return nil, fmt.Errorf("failed to read proxy response: %w", err)
}

response := string(buffer[:n])
if !strings.Contains(response, "200") {
conn.Close()
return nil, fmt.Errorf("proxy CONNECT failed: %s", response)
}

return conn, nil
}

// Run starts a new SSH session and runs the cmd, it returns CombinedOutput and err if any.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ require (
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.5
golang.org/x/crypto v0.6.0
golang.org/x/net v0.7.0
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand Down
43 changes: 43 additions & 0 deletions goph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func TestGoph(t *testing.T) {
t.Run("gophRunTest", gophRunTest)
t.Run("gophAuthTest", gophAuthTest)
t.Run("gophWrongPassTest", gophWrongPassTest)
t.Run("gophProxyConfigTest", gophProxyConfigTest)
}

func gophAuthTest(t *testing.T) {
Expand Down Expand Up @@ -198,3 +199,45 @@ func newServer(port string) {
}
}()
}

func gophProxyConfigTest(t *testing.T) {
// Test proxy config creation
proxyConfig := &goph.ProxyConfig{
Type: goph.ProxyTypeSOCKS5,
Addr: "127.0.0.1",
Port: 1080,
User: "proxyuser",
Password: "proxypass",
}

config := &goph.Config{
User: "testuser",
Addr: "127.0.0.1",
Port: 22,
Auth: goph.Password("testpass"),
Timeout: goph.DefaultTimeout,
Callback: ssh.InsecureIgnoreHostKey(),
Proxy: proxyConfig,
}

if config.Proxy.Type != goph.ProxyTypeSOCKS5 {
t.Errorf("expected proxy type SOCKS5, got %v", config.Proxy.Type)
}

if config.Proxy.Addr != "127.0.0.1" {
t.Errorf("expected proxy addr 127.0.0.1, got %s", config.Proxy.Addr)
}

if config.Proxy.Port != 1080 {
t.Errorf("expected proxy port 1080, got %d", config.Proxy.Port)
}

if config.Proxy.User != "proxyuser" {
t.Errorf("expected proxy user proxyuser, got %s", config.Proxy.User)
}

if config.Proxy.Password != "proxypass" {
t.Errorf("expected proxy password proxypass, got %s", config.Proxy.Password)
}
}