2023-03-14 15:22:52 +00:00
|
|
|
package ssh
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"github.com/rs/zerolog/log"
|
2023-05-15 19:07:29 +00:00
|
|
|
"github.com/thomiceli/opengist/internal/config"
|
|
|
|
"github.com/thomiceli/opengist/internal/models"
|
2023-03-14 15:22:52 +00:00
|
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"io"
|
|
|
|
"net"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
"syscall"
|
|
|
|
)
|
|
|
|
|
|
|
|
func Start() {
|
2023-04-06 23:52:56 +00:00
|
|
|
if !config.C.SshGit {
|
2023-03-14 15:22:52 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
sshConfig := &ssh.ServerConfig{
|
|
|
|
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
2023-05-01 00:55:34 +00:00
|
|
|
strKey := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key)))
|
|
|
|
_, err := models.SSHKeyDoesExists(strKey)
|
2023-04-03 23:16:22 +00:00
|
|
|
if err != nil {
|
|
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Warn().Msg("Invalid SSH authentication attempt from " + conn.RemoteAddr().String())
|
|
|
|
return nil, errors.New("unknown public key")
|
2023-03-14 15:22:52 +00:00
|
|
|
}
|
2023-05-01 00:55:34 +00:00
|
|
|
return &ssh.Permissions{Extensions: map[string]string{"key": strKey}}, nil
|
2023-03-14 15:22:52 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
key, err := setupHostKey()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal().Err(err).Msg("SSH: Could not setup host key")
|
|
|
|
}
|
|
|
|
|
|
|
|
sshConfig.AddHostKey(key)
|
|
|
|
go listen(sshConfig)
|
|
|
|
}
|
|
|
|
|
|
|
|
func listen(serverConfig *ssh.ServerConfig) {
|
2023-04-06 23:52:56 +00:00
|
|
|
log.Info().Msg("Starting SSH server on ssh://" + config.C.SshHost + ":" + config.C.SshPort)
|
|
|
|
listener, err := net.Listen("tcp", config.C.SshHost+":"+config.C.SshPort)
|
2023-03-14 15:22:52 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal().Err(err).Msg("SSH: Failed to start SSH server")
|
|
|
|
}
|
|
|
|
defer listener.Close()
|
|
|
|
|
|
|
|
for {
|
|
|
|
nConn, err := listener.Accept()
|
|
|
|
if err != nil {
|
|
|
|
errorSsh("Failed to accept incoming connection", err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
sConn, channels, reqs, err := ssh.NewServerConn(nConn, serverConfig)
|
|
|
|
if err != nil {
|
|
|
|
if !(err != io.EOF && !errors.Is(err, syscall.ECONNRESET)) {
|
|
|
|
errorSsh("Failed to handshake", err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
go ssh.DiscardRequests(reqs)
|
2023-05-01 00:55:34 +00:00
|
|
|
go handleConnexion(channels, sConn.Permissions.Extensions["key"], sConn.RemoteAddr().String())
|
2023-03-14 15:22:52 +00:00
|
|
|
}()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-01 00:55:34 +00:00
|
|
|
func handleConnexion(channels <-chan ssh.NewChannel, key string, ip string) {
|
2023-03-14 15:22:52 +00:00
|
|
|
for channel := range channels {
|
|
|
|
if channel.ChannelType() != "session" {
|
|
|
|
_ = channel.Reject(ssh.UnknownChannelType, "Unknown channel type")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
ch, reqs, err := channel.Accept()
|
|
|
|
if err != nil {
|
|
|
|
errorSsh("Could not accept channel", err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
go func(in <-chan *ssh.Request) {
|
|
|
|
defer func() {
|
|
|
|
_ = ch.Close()
|
|
|
|
}()
|
|
|
|
for req := range in {
|
|
|
|
switch req.Type {
|
|
|
|
case "env":
|
|
|
|
|
|
|
|
case "shell":
|
|
|
|
_, _ = ch.Write([]byte("Successfully connected to Opengist SSH server.\r\n"))
|
|
|
|
_, _ = ch.SendRequest("exit-status", false, []byte{0, 0, 0, 0})
|
|
|
|
return
|
|
|
|
case "exec":
|
|
|
|
payloadCmd := string(req.Payload)
|
|
|
|
i := strings.Index(payloadCmd, "git")
|
|
|
|
if i != -1 {
|
|
|
|
payloadCmd = payloadCmd[i:]
|
|
|
|
}
|
|
|
|
|
2023-05-01 00:55:34 +00:00
|
|
|
if err = runGitCommand(ch, payloadCmd, key, ip); err != nil {
|
2023-03-14 15:22:52 +00:00
|
|
|
_, _ = ch.Stderr().Write([]byte("Opengist: " + err.Error() + "\r\n"))
|
|
|
|
}
|
|
|
|
_, _ = ch.SendRequest("exit-status", false, []byte{0, 0, 0, 0})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}(reqs)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func setupHostKey() (ssh.Signer, error) {
|
|
|
|
dir := filepath.Join(config.GetHomeDir(), "ssh")
|
|
|
|
|
|
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
keyPath := filepath.Join(dir, "opengist-ed25519")
|
|
|
|
if _, err := os.Stat(keyPath); err != nil && !os.IsExist(err) {
|
2023-04-06 23:52:56 +00:00
|
|
|
cmd := exec.Command(config.C.SshKeygen,
|
2023-03-14 15:22:52 +00:00
|
|
|
"-t", "ssh-ed25519",
|
|
|
|
"-f", keyPath,
|
|
|
|
"-m", "PEM",
|
|
|
|
"-N", "")
|
|
|
|
err = cmd.Run()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
keyData, err := os.ReadFile(keyPath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
signer, err := ssh.ParsePrivateKey(keyData)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return signer, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func errorSsh(message string, err error) {
|
|
|
|
log.Error().Err(err).Msg("SSH: " + message)
|
|
|
|
}
|