aboutsummaryrefslogtreecommitdiff
path: root/internal/ssh/ssh.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/ssh/ssh.go')
-rw-r--r--internal/ssh/ssh.go187
1 files changed, 187 insertions, 0 deletions
diff --git a/internal/ssh/ssh.go b/internal/ssh/ssh.go
new file mode 100644
index 00000000..c7368383
--- /dev/null
+++ b/internal/ssh/ssh.go
@@ -0,0 +1,187 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package ssh
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "github.com/unknwon/com"
+ "golang.org/x/crypto/ssh"
+ log "gopkg.in/clog.v1"
+
+ "gogs.io/gogs/internal/db"
+ "gogs.io/gogs/internal/setting"
+)
+
+func cleanCommand(cmd string) string {
+ i := strings.Index(cmd, "git")
+ if i == -1 {
+ return cmd
+ }
+ return cmd[i:]
+}
+
+func handleServerConn(keyID string, chans <-chan ssh.NewChannel) {
+ for newChan := range chans {
+ if newChan.ChannelType() != "session" {
+ newChan.Reject(ssh.UnknownChannelType, "unknown channel type")
+ continue
+ }
+
+ ch, reqs, err := newChan.Accept()
+ if err != nil {
+ log.Error(3, "Error accepting channel: %v", err)
+ continue
+ }
+
+ go func(in <-chan *ssh.Request) {
+ defer ch.Close()
+ for req := range in {
+ payload := cleanCommand(string(req.Payload))
+ switch req.Type {
+ case "env":
+ args := strings.Split(strings.Replace(payload, "\x00", "", -1), "\v")
+ if len(args) != 2 {
+ log.Warn("SSH: Invalid env arguments: '%#v'", args)
+ continue
+ }
+ args[0] = strings.TrimLeft(args[0], "\x04")
+ _, _, err := com.ExecCmdBytes("env", args[0]+"="+args[1])
+ if err != nil {
+ log.Error(3, "env: %v", err)
+ return
+ }
+ case "exec":
+ cmdName := strings.TrimLeft(payload, "'()")
+ log.Trace("SSH: Payload: %v", cmdName)
+
+ args := []string{"serv", "key-" + keyID, "--config=" + setting.CustomConf}
+ log.Trace("SSH: Arguments: %v", args)
+ cmd := exec.Command(setting.AppPath, args...)
+ cmd.Env = append(os.Environ(), "SSH_ORIGINAL_COMMAND="+cmdName)
+
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ log.Error(3, "SSH: StdoutPipe: %v", err)
+ return
+ }
+ stderr, err := cmd.StderrPipe()
+ if err != nil {
+ log.Error(3, "SSH: StderrPipe: %v", err)
+ return
+ }
+ input, err := cmd.StdinPipe()
+ if err != nil {
+ log.Error(3, "SSH: StdinPipe: %v", err)
+ return
+ }
+
+ // FIXME: check timeout
+ if err = cmd.Start(); err != nil {
+ log.Error(3, "SSH: Start: %v", err)
+ return
+ }
+
+ req.Reply(true, nil)
+ go io.Copy(input, ch)
+ io.Copy(ch, stdout)
+ io.Copy(ch.Stderr(), stderr)
+
+ if err = cmd.Wait(); err != nil {
+ log.Error(3, "SSH: Wait: %v", err)
+ return
+ }
+
+ ch.SendRequest("exit-status", false, []byte{0, 0, 0, 0})
+ return
+ default:
+ }
+ }
+ }(reqs)
+ }
+}
+
+func listen(config *ssh.ServerConfig, host string, port int) {
+ listener, err := net.Listen("tcp", host+":"+com.ToStr(port))
+ if err != nil {
+ log.Fatal(4, "Fail to start SSH server: %v", err)
+ }
+ for {
+ // Once a ServerConfig has been configured, connections can be accepted.
+ conn, err := listener.Accept()
+ if err != nil {
+ log.Error(3, "SSH: Error accepting incoming connection: %v", err)
+ continue
+ }
+
+ // Before use, a handshake must be performed on the incoming net.Conn.
+ // It must be handled in a separate goroutine,
+ // otherwise one user could easily block entire loop.
+ // For example, user could be asked to trust server key fingerprint and hangs.
+ go func() {
+ log.Trace("SSH: Handshaking for %s", conn.RemoteAddr())
+ sConn, chans, reqs, err := ssh.NewServerConn(conn, config)
+ if err != nil {
+ if err == io.EOF {
+ log.Warn("SSH: Handshaking was terminated: %v", err)
+ } else {
+ log.Error(3, "SSH: Error on handshaking: %v", err)
+ }
+ return
+ }
+
+ log.Trace("SSH: Connection from %s (%s)", sConn.RemoteAddr(), sConn.ClientVersion())
+ // The incoming Request channel must be serviced.
+ go ssh.DiscardRequests(reqs)
+ go handleServerConn(sConn.Permissions.Extensions["key-id"], chans)
+ }()
+ }
+}
+
+// Listen starts a SSH server listens on given port.
+func Listen(host string, port int, ciphers []string) {
+ config := &ssh.ServerConfig{
+ Config: ssh.Config{
+ Ciphers: ciphers,
+ },
+ PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
+ pkey, err := db.SearchPublicKeyByContent(strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key))))
+ if err != nil {
+ log.Error(3, "SearchPublicKeyByContent: %v", err)
+ return nil, err
+ }
+ return &ssh.Permissions{Extensions: map[string]string{"key-id": com.ToStr(pkey.ID)}}, nil
+ },
+ }
+
+ keyPath := filepath.Join(setting.AppDataPath, "ssh/gogs.rsa")
+ if !com.IsExist(keyPath) {
+ os.MkdirAll(filepath.Dir(keyPath), os.ModePerm)
+ _, stderr, err := com.ExecCmd(setting.SSH.KeygenPath, "-f", keyPath, "-t", "rsa", "-m", "PEM", "-N", "")
+ if err != nil {
+ panic(fmt.Sprintf("Fail to generate private key: %v - %s", err, stderr))
+ }
+ log.Trace("SSH: New private key is generateed: %s", keyPath)
+ }
+
+ privateBytes, err := ioutil.ReadFile(keyPath)
+ if err != nil {
+ panic("SSH: Fail to load private key: " + err.Error())
+ }
+ private, err := ssh.ParsePrivateKey(privateBytes)
+ if err != nil {
+ panic("SSH: Fail to parse private key: " + err.Error())
+ }
+ config.AddHostKey(private)
+
+ go listen(config, host, port)
+}