ssh_server.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package server
  15. import (
  16. "encoding/base64"
  17. "fmt"
  18. "io"
  19. "net"
  20. "net/http"
  21. "strconv"
  22. "time"
  23. "github.com/gorilla/websocket"
  24. "github.com/pkg/errors"
  25. "github.com/pkg/sftp"
  26. "golang.org/x/crypto/ssh"
  27. "yunion.io/x/jsonutils"
  28. "yunion.io/x/log"
  29. "yunion.io/x/onecloud/pkg/webconsole/options"
  30. "yunion.io/x/onecloud/pkg/webconsole/session"
  31. )
  32. type WebsocketServer struct {
  33. Session *session.SSession
  34. Host string
  35. Port int
  36. Username string
  37. Password string
  38. PrivateKey string
  39. session *ssh.Session
  40. StdinPipe io.WriteCloser
  41. StdoutPipe io.Reader
  42. StderrPipe io.Reader
  43. ws *websocket.Conn
  44. conn *ssh.Client
  45. sshNetConn net.Conn
  46. sftp *sftp.Client
  47. timer *time.Timer
  48. }
  49. func NewSshServer(s *session.SSession) (*WebsocketServer, error) {
  50. info := s.ISessionData.(*session.SSshSession)
  51. server := &WebsocketServer{
  52. Session: s,
  53. Host: info.Host,
  54. Port: info.Port,
  55. Username: info.Username,
  56. Password: info.Password,
  57. PrivateKey: info.PrivateKey,
  58. }
  59. return server, nil
  60. }
  61. func writeToWebsocket(reader io.Reader, s *WebsocketServer) error {
  62. var data = make([]byte, 1024)
  63. for {
  64. n, err := reader.Read(data)
  65. if err != nil {
  66. return errors.Wrap(err, "read data from reader")
  67. }
  68. out := data[:n]
  69. go s.Session.GetRecorder().Write("", string(out))
  70. if err := s.ws.WriteMessage(websocket.BinaryMessage, out); err != nil {
  71. return errors.Wrapf(err, "write data to websocket, out: %s", string(out))
  72. }
  73. }
  74. }
  75. func (s *WebsocketServer) initWs(w http.ResponseWriter, r *http.Request) error {
  76. username := s.Username
  77. privateKey := s.PrivateKey
  78. password := s.Password
  79. config := &ssh.ClientConfig{
  80. Timeout: 5 * time.Second,
  81. User: username,
  82. HostKeyCallback: ssh.InsecureIgnoreHostKey(),
  83. Auth: []ssh.AuthMethod{
  84. ssh.Password(password),
  85. },
  86. }
  87. if len(privateKey) > 0 {
  88. if signer, err := ssh.ParsePrivateKey([]byte(privateKey)); err == nil {
  89. config.Auth = append(config.Auth, ssh.PublicKeys(signer))
  90. }
  91. }
  92. var err error
  93. addr := net.JoinHostPort(s.Host, strconv.Itoa(s.Port))
  94. s.conn, s.sshNetConn, err = NewSshClient("tcp", addr, config)
  95. if err != nil {
  96. return errors.Wrapf(err, "dial %s", addr)
  97. }
  98. s.sftp, err = sftp.NewClient(s.conn)
  99. if err != nil {
  100. return errors.Wrapf(err, "new sftp client")
  101. }
  102. addSftpClient(s.Session.Id, s.sftp)
  103. s.session, err = s.conn.NewSession()
  104. if err != nil {
  105. return errors.Wrapf(err, "NewSession")
  106. }
  107. s.StdinPipe, err = s.session.StdinPipe()
  108. if err != nil {
  109. return errors.Wrapf(err, "StdinPipe")
  110. }
  111. s.StdoutPipe, err = s.session.StdoutPipe()
  112. if err != nil {
  113. return errors.Wrapf(err, "StdoutPipe")
  114. }
  115. s.StderrPipe, err = s.session.StderrPipe()
  116. if err != nil {
  117. return errors.Wrapf(err, "StderrPipe")
  118. }
  119. var up = websocket.Upgrader{
  120. ReadBufferSize: 1024,
  121. WriteBufferSize: 1024,
  122. CheckOrigin: func(r *http.Request) bool {
  123. return true
  124. },
  125. }
  126. s.ws, err = up.Upgrade(w, r, nil)
  127. if err != nil {
  128. return errors.Wrapf(err, "upgrade")
  129. }
  130. modes := ssh.TerminalModes{
  131. ssh.ECHO: 1,
  132. ssh.TTY_OP_ISPEED: 14400,
  133. ssh.TTY_OP_OSPEED: 14400,
  134. }
  135. err = s.session.RequestPty("xterm-256color", 120, 32, modes)
  136. if err != nil {
  137. return errors.Wrapf(err, "request pty xterm")
  138. }
  139. err = s.session.Shell()
  140. if err != nil {
  141. return errors.Wrapf(err, "Shell")
  142. }
  143. return nil
  144. }
  145. // ref: https://github.com/golang/go/issues/19338#issuecomment-539057790
  146. func NewSshClient(network, addr string, conf *ssh.ClientConfig) (*ssh.Client, net.Conn, error) {
  147. conn, err := net.DialTimeout(network, addr, conf.Timeout)
  148. if err != nil {
  149. return nil, nil, errors.Wrapf(err, "dial %s %s", network, addr)
  150. }
  151. if conf.Timeout > 0 {
  152. conn.SetDeadline(time.Now().Add(conf.Timeout))
  153. }
  154. c, chans, reqs, err := ssh.NewClientConn(conn, addr, conf)
  155. if err != nil {
  156. return nil, nil, errors.Wrapf(err, "new client conn %s", addr)
  157. }
  158. if conf.Timeout > 0 {
  159. conn.SetDeadline(time.Time{})
  160. }
  161. return ssh.NewClient(c, chans, reqs), conn, nil
  162. }
  163. func (s *WebsocketServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  164. logPrefix := fmt.Sprintf("ssh %s@%s:%d, session_id: %s", s.Username, s.Host, s.Port, s.Session.Id)
  165. err := s.initWs(w, r)
  166. if err != nil {
  167. log.Errorf("%s, initWs error: %v", logPrefix, err)
  168. return
  169. }
  170. done := make(chan bool, 3)
  171. keepAliveDone := make(chan struct{})
  172. go func() {
  173. if err := sshKeepAlive(s.conn, s.sshNetConn, keepAliveDone); err != nil {
  174. log.Errorf("%s, keepalive error: %v", logPrefix, err)
  175. }
  176. }()
  177. setDone := func() {
  178. done <- true
  179. }
  180. for _, reader := range []io.Reader{s.StdoutPipe, s.StderrPipe} {
  181. tmpReader := reader
  182. go func() {
  183. if err := writeToWebsocket(tmpReader, s); err != nil {
  184. log.Warningf("%s, writeToWebsocket error: %v", logPrefix, err)
  185. }
  186. }()
  187. }
  188. go func() {
  189. defer setDone()
  190. for {
  191. _, p, err := s.ws.ReadMessage()
  192. if err != nil {
  193. return
  194. }
  195. if options.Options.SshSessionTimeoutMinutes > 0 && s.timer != nil {
  196. s.timer.Reset(time.Duration(options.Options.SshSessionTimeoutMinutes) * time.Minute)
  197. }
  198. input := struct {
  199. Type string `json:"type" choices:"resize|input|heartbeat"`
  200. Data struct {
  201. Cols int
  202. Rows int
  203. Data string `json:"data"`
  204. Base64 bool
  205. }
  206. }{}
  207. obj, err := jsonutils.Parse(p)
  208. if err != nil {
  209. log.Errorf("%s, parse %s error: %v", logPrefix, string(p), err)
  210. continue
  211. }
  212. err = obj.Unmarshal(&input)
  213. if err != nil {
  214. log.Errorf("%s, unmarshal %s error: %v", logPrefix, string(p), err)
  215. continue
  216. }
  217. switch input.Type {
  218. case "close":
  219. return
  220. case "resize":
  221. err = s.session.WindowChange(input.Data.Rows, input.Data.Cols)
  222. if err != nil {
  223. log.Errorf("%s, resize %dx%d error: %v", logPrefix, input.Data.Cols, input.Data.Rows, err)
  224. }
  225. case "input":
  226. if input.Data.Base64 {
  227. data, _ := base64.StdEncoding.DecodeString(input.Data.Data)
  228. input.Data.Data = string(data)
  229. }
  230. go s.Session.GetRecorder().Write(input.Data.Data, "")
  231. _, err = s.StdinPipe.Write([]byte(input.Data.Data))
  232. if err != nil {
  233. log.Errorf("%s, write %s error: %v", logPrefix, input.Data.Data, err)
  234. return
  235. }
  236. case "heartbeat":
  237. continue
  238. default:
  239. log.Errorf("%s, unknow msg type %s", logPrefix, input.Type)
  240. }
  241. }
  242. }()
  243. defer func() {
  244. s.ws.Close()
  245. s.StdinPipe.Close()
  246. s.session.Close()
  247. delSftpClient(s.Session.Id)
  248. s.sftp.Close()
  249. s.conn.Close()
  250. if !options.Options.KeepWebsocketSession {
  251. s.Session.Close()
  252. }
  253. keepAliveDone <- struct{}{}
  254. }()
  255. stop := make(chan bool)
  256. go func() {
  257. s.timer = time.NewTimer(time.Microsecond * 100)
  258. if options.Options.SshSessionTimeoutMinutes > 0 {
  259. s.timer.Reset(time.Duration(options.Options.SshSessionTimeoutMinutes) * time.Minute)
  260. }
  261. defer s.timer.Stop()
  262. defer setDone()
  263. for {
  264. select {
  265. case <-stop:
  266. return
  267. case <-s.timer.C:
  268. if options.Options.SshSessionTimeoutMinutes > 0 {
  269. return
  270. }
  271. s.timer.Reset(time.Microsecond * 100)
  272. }
  273. }
  274. }()
  275. go func() {
  276. defer setDone()
  277. err = s.session.Wait()
  278. if err != nil {
  279. log.Warningf("%s wait error: %v", logPrefix, err)
  280. s.StdinPipe.Write([]byte(err.Error()))
  281. }
  282. }()
  283. <-done
  284. stop <- true
  285. }
  286. func sshKeepAlive(cli *ssh.Client, conn net.Conn, done <-chan struct{}) error {
  287. // ref:
  288. // - https://github.com/golang/go/issues/21478
  289. // - https://github.com/scylladb/go-sshtools/blob/master/keepalive.go#L36
  290. const keepAliveInterval = 15 * time.Second
  291. t := time.NewTicker(keepAliveInterval)
  292. defer t.Stop()
  293. for {
  294. deadline := time.Now().Add(keepAliveInterval).Add(15 * time.Second)
  295. if err := conn.SetDeadline(deadline); err != nil {
  296. return errors.Wrap(err, "failed to set deadline")
  297. }
  298. select {
  299. case <-t.C:
  300. _, _, err := cli.SendRequest("keepalive@openssh.com", true, nil)
  301. if err != nil {
  302. return errors.Wrap(err, "failed to send keep alive")
  303. }
  304. case <-done:
  305. return nil
  306. }
  307. }
  308. }