├── go.mod ├── README.md ├── go.sum └── main.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mattn/sshgo 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/mattn/go-colorable v0.1.8 7 | github.com/mattn/go-runewidth v0.0.13 8 | github.com/mattn/go-tty v0.0.3 9 | github.com/mitchellh/go-homedir v1.1.0 10 | golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e 11 | ) 12 | 13 | require ( 14 | github.com/mattn/go-isatty v0.0.12 // indirect 15 | github.com/rivo/uniseg v0.2.0 // indirect 16 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect 17 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sshgo 2 | 3 | implementation of ssh client 4 | 5 | ## Usage 6 | 7 | ``` 8 | Usage of sshgo: 9 | -P int 10 | port (default 22) 11 | -f string 12 | private key 13 | -p string 14 | password 15 | -u string 16 | user 17 | ``` 18 | 19 | ## Warning 20 | 21 | Be careful to use this. The command-line arguments with passing password/passphrase is dangerous. Anyone can steal your password. 22 | 23 | ## Installation 24 | 25 | ``` 26 | $ go get github.com/mattn/sshgo 27 | ``` 28 | 29 | ## License 30 | 31 | MIT 32 | 33 | ## Author 34 | 35 | Yasuhiro Matsumoto (a.k.a. mattn) 36 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 2 | github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= 3 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 4 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 5 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 6 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 7 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 8 | github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 9 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 10 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 11 | github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= 12 | github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= 13 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 14 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 15 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 16 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 17 | golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e h1:VvfwVmMH40bpMeizC9/K7ipM5Qjucuu16RWfneFPyhQ= 18 | golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 19 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 20 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 21 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 22 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 23 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 24 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 25 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 26 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 27 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= 28 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 29 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= 30 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 31 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 32 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 33 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "net" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "os/signal" 13 | "path/filepath" 14 | "runtime" 15 | "strconv" 16 | "strings" 17 | "time" 18 | 19 | "github.com/mattn/go-colorable" 20 | "github.com/mattn/go-runewidth" 21 | "github.com/mattn/go-tty" 22 | "github.com/mitchellh/go-homedir" 23 | 24 | "golang.org/x/crypto/ssh" 25 | "golang.org/x/crypto/ssh/agent" 26 | "golang.org/x/crypto/ssh/terminal" 27 | ) 28 | 29 | var ( 30 | user = flag.String("u", "", "user") 31 | password = flag.String("p", "", "password") 32 | askPassword = flag.Bool("w", false, "ask password") 33 | privateKey = flag.String("f", "", "private key") 34 | port = flag.Int("P", 22, "port") 35 | proxy = flag.String("x", "", "proxy server") 36 | timeout = flag.Duration("T", 0*time.Second, "timeout") 37 | openPTY = flag.Bool("o", false, "open pty") 38 | ) 39 | 40 | func pprompt(prompt string) (string, error) { 41 | t, err := tty.Open() 42 | if err != nil { 43 | return "", err 44 | } 45 | defer t.Close() 46 | fmt.Print(prompt) 47 | defer t.Output().WriteString("\r" + strings.Repeat(" ", runewidth.StringWidth(prompt)) + "\r") 48 | return t.ReadPasswordClear() 49 | } 50 | 51 | func getSigners(keyfile string, password string) ([]ssh.Signer, error) { 52 | buf, err := ioutil.ReadFile(keyfile) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | if password != "" { 58 | k, err := ssh.ParsePrivateKeyWithPassphrase(buf, []byte(password)) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return []ssh.Signer{k}, nil 63 | } 64 | 65 | k, err := ssh.ParsePrivateKey(buf) 66 | if err != nil { 67 | return nil, err 68 | } 69 | return []ssh.Signer{k}, nil 70 | } 71 | 72 | func run() int { 73 | flag.Parse() 74 | if flag.NArg() == 0 { 75 | flag.Usage() 76 | return 2 77 | } 78 | 79 | host := flag.Arg(0) 80 | if *user == "" { 81 | if strings.Contains(host, "@") { 82 | tok := strings.Split(host, "@") 83 | if len(tok) != 2 { 84 | fmt.Fprintln(os.Stderr, "invalid hostname") 85 | return 1 86 | } 87 | *user, host = tok[0], tok[1] 88 | if h, p, err := net.SplitHostPort(host); err == nil { 89 | host = h 90 | if pn, err := strconv.ParseUint(p, 10, 64); err == nil { 91 | *port = int(pn) 92 | } 93 | } 94 | } else { 95 | if runtime.GOOS == "windows" { 96 | *user = os.Getenv("USERNAME") 97 | } else { 98 | *user = os.Getenv("USER") 99 | } 100 | } 101 | } 102 | 103 | if flag.NArg() == 1 { 104 | *openPTY = true 105 | } 106 | 107 | var authMethods []ssh.AuthMethod 108 | 109 | if *askPassword || *password != "" { 110 | authMethods = append(authMethods, ssh.PasswordCallback(func() (string, error) { 111 | if *askPassword { 112 | return pprompt("password: ") 113 | } 114 | return *password, nil 115 | })) 116 | } 117 | 118 | if *privateKey == "" || *privateKey == "none" { 119 | sshsock := os.ExpandEnv("$SSH_AUTH_SOCK") 120 | if sshsock != "" { 121 | addr, _ := net.ResolveUnixAddr("unix", sshsock) 122 | agentConn, _ := net.DialUnix("unix", nil, addr) 123 | ag := agent.NewClient(agentConn) 124 | keys, err := ag.List() 125 | if err != nil { 126 | fmt.Fprintln(os.Stderr, err) 127 | return 1 128 | } 129 | if len(keys) > 0 { 130 | authMethods = append(authMethods, ssh.PublicKeysCallback(ag.Signers)) 131 | } 132 | } 133 | } 134 | 135 | if *privateKey == "" { 136 | home, err := homedir.Dir() 137 | if err != nil { 138 | fmt.Fprintln(os.Stderr, err) 139 | return 1 140 | } 141 | *privateKey = filepath.Join(home, ".ssh", "id_rsa") 142 | } 143 | if *privateKey == "none" { 144 | *privateKey = "" 145 | } 146 | 147 | if *privateKey != "" { 148 | _, err := os.Stat(*privateKey) 149 | if len(authMethods) == 0 && os.IsNotExist(err) { 150 | fmt.Fprintf(os.Stderr, "No such private key file: %s\n", *privateKey) 151 | return 1 152 | } 153 | authMethods = append(authMethods, ssh.PublicKeysCallback(func() ([]ssh.Signer, error) { 154 | if *askPassword { 155 | p, err := pprompt("passphrase: ") 156 | if err != nil { 157 | return nil, err 158 | } 159 | *password = p 160 | } 161 | return getSigners(*privateKey, *password) 162 | })) 163 | } 164 | 165 | config := &ssh.ClientConfig{ 166 | User: *user, 167 | Auth: authMethods, 168 | Timeout: *timeout, 169 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 170 | } 171 | 172 | hostport := fmt.Sprintf("%s:%d", host, *port) 173 | 174 | var conn *ssh.Client 175 | var err error 176 | 177 | if *proxy != "" { 178 | proxyUrl, err := url.Parse(*proxy) 179 | if err != nil { 180 | fmt.Fprintf(os.Stderr, "cannot open new session: %v\n", err) 181 | return 1 182 | } 183 | tcp, err := net.Dial("tcp", proxyUrl.Host) 184 | if err != nil { 185 | fmt.Fprintf(os.Stderr, "cannot open new session: %v\n", err) 186 | return 1 187 | } 188 | connReq := &http.Request{ 189 | Method: "CONNECT", 190 | URL: &url.URL{Path: hostport}, 191 | Host: hostport, 192 | Header: make(http.Header), 193 | } 194 | if proxyUrl.User != nil { 195 | if p, ok := proxyUrl.User.Password(); ok { 196 | connReq.SetBasicAuth(proxyUrl.User.Username(), p) 197 | } 198 | } 199 | connReq.Write(tcp) 200 | resp, err := http.ReadResponse(bufio.NewReader(tcp), connReq) 201 | if err != nil { 202 | fmt.Fprintf(os.Stderr, "cannot open new session: %v\n", err) 203 | return 1 204 | } 205 | defer resp.Body.Close() 206 | 207 | c, chans, reqs, err := ssh.NewClientConn(tcp, hostport, config) 208 | if err != nil { 209 | fmt.Fprintf(os.Stderr, "cannot open new session: %v\n", err) 210 | return 1 211 | } 212 | conn = ssh.NewClient(c, chans, reqs) 213 | } else { 214 | conn, err = ssh.Dial("tcp", hostport, config) 215 | } 216 | 217 | if err != nil { 218 | fmt.Fprintf(os.Stderr, "cannot connect %v: %v\n", hostport, err) 219 | return 1 220 | } 221 | defer conn.Close() 222 | 223 | session, err := conn.NewSession() 224 | if err != nil { 225 | fmt.Fprintf(os.Stderr, "cannot open new session: %v\n", err) 226 | return 1 227 | } 228 | defer session.Close() 229 | 230 | if *timeout > 0 { 231 | go func() { 232 | time.Sleep(*timeout) 233 | conn.Close() 234 | }() 235 | } 236 | 237 | if *openPTY { 238 | st, err := terminal.MakeRaw(int(os.Stdin.Fd())) 239 | if err != nil { 240 | fmt.Fprintf(os.Stderr, "cannot open new session: %v\n", err) 241 | return 1 242 | } 243 | defer terminal.Restore(int(os.Stdin.Fd()), st) 244 | 245 | session.Stdin = os.Stdin 246 | session.Stdout = colorable.NewColorableStdout() 247 | session.Stderr = colorable.NewColorableStderr() 248 | if *openPTY { 249 | err = session.RequestPty("vt100", 25, 80, ssh.TerminalModes{}) 250 | if err != nil { 251 | fmt.Fprint(os.Stderr, err) 252 | return 1 253 | } 254 | } 255 | c := make(chan os.Signal, 10) 256 | defer close(c) 257 | signal.Notify(c, os.Interrupt) 258 | go func() { 259 | for { 260 | if _, ok := <-c; !ok { 261 | break 262 | } 263 | session.Signal(ssh.SIGINT) 264 | } 265 | }() 266 | err = session.Shell() 267 | session.Wait() 268 | } else { 269 | session.Stdin = os.Stdin 270 | session.Stdout = os.Stdout 271 | session.Stderr = os.Stderr 272 | err = session.Run(strings.Join(flag.Args()[1:], " ")) 273 | } 274 | if err != nil { 275 | fmt.Fprint(os.Stderr, err) 276 | if ee, ok := err.(*ssh.ExitError); ok { 277 | return ee.ExitStatus() 278 | } 279 | return 1 280 | } 281 | return 0 282 | } 283 | 284 | func main() { 285 | os.Exit(run()) 286 | } 287 | --------------------------------------------------------------------------------