mirror of
https://github.com/thomiceli/opengist.git
synced 2025-07-09 17:38:04 +02:00
Initial commit
This commit is contained in:
103
internal/ssh/git-ssh.go
Normal file
103
internal/ssh/git-ssh.go
Normal file
@ -0,0 +1,103 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"opengist/internal/git"
|
||||
"opengist/internal/models"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func runGitCommand(ch ssh.Channel, gitCmd string, keyID uint) error {
|
||||
verb, args := parseCommand(gitCmd)
|
||||
if !strings.HasPrefix(verb, "git-") {
|
||||
verb = ""
|
||||
}
|
||||
verb = strings.TrimPrefix(verb, "git-")
|
||||
|
||||
if verb != "upload-pack" && verb != "receive-pack" {
|
||||
return errors.New("invalid command")
|
||||
}
|
||||
|
||||
repoFullName := strings.ToLower(strings.Trim(args, "'"))
|
||||
repoFields := strings.SplitN(repoFullName, "/", 2)
|
||||
if len(repoFields) != 2 {
|
||||
return errors.New("invalid gist path")
|
||||
}
|
||||
|
||||
userName := strings.ToLower(repoFields[0])
|
||||
gistName := strings.TrimSuffix(strings.ToLower(repoFields[1]), ".git")
|
||||
|
||||
gist, err := models.GetGist(userName, gistName)
|
||||
if err != nil {
|
||||
return errors.New("gist not found")
|
||||
}
|
||||
|
||||
if verb == "receive-pack" {
|
||||
user, err := models.GetUserBySSHKeyID(keyID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
errorSsh("Failed to get user by SSH key id", err)
|
||||
return errors.New("internal server error")
|
||||
}
|
||||
|
||||
if user.ID != gist.UserID {
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
}
|
||||
|
||||
_ = models.SSHKeyLastUsedNow(keyID)
|
||||
|
||||
repositoryPath, err := git.GetRepositoryPath(gist.User.Username, gist.Uuid)
|
||||
if err != nil {
|
||||
errorSsh("Failed to get repository path", err)
|
||||
return errors.New("internal server error")
|
||||
}
|
||||
|
||||
cmd := exec.Command("git", verb, repositoryPath)
|
||||
cmd.Dir = repositoryPath
|
||||
|
||||
stdin, _ := cmd.StdinPipe()
|
||||
stdout, _ := cmd.StdoutPipe()
|
||||
stderr, _ := cmd.StderrPipe()
|
||||
|
||||
if err = cmd.Start(); err != nil {
|
||||
errorSsh("Failed to start git command", err)
|
||||
return errors.New("internal server error")
|
||||
}
|
||||
|
||||
// avoid blocking
|
||||
go func() {
|
||||
_, _ = io.Copy(stdin, ch)
|
||||
}()
|
||||
_, _ = io.Copy(ch, stdout)
|
||||
_, _ = io.Copy(ch, stderr)
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
errorSsh("Failed to wait for git command", err)
|
||||
return errors.New("internal server error")
|
||||
}
|
||||
|
||||
// updatedAt is updated only if serviceType is receive-pack
|
||||
if verb == "receive-pack" {
|
||||
_ = models.GistLastActiveNow(gist.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseCommand(cmd string) (string, string) {
|
||||
split := strings.SplitN(cmd, " ", 2)
|
||||
|
||||
if len(split) != 2 {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return split[0], strings.Replace(split[1], "'/", "'", 1)
|
||||
}
|
153
internal/ssh/run.go
Normal file
153
internal/ssh/run.go
Normal file
@ -0,0 +1,153 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"net"
|
||||
"opengist/internal/config"
|
||||
"opengist/internal/models"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func Start() {
|
||||
if !config.C.SSH.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
sshConfig := &ssh.ServerConfig{
|
||||
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
pkey, err := models.GetSSHKeyByContent(strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key))))
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
return &ssh.Permissions{Extensions: map[string]string{"key-id": strconv.Itoa(int(pkey.ID))}}, nil
|
||||
},
|
||||
}
|
||||
|
||||
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) {
|
||||
log.Info().Msg("Starting SSH server on ssh://" + config.C.SSH.Host + ":" + config.C.SSH.Port)
|
||||
listener, err := net.Listen("tcp", config.C.SSH.Host+":"+config.C.SSH.Port)
|
||||
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)
|
||||
keyID, _ := strconv.Atoi(sConn.Permissions.Extensions["key-id"])
|
||||
go handleConnexion(channels, uint(keyID))
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func handleConnexion(channels <-chan ssh.NewChannel, keyID uint) {
|
||||
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:]
|
||||
}
|
||||
|
||||
if err = runGitCommand(ch, payloadCmd, keyID); err != nil {
|
||||
_, _ = 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) {
|
||||
cmd := exec.Command(config.C.SSH.Keygen,
|
||||
"-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)
|
||||
}
|
Reference in New Issue
Block a user