ssh.go 5.5 KB

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