2019-02-07 10:13:12 +03:00
// Copyright 2017 The Gitea Authors. All rights reserved.
2014-07-26 08:24:27 +04:00
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ssh
import (
2019-02-07 10:13:12 +03:00
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
2019-07-07 04:28:09 +03:00
"fmt"
2015-11-09 00:59:56 +03:00
"io"
2014-07-26 08:24:27 +04:00
"os"
"os/exec"
2015-11-09 00:59:56 +03:00
"path/filepath"
2014-07-26 08:24:27 +04:00
"strings"
2019-07-07 04:28:09 +03:00
"sync"
"syscall"
2014-07-26 08:24:27 +04:00
2016-11-10 19:24:48 +03:00
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
2019-07-07 04:28:09 +03:00
"github.com/gliderlabs/ssh"
2019-08-23 19:40:30 +03:00
"github.com/unknwon/com"
2019-07-07 04:28:09 +03:00
gossh "golang.org/x/crypto/ssh"
2014-07-26 08:24:27 +04:00
)
2019-07-07 04:28:09 +03:00
type contextKey string
const giteaKeyID = contextKey ( "gitea-key-id" )
func getExitStatusFromError ( err error ) int {
if err == nil {
return 0
2015-11-09 00:59:56 +03:00
}
2019-07-07 04:28:09 +03:00
exitErr , ok := err . ( * exec . ExitError )
if ! ok {
return 1
}
2015-11-09 00:59:56 +03:00
2019-07-07 04:28:09 +03:00
waitStatus , ok := exitErr . Sys ( ) . ( syscall . WaitStatus )
if ! ok {
// This is a fallback and should at least let us return something useful
// when running on Windows, even if it isn't completely accurate.
if exitErr . Success ( ) {
return 0
2014-07-26 08:24:27 +04:00
}
2019-07-07 04:28:09 +03:00
return 1
2014-07-26 08:24:27 +04:00
}
2019-07-07 04:28:09 +03:00
return waitStatus . ExitStatus ( )
2014-07-26 08:24:27 +04:00
}
2019-07-07 04:28:09 +03:00
func sessionHandler ( session ssh . Session ) {
keyID := session . Context ( ) . Value ( giteaKeyID ) . ( int64 )
command := session . RawCommand ( )
log . Trace ( "SSH: Payload: %v" , command )
args := [ ] string { "serv" , "key-" + com . ToStr ( keyID ) , "--config=" + setting . CustomConf }
log . Trace ( "SSH: Arguments: %v" , args )
cmd := exec . Command ( setting . AppPath , args ... )
cmd . Env = append (
os . Environ ( ) ,
"SSH_ORIGINAL_COMMAND=" + command ,
"SKIP_MINWINSVC=1" ,
)
stdout , err := cmd . StdoutPipe ( )
2014-07-26 08:24:27 +04:00
if err != nil {
2019-07-07 04:28:09 +03:00
log . Error ( "SSH: StdoutPipe: %v" , err )
return
2014-07-26 08:24:27 +04:00
}
2019-07-07 04:28:09 +03:00
stderr , err := cmd . StderrPipe ( )
if err != nil {
log . Error ( "SSH: StderrPipe: %v" , err )
return
}
stdin , err := cmd . StdinPipe ( )
if err != nil {
log . Error ( "SSH: StdinPipe: %v" , err )
return
}
wg := & sync . WaitGroup { }
wg . Add ( 2 )
if err = cmd . Start ( ) ; err != nil {
log . Error ( "SSH: Start: %v" , err )
return
}
go func ( ) {
defer stdin . Close ( )
if _ , err := io . Copy ( stdin , session ) ; err != nil {
log . Error ( "Failed to write session to stdin. %s" , err )
}
} ( )
go func ( ) {
defer wg . Done ( )
if _ , err := io . Copy ( session , stdout ) ; err != nil {
log . Error ( "Failed to write stdout to session. %s" , err )
}
} ( )
go func ( ) {
defer wg . Done ( )
if _ , err := io . Copy ( session . Stderr ( ) , stderr ) ; err != nil {
log . Error ( "Failed to write stderr to session. %s" , err )
2014-07-26 08:24:27 +04:00
}
2019-07-07 04:28:09 +03:00
} ( )
// Ensure all the output has been written before we wait on the command
// to exit.
wg . Wait ( )
// Wait for the command to exit and log any errors we get
err = cmd . Wait ( )
if err != nil {
log . Error ( "SSH: Wait: %v" , err )
}
if err := session . Exit ( getExitStatusFromError ( err ) ) ; err != nil {
log . Error ( "Session failed to exit. %s" , err )
}
}
func publicKeyHandler ( ctx ssh . Context , key ssh . PublicKey ) bool {
if ctx . User ( ) != setting . SSH . BuiltinServerUser {
return false
}
2016-02-01 20:10:49 +03:00
2019-07-07 04:28:09 +03:00
pkey , err := models . SearchPublicKeyByContent ( strings . TrimSpace ( string ( gossh . MarshalAuthorizedKey ( key ) ) ) )
if err != nil {
log . Error ( "SearchPublicKeyByContent: %v" , err )
return false
2014-07-26 08:24:27 +04:00
}
2019-07-07 04:28:09 +03:00
ctx . SetValue ( giteaKeyID , pkey . ID )
return true
2014-07-26 08:24:27 +04:00
}
// Listen starts a SSH server listens on given port.
2017-11-02 18:26:41 +03:00
func Listen ( host string , port int , ciphers [ ] string , keyExchanges [ ] string , macs [ ] string ) {
2019-07-07 04:28:09 +03:00
// TODO: Handle ciphers, keyExchanges, and macs
srv := ssh . Server {
Addr : fmt . Sprintf ( "%s:%d" , host , port ) ,
PublicKeyHandler : publicKeyHandler ,
Handler : sessionHandler ,
// We need to explicitly disable the PtyCallback so text displays
// properly.
PtyCallback : func ( ctx ssh . Context , pty ssh . Pty ) bool {
return false
2014-07-26 08:24:27 +04:00
} ,
}
2015-11-14 21:21:31 +03:00
keyPath := filepath . Join ( setting . AppDataPath , "ssh/gogs.rsa" )
if ! com . IsExist ( keyPath ) {
2016-12-01 02:56:15 +03:00
filePath := filepath . Dir ( keyPath )
if err := os . MkdirAll ( filePath , os . ModePerm ) ; err != nil {
2019-04-02 10:48:31 +03:00
log . Error ( "Failed to create dir %s: %v" , filePath , err )
2016-12-01 02:56:15 +03:00
}
2019-02-07 10:13:12 +03:00
err := GenKeyPair ( keyPath )
2015-11-14 21:21:31 +03:00
if err != nil {
2019-04-02 10:48:31 +03:00
log . Fatal ( "Failed to generate private key: %v" , err )
2015-11-14 21:21:31 +03:00
}
2019-07-07 04:28:09 +03:00
log . Trace ( "New private key is generated: %s" , keyPath )
2015-11-14 21:21:31 +03:00
}
2019-07-07 04:28:09 +03:00
err := srv . SetOption ( ssh . HostKeyFile ( keyPath ) )
2014-07-26 08:24:27 +04:00
if err != nil {
2019-07-07 04:28:09 +03:00
log . Error ( "Failed to set Host Key. %s" , err )
2014-07-26 08:24:27 +04:00
}
2019-10-15 16:39:51 +03:00
go listen ( & srv )
2019-07-07 04:28:09 +03:00
2014-07-26 08:24:27 +04:00
}
2019-02-07 10:13:12 +03:00
// GenKeyPair make a pair of public and private keys for SSH access.
// Public key is encoded in the format for inclusion in an OpenSSH authorized_keys file.
// Private Key generated is PEM encoded
func GenKeyPair ( keyPath string ) error {
privateKey , err := rsa . GenerateKey ( rand . Reader , 2048 )
if err != nil {
return err
}
privateKeyPEM := & pem . Block { Type : "RSA PRIVATE KEY" , Bytes : x509 . MarshalPKCS1PrivateKey ( privateKey ) }
f , err := os . OpenFile ( keyPath , os . O_RDWR | os . O_CREATE | os . O_TRUNC , 0600 )
if err != nil {
return err
}
2019-06-12 22:41:28 +03:00
defer func ( ) {
if err = f . Close ( ) ; err != nil {
log . Error ( "Close: %v" , err )
}
} ( )
2019-02-07 10:13:12 +03:00
if err := pem . Encode ( f , privateKeyPEM ) ; err != nil {
return err
}
// generate public key
2019-07-07 04:28:09 +03:00
pub , err := gossh . NewPublicKey ( & privateKey . PublicKey )
2019-02-07 10:13:12 +03:00
if err != nil {
return err
}
2019-07-07 04:28:09 +03:00
public := gossh . MarshalAuthorizedKey ( pub )
2019-02-07 10:13:12 +03:00
p , err := os . OpenFile ( keyPath + ".pub" , os . O_RDWR | os . O_CREATE | os . O_TRUNC , 0600 )
if err != nil {
return err
}
2019-06-12 22:41:28 +03:00
defer func ( ) {
if err = p . Close ( ) ; err != nil {
log . Error ( "Close: %v" , err )
}
} ( )
2019-02-07 10:13:12 +03:00
_ , err = p . Write ( public )
return err
}