Lindenii Project Forge
Separate code/README rendering and unsafe to their own packages
package main
package render
import ( "bytes" "html/template" chromaHTML "github.com/alecthomas/chroma/v2/formatters/html" chromaLexers "github.com/alecthomas/chroma/v2/lexers" chromaStyles "github.com/alecthomas/chroma/v2/styles" )
func renderHighlightedFile(filename, content string) template.HTML {
func Highlight(filename, content string) template.HTML {
lexer := chromaLexers.Match(filename) if lexer == nil { lexer = chromaLexers.Fallback } iterator, err := lexer.Tokenise(nil, content) if err != nil { return template.HTML("<pre>Error tokenizing file: " + err.Error() + "</pre>") //#nosec G203` } var buf bytes.Buffer style := chromaStyles.Get("autumn") formatter := chromaHTML.New( chromaHTML.WithClasses(true), chromaHTML.TabWidth(8), ) if err := formatter.Format(&buf, style, iterator); err != nil { return template.HTML("<pre>Error formatting file: " + err.Error() + "</pre>") //#nosec G203 } return template.HTML(buf.Bytes()) //#nosec G203 }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> // //go:build linux package main import ( "bytes" "context" "encoding/binary" "errors" "fmt" "io" "net" "os" "path/filepath" "strconv" "strings" "syscall" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/jackc/pgx/v5"
"go.lindenii.runxiyu.org/forge/misc"
"go.lindenii.runxiyu.org/lindenii-common/ansiec" "go.lindenii.runxiyu.org/lindenii-common/clog" ) var ( errGetFD = errors.New("unable to get file descriptor") errGetUcred = errors.New("failed getsockopt") ) // hooksHandler handles a connection from hookc via the // unix socket. func hooksHandler(conn net.Conn) { var ctx context.Context var cancel context.CancelFunc var ucred *syscall.Ucred var err error var cookie []byte var packPass packPass var sshStderr io.Writer var hookRet byte defer conn.Close() ctx, cancel = context.WithCancel(context.Background()) defer cancel() // There aren't reasonable cases where someone would run this as // another user. if ucred, err = getUcred(conn); err != nil { if _, err = conn.Write([]byte{1}); err != nil { return } writeRedError(conn, "\nUnable to get peer credentials: %v", err) return } uint32uid := uint32(os.Getuid()) //#nosec G115 if ucred.Uid != uint32uid { if _, err = conn.Write([]byte{1}); err != nil { return } writeRedError(conn, "\nUID mismatch") return } cookie = make([]byte, 64) if _, err = conn.Read(cookie); err != nil { if _, err = conn.Write([]byte{1}); err != nil { return } writeRedError(conn, "\nFailed to read cookie: %v", err) return } { var ok bool
packPass, ok = packPasses.Load(bytesToString(cookie))
packPass, ok = packPasses.Load(misc.BytesToString(cookie))
if !ok { if _, err = conn.Write([]byte{1}); err != nil { return } writeRedError(conn, "\nInvalid handler cookie") return } } sshStderr = packPass.session.Stderr() _, _ = sshStderr.Write([]byte{'\n'}) hookRet = func() byte { var argc64 uint64 if err = binary.Read(conn, binary.NativeEndian, &argc64); err != nil { writeRedError(sshStderr, "Failed to read argc: %v", err) return 1 } var args []string for range argc64 { var arg bytes.Buffer for { nextByte := make([]byte, 1) n, err := conn.Read(nextByte) if err != nil || n != 1 { writeRedError(sshStderr, "Failed to read arg: %v", err) return 1 } if nextByte[0] == 0 { break } arg.WriteByte(nextByte[0]) } args = append(args, arg.String()) } gitEnv := make(map[string]string) for { var envLine bytes.Buffer for { nextByte := make([]byte, 1) n, err := conn.Read(nextByte) if err != nil || n != 1 { writeRedError(sshStderr, "Failed to read environment variable: %v", err) return 1 } if nextByte[0] == 0 { break } envLine.WriteByte(nextByte[0]) } if envLine.Len() == 0 { break } kv := envLine.String() parts := strings.SplitN(kv, "=", 2) if len(parts) < 2 { writeRedError(sshStderr, "Invalid environment variable line: %v", kv) return 1 } gitEnv[parts[0]] = parts[1] } var stdin bytes.Buffer if _, err = io.Copy(&stdin, conn); err != nil { writeRedError(conn, "Failed to read to the stdin buffer: %v", err) } switch filepath.Base(args[0]) { case "pre-receive": if packPass.directAccess { return 0 } allOK := true for { var line, oldOID, rest, newIOID, refName string var found bool var oldHash, newHash plumbing.Hash var oldCommit, newCommit *object.Commit var pushOptCount int pushOptCount, err = strconv.Atoi(gitEnv["GIT_PUSH_OPTION_COUNT"]) if err != nil { writeRedError(sshStderr, "Failed to parse GIT_PUSH_OPTION_COUNT: %v", err) return 1 } // TODO: Allow existing users (even if they are already federated or registered) to add a federated user ID... though perhaps this should be in the normal SSH interface instead of the git push interface? // Also it'd be nice to be able to combine users or whatever if packPass.contribReq == "federated" && packPass.userType != "federated" && packPass.userType != "registered" { if pushOptCount == 0 { writeRedError(sshStderr, "This repo requires contributors to be either federated or registered users. You must supply your federated user ID as a push option. For example, git push -o fedid=sr.ht:runxiyu") return 1 } for pushOptIndex := range pushOptCount { pushOpt, ok := gitEnv[fmt.Sprintf("GIT_PUSH_OPTION_%d", pushOptIndex)] if !ok { writeRedError(sshStderr, "Failed to get push option %d", pushOptIndex) return 1 } if strings.HasPrefix(pushOpt, "fedid=") { fedUserID := strings.TrimPrefix(pushOpt, "fedid=") service, username, found := strings.Cut(fedUserID, ":") if !found { writeRedError(sshStderr, "Invalid federated user identifier %#v does not contain a colon", fedUserID) return 1 } ok, err := fedauth(ctx, packPass.userID, service, username, packPass.pubkey) if err != nil { writeRedError(sshStderr, "Failed to verify federated user identifier %#v: %v", fedUserID, err) return 1 } if !ok { writeRedError(sshStderr, "Failed to verify federated user identifier %#v: you don't seem to be on the list", fedUserID) return 1 } break } if pushOptIndex == pushOptCount-1 { writeRedError(sshStderr, "This repo requires contributors to be either federated or registered users. You must supply your federated user ID as a push option. For example, git push -o fedid=sr.ht:runxiyu") return 1 } } } line, err = stdin.ReadString('\n') if errors.Is(err, io.EOF) { break } else if err != nil { writeRedError(sshStderr, "Failed to read pre-receive line: %v", err) return 1 } line = line[:len(line)-1] oldOID, rest, found = strings.Cut(line, " ") if !found { writeRedError(sshStderr, "Invalid pre-receive line: %v", line) return 1 } newIOID, refName, found = strings.Cut(rest, " ") if !found { writeRedError(sshStderr, "Invalid pre-receive line: %v", line) return 1 } if strings.HasPrefix(refName, "refs/heads/contrib/") { if allZero(oldOID) { // New branch fmt.Fprintln(sshStderr, ansiec.Blue+"POK"+ansiec.Reset, refName) var newMRLocalID int if packPass.userID != 0 { err = database.QueryRow(ctx, "INSERT INTO merge_requests (repo_id, creator, source_ref, status) VALUES ($1, $2, $3, 'open') RETURNING repo_local_id", packPass.repoID, packPass.userID, strings.TrimPrefix(refName, "refs/heads/"), ).Scan(&newMRLocalID) } else { err = database.QueryRow(ctx, "INSERT INTO merge_requests (repo_id, source_ref, status) VALUES ($1, $2, 'open') RETURNING repo_local_id", packPass.repoID, strings.TrimPrefix(refName, "refs/heads/"), ).Scan(&newMRLocalID) } if err != nil { writeRedError(sshStderr, "Error creating merge request: %v", err) return 1 } mergeRequestWebURL := fmt.Sprintf("%s/contrib/%d/", genHTTPRemoteURL(packPass.groupPath, packPass.repoName), newMRLocalID) fmt.Fprintln(sshStderr, ansiec.Blue+"Created merge request at", mergeRequestWebURL+ansiec.Reset) select { case ircSendBuffered <- "PRIVMSG #chat :New merge request at " + mergeRequestWebURL: default: clog.Error("IRC SendQ exceeded") } } else { // Existing contrib branch var existingMRUser int var isAncestor bool err = database.QueryRow(ctx, "SELECT COALESCE(creator, 0) FROM merge_requests WHERE source_ref = $1 AND repo_id = $2", strings.TrimPrefix(refName, "refs/heads/"), packPass.repoID, ).Scan(&existingMRUser) if err != nil { if errors.Is(err, pgx.ErrNoRows) { writeRedError(sshStderr, "No existing merge request for existing contrib branch: %v", err) } else { writeRedError(sshStderr, "Error querying for existing merge request: %v", err) } return 1 } if existingMRUser == 0 { allOK = false fmt.Fprintln(sshStderr, ansiec.Red+"NAK"+ansiec.Reset, refName, "(branch belongs to unowned MR)") continue } if existingMRUser != packPass.userID { allOK = false fmt.Fprintln(sshStderr, ansiec.Red+"NAK"+ansiec.Reset, refName, "(branch belongs another user's MR)") continue } oldHash = plumbing.NewHash(oldOID) if oldCommit, err = packPass.repo.CommitObject(oldHash); err != nil { writeRedError(sshStderr, "Daemon failed to get old commit: %v", err) return 1 } // Potential BUG: I'm not sure if new_commit is guaranteed to be // detectable as they haven't been merged into the main repo's // objects yet. But it seems to work, and I don't think there's // any reason for this to only work intermitently. newHash = plumbing.NewHash(newIOID) if newCommit, err = packPass.repo.CommitObject(newHash); err != nil { writeRedError(sshStderr, "Daemon failed to get new commit: %v", err) return 1 } if isAncestor, err = oldCommit.IsAncestor(newCommit); err != nil { writeRedError(sshStderr, "Daemon failed to check if old commit is ancestor: %v", err) return 1 } if !isAncestor { // TODO: Create MR snapshot ref instead allOK = false fmt.Fprintln(sshStderr, ansiec.Red+"NAK"+ansiec.Reset, refName, "(force pushes are not supported yet)") continue } fmt.Fprintln(sshStderr, ansiec.Blue+"POK"+ansiec.Reset, refName) } } else { // Non-contrib branch allOK = false fmt.Fprintln(sshStderr, ansiec.Red+"NAK"+ansiec.Reset, refName, "(you cannot push to branches outside of contrib/*)") } } fmt.Fprintln(sshStderr) if allOK { fmt.Fprintln(sshStderr, "Overall "+ansiec.Green+"ACK"+ansiec.Reset+" (all checks passed)") return 0 } fmt.Fprintln(sshStderr, "Overall "+ansiec.Red+"NAK"+ansiec.Reset+" (one or more branches failed checks)") return 1 default: fmt.Fprintln(sshStderr, ansiec.Red+"Invalid hook:", args[0]+ansiec.Reset) return 1 } }() fmt.Fprintln(sshStderr) _, _ = conn.Write([]byte{hookRet}) } // serveGitHooks handles connections on the specified network listener and // treats incoming connections as those from git hook handlers by spawning // sessions. The listener must be a SOCK_STREAM UNIX domain socket. The // function itself blocks. func serveGitHooks(listener net.Listener) error { for { conn, err := listener.Accept() if err != nil { return err } go hooksHandler(conn) } } // getUcred fetches connection credentials as a [syscall.Ucred] from a given // [net.Conn]. It panics when conn is not a [net.UnixConn]. func getUcred(conn net.Conn) (ucred *syscall.Ucred, err error) { unixConn := conn.(*net.UnixConn) var unixConnFD *os.File if unixConnFD, err = unixConn.File(); err != nil { return nil, errGetFD } defer unixConnFD.Close() if ucred, err = syscall.GetsockoptUcred(int(unixConnFD.Fd()), syscall.SOL_SOCKET, syscall.SO_PEERCRED); err != nil { return nil, errGetUcred } return ucred, nil } // allZero returns true if all runes in a given string are '0'. The comparison // is not constant time and must not be used in contexts where time-based side // channel attacks are a concern. func allZero(s string) bool { for _, r := range s { if r != '0' { return false } } return true }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> // //go:build !linux package main import ( "bytes" "context" "encoding/binary" "errors" "fmt" "io" "net" "path/filepath" "strconv" "strings" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/jackc/pgx/v5"
"go.lindenii.runxiyu.org/forge/misc"
"go.lindenii.runxiyu.org/lindenii-common/ansiec" "go.lindenii.runxiyu.org/lindenii-common/clog" ) var errGetFD = errors.New("unable to get file descriptor") // hooksHandler handles a connection from hookc via the // unix socket. func hooksHandler(conn net.Conn) { var ctx context.Context var cancel context.CancelFunc var err error var cookie []byte var packPass packPass var sshStderr io.Writer var hookRet byte defer conn.Close() ctx, cancel = context.WithCancel(context.Background()) defer cancel() // TODO: Validate that the connection is from the right user. cookie = make([]byte, 64) if _, err = conn.Read(cookie); err != nil { if _, err = conn.Write([]byte{1}); err != nil { return } writeRedError(conn, "\nFailed to read cookie: %v", err) return } { var ok bool
packPass, ok = packPasses.Load(bytesToString(cookie))
packPass, ok = packPasses.Load(misc.BytesToString(cookie))
if !ok { if _, err = conn.Write([]byte{1}); err != nil { return } writeRedError(conn, "\nInvalid handler cookie") return } } sshStderr = packPass.session.Stderr() _, _ = sshStderr.Write([]byte{'\n'}) hookRet = func() byte { var argc64 uint64 if err = binary.Read(conn, binary.NativeEndian, &argc64); err != nil { writeRedError(sshStderr, "Failed to read argc: %v", err) return 1 } var args []string for range argc64 { var arg bytes.Buffer for { nextByte := make([]byte, 1) n, err := conn.Read(nextByte) if err != nil || n != 1 { writeRedError(sshStderr, "Failed to read arg: %v", err) return 1 } if nextByte[0] == 0 { break } arg.WriteByte(nextByte[0]) } args = append(args, arg.String()) } gitEnv := make(map[string]string) for { var envLine bytes.Buffer for { nextByte := make([]byte, 1) n, err := conn.Read(nextByte) if err != nil || n != 1 { writeRedError(sshStderr, "Failed to read environment variable: %v", err) return 1 } if nextByte[0] == 0 { break } envLine.WriteByte(nextByte[0]) } if envLine.Len() == 0 { break } kv := envLine.String() parts := strings.SplitN(kv, "=", 2) if len(parts) < 2 { writeRedError(sshStderr, "Invalid environment variable line: %v", kv) return 1 } gitEnv[parts[0]] = parts[1] } var stdin bytes.Buffer if _, err = io.Copy(&stdin, conn); err != nil { writeRedError(conn, "Failed to read to the stdin buffer: %v", err) } switch filepath.Base(args[0]) { case "pre-receive": if packPass.directAccess { return 0 } allOK := true for { var line, oldOID, rest, newIOID, refName string var found bool var oldHash, newHash plumbing.Hash var oldCommit, newCommit *object.Commit var pushOptCount int pushOptCount, err = strconv.Atoi(gitEnv["GIT_PUSH_OPTION_COUNT"]) if err != nil { writeRedError(sshStderr, "Failed to parse GIT_PUSH_OPTION_COUNT: %v", err) return 1 } // TODO: Allow existing users (even if they are already federated or registered) to add a federated user ID... though perhaps this should be in the normal SSH interface instead of the git push interface? // Also it'd be nice to be able to combine users or whatever if packPass.contribReq == "federated" && packPass.userType != "federated" && packPass.userType != "registered" { if pushOptCount == 0 { writeRedError(sshStderr, "This repo requires contributors to be either federated or registered users. You must supply your federated user ID as a push option. For example, git push -o fedid=sr.ht:runxiyu") return 1 } for pushOptIndex := range pushOptCount { pushOpt, ok := gitEnv[fmt.Sprintf("GIT_PUSH_OPTION_%d", pushOptIndex)] if !ok { writeRedError(sshStderr, "Failed to get push option %d", pushOptIndex) return 1 } if strings.HasPrefix(pushOpt, "fedid=") { fedUserID := strings.TrimPrefix(pushOpt, "fedid=") service, username, found := strings.Cut(fedUserID, ":") if !found { writeRedError(sshStderr, "Invalid federated user identifier %#v does not contain a colon", fedUserID) return 1 } ok, err := fedauth(ctx, packPass.userID, service, username, packPass.pubkey) if err != nil { writeRedError(sshStderr, "Failed to verify federated user identifier %#v: %v", fedUserID, err) return 1 } if !ok { writeRedError(sshStderr, "Failed to verify federated user identifier %#v: you don't seem to be on the list", fedUserID) return 1 } break } if pushOptIndex == pushOptCount-1 { writeRedError(sshStderr, "This repo requires contributors to be either federated or registered users. You must supply your federated user ID as a push option. For example, git push -o fedid=sr.ht:runxiyu") return 1 } } } line, err = stdin.ReadString('\n') if errors.Is(err, io.EOF) { break } else if err != nil { writeRedError(sshStderr, "Failed to read pre-receive line: %v", err) return 1 } line = line[:len(line)-1] oldOID, rest, found = strings.Cut(line, " ") if !found { writeRedError(sshStderr, "Invalid pre-receive line: %v", line) return 1 } newIOID, refName, found = strings.Cut(rest, " ") if !found { writeRedError(sshStderr, "Invalid pre-receive line: %v", line) return 1 } if strings.HasPrefix(refName, "refs/heads/contrib/") { if allZero(oldOID) { // New branch fmt.Fprintln(sshStderr, ansiec.Blue+"POK"+ansiec.Reset, refName) var newMRLocalID int if packPass.userID != 0 { err = database.QueryRow(ctx, "INSERT INTO merge_requests (repo_id, creator, source_ref, status) VALUES ($1, $2, $3, 'open') RETURNING repo_local_id", packPass.repoID, packPass.userID, strings.TrimPrefix(refName, "refs/heads/"), ).Scan(&newMRLocalID) } else { err = database.QueryRow(ctx, "INSERT INTO merge_requests (repo_id, source_ref, status) VALUES ($1, $2, 'open') RETURNING repo_local_id", packPass.repoID, strings.TrimPrefix(refName, "refs/heads/"), ).Scan(&newMRLocalID) } if err != nil { writeRedError(sshStderr, "Error creating merge request: %v", err) return 1 } mergeRequestWebURL := fmt.Sprintf("%s/contrib/%d/", genHTTPRemoteURL(packPass.groupPath, packPass.repoName), newMRLocalID) fmt.Fprintln(sshStderr, ansiec.Blue+"Created merge request at", mergeRequestWebURL+ansiec.Reset) select { case ircSendBuffered <- "PRIVMSG #chat :New merge request at " + mergeRequestWebURL: default: clog.Error("IRC SendQ exceeded") } } else { // Existing contrib branch var existingMRUser int var isAncestor bool err = database.QueryRow(ctx, "SELECT COALESCE(creator, 0) FROM merge_requests WHERE source_ref = $1 AND repo_id = $2", strings.TrimPrefix(refName, "refs/heads/"), packPass.repoID, ).Scan(&existingMRUser) if err != nil { if errors.Is(err, pgx.ErrNoRows) { writeRedError(sshStderr, "No existing merge request for existing contrib branch: %v", err) } else { writeRedError(sshStderr, "Error querying for existing merge request: %v", err) } return 1 } if existingMRUser == 0 { allOK = false fmt.Fprintln(sshStderr, ansiec.Red+"NAK"+ansiec.Reset, refName, "(branch belongs to unowned MR)") continue } if existingMRUser != packPass.userID { allOK = false fmt.Fprintln(sshStderr, ansiec.Red+"NAK"+ansiec.Reset, refName, "(branch belongs another user's MR)") continue } oldHash = plumbing.NewHash(oldOID) if oldCommit, err = packPass.repo.CommitObject(oldHash); err != nil { writeRedError(sshStderr, "Daemon failed to get old commit: %v", err) return 1 } // Potential BUG: I'm not sure if new_commit is guaranteed to be // detectable as they haven't been merged into the main repo's // objects yet. But it seems to work, and I don't think there's // any reason for this to only work intermitently. newHash = plumbing.NewHash(newIOID) if newCommit, err = packPass.repo.CommitObject(newHash); err != nil { writeRedError(sshStderr, "Daemon failed to get new commit: %v", err) return 1 } if isAncestor, err = oldCommit.IsAncestor(newCommit); err != nil { writeRedError(sshStderr, "Daemon failed to check if old commit is ancestor: %v", err) return 1 } if !isAncestor { // TODO: Create MR snapshot ref instead allOK = false fmt.Fprintln(sshStderr, ansiec.Red+"NAK"+ansiec.Reset, refName, "(force pushes are not supported yet)") continue } fmt.Fprintln(sshStderr, ansiec.Blue+"POK"+ansiec.Reset, refName) } } else { // Non-contrib branch allOK = false fmt.Fprintln(sshStderr, ansiec.Red+"NAK"+ansiec.Reset, refName, "(you cannot push to branches outside of contrib/*)") } } fmt.Fprintln(sshStderr) if allOK { fmt.Fprintln(sshStderr, "Overall "+ansiec.Green+"ACK"+ansiec.Reset+" (all checks passed)") return 0 } fmt.Fprintln(sshStderr, "Overall "+ansiec.Red+"NAK"+ansiec.Reset+" (one or more branches failed checks)") return 1 default: fmt.Fprintln(sshStderr, ansiec.Red+"Invalid hook:", args[0]+ansiec.Reset) return 1 } }() fmt.Fprintln(sshStderr) _, _ = conn.Write([]byte{hookRet}) } // serveGitHooks handles connections on the specified network listener and // treats incoming connections as those from git hook handlers by spawning // sessions. The listener must be a SOCK_STREAM UNIX domain socket. The // function itself blocks. func serveGitHooks(listener net.Listener) error { for { conn, err := listener.Accept() if err != nil { return err } go hooksHandler(conn) } } // allZero returns true if all runes in a given string are '0'. The comparison // is not constant time and must not be used in contexts where time-based side // channel attacks are a concern. func allZero(s string) bool { for _, r := range s { if r != '0' { return false } } return true }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package main import ( "bytes" "context" "encoding/hex" "errors" "os" "os/exec" "path" "sort" "strings"
"go.lindenii.runxiyu.org/forge/misc"
) func writeTree(ctx context.Context, repoPath string, entries []treeEntry) (string, error) { var buf bytes.Buffer sort.Slice(entries, func(i, j int) bool { nameI, nameJ := entries[i].name, entries[j].name if nameI == nameJ { // meh return !(entries[i].mode == "40000") && (entries[j].mode == "40000") } if strings.HasPrefix(nameJ, nameI) && len(nameI) < len(nameJ) { return !(entries[i].mode == "40000") } if strings.HasPrefix(nameI, nameJ) && len(nameJ) < len(nameI) { return entries[j].mode == "40000" } return nameI < nameJ }) for _, e := range entries { buf.WriteString(e.mode) buf.WriteByte(' ') buf.WriteString(e.name) buf.WriteByte(0) buf.Write(e.sha) } cmd := exec.CommandContext(ctx, "git", "hash-object", "-w", "-t", "tree", "--stdin") cmd.Env = append(os.Environ(), "GIT_DIR="+repoPath) cmd.Stdin = &buf var out bytes.Buffer cmd.Stdout = &out if err := cmd.Run(); err != nil { return "", err } return strings.TrimSpace(out.String()), nil } func buildTreeRecursive(ctx context.Context, repoPath, baseTree string, updates map[string][]byte) (string, error) { treeCache := make(map[string][]treeEntry) var walk func(string, string) error walk = func(prefix, sha string) error { cmd := exec.CommandContext(ctx, "git", "cat-file", "tree", sha) cmd.Env = append(os.Environ(), "GIT_DIR="+repoPath) var out bytes.Buffer cmd.Stdout = &out if err := cmd.Run(); err != nil { return err } data := out.Bytes() i := 0 var entries []treeEntry for i < len(data) { modeEnd := bytes.IndexByte(data[i:], ' ') if modeEnd < 0 { return errors.New("invalid tree format") }
mode := bytesToString(data[i : i+modeEnd])
mode := misc.BytesToString(data[i : i+modeEnd])
i += modeEnd + 1 nameEnd := bytes.IndexByte(data[i:], 0) if nameEnd < 0 { return errors.New("missing null after filename") }
name := bytesToString(data[i : i+nameEnd])
name := misc.BytesToString(data[i : i+nameEnd])
i += nameEnd + 1 if i+20 > len(data) { return errors.New("unexpected EOF in SHA") } shaBytes := data[i : i+20] i += 20 entries = append(entries, treeEntry{ mode: mode, name: name, sha: shaBytes, }) if mode == "40000" { subPrefix := path.Join(prefix, name) if err := walk(subPrefix, hex.EncodeToString(shaBytes)); err != nil { return err } } } treeCache[prefix] = entries return nil } if err := walk("", baseTree); err != nil { return "", err } for filePath, blobSha := range updates { parts := strings.Split(filePath, "/") dir := strings.Join(parts[:len(parts)-1], "/") name := parts[len(parts)-1] entries := treeCache[dir] found := false for i, e := range entries { if e.name == name { if blobSha == nil { // Remove TODO entries = append(entries[:i], entries[i+1:]...) } else { entries[i].sha = blobSha } found = true break } } if !found && blobSha != nil { entries = append(entries, treeEntry{ mode: "100644", name: name, sha: blobSha, }) } treeCache[dir] = entries } built := make(map[string][]byte) var build func(string) ([]byte, error) build = func(prefix string) ([]byte, error) { entries := treeCache[prefix] for i, e := range entries { if e.mode == "40000" { subPrefix := path.Join(prefix, e.name) if sha, ok := built[subPrefix]; ok { entries[i].sha = sha continue } newShaStr, err := build(subPrefix) if err != nil { return nil, err } entries[i].sha = newShaStr } } shaStr, err := writeTree(ctx, repoPath, entries) if err != nil { return nil, err } shaBytes, err := hex.DecodeString(shaStr) if err != nil { return nil, err } built[prefix] = shaBytes return shaBytes, nil } rootShaBytes, err := build("") if err != nil { return "", err } return hex.EncodeToString(rootShaBytes), nil } type treeEntry struct { mode string // like "100644" name string // individual name sha []byte }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package main import ( "net/http" "strings" "go.lindenii.runxiyu.org/forge/git2c"
"go.lindenii.runxiyu.org/forge/render"
) type commitDisplay struct { Hash string Author string Email string Date string Message string } // httpHandleRepoIndex provides the front page of a repo using git2d. func httpHandleRepoIndex(w http.ResponseWriter, req *http.Request, params map[string]any) { repoName := params["repo_name"].(string) groupPath := params["group_path"].([]string) _, repoPath, _, _, _, _, _ := getRepoInfo(req.Context(), groupPath, repoName, "") // TODO: Don't use getRepoInfo var notes []string if strings.Contains(repoName, "\n") || sliceContainsNewlines(groupPath) { notes = append(notes, "Path contains newlines; HTTP Git access impossible") } client, err := git2c.NewClient(config.Git.Socket) if err != nil { errorPage500(w, params, err.Error()) return } defer client.Close() commits, readme, err := client.Cmd1(repoPath) if err != nil { errorPage500(w, params, err.Error()) return } params["commits"] = commits params["readme_filename"] = readme.Filename
_, params["readme"] = renderReadme(readme.Content, readme.Filename)
_, params["readme"] = render.Readme(readme.Content, readme.Filename)
params["notes"] = notes renderTemplate(w, "repo_index", params) // TODO: Caching }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package main import ( "html/template" "net/http" "strings" "go.lindenii.runxiyu.org/forge/git2c"
"go.lindenii.runxiyu.org/forge/render"
) // httpHandleRepoTree provides a friendly, syntax-highlighted view of // individual files, and provides directory views that link to these files. // // TODO: Do not highlight files that are too large. func httpHandleRepoTree(writer http.ResponseWriter, request *http.Request, params map[string]any) { repoName := params["repo_name"].(string) groupPath := params["group_path"].([]string) rawPathSpec := params["rest"].(string) pathSpec := strings.TrimSuffix(rawPathSpec, "/") params["path_spec"] = pathSpec _, repoPath, _, _, _, _, _ := getRepoInfo(request.Context(), groupPath, repoName, "") client, err := git2c.NewClient(config.Git.Socket) if err != nil { errorPage500(writer, params, err.Error()) return } defer client.Close() files, content, err := client.Cmd2(repoPath, pathSpec) if err != nil { errorPage500(writer, params, err.Error()) return } switch { case files != nil: params["files"] = files params["readme_filename"] = "README.md" params["readme"] = template.HTML("<p>README rendering here is WIP again</p>") // TODO renderTemplate(writer, "repo_tree_dir", params) case content != "":
rendered := renderHighlightedFile(pathSpec, content)
rendered := render.Highlight(pathSpec, content)
params["file_contents"] = rendered renderTemplate(writer, "repo_tree_file", params) default: errorPage500(writer, params, "Unknown object type, something is seriously wrong") } }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package main import ( "bytes" "crypto/rand" "encoding/hex" "fmt" "io" "os" "os/exec" "strings" "time" "github.com/bluekeyes/go-gitdiff/gitdiff" "github.com/go-git/go-git/v5"
"go.lindenii.runxiyu.org/forge/misc"
) func lmtpHandlePatch(session *lmtpSession, groupPath []string, repoName string, mbox io.Reader) (err error) { var diffFiles []*gitdiff.File var preamble string if diffFiles, preamble, err = gitdiff.Parse(mbox); err != nil { return fmt.Errorf("failed to parse patch: %w", err) } var header *gitdiff.PatchHeader if header, err = gitdiff.ParsePatchHeader(preamble); err != nil { return fmt.Errorf("failed to parse patch headers: %w", err) } var repo *git.Repository var fsPath string repo, _, _, fsPath, err = openRepo(session.ctx, groupPath, repoName) if err != nil { return fmt.Errorf("failed to open repo: %w", err) } headRef, err := repo.Head() if err != nil { return fmt.Errorf("failed to get repo head hash: %w", err) } headCommit, err := repo.CommitObject(headRef.Hash()) if err != nil { return fmt.Errorf("failed to get repo head commit: %w", err) } headTree, err := headCommit.Tree() if err != nil { return fmt.Errorf("failed to get repo head tree: %w", err) } headTreeHash := headTree.Hash.String() blobUpdates := make(map[string][]byte) for _, diffFile := range diffFiles { sourceFile, err := headTree.File(diffFile.OldName) if err != nil { return fmt.Errorf("failed to get file at old name %#v: %w", diffFile.OldName, err) } sourceString, err := sourceFile.Contents() if err != nil { return fmt.Errorf("failed to get contents: %w", err) }
sourceBuf := bytes.NewReader(stringToBytes(sourceString))
sourceBuf := bytes.NewReader(misc.StringToBytes(sourceString))
var patchedBuf bytes.Buffer if err := gitdiff.Apply(&patchedBuf, sourceBuf, diffFile); err != nil { return fmt.Errorf("failed to apply patch: %w", err) } var hashBuf bytes.Buffer // It's really difficult to do this via go-git so we're just // going to use upstream git for now. // TODO cmd := exec.CommandContext(session.ctx, "git", "hash-object", "-w", "-t", "blob", "--stdin") cmd.Env = append(os.Environ(), "GIT_DIR="+fsPath) cmd.Stdout = &hashBuf cmd.Stdin = &patchedBuf if err := cmd.Run(); err != nil { return fmt.Errorf("failed to run git hash-object: %w", err) } newHashStr := strings.TrimSpace(hashBuf.String()) newHash, err := hex.DecodeString(newHashStr) if err != nil { return fmt.Errorf("failed to decode hex string from git: %w", err) } blobUpdates[diffFile.NewName] = newHash if diffFile.NewName != diffFile.OldName { blobUpdates[diffFile.OldName] = nil // Mark for deletion. } } newTreeSha, err := buildTreeRecursive(session.ctx, fsPath, headTreeHash, blobUpdates) if err != nil { return fmt.Errorf("failed to recursively build a tree: %w", err) } commitMsg := header.Title if header.Body != "" { commitMsg += "\n\n" + header.Body } env := append(os.Environ(), "GIT_DIR="+fsPath, "GIT_AUTHOR_NAME="+header.Author.Name, "GIT_AUTHOR_EMAIL="+header.Author.Email, "GIT_AUTHOR_DATE="+header.AuthorDate.Format(time.RFC3339), ) commitCmd := exec.CommandContext(session.ctx, "git", "commit-tree", newTreeSha, "-p", headCommit.Hash.String(), "-m", commitMsg) commitCmd.Env = env var commitOut bytes.Buffer commitCmd.Stdout = &commitOut if err := commitCmd.Run(); err != nil { return fmt.Errorf("failed to commit tree: %w", err) } newCommitSha := strings.TrimSpace(commitOut.String()) newBranchName := rand.Text() refCmd := exec.CommandContext(session.ctx, "git", "update-ref", "refs/heads/contrib/"+newBranchName, newCommitSha) //#nosec G204 refCmd.Env = append(os.Environ(), "GIT_DIR="+fsPath) if err := refCmd.Run(); err != nil { return fmt.Errorf("failed to update ref: %w", err) } return nil }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package main import ( "bytes" "html" "html/template" "strings" "github.com/microcosm-cc/bluemonday" "github.com/niklasfasching/go-org/org" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" ) var markdownConverter = goldmark.New(goldmark.WithExtensions(extension.GFM)) // escapeHTML just escapes a string and wraps it in [template.HTML]. func escapeHTML(s string) template.HTML { return template.HTML(html.EscapeString(s)) //#nosec G203 } // renderReadme renders and sanitizes README content from a byte slice and filename. func renderReadme(data []byte, filename string) (string, template.HTML) { switch strings.ToLower(filename) { case "readme": return "README", template.HTML("<pre>" + html.EscapeString(bytesToString(data)) + "</pre>") //#nosec G203 case "readme.md": var buf bytes.Buffer if err := markdownConverter.Convert(data, &buf); err != nil { return "Error fetching README", escapeHTML("Unable to render README: " + err.Error()) } return "README.md", template.HTML(bluemonday.UGCPolicy().SanitizeBytes(buf.Bytes())) //#nosec G203 case "readme.org": htmlStr, err := org.New().Parse(strings.NewReader(bytesToString(data)), filename).Write(org.NewHTMLWriter()) if err != nil { return "Error fetching README", escapeHTML("Unable to render README: " + err.Error()) } return "README.org", template.HTML(bluemonday.UGCPolicy().Sanitize(htmlStr)) //#nosec G203 default: return filename, template.HTML("<pre>" + html.EscapeString(bytesToString(data)) + "</pre>") //#nosec G203 } }
package render import ( "html" "html/template" ) // EscapeHTML just escapes a string and wraps it in [template.HTML]. func EscapeHTML(s string) template.HTML { return template.HTML(html.EscapeString(s)) //#nosec G203 }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package render import ( "bytes" "html" "html/template" "strings" "github.com/microcosm-cc/bluemonday" "github.com/niklasfasching/go-org/org" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" "go.lindenii.runxiyu.org/forge/misc" ) var markdownConverter = goldmark.New(goldmark.WithExtensions(extension.GFM)) // renderReadme renders and sanitizes README content from a byte slice and filename. func Readme(data []byte, filename string) (string, template.HTML) { switch strings.ToLower(filename) { case "readme": return "README", template.HTML("<pre>" + html.EscapeString(misc.BytesToString(data)) + "</pre>") //#nosec G203 case "readme.md": var buf bytes.Buffer if err := markdownConverter.Convert(data, &buf); err != nil { return "Error fetching README", EscapeHTML("Unable to render README: " + err.Error()) } return "README.md", template.HTML(bluemonday.UGCPolicy().SanitizeBytes(buf.Bytes())) //#nosec G203 case "readme.org": htmlStr, err := org.New().Parse(strings.NewReader(misc.BytesToString(data)), filename).Write(org.NewHTMLWriter()) if err != nil { return "Error fetching README", EscapeHTML("Unable to render README: " + err.Error()) } return "README.org", template.HTML(bluemonday.UGCPolicy().Sanitize(htmlStr)) //#nosec G203 default: return filename, template.HTML("<pre>" + html.EscapeString(misc.BytesToString(data)) + "</pre>") //#nosec G203 } }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package main import ( "embed" "html/template" "io/fs" "net/http" "github.com/tdewolff/minify/v2" "github.com/tdewolff/minify/v2/html"
"go.lindenii.runxiyu.org/forge/misc"
) //go:embed LICENSE source.tar.gz var sourceFS embed.FS var sourceHandler = http.StripPrefix( "/-/source/", http.FileServer(http.FS(sourceFS)), ) //go:embed templates/* static/* //go:embed hookc/hookc git2d/git2d var resourcesFS embed.FS var templates *template.Template // loadTemplates minifies and loads HTML templates. func loadTemplates() (err error) { minifier := minify.New() minifierOptions := html.Minifier{ TemplateDelims: [2]string{"{{", "}}"}, KeepDefaultAttrVals: true, } //exhaustruct:ignore minifier.Add("text/html", &minifierOptions) templates = template.New("templates").Funcs(template.FuncMap{ "first_line": firstLine, "path_escape": pathEscape, "query_escape": queryEscape, "dereference_error": dereferenceOrZero[error], "minus": minus, }) err = fs.WalkDir(resourcesFS, "templates", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() { content, err := fs.ReadFile(resourcesFS, path) if err != nil { return err } minified, err := minifier.Bytes("text/html", content) if err != nil { return err }
_, err = templates.Parse(bytesToString(minified))
_, err = templates.Parse(misc.BytesToString(minified))
if err != nil { return err } } return nil }) return err } var staticHandler http.Handler // This init sets up static handlers. The resulting handlers must be // used in the HTTP router, and do nothing unless called from elsewhere. func init() { staticFS, err := fs.Sub(resourcesFS, "static") if err != nil { panic(err) } staticHandler = http.StripPrefix("/-/static/", http.FileServer(http.FS(staticFS))) }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package main import ( "fmt" "net" "os" "strings" gliderSSH "github.com/gliderlabs/ssh"
"go.lindenii.runxiyu.org/forge/misc"
"go.lindenii.runxiyu.org/lindenii-common/ansiec" "go.lindenii.runxiyu.org/lindenii-common/clog" goSSH "golang.org/x/crypto/ssh" ) var ( serverPubkeyString string serverPubkeyFP string serverPubkey goSSH.PublicKey ) // serveSSH serves SSH on a [net.Listener]. The listener should generally be a // TCP listener, although AF_UNIX SOCK_STREAM listeners may be appropriate in // rare cases. func serveSSH(listener net.Listener) error { var hostKeyBytes []byte var hostKey goSSH.Signer var err error var server *gliderSSH.Server if hostKeyBytes, err = os.ReadFile(config.SSH.Key); err != nil { return err } if hostKey, err = goSSH.ParsePrivateKey(hostKeyBytes); err != nil { return err } serverPubkey = hostKey.PublicKey()
serverPubkeyString = bytesToString(goSSH.MarshalAuthorizedKey(serverPubkey))
serverPubkeyString = misc.BytesToString(goSSH.MarshalAuthorizedKey(serverPubkey))
serverPubkeyFP = goSSH.FingerprintSHA256(serverPubkey) server = &gliderSSH.Server{ Handler: func(session gliderSSH.Session) { clientPubkey := session.PublicKey() var clientPubkeyStr string if clientPubkey != nil {
clientPubkeyStr = strings.TrimSuffix(bytesToString(goSSH.MarshalAuthorizedKey(clientPubkey)), "\n")
clientPubkeyStr = strings.TrimSuffix(misc.BytesToString(goSSH.MarshalAuthorizedKey(clientPubkey)), "\n")
} clog.Info("Incoming SSH: " + session.RemoteAddr().String() + " " + clientPubkeyStr + " " + session.RawCommand()) fmt.Fprintln(session.Stderr(), ansiec.Blue+"Lindenii Forge "+VERSION+", source at "+strings.TrimSuffix(config.HTTP.Root, "/")+"/-/source/"+ansiec.Reset+"\r") cmd := session.Command() if len(cmd) < 2 { fmt.Fprintln(session.Stderr(), "Insufficient arguments\r") return } switch cmd[0] { case "git-upload-pack": if len(cmd) > 2 { fmt.Fprintln(session.Stderr(), "Too many arguments\r") return } err = sshHandleUploadPack(session, clientPubkeyStr, cmd[1]) case "git-receive-pack": if len(cmd) > 2 { fmt.Fprintln(session.Stderr(), "Too many arguments\r") return } err = sshHandleRecvPack(session, clientPubkeyStr, cmd[1]) default: fmt.Fprintln(session.Stderr(), "Unsupported command: "+cmd[0]+"\r") return } if err != nil { fmt.Fprintln(session.Stderr(), err.Error()) return } }, PublicKeyHandler: func(_ gliderSSH.Context, _ gliderSSH.PublicKey) bool { return true }, KeyboardInteractiveHandler: func(_ gliderSSH.Context, _ goSSH.KeyboardInteractiveChallenge) bool { return true }, // It is intentional that we do not check any credentials and accept all connections. // This allows all users to connect and clone repositories. However, the public key // is passed to handlers, so e.g. the push handler could check the key and reject the // push if it needs to. } //exhaustruct:ignore server.AddHostKey(hostKey) if err = server.Serve(listener); err != nil { clog.Fatal(1, "Serving SSH: "+err.Error()) } return nil }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package main
package misc
import "unsafe"
// stringToBytes converts a string to a byte slice without copying the string.
// StringToBytes converts a string to a byte slice without copying the string.
// Memory is borrowed from the string. // The resulting byte slice must not be modified in any form.
func stringToBytes(s string) (bytes []byte) {
func StringToBytes(s string) (bytes []byte) {
return unsafe.Slice(unsafe.StringData(s), len(s)) }
// bytesToString converts a byte slice to a string without copying the bytes.
// BytesToString converts a byte slice to a string without copying the bytes.
// Memory is borrowed from the byte slice. // The source byte slice must not be modified.
func bytesToString(b []byte) string {
func BytesToString(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b)) }