使用go的ssh包快速打造一個本地命令行ssh客戶端


熱身運動🏂

在開始之前,先來個熱身運動。雖然標題黨寫着快速打造一個ssh客戶端,但是和跑步一樣,在運動前還是需要先熱身一下,不然到時候身體(大腦)會吃不消。所以,在開始前,我們先來科普一下ssh的一些東西。

先來說說ssh,這里的ssh是指由IETF的網絡小組(Network Working Group)所制定的為建立在應用層和傳輸層基礎上的安全協議。(對於了解這個協議的請忽略本段文字😱)點這里了解更多ssh介紹

寫過java web應用的同學應該還知道另一個ssh(struts+spring+hibernate),當然今天的主角並不是它。😅

其實接觸過后端開發的同學對於ssh應該都不陌生,可能每天你都在使用它,沒錯,當你要遠程登錄服務器的時候,大多數情況下都離不開它,儼然已經成為Linux系統的標准配置。所以,如果你使用的是Linux操作系統,那么默認情況下就已經自帶ssh的客戶端了,於是乎你直接可以在Linux的shell中執行: ssh user@host 就可以安全的登錄到了遠程主機host。對於ssh的更多命令或者玩法今天就不多介紹了,因為這不是今天的主要目標,今天的主要任務是實現一個和Linux操作系統中默認自帶的ssh命令行客戶端一樣的使用go語言開發的ssh命令行客戶端,當然由於時間篇幅有限,這次並不會實現原生ssh命令行客戶端的全部功能,主要是能夠實現遠程登錄到遠程host,並能進行命令行操作。對於其他高級命令,如端口轉發等將在后續完成。

工欲善其事必先利其器🔪

既然說了要快速打造,那么必然需要借助一些現有的工具包了,這邊為了完成這個客戶端,筆者對原生的go語言的ssh包進行了一下封裝做了一個小工具包gosshtool,可以從github找到。有了它,再來做ssh的客戶端就輕松多了。

 

開始設計💻

首先,要完成一個命令行的ssh客戶端,我們先來看下Linux下自帶的ssh客戶端是怎么工作的。這里所說的怎么工作,會站在比較高層的角度,因為ssh的整個通訊協議比較復雜,這里不過多介紹,原因是go提供的ssh包已經把底層的一些協議實現了,這里沒必要自己再寫一套實現出來,如果你確實對底層協議有興趣,可以自己去網上查閱文檔。那么站在比較高層角度來看,是如何的呢? >我們還原一個最常見的場景:某一天,你想登錄遠程主機,於是你打開了Linux的shell, 輸入

  1. ssh user@host

然后輸入密碼后順利的登錄了host這個主機,接着你在shell輸入一些命令,比如

  1. ls

查看遠程主機當前目錄下所有文件。

上述場景的過程,我們可以簡單畫一個圖,來看看你這些操作是怎么與遠程主機通訊的,如下圖: 

 

根據上圖,我們開始設計,首先要想辦法讀取用戶的鍵盤輸入,如:輸入pwd 在go語言中,我們可以使用os和bufio兩個包,關鍵代碼如下:

  1. inputReader := bufio.NewReader(os.Stdin)
  2. input, err := inputReader.ReadString('\n')

如上代碼,我們就可以讀取以換行結束的字符串。

這樣完成了圖中的第一步,第二步,我們將要建立與遠程主機的ssh連接,這時候可以用到前面介紹的工具gosshtool了,有了它完成這一步變得輕松許多。在介紹這一步之前,我們先來對這個將要實現的客戶端再多啰嗦幾句,為了使我們的客戶端看起來更像Linux自帶的ssh客戶端,我們假設將要做的這個客戶端名字叫sshcmd,我們將要完成的任務是到時候生成一個叫sshcmd的可執行文件,然后執行

  1. ./sshcmd user@host

就建立了遠程ssh連接,並返回遠程主機登錄信息,接着你可以繼續在控制台輸入后續命令,這些命令實際上是在遠程主機執行的,就像Linux自帶的ssh客戶端一樣。所以,我們還要用到go的一個叫做flag的包,這個包在寫命令行程序的時候非常有用,它可以方便的對命令參數進行解析。所以我們會寫到如下關鍵代碼:

  1. func main() {
  2. flag.StringVar(&host, "h", "", "host")
  3. flag.StringVar(&passwd, "p", "", "password")
  4. flag.Parse()
  5. hostsp := strings.Split(host, "@")
  6. user = hostsp[0]
  7. host = hostsp[1]
  8. }

我們從命令行讀取了user,host,password三個重要參數。有了它們,可以就可以建立ssh連接了關鍵代碼如下:

  1. config := &gosshtool.SSHClientConfig{
  2. User: user,
  3. Password: passwd,
  4. Host: host,
  5. }
  6. sshclient := gosshtool.NewSSHClient(config)
  7. _, err := sshclient.Connect()
  8. if err == nil {
  9. fmt.Println("ssh connect success")
  10. } else {
  11. fmt.Println("ssh connect failed")
  12. }
  13. modes := ssh.TerminalModes{
  14. ssh.ECHO: 0,
  15. ssh.TTY_OP_ISPEED: 14400,
  16. ssh.TTY_OP_OSPEED: 14400,
  17. }
  18. pty := &gosshtool.PtyInfo{
  19. Term: "xterm-256color",
  20. H: 80,
  21. W: 40,
  22. Modes: modes,
  23. }
  24. session, err := sshclient.Pipe(conn, pty, nil, 30)
  25. if err != nil {
  26. fmt.Println(err)
  27. }
  28. defer session.Close()

 

我們使用了gosshtool的NewSSHClient方法創建了一個客戶端,並調用Connect()建立了連接,最后使用了Pipe(conn, pty, nil, 30)方法創建了一個保持會話,這樣就號好了。這一切看起來如此簡單,都要歸功於Pipe這個方法,它的第一個參數是一個ReadWriteCloser接口類型,只要實現了該接口的結構都可以傳入,這里我們會使用TCPConn這個結構,該結構實現了net.Conn接口,而net.Conn接口也是實現了ReadWriteCloser接口的。這個參數非常重要,我們建立了連接后,后續的通信全靠它了。你如果熟悉ReadWriteCloser接口,其實你就知道這個接口又組合了三個接口:

  1. type ReadWriteCloser interface {
  2. Reader
  3. Writer
  4. Closer
  5. }
  6.  
  7. type Writer interface {
  8. Write(p []byte) (n int, err error)
  9. }
  10.  
  11. type Reader interface {
  12. Read(p []byte) (n int, err error)
  13. }
  14.  
  15. type Closer interface {
  16. Close() error
  17. }

再看net.Conn接口:

我們使用了gosshtool的NewSSHClient方法創建了一個客戶端,並調用Connect()建立了連接,最后使用了Pipe(conn, pty, nil, 30)方法創建了一個保持會話,這樣就號好了。這一切看起來如此簡單,都要歸功於Pipe這個方法,它的第一個參數是一個ReadWriteCloser接口類型,只要實現了該接口的結構都可以傳入,這里我們會使用TCPConn這個結構,該結構實現了net.Conn接口,而net.Conn接口也是實現了ReadWriteCloser接口的。這個參數非常重要,我們建立了連接后,后續的通信全靠它了。你如果熟悉ReadWriteCloser接口,其實你就知道這個接口又組合了三個接口:

  1. type ReadWriteCloser interface {
  2. Reader
  3. Writer
  4. Closer
  5. }
  6.  
  7. type Writer interface {
  8. Write(p []byte) (n int, err error)
  9. }
  10.  
  11. type Reader interface {
  12. Read(p []byte) (n int, err error)
  13. }
  14.  
  15. type Closer interface {
  16. Close() error
  17. }

再看net.Conn接口:

 

  1. type Conn interface {
  2. // Read從連接中讀取數據
  3. // Read方法可能會在超過某個固定時間限制后超時返回錯誤,該錯誤的Timeout()方法返回真
  4. Read(b []byte) (n int, err error)
  5. // Write從連接中寫入數據
  6. // Write方法可能會在超過某個固定時間限制后超時返回錯誤,該錯誤的Timeout()方法返回真
  7. Write(b []byte) (n int, err error)
  8. // Close方法關閉該連接
  9. // 並會導致任何阻塞中的Read或Write方法不再阻塞並返回錯誤
  10. Close() error
  11. // 返回本地網絡地址
  12. LocalAddr() Addr
  13. // 返回遠端網絡地址
  14. RemoteAddr() Addr
  15. // 設定該連接的讀寫deadline,等價於同時調用SetReadDeadline和SetWriteDeadline
  16. // deadline是一個絕對時間,超過該時間后I/O操作就會直接因超時失敗返回而不會阻塞
  17. // deadline對之后的所有I/O操作都起效,而不僅僅是下一次的讀或寫操作
  18. // 參數t為零值表示不設置期限
  19. SetDeadline(t time.Time) error
  20. // 設定該連接的讀操作deadline,參數t為零值表示不設置期限
  21. SetReadDeadline(t time.Time) error
  22. // 設定該連接的寫操作deadline,參數t為零值表示不設置期限
  23. // 即使寫入超時,返回值n也可能>0,說明成功寫入了部分數據
  24. SetWriteDeadline(t time.Time) error
  25. }

對比下會發現Conn接口也實現了

  • Read(b []byte) (n int, err error)
  • Write(b []byte) (n int, err error)
  • Close() error

這也說明了,確實我們可以將net.Conn的參數傳入。通過接口方法,其實也可以看出這些接口都有一個共同作用,可以對字節進行讀寫操作。而我們要與遠程主機網絡通信,當然少不了這些。因此,所有實現以上三個方法的結構都是可以傳入並於遠程主機建立的ssh連接通信的。這里,我們的想法是:在本地起一個socket服務,並接受標准輸入,最終將標准輸入的數據通過Pipe轉發給遠程主機,實現本地終端輸入命令通過ssh協議遠程執行如下圖:

正如圖中所示,實際上Pipe方法可以理解為將tcp連接轉成了ssh連接並可以通過它傳遞數據。當然也可以將websocket的連接轉成ssh連接,這樣就可以實現基於web網頁的ssh客戶端了,也是非常簡單的,這個后續介紹。介紹到這里,大部分關鍵的點都已經說完了,這里只是簡單實現了一個最簡單版本的ssh命令行客戶端,當然通過gosshtool還可以做很多好玩的東西,比如部署工具,本地轉發服務,命令行運維工具等。最后,最最關鍵的,放上本次實踐的完整源碼: sshcmd源碼

總結

本文介紹了如何打造一個本地命令行ssh客戶端,如果基於現成的工具包確實沒多少工作量,而且大部分功能都實現比較粗糙,權當拋磚引玉。

文檔信息

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM