ssh.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package ssh
  5. import (
  6. "context"
  7. "io"
  8. "net"
  9. "os"
  10. "os/exec"
  11. "path/filepath"
  12. "strings"
  13. "syscall"
  14. "github.com/pkg/errors"
  15. "github.com/sourcegraph/run"
  16. "github.com/unknwon/com"
  17. "golang.org/x/crypto/ssh"
  18. log "unknwon.dev/clog/v2"
  19. "gogs.io/gogs/internal/conf"
  20. "gogs.io/gogs/internal/database"
  21. "gogs.io/gogs/internal/osutil"
  22. )
  23. func cleanCommand(cmd string) string {
  24. i := strings.Index(cmd, "git")
  25. if i == -1 {
  26. return cmd
  27. }
  28. return cmd[i:]
  29. }
  30. func handleServerConn(keyID string, chans <-chan ssh.NewChannel) {
  31. for newChan := range chans {
  32. if newChan.ChannelType() != "session" {
  33. _ = newChan.Reject(ssh.UnknownChannelType, "unknown channel type")
  34. continue
  35. }
  36. ch, reqs, err := newChan.Accept()
  37. if err != nil {
  38. log.Error("Error accepting channel: %v", err)
  39. continue
  40. }
  41. go func(in <-chan *ssh.Request) {
  42. defer func() {
  43. _ = ch.Close()
  44. }()
  45. for req := range in {
  46. payload := cleanCommand(string(req.Payload))
  47. switch req.Type {
  48. case "env":
  49. // We only need to accept the request and do nothing since whatever environment
  50. // variables being set here won't be used in subsequent commands anyway.
  51. case "exec":
  52. cmdName := strings.TrimLeft(payload, "'()")
  53. log.Trace("SSH: Payload: %v", cmdName)
  54. args := []string{"serv", "key-" + keyID, "--config=" + conf.CustomConf}
  55. log.Trace("SSH: Arguments: %v", args)
  56. cmd := exec.Command(conf.AppPath(), args...)
  57. cmd.Env = append(os.Environ(), "SSH_ORIGINAL_COMMAND="+cmdName)
  58. stdout, err := cmd.StdoutPipe()
  59. if err != nil {
  60. log.Error("SSH: StdoutPipe: %v", err)
  61. return
  62. }
  63. stderr, err := cmd.StderrPipe()
  64. if err != nil {
  65. log.Error("SSH: StderrPipe: %v", err)
  66. return
  67. }
  68. input, err := cmd.StdinPipe()
  69. if err != nil {
  70. log.Error("SSH: StdinPipe: %v", err)
  71. return
  72. }
  73. // FIXME: check timeout
  74. if err = cmd.Start(); err != nil {
  75. log.Error("SSH: Start: %v", err)
  76. return
  77. }
  78. _ = req.Reply(true, nil)
  79. go func() {
  80. _, _ = io.Copy(input, ch)
  81. }()
  82. _, _ = io.Copy(ch, stdout)
  83. _, _ = io.Copy(ch.Stderr(), stderr)
  84. if err = cmd.Wait(); err != nil {
  85. log.Error("SSH: Wait: %v", err)
  86. return
  87. }
  88. _, _ = ch.SendRequest("exit-status", false, []byte{0, 0, 0, 0})
  89. return
  90. default:
  91. }
  92. }
  93. }(reqs)
  94. }
  95. }
  96. func listen(config *ssh.ServerConfig, host string, port int) {
  97. listener, err := net.Listen("tcp", host+":"+com.ToStr(port))
  98. if err != nil {
  99. log.Fatal("Failed to start SSH server: %v", err)
  100. }
  101. for {
  102. // Once a ServerConfig has been configured, connections can be accepted.
  103. conn, err := listener.Accept()
  104. if err != nil {
  105. log.Error("SSH: Error accepting incoming connection: %v", err)
  106. continue
  107. }
  108. // Before use, a handshake must be performed on the incoming net.Conn.
  109. // It must be handled in a separate goroutine,
  110. // otherwise one user could easily block entire loop.
  111. // For example, user could be asked to trust server key fingerprint and hangs.
  112. go func() {
  113. log.Trace("SSH: Handshaking for %s", conn.RemoteAddr())
  114. sConn, chans, reqs, err := ssh.NewServerConn(conn, config)
  115. if err != nil {
  116. if err == io.EOF || errors.Is(err, syscall.ECONNRESET) {
  117. log.Trace("SSH: Handshaking was terminated: %v", err)
  118. } else {
  119. log.Error("SSH: Error on handshaking: %v", err)
  120. }
  121. return
  122. }
  123. log.Trace("SSH: Connection from %s (%s)", sConn.RemoteAddr(), sConn.ClientVersion())
  124. // The incoming Request channel must be serviced.
  125. go ssh.DiscardRequests(reqs)
  126. go handleServerConn(sConn.Permissions.Extensions["key-id"], chans)
  127. }()
  128. }
  129. }
  130. // Listen starts a SSH server listens on given port.
  131. func Listen(opts conf.SSHOpts, appDataPath string) {
  132. config := &ssh.ServerConfig{
  133. Config: ssh.Config{
  134. Ciphers: opts.ServerCiphers,
  135. MACs: opts.ServerMACs,
  136. },
  137. PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
  138. pkey, err := database.SearchPublicKeyByContent(strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key))))
  139. if err != nil {
  140. if !database.IsErrKeyNotExist(err) {
  141. log.Error("SearchPublicKeyByContent: %v", err)
  142. }
  143. return nil, err
  144. }
  145. return &ssh.Permissions{Extensions: map[string]string{"key-id": com.ToStr(pkey.ID)}}, nil
  146. },
  147. }
  148. keys, err := setupHostKeys(appDataPath, opts.ServerAlgorithms)
  149. if err != nil {
  150. log.Fatal("SSH: Failed to setup host keys: %v", err)
  151. }
  152. for _, key := range keys {
  153. config.AddHostKey(key)
  154. }
  155. go listen(config, opts.ListenHost, opts.ListenPort)
  156. }
  157. func setupHostKeys(appDataPath string, algorithms []string) ([]ssh.Signer, error) {
  158. dir := filepath.Join(appDataPath, "ssh")
  159. err := os.MkdirAll(dir, os.ModePerm)
  160. if err != nil {
  161. return nil, errors.Wrapf(err, "create host key directory")
  162. }
  163. var hostKeys []ssh.Signer
  164. for _, algo := range algorithms {
  165. keyPath := filepath.Join(dir, "gogs."+algo)
  166. if !osutil.IsExist(keyPath) {
  167. args := []string{
  168. conf.SSH.KeygenPath,
  169. "-t", algo,
  170. "-f", keyPath,
  171. "-m", "PEM",
  172. "-N", run.Arg(""),
  173. }
  174. err = run.Cmd(context.Background(), args...).Run().Wait()
  175. if err != nil {
  176. return nil, errors.Wrapf(err, "generate host key with args %v", args)
  177. }
  178. log.Trace("SSH: New private key is generated: %s", keyPath)
  179. }
  180. keyData, err := os.ReadFile(keyPath)
  181. if err != nil {
  182. return nil, errors.Wrapf(err, "read host key %q", keyPath)
  183. }
  184. signer, err := ssh.ParsePrivateKey(keyData)
  185. if err != nil {
  186. return nil, errors.Wrapf(err, "parse host key %q", keyPath)
  187. }
  188. hostKeys = append(hostKeys, signer)
  189. }
  190. return hostKeys, nil
  191. }