Lightweight and easily self-hostable Git forge. git.poy.ooo
git-forge selfhosting homebrew
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245package ssh
import ( "farga/db" "farga/db/types" "farga/lib" "farga/lib/git" "fmt" "io" "log" "net" "os/exec" "path/filepath" "strings"
"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(d *db.DB) *ssh.ServerConfig { config := &ssh.ServerConfig{ PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { return AuthenticatePublicKey(d, 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(d *db.DB, port int) { config := SSHConfig(d) listener, err := net.Listen("tcp", fmt.Sprint("0.0.0.0:", port)) if err != nil { log.Fatalf("listen: %v", err) } fmt.Println(fmt.Sprint("Listening for SSH on 0.0.0.0:", lib.SSHPort))
for { conn, err := listener.Accept() if err != nil { log.Printf("accept: %v", err) continue } go handleConn(d, conn, config) }}
// Function commented since I might forget how it worksfunc handleConn(d *db.DB, 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(d, newChannel, sshConn.User(), sshConn.Permissions) }
// sshConn is used just to get the username, in this case. defer sshConn.Close()}
// Also commented. Handles the individual channel.func handleSessionChannel(d *db.DB, newChannel ssh.NewChannel, _ 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(d, channel, req, sshPerms.Extensions) return // No? default: if req.WantReply { req.Reply(false, nil) } } }}
func handleExec(d *db.DB, channel ssh.Channel, req *ssh.Request, extensions map[string]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("invalid command: %v\n", err)) return }
// Retrieve the user ID we previously got. userID, _ := uuid.Parse(extensions[userIDKey])
repoUser, repoName, err := getUserAndRepo(d, command.Args) if err != nil { closeChannel(channel, req, "No such user.\n") return }
repoMetadata := types.Repo{ UserID: &repoUser.ID, Name: repoName, }
repoMetadata, err = d.GetRepo(repoMetadata)
repoPath := git.GetRepoPath(&repoUser.ID, &repoMetadata.ID)
if err != nil { req.Reply(false, nil) fmt.Fprintf(channel.Stderr(), "repo not found: %v\n", err) channel.CloseWrite() sendExitStatus(channel, 1) return }
// Authorization: does sshUser have access to repoPath? if (repoMetadata.IsPrivate || strings.Contains(command.Command, "upload")) && *repoMetadata.UserID != userID { fmt.Println("unauthorized") fmt.Println(repoMetadata.UserID) fmt.Println(userID) req.Reply(false, nil) fmt.Fprintln(channel.Stderr(), "access denied") channel.CloseWrite() sendExitStatus(channel, 1) return }
req.Reply(true, nil)
cmd := exec.Command(command.Command, repoPath) cmd.Stdin = channel runGitCommand(cmd, channel)}
func runGitCommand(cmd *exec.Cmd, channel ssh.Channel) { // 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 }
go io.Copy(channel, stdout) go io.Copy(channel.Stderr(), stderr)
err := cmd.Wait() channel.CloseWrite()
exitCode := 0 if exitErr, ok := err.(*exec.ExitError); ok { exitCode = exitErr.ExitCode() } else if err != nil { exitCode = 1 }
sendExitStatus(channel, exitCode)}