pax_global_header00006660000000000000000000000064145034160560014516gustar00rootroot0000000000000052 comment=d955155146ed06df49062105dfc6277b1eec7a81 goph-1.4.0/000077500000000000000000000000001450341605600124555ustar00rootroot00000000000000goph-1.4.0/.github/000077500000000000000000000000001450341605600140155ustar00rootroot00000000000000goph-1.4.0/.github/goph.png000066400000000000000000000044331450341605600154640ustar00rootroot00000000000000PNG  IHDRk?sRGB, pHYs  LPLTE(0)tRNS #$ !  p `֭gd.@}3+xjQ61oKM=JY-2ka,hiVcq4v~0St lNFBZL97UT|)mH>?ڸ[uD%]w݇&Gr(bEǍɛONIDATx_Uc!Xe Ŗ0ݜVL,p",I(!|Tȣ D(_ ehF/RmjkXs<;̧q9{;B0 0 0 0 0 0 0 q,JHL"HVLI'-ID\T;ETJl4F3fhHIYKfK$9TE{-_tS#T"]5ăhyh >1-"w]\ΪDH="ϨIPZ0D6Q\XyyxB BtOFϔV BVl"v,̭WhGun*EzJ'DHyKE8 <0! 'gbm "g,ǐ:}T_i3u0N"7Rq|D,66Y 4~;85ڝiQj"ŗʛ C5-et\L10b˭m%v`*_ vhUtN*Сx(Uu豺L#\@_{_Pu@q'- LޓY&.y+e:v+!ށѓ\_E[eKT?wR|)]{at)8 N38+DCjϫEyuD\mfWp ;QHGDpUdEh3"-Cj,XK0cBrQuBX!MV Â&UB'L DhrhuD>sd4[Q5y&As".KD$K#f`xfق9hMbNħNKD&Ga^S5y =7L֟ha.!˔Ϝֈz".U0#"*D1xsU0a0!b aͯz"c^ԢE6!b倰J8:j20.WiQcI:" ~A2hTD7*7htND@V^}z=D;[t#)(.h.tg#. BWI鎾  D;T&C$R"~*-bWJ%-SV ,)(䞥${-0D.Ea1ר2!ෑ|5=+{ygV"mx_qEHmD8!=<0m±7T:m&GNvU}§ⱓQ8i;˴;3C T[z=vw@DqHNک^OU<bUBlYV?DHsWпp+k,bKXF}1zjSa jGM Oza4aaaaaaalͿ $.aIENDB`goph-1.4.0/LICENSE000066400000000000000000000020711450341605600134620ustar00rootroot00000000000000MIT License Copyright (c) 2020-present Mohamed El Bahja Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. goph-1.4.0/README.md000066400000000000000000000106051450341605600137360ustar00rootroot00000000000000

Golang SSH Client.

Fast and easy golang ssh client module.

Goph is a lightweight Go SSH client focusing on simplicity!

InstallationFeaturesUsageExamplesLicense

## 🚀  Installation and Documentation ```bash go get github.com/melbahja/goph ``` You can find the docs at [go docs](https://pkg.go.dev/github.com/melbahja/goph). ## 🤘  Features - Easy to use and **simple API**. - Supports **known hosts** by default. - Supports connections with **passwords**. - Supports connections with **private keys**. - Supports connections with **protected private keys** with passphrase. - Supports **upload** files from local to remote. - Supports **download** files from remote to local. - Supports connections with **ssh agent** (Unix systems only). - Supports adding new hosts to **known_hosts file**. - Supports **file system operations** like: `Open, Create, Chmod...` - Supports **context.Context** for command cancellation. ## 📄  Usage Run a command via ssh: ```go package main import ( "log" "fmt" "github.com/melbahja/goph" ) func main() { // Start new ssh connection with private key. auth, err := goph.Key("/home/mohamed/.ssh/id_rsa", "") if err != nil { log.Fatal(err) } client, err := goph.New("root", "192.1.1.3", auth) if err != nil { log.Fatal(err) } // Defer closing the network connection. defer client.Close() // Execute your command. out, err := client.Run("ls /tmp/") if err != nil { log.Fatal(err) } // Get your output as []byte. fmt.Println(string(out)) } ``` #### 🔐 Start Connection With Protected Private Key: ```go auth, err := goph.Key("/home/mohamed/.ssh/id_rsa", "you_passphrase_here") if err != nil { // handle error } client, err := goph.New("root", "192.1.1.3", auth) ``` #### 🔑 Start Connection With Password: ```go client, err := goph.New("root", "192.1.1.3", goph.Password("you_password_here")) ``` #### ☛ Start Connection With SSH Agent (Unix systems only): ```go auth, err := goph.UseAgent() if err != nil { // handle error } 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") ``` #### ⤵️ Download Remote File to Local: ```go err := client.Download("/path/to/remote/file", "/path/to/local/file") ``` #### ☛ Execute Bash Commands: ```go out, err := client.Run("bash -c 'printenv'") ``` #### ☛ Execute Bash Command with timeout: ```go context, cancel := context.WithTimeout(ctx, time.Second) defer cancel() // will send SIGINT and return error after 1 second out, err := client.RunContext(ctx, "sleep 5") ``` #### ☛ Execute Bash Command With Env Variables: ```go out, err := client.Run(`env MYVAR="MY VALUE" bash -c 'echo $MYVAR;'`) ``` #### 🥪 Using Goph Cmd: `Goph.Cmd` struct is like the Go standard `os/exec.Cmd`. ```go // Get new `Goph.Cmd` cmd, err := client.Command("ls", "-alh", "/tmp") // or with context: // cmd, err := client.CommandContext(ctx, "ls", "-alh", "/tmp") if err != nil { // handle the error! } // You can set env vars, but the server must be configured to `AcceptEnv line`. cmd.Env = []string{"MY_VAR=MYVALUE"} // Run you command. err = cmd.Run() ``` 🗒️ Just like `os/exec.Cmd` you can run `CombinedOutput, Output, Start, Wait`, and [`ssh.Session`](https://pkg.go.dev/golang.org/x/crypto/ssh#Session) methods like `Signal`... #### 📂 File System Operations Via SFTP: You can easily get a [SFTP](https://github.com/pkg/sftp) client from Goph client: ```go sftp, err := client.NewSftp() if err != nil { // handle the error! } file, err := sftp.Create("/tmp/remote_file") file.Write([]byte(`Hello world`)) file.Close() ``` 🗒️ For more file operations see [SFTP Docs](https://github.com/pkg/sftp). ## 🥙  Examples See [Examples](https://github.com/melbahja/ssh/blob/master/examples). ## 🤝  Missing a Feature? Feel free to open a new issue, or contact me. ## 📘  License Goph is provided under the [MIT License](https://github.com/melbahja/goph/blob/master/LICENSE). goph-1.4.0/auth.go000066400000000000000000000052461450341605600137540ustar00rootroot00000000000000// Copyright 2020 Mohammed El Bahja. All rights reserved. // Use of this source code is governed by a MIT license. package goph import ( "fmt" "io/ioutil" "net" "os" "strings" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" ) // Auth represents ssh auth methods. type Auth []ssh.AuthMethod // Password returns password auth method. func Password(pass string) Auth { return Auth{ ssh.Password(pass), } } // KeyboardInteractive returns password keyboard interactive auth method as fallback of password auth method. func KeyboardInteractive(pass string) Auth { return Auth{ ssh.Password(pass), ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) (answers []string, err error) { for _, q := range questions { if strings.Contains(strings.ToLower(q), "password") { answers = append(answers, pass) } else { answers = append(answers, "") } } return answers, nil }), } } // Key returns auth method from private key with or without passphrase. func Key(prvFile string, passphrase string) (Auth, error) { signer, err := GetSigner(prvFile, passphrase) if err != nil { return nil, err } return Auth{ ssh.PublicKeys(signer), }, nil } func RawKey(privateKey string, passphrase string) (Auth, error) { signer, err := GetSignerForRawKey([]byte(privateKey), passphrase) if err != nil { return nil, err } return Auth{ ssh.PublicKeys(signer), }, nil } // HasAgent checks if ssh agent exists. func HasAgent() bool { return os.Getenv("SSH_AUTH_SOCK") != "" } // UseAgent auth via ssh agent, (Unix systems only) func UseAgent() (Auth, error) { sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")) if err != nil { return nil, fmt.Errorf("could not find ssh agent: %w", err) } return Auth{ ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers), }, nil } // GetSigner returns ssh signer from private key file. func GetSigner(prvFile string, passphrase string) (ssh.Signer, error) { var ( err error signer ssh.Signer ) privateKey, err := ioutil.ReadFile(prvFile) if err != nil { return nil, err } else if passphrase != "" { signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(passphrase)) } else { signer, err = ssh.ParsePrivateKey(privateKey) } return signer, err } // GetSignerForRawKey returns ssh signer from private key file. func GetSignerForRawKey(privateKey []byte, passphrase string) (ssh.Signer, error) { var ( err error signer ssh.Signer ) if passphrase != "" { signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(passphrase)) } else { signer, err = ssh.ParsePrivateKey(privateKey) } return signer, err } goph-1.4.0/client.go000066400000000000000000000105411450341605600142630ustar00rootroot00000000000000// Copyright 2020 Mohammed El Bahja. All rights reserved. // Use of this source code is governed by a MIT license. package goph import ( "context" "fmt" "io" "net" "os" "time" "github.com/pkg/sftp" "golang.org/x/crypto/ssh" ) // Client represents Goph client. type Client struct { *ssh.Client Config *Config } // Config for Client. type Config struct { Auth Auth User string Addr string Port uint Timeout time.Duration Callback ssh.HostKeyCallback BannerCallback ssh.BannerCallback } // DefaultTimeout is the timeout of ssh client connection. var DefaultTimeout = 20 * time.Second // New starts a new ssh connection, the host public key must be in known hosts. func New(user string, addr string, auth Auth) (c *Client, err error) { callback, err := DefaultKnownHosts() if err != nil { return } c, err = NewConn(&Config{ User: user, Addr: addr, Port: 22, Auth: auth, Timeout: DefaultTimeout, Callback: callback, }) return } // NewUnknown starts a ssh connection get client without cheking knownhosts. // PLEASE AVOID USING THIS, UNLESS YOU KNOW WHAT ARE YOU DOING! // if there a "man in the middle proxy", this can harm you! // You can add the key to know hosts and use New() func instead! func NewUnknown(user string, addr string, auth Auth) (*Client, error) { return NewConn(&Config{ User: user, Addr: addr, Port: 22, Auth: auth, Timeout: DefaultTimeout, Callback: ssh.InsecureIgnoreHostKey(), }) } // NewConn returns new client and error if any. func NewConn(config *Config) (c *Client, err error) { c = &Client{ Config: config, } c.Client, err = Dial("tcp", config) return } // 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{ User: c.User, Auth: c.Auth, Timeout: c.Timeout, HostKeyCallback: c.Callback, BannerCallback: c.BannerCallback, }) } // Run starts a new SSH session and runs the cmd, it returns CombinedOutput and err if any. func (c Client) Run(cmd string) ([]byte, error) { var ( err error sess *ssh.Session ) if sess, err = c.NewSession(); err != nil { return nil, err } defer sess.Close() return sess.CombinedOutput(cmd) } // Run starts a new SSH session with context and runs the cmd. It returns CombinedOutput and err if any. func (c Client) RunContext(ctx context.Context, name string) ([]byte, error) { cmd, err := c.CommandContext(ctx, name) if err != nil { return nil, err } return cmd.CombinedOutput() } // Command returns new Cmd and error if any. func (c Client) Command(name string, args ...string) (*Cmd, error) { var ( sess *ssh.Session err error ) if sess, err = c.NewSession(); err != nil { return nil, err } return &Cmd{ Path: name, Args: args, Session: sess, Context: context.Background(), }, nil } // Command returns new Cmd with context and error, if any. func (c Client) CommandContext(ctx context.Context, name string, args ...string) (*Cmd, error) { cmd, err := c.Command(name, args...) if err != nil { return cmd, err } cmd.Context = ctx return cmd, nil } // NewSftp returns new sftp client and error if any. func (c Client) NewSftp(opts ...sftp.ClientOption) (*sftp.Client, error) { return sftp.NewClient(c.Client, opts...) } // Close client net connection. func (c Client) Close() error { return c.Client.Close() } // Upload a local file to remote server! func (c Client) Upload(localPath string, remotePath string) (err error) { local, err := os.Open(localPath) if err != nil { return } defer local.Close() ftp, err := c.NewSftp() if err != nil { return } defer ftp.Close() remote, err := ftp.Create(remotePath) if err != nil { return } defer remote.Close() _, err = io.Copy(remote, local) return } // Download file from remote server! func (c Client) Download(remotePath string, localPath string) (err error) { local, err := os.Create(localPath) if err != nil { return } defer local.Close() ftp, err := c.NewSftp() if err != nil { return } defer ftp.Close() remote, err := ftp.Open(remotePath) if err != nil { return } defer remote.Close() if _, err = io.Copy(local, remote); err != nil { return } return local.Sync() } goph-1.4.0/cmd.go000066400000000000000000000050101450341605600135430ustar00rootroot00000000000000// Copyright 2020 Mohammed El Bahja. All rights reserved. // Use of this source code is governed by a MIT license. package goph import ( "context" "fmt" "github.com/pkg/errors" "golang.org/x/crypto/ssh" "strings" ) // Cmd it's like os/exec.Cmd but for ssh session. type Cmd struct { // Path to command executable filename Path string // Command args. Args []string // Session env vars. Env []string // SSH session. *ssh.Session // Context for cancellation Context context.Context } // CombinedOutput runs cmd on the remote host and returns its combined stdout and stderr. func (c *Cmd) CombinedOutput() ([]byte, error) { if err := c.init(); err != nil { return nil, errors.Wrap(err, "cmd init") } return c.runWithContext(func() ([]byte, error) { return c.Session.CombinedOutput(c.String()) }) } // Output runs cmd on the remote host and returns its stdout. func (c *Cmd) Output() ([]byte, error) { if err := c.init(); err != nil { return nil, errors.Wrap(err, "cmd init") } return c.runWithContext(func() ([]byte, error) { return c.Session.Output(c.String()) }) } // Run runs cmd on the remote host. func (c *Cmd) Run() error { if err := c.init(); err != nil { return errors.Wrap(err, "cmd init") } _, err := c.runWithContext(func() ([]byte, error) { return nil, c.Session.Run(c.String()) }) return err } // Start runs the command on the remote host. func (c *Cmd) Start() error { if err := c.init(); err != nil { return errors.Wrap(err, "cmd init") } return c.Session.Start(c.String()) } // String return the command line string. func (c *Cmd) String() string { return fmt.Sprintf("%s %s", c.Path, strings.Join(c.Args, " ")) } // Init inits and sets session env vars. func (c *Cmd) init() (err error) { // Set session env vars var env []string for _, value := range c.Env { env = strings.Split(value, "=") if err = c.Setenv(env[0], strings.Join(env[1:], "=")); err != nil { return } } return nil } // Command with context output. type ctxCmdOutput struct { output []byte err error } // Executes the given callback within session. Sends SIGINT when the context is canceled. func (c *Cmd) runWithContext(callback func() ([]byte, error)) ([]byte, error) { outputChan := make(chan ctxCmdOutput) go func() { output, err := callback() outputChan <- ctxCmdOutput{ output: output, err: err, } }() select { case <-c.Context.Done(): _ = c.Session.Signal(ssh.SIGINT) return nil, c.Context.Err() case result := <-outputChan: return result.output, result.err } } goph-1.4.0/examples/000077500000000000000000000000001450341605600142735ustar00rootroot00000000000000goph-1.4.0/examples/goph/000077500000000000000000000000001450341605600152305ustar00rootroot00000000000000goph-1.4.0/examples/goph/main.go000066400000000000000000000145421450341605600165110ustar00rootroot00000000000000package main import ( "bufio" "context" "errors" "flag" "fmt" "log" "net" "os" osuser "os/user" "path/filepath" "strings" "time" "github.com/melbahja/goph" "github.com/pkg/sftp" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/terminal" ) // // Run command and auth via password: // > go run main.go --ip 192.168.122.102 --pass --cmd ls // // Run command and auth via private key: // > go run main.go --ip 192.168.122.102 --cmd ls // Or: // > go run main.go --ip 192.168.122.102 --key /path/to/private_key --cmd ls // // Run command and auth with private key and passphrase: // > go run main.go --ip 192.168.122.102 --passphrase --cmd ls // // Run a command and interrupt it after 1 second: // > go run main.go --ip 192.168.122.102 --cmd "sleep 10" --timeout=1s // // You can test with the interactive mode without passing --cmd flag. // var ( err error auth goph.Auth client *goph.Client addr string user string port uint key string cmd string pass bool passphrase bool timeout time.Duration agent bool sftpc *sftp.Client ) func init() { usr, err := osuser.Current() if err != nil { fmt.Println("couldn't determine current user. defaulting to 'root'") usr.Username = "root" } flag.StringVar(&addr, "ip", "127.0.0.1", "machine ip address.") flag.StringVar(&user, "user", usr.Username, "ssh user.") flag.UintVar(&port, "port", 22, "ssh port number.") flag.StringVar(&key, "key", filepath.Join(os.Getenv("HOME"), ".ssh", "id_rsa"), "private key path.") flag.StringVar(&cmd, "cmd", "", "command to run.") flag.BoolVar(&pass, "pass", false, "ask for ssh password instead of private key.") flag.BoolVar(&agent, "agent", false, "use ssh agent for authentication (unix systems only).") flag.BoolVar(&passphrase, "passphrase", false, "ask for private key passphrase.") flag.DurationVar(&timeout, "timeout", 0, "interrupt a command with SIGINT after a given timeout (0 means no timeout)") } func VerifyHost(host string, remote net.Addr, key ssh.PublicKey) error { // // If you want to connect to new hosts. // here your should check new connections public keys // if the key not trusted you shuld return an error // // hostFound: is host in known hosts file. // err: error if key not in known hosts file OR host in known hosts file but key changed! hostFound, err := goph.CheckKnownHost(host, remote, key, "") // Host in known hosts but key mismatch! // Maybe because of MAN IN THE MIDDLE ATTACK! if hostFound && err != nil { return err } // handshake because public key already exists. if hostFound && err == nil { return nil } // Ask user to check if he trust the host public key. if askIsHostTrusted(host, key) == false { // Make sure to return error on non trusted keys. return errors.New("you typed no, aborted!") } // Add the new host to known hosts file. return goph.AddKnownHost(host, remote, key, "") } func main() { flag.Parse() var err error if agent || goph.HasAgent() { auth, err = goph.UseAgent() } else if pass { auth = goph.Password(askPass("Enter SSH Password: ")) } else { auth, err = goph.Key(key, getPassphrase(passphrase)) } if err != nil { panic(err) } client, err = goph.NewConn(&goph.Config{ User: user, Addr: addr, Port: port, Auth: auth, Callback: VerifyHost, }) if err != nil { panic(err) } // Close client net connection defer client.Close() // If the cmd flag exists if cmd != "" { ctx := context.Background() // create a context with timeout, if supplied in the argumetns if timeout > 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, timeout) defer cancel() } out, err := client.RunContext(ctx, cmd) fmt.Println(string(out), err) return } // else open interactive mode. playWithSSHJustForTestingThisProgram(client) } func askPass(msg string) string { fmt.Print(msg) pass, err := terminal.ReadPassword(0) if err != nil { panic(err) } fmt.Println("") return strings.TrimSpace(string(pass)) } func getPassphrase(ask bool) string { if ask { return askPass("Enter Private Key Passphrase: ") } return "" } func askIsHostTrusted(host string, key ssh.PublicKey) bool { reader := bufio.NewReader(os.Stdin) fmt.Printf("Unknown Host: %s \nFingerprint: %s \n", host, ssh.FingerprintSHA256(key)) fmt.Print("Would you like to add it? type yes or no: ") a, err := reader.ReadString('\n') if err != nil { log.Fatal(err) } return strings.ToLower(strings.TrimSpace(a)) == "yes" } func getSftp(client *goph.Client) *sftp.Client { var err error if sftpc == nil { sftpc, err = client.NewSftp() if err != nil { panic(err) } } return sftpc } func playWithSSHJustForTestingThisProgram(client *goph.Client) { fmt.Println("Welcome To Goph :D") fmt.Printf("Connected to %s\n", client.Config.Addr) fmt.Println("Type your shell command and enter.") fmt.Println("To download file from remote type: download remote/path local/path") fmt.Println("To upload file to remote type: upload local/path remote/path") fmt.Println("To create a remote dir type: mkdirall /path/to/remote/newdir") fmt.Println("To exit type: exit") scanner := bufio.NewScanner(os.Stdin) fmt.Print("> ") var ( out []byte err error cmd string parts []string ) loop: for scanner.Scan() { err = nil cmd = scanner.Text() parts = strings.Split(cmd, " ") if len(parts) < 1 { continue } switch parts[0] { case "exit": fmt.Println("goph bye!") break loop case "download": if len(parts) != 3 { fmt.Println("please type valid download command!") continue loop } err = client.Download(parts[1], parts[2]) fmt.Println("download err: ", err) break case "upload": if len(parts) != 3 { fmt.Println("please type valid upload command!") continue loop } err = client.Upload(parts[1], parts[2]) fmt.Println("upload err: ", err) break case "mkdirall": if len(parts) != 2 { fmt.Println("please type valid mkdirall command!") continue loop } ftp := getSftp(client) err = ftp.MkdirAll(parts[1]) fmt.Printf("mkdirall err(%v) you can check via: stat %s\n", err, parts[1]) default: command, err := client.Command(parts[0], parts[1:]...) if err != nil { panic(err) } out, err = command.CombinedOutput() fmt.Println(string(out), err) } fmt.Print("> ") } } goph-1.4.0/go.mod000066400000000000000000000002151450341605600135610ustar00rootroot00000000000000module github.com/melbahja/goph go 1.13 require ( github.com/pkg/errors v0.9.1 github.com/pkg/sftp v1.13.5 golang.org/x/crypto v0.6.0 ) goph-1.4.0/go.sum000066400000000000000000000115721450341605600136160ustar00rootroot00000000000000github.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/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go= github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg= 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= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 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/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= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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= goph-1.4.0/goph_test.go000066400000000000000000000134621450341605600150060ustar00rootroot00000000000000package goph_test import ( "fmt" "log" "net" "testing" "github.com/melbahja/goph" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/terminal" ) var privateBytes = []byte(` # random generated pk -----BEGIN OPENSSH PRIVATE KEY----- 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 -----END OPENSSH PRIVATE KEY-----`) func TestGoph(t *testing.T) { t.Run("gophRunTest", gophRunTest) t.Run("gophAuthTest", gophAuthTest) t.Run("gophWrongPassTest", gophWrongPassTest) } func gophAuthTest(t *testing.T) { newServer("2020") _, err := goph.NewConn(&goph.Config{ Addr: "127.0.10.10", Port: 2020, User: "melbahja", Auth: goph.Password("123456"), Callback: ssh.InsecureIgnoreHostKey(), }) if err != nil { t.Error(err) } } func gophRunTest(t *testing.T) { newServer("2021") client, err := goph.NewConn(&goph.Config{ Addr: "127.0.10.10", Port: 2021, User: "melbahja", Auth: goph.Password("123456"), Callback: ssh.InsecureIgnoreHostKey(), }) if err != nil { t.Errorf("connect error: %s", err) } _, err = client.Run("ls") if err != nil { t.Errorf("run error: %s", err) } } func gophWrongPassTest(t *testing.T) { newServer("2022") _, err := goph.NewConn(&goph.Config{ Addr: "127.0.10.10", Port: 2022, User: "melbahja", Auth: goph.Password("12345"), Callback: ssh.InsecureIgnoreHostKey(), }) if err == nil { t.Error("it should return an error") } } func newServer(port string) { config := &ssh.ServerConfig{ // Remove to disable password auth. PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { // a production setting. if c.User() == "melbahja" && string(pass) == "123456" { return nil, nil } return nil, fmt.Errorf("password rejected for %q", c.User()) }, } private, err := ssh.ParsePrivateKey(privateBytes) if err != nil { log.Fatal("Failed to parse private key: ", err) } config.AddHostKey(private) // Once a ServerConfig has been configured, connections can be // accepted. listener, err := net.Listen("tcp", "127.0.10.10:"+port) if err != nil { log.Fatal("failed to listen for connection: ", err) } go func() { nConn, err := listener.Accept() if err != nil { log.Fatal("failed to accept incoming connection: ", err) } // Before use, a handshake must be performed on the incoming // net.Conn. _, chans, reqs, err := ssh.NewServerConn(nConn, config) if err != nil { log.Fatal("failed to handshake: ", err) } // The incoming Request channel must be serviced. go ssh.DiscardRequests(reqs) // Service the incoming Channel channel. for newChannel := range chans { if newChannel.ChannelType() != "session" { newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") continue } channel, requests, err := newChannel.Accept() if err != nil { log.Fatalf("Could not accept channel: %v", err) } go func(in <-chan *ssh.Request) { for req := range in { switch req.Type { case "exec": // just return error 0 without exec. channel.SendRequest("exit-status", false, []byte{0, 0, 0, 0}) } req.Reply(req.Type == "exec", nil) } }(requests) term := terminal.NewTerminal(channel, "> ") go func() { defer channel.Close() for { line, err := term.ReadLine() if err != nil { break } fmt.Println(line) } }() } }() } goph-1.4.0/hosts.go000066400000000000000000000056011450341605600141460ustar00rootroot00000000000000// Copyright 2020 Mohammed El Bahja. All rights reserved. // Use of this source code is governed by a MIT license. package goph import ( "errors" "fmt" "net" "os" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/knownhosts" ) // DefaultKnownHosts returns host key callback from default known hosts path, and error if any. func DefaultKnownHosts() (ssh.HostKeyCallback, error) { path, err := DefaultKnownHostsPath() if err != nil { return nil, err } return KnownHosts(path) } // KnownHosts returns host key callback from a custom known hosts path. func KnownHosts(file string) (ssh.HostKeyCallback, error) { return knownhosts.New(file) } // CheckKnownHost checks is host in known hosts file. // it returns is the host found in known_hosts file and error, if the host found in // known_hosts file and error not nil that means public key mismatch, maybe MAN IN THE MIDDLE ATTACK! you should not handshake. func CheckKnownHost(host string, remote net.Addr, key ssh.PublicKey, knownFile string) (found bool, err error) { var keyErr *knownhosts.KeyError // Fallback to default known_hosts file if knownFile == "" { path, err := DefaultKnownHostsPath() if err != nil { return false, err } knownFile = path } // Get host key callback callback, err := KnownHosts(knownFile) if err != nil { return false, err } // check if host already exists. err = callback(host, remote, key) // Known host already exists. if err == nil { return true, nil } // Make sure that the error returned from the callback is host not in file error. // If keyErr.Want is greater than 0 length, that means host is in file with different key. if errors.As(err, &keyErr) && len(keyErr.Want) > 0 { return true, keyErr } // Some other error occurred and safest way to handle is to pass it back to user. if err != nil { return false, err } // Key is not trusted because it is not in the file. return false, nil } // AddKnownHost add a a host to known hosts file. func AddKnownHost(host string, remote net.Addr, key ssh.PublicKey, knownFile string) (err error) { // Fallback to default known_hosts file if knownFile == "" { path, err := DefaultKnownHostsPath() if err != nil { return err } knownFile = path } f, err := os.OpenFile(knownFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) if err != nil { return err } defer f.Close() remoteNormalized := knownhosts.Normalize(remote.String()) hostNormalized := knownhosts.Normalize(host) addresses := []string{remoteNormalized} if hostNormalized != remoteNormalized { addresses = append(addresses, hostNormalized) } _, err = f.WriteString(knownhosts.Line(addresses, key) + "\n") return err } // DefaultKnownHostsPath returns default user knows hosts file. func DefaultKnownHostsPath() (string, error) { home, err := os.UserHomeDir() if err != nil { return "", err } return fmt.Sprintf("%s/.ssh/known_hosts", home), err }