Something went wrong. Try again.
Lightweight and easily self-hostable Git forge.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274package ssh
import ( "farga/db/types" "farga/lib" "farga/lib/git" "fmt" "io" "log" "net" "os/exec" "path/filepath" "strings" "sync"
"crypto/ed25519" "crypto/rand" "encoding/pem" "os"
"github.com/google/uuid" "golang.org/x/crypto/ssh")
// Tries to load the host's keys. If there are none, it makes new keys.func loadOrGenerateHostKey(path string) (ssh.Signer, error) { // Is there one already? if data, err := os.ReadFile(path); err == nil { signer, err := ssh.ParsePrivateKey(data) if err != nil { return nil, fmt.Errorf("parse host key: %w", err) } return signer, nil }
// Generate a new ed25519 priv key. _, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, fmt.Errorf("generate private key: %w", err) }
// Marshal to OpenSSH PEM format. privPemBlock, err := ssh.MarshalPrivateKey(priv, "") if err != nil { return nil, fmt.Errorf("marshal key: %w", err) }
// Save key. if err := os.WriteFile(path, pem.EncodeToMemory(privPemBlock), 0600); err != nil { return nil, fmt.Errorf("write host key: %w", err) }
signer, err := ssh.NewSignerFromKey(priv) if err != nil { return nil, fmt.Errorf("signer: %w", err) } return signer, nil}
func SSHConfig(store Store) *ssh.ServerConfig { config := &ssh.ServerConfig{ PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { return AuthenticatePublicKey(store, conn, key) }, }
keyPath, err := filepath.Abs(lib.SSHHostKeyPath) if err != nil { log.Fatal("Maybe the SSH host key path is improperly configured.") }
hostKey, err := loadOrGenerateHostKey(keyPath) // todo if err != nil { log.Fatalf("host key: %v", err) } config.AddHostKey(hostKey)
return config}
func ServeSSH(store Store, port uint) { SSHConfig := SSHConfig(store) listener, err := net.Listen("tcp", fmt.Sprint("0.0.0.0:", port)) if err != nil { log.Fatalf("listen: %v", err) } fmt.Printf("Listening for SSH on 0.0.0.0:%v\n", port)
for { conn, err := listener.Accept() if err != nil { log.Printf("accept: %v", err) continue } go handleConn(store, conn, SSHConfig) }}
// Function commented since I might forget how it worksfunc handleConn(store Store, conn net.Conn, config *ssh.ServerConfig) { // NewServerConn upgrades the connection from raw TCP to SSH. // PublicKeyCallback will be fired and the client will be authenticated // // sshConn: the connection // chans: SSH Channels that can request stuff?. // reqs: Connection-level requests. Stuff such as `keepalive`. sshConn, chans, reqs, err := ssh.NewServerConn(conn, config) if err != nil { log.Printf("handshake failed: %v", err) return }
// SSH global requests do not drain themselves. Avoids deadlocks. go ssh.DiscardRequests(reqs)
// Git clients only use the "session" channel. Therefore, all other channels must be rejected. for newChannel := range chans { if newChannel.ChannelType() != "session" { newChannel.Reject(ssh.UnknownChannelType, "unsupported channel type") continue } go handleSessionChannel(store, newChannel, sshConn.RemoteAddr().String(), sshConn.Permissions) }
// sshConn is used just to get the remote address. defer sshConn.Close()}
// Also commented. Handles the individual channel.func handleSessionChannel(store Store, newChannel ssh.NewChannel, remoteAddr string, sshPerms *ssh.Permissions) { // Accept the channel since it IS the git channel. // // channel: bi-direcional stream (stdin stdout stderr etc.) // requests: channel-level requests, such as "kill channel" channel, requests, err := newChannel.Accept() if err != nil { log.Printf("accept channel: %v", err) return } defer channel.Close()
// For every request sent, for req := range requests { switch req.Type { // Is it a command? case "exec": // YEAH, RUN IT! handleExec(store, channel, req, sshPerms.Extensions, remoteAddr) return // No? default: if req.WantReply { req.Reply(false, nil) } } }}
func handleExec(store Store, channel ssh.Channel, req *ssh.Request, extensions map[string]string, remoteAddr string) { // unmarshal the SSH into a string var payload struct{ Command string } if err := ssh.Unmarshal(req.Payload, &payload); err != nil { req.Reply(false, nil) return }
// Is the string valid? command, err := parseGitCommand(payload.Command) if err != nil { closeChannel(channel, req, fmt.Sprintf("Not a Git command: '%s'\nYou're only allowed to use Git commands.\n", payload.Command)) return }
isAnonymous := extensions[isAnonymousKey] == "true"
actor := "anonymous" if !isAnonymous { actor = extensions[userHandleKey] }
var userID uuid.UUID
if !isAnonymous { // Retrieve the user ID we previously got. userID, _ = uuid.Parse(extensions[userIDKey]) }
repoUser, repoName, err := UserAndRepo(store, command.Args) if err != nil { closeChannel(channel, req, "No such user.\n") return }
repoMetadata := types.Repo{ UserID: &repoUser.ID, Name: repoName, }
repoMetadata, err = store.Repo(repoMetadata)
if err != nil || repoMetadata.ID == uuid.Nil { req.Reply(false, nil) if isAnonymous { fmt.Fprintln(channel.Stderr(), "Access DENIED.", err) } else { fmt.Fprintln(channel.Stderr(), "Repo not found.") } channel.CloseWrite() sendExitStatus(channel, 1) return }
repoPath := git.RepoPath(repoUser.ID, repoMetadata.ID)
// Authorization: does sshUser have access to repoPath? isWrite := strings.Contains(command.Command, "receive") if (repoMetadata.IsPrivate || isWrite) && *repoMetadata.UserID != userID { log.Printf("ssh: denied %s %s/%s for %s from %s", command.Command, repoUser.Handle, repoName, actor, remoteAddr) req.Reply(false, nil) fmt.Fprintln(channel.Stderr(), "Access DENIED.") channel.CloseWrite() sendExitStatus(channel, 1) return }
req.Reply(true, nil)
log.Printf("ssh: %s %s/%s for %s from %s", command.Command, repoUser.Handle, repoName, actor, remoteAddr)
cmd := exec.Command(command.Command, repoPath) cmd.Stdin = channel runGitCommand(cmd, channel, remoteAddr)}
func runGitCommand(cmd *exec.Cmd, channel ssh.Channel, remoteAddr string) { // stdin, _ := cmd.StdinPipe() stdout, _ := cmd.StdoutPipe() stderr, _ := cmd.StderrPipe()
if err := cmd.Start(); err != nil { fmt.Fprintf(channel.Stderr(), "failed to start: %v\n", err) sendExitStatus(channel, 1) return }
// coordinate goroutines var wg sync.WaitGroup wg.Add(2) // stdout and stderr
go func() { defer wg.Done() io.Copy(channel, stdout) }()
go func() { defer wg.Done() io.Copy(channel.Stderr(), stderr) }()
err := cmd.Wait() wg.Wait()
exitCode := 0 if exitErr, ok := err.(*exec.ExitError); ok { exitCode = exitErr.ExitCode() } else if err != nil { exitCode = 1 }
log.Printf("ssh: %v from %s exited with status %d", cmd.Args, remoteAddr, exitCode) sendExitStatus(channel, exitCode) channel.CloseWrite()}