次の案件でGo言語を使いたいと思って勉強しているmuraveさん、サーバー間連携で使うかもしれないと”golang.org/x/crypto/ssh”を使用するサンプルを作ることにしました。
Golangでターミナル接続できるsshクライアントコマンドを作成する(制御キー対応) | 俺的備忘録 〜なんかいろいろ〜
上記の記事を参考にしてSSHクライアントを書いてみました。
コマンドラインの解析にはflagパッケージを使用し、公開鍵認証のみ対応です。
// sshクライアントサンプル(鍵ファイルでの接続のみ対応)
package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/user"
"path/filepath"
"strings"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/terminal"
)
func main() {
host, userName, absKeyPath := parseCommand()
auth := []ssh.AuthMethod{}
key, err := ioutil.ReadFile(absKeyPath)
fatalLogWhenErr(err, "private key")
signer, err := ssh.ParsePrivateKey(key)
fatalLogWhenErr(err, "signer")
auth = append(auth, ssh.PublicKeys(signer))
sshConfig := &ssh.ClientConfig{
User: userName,
Auth: auth,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
startSession(host, sshConfig)
}
// parseCommand コマンドラインをパースする
func parseCommand() (string, string, string) {
// ヘルプメッセージにホストの書き方を追加
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "%s hostname:22\n", os.Args[0])
flag.PrintDefaults()
}
flagUsername := flag.String("user", "", "user name (default:current user)")
flagKey := flag.String("key", "~/.ssh/id_rsa", "private key")
flag.Parse()
host := flag.Arg(0)
if host == "" {
flag.Usage()
os.Exit(1)
}
fmt.Println(*flagUsername, flag.Arg(0))
usr, err := user.Current()
fatalLogWhenErr(err, "")
userName := usr.Username
if *flagUsername != "" {
userName = *flagUsername
}
keyPath := *flagKey
var absKeyPath string
if keyPath[0:1] == "~" {
absKeyPath = strings.Replace(keyPath, "~", usr.HomeDir, 1)
} else {
var err error
absKeyPath, err = filepath.Abs(keyPath)
if err != nil {
log.Fatal(err)
}
}
return host, userName, absKeyPath
}
// startSession sshセッションを開始
func startSession(host string, sshConfig *ssh.ClientConfig) {
client, err := ssh.Dial("tcp", host, sshConfig)
fatalLogWhenErr(err, "dial")
session, err := client.NewSession()
fatalLogWhenErr(err, "new session")
defer session.Close()
// キー入力を接続先が認識できる形式に変換
fd := int(os.Stdin.Fd())
state, err := terminal.MakeRaw(fd)
fatalLogWhenErr(err, "tarminal make raw")
defer terminal.Restore(fd, state)
w, h, err := terminal.GetSize(fd)
fatalLogWhenErr(err, "tarminal get size")
termmodes := ssh.TerminalModes{
ssh.ECHO: 1,
ssh.TTY_OP_ISPEED: 14400,
ssh.TTY_OP_OSPEED: 14400,
}
term := os.Getenv("TERM")
err = session.RequestPty(term, h, w, termmodes)
fatalLogWhenErr(err, "session request pty")
session.Stdout = os.Stdout
session.Stderr = os.Stderr
session.Stdin = os.Stdin
err = session.Shell()
fatalLogWhenErr(err, "session shell")
err = session.Wait()
fatalLogWhenErr(err, "session wait")
}
// fatalLogWhenErr errをチェックしてerrorの場合にはfatal logを出力
func fatalLogWhenErr(err error, msg string) {
if err != nil {
if len(msg) > 0 {
log.Fatalf("%s error: %v", msg, err)
} else {
log.Fatal(err)
}
}
}
以下のような感じで普通に使えました(./sshがビルドしたバイナリでhostname等は隠しています)
$ ./ssh
./ssh hostname:22
-key string
private key (default "~/.ssh/id_rsa")
-user string
user name (default:current user)
$ ./ssh -user root ***.***.***.***:22
Last login: Thu Jan 9 15:45:57 2020 from ***-**-**-***.***.***.**
[root ~]# pwd
/root
[root ~]# exit
logout