diff options
author | Runxi Yu <me@runxiyu.org> | 2025-08-12 22:30:39 +0800 |
---|---|---|
committer | Runxi Yu <me@runxiyu.org> | 2025-08-12 22:30:39 +0800 |
commit | baaf3d6121a3a05509de704b339ad5a16efd9334 (patch) | |
tree | 9fa2bc1d96d6e985f6c1fbae2253756b9d44a636 | |
parent | Fix selection box text color (diff) | |
download | forge-test-0001.tar.gz forge-test-0001.tar.zst forge-test-0001.zip |
Shittest-0001
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | forged/internal/config/config.go (renamed from forged/internal/unsorted/config.go) | 41 | ||||
-rw-r--r-- | forged/internal/models/users.go (renamed from forged/internal/unsorted/users.go) | 9 | ||||
-rw-r--r-- | forged/internal/repos/acl.go (renamed from forged/internal/unsorted/acl.go) | 9 | ||||
-rw-r--r-- | forged/internal/repos/repos.go (renamed from forged/internal/unsorted/git_misc.go) | 11 | ||||
-rw-r--r-- | forged/internal/server/fedauth.go (renamed from forged/internal/unsorted/fedauth.go) | 2 | ||||
-rw-r--r-- | forged/internal/server/git_hooks_handle_linux.go (renamed from forged/internal/unsorted/git_hooks_handle_linux.go) | 55 | ||||
-rw-r--r-- | forged/internal/server/git_hooks_handle_other.go (renamed from forged/internal/unsorted/git_hooks_handle_other.go) | 51 | ||||
-rw-r--r-- | forged/internal/server/git_init.go (renamed from forged/internal/unsorted/git_init.go) | 2 | ||||
-rw-r--r-- | forged/internal/server/git_plumbing.go (renamed from forged/internal/unsorted/git_plumbing.go) | 2 | ||||
-rw-r--r-- | forged/internal/server/git_ref.go (renamed from forged/internal/unsorted/git_ref.go) | 2 | ||||
-rw-r--r-- | forged/internal/server/lmtp_handle_patch.go (renamed from forged/internal/unsorted/lmtp_handle_patch.go) | 5 | ||||
-rw-r--r-- | forged/internal/server/lmtp_server.go (renamed from forged/internal/unsorted/lmtp_server.go) | 2 | ||||
-rw-r--r-- | forged/internal/server/server.go (renamed from forged/internal/unsorted/server.go) | 56 | ||||
-rw-r--r-- | forged/internal/server/unsorted.go (renamed from forged/internal/unsorted/unsorted.go) | 4 | ||||
-rw-r--r-- | forged/internal/server/version.go (renamed from forged/internal/unsorted/version.go) | 2 | ||||
-rw-r--r-- | forged/internal/ssh/receive_pack.go (renamed from forged/internal/unsorted/ssh_handle_receive_pack.go) | 19 | ||||
-rw-r--r-- | forged/internal/ssh/server.go (renamed from forged/internal/unsorted/ssh_server.go) | 49 | ||||
-rw-r--r-- | forged/internal/ssh/upload_pack.go (renamed from forged/internal/unsorted/ssh_handle_upload_pack.go) | 2 | ||||
-rw-r--r-- | forged/internal/ssh/utils.go (renamed from forged/internal/unsorted/ssh_utils.go) | 23 | ||||
-rw-r--r-- | forged/internal/web/database.go (renamed from forged/internal/unsorted/database.go) | 2 | ||||
-rw-r--r-- | forged/internal/web/http_auth.go (renamed from forged/internal/unsorted/http_auth.go) | 2 | ||||
-rw-r--r-- | forged/internal/web/http_handle_branches.go (renamed from forged/internal/unsorted/http_handle_branches.go) | 2 | ||||
-rw-r--r-- | forged/internal/web/http_handle_group_index.go (renamed from forged/internal/unsorted/http_handle_group_index.go) | 31 | ||||
-rw-r--r-- | forged/internal/web/http_handle_index.go (renamed from forged/internal/unsorted/http_handle_index.go) | 6 | ||||
-rw-r--r-- | forged/internal/web/http_handle_login.go (renamed from forged/internal/unsorted/http_handle_login.go) | 11 | ||||
-rw-r--r-- | forged/internal/web/http_handle_repo_commit.go (renamed from forged/internal/unsorted/http_handle_repo_commit.go) | 9 | ||||
-rw-r--r-- | forged/internal/web/http_handle_repo_contrib_index.go (renamed from forged/internal/unsorted/http_handle_repo_contrib_index.go) | 9 | ||||
-rw-r--r-- | forged/internal/web/http_handle_repo_contrib_one.go (renamed from forged/internal/unsorted/http_handle_repo_contrib_one.go) | 21 | ||||
-rw-r--r-- | forged/internal/web/http_handle_repo_index.go (renamed from forged/internal/unsorted/http_handle_repo_index.go) | 10 | ||||
-rw-r--r-- | forged/internal/web/http_handle_repo_info.go (renamed from forged/internal/unsorted/http_handle_repo_info.go) | 2 | ||||
-rw-r--r-- | forged/internal/web/http_handle_repo_log.go (renamed from forged/internal/unsorted/http_handle_repo_log.go) | 10 | ||||
-rw-r--r-- | forged/internal/web/http_handle_repo_raw.go (renamed from forged/internal/unsorted/http_handle_repo_raw.go) | 12 | ||||
-rw-r--r-- | forged/internal/web/http_handle_repo_tree.go (renamed from forged/internal/unsorted/http_handle_repo_tree.go) | 12 | ||||
-rw-r--r-- | forged/internal/web/http_handle_repo_upload_pack.go (renamed from forged/internal/unsorted/http_handle_repo_upload_pack.go) | 10 | ||||
-rw-r--r-- | forged/internal/web/http_handle_users.go (renamed from forged/internal/unsorted/http_handle_users.go) | 6 | ||||
-rw-r--r-- | forged/internal/web/remote_url.go (renamed from forged/internal/unsorted/remote_url.go) | 2 | ||||
-rw-r--r-- | forged/internal/web/resources.go (renamed from forged/internal/unsorted/resources.go) | 2 | ||||
-rw-r--r-- | forged/internal/web/server.go (renamed from forged/internal/unsorted/http_server.go) | 42 | ||||
-rw-r--r-- | forged/internal/web/template.go (renamed from forged/internal/unsorted/http_template.go) | 2 | ||||
-rw-r--r-- | forged/internal/web/web.go | 48 | ||||
-rw-r--r-- | forged/main.go | 4 |
42 files changed, 337 insertions, 266 deletions
@@ -17,7 +17,7 @@ EMBED = git2d/git2d hookc/hookc $(wildcard LICENSE*) $(wildcard forged/static/*) EMBED_ = $(EMBED:%=forged/internal/embed/%) forge: $(EMBED_) $(SOURCE_FILES) - CGO_ENABLED=0 go build -o forge -ldflags '-extldflags "-f no-PIC -static" -X "go.lindenii.runxiyu.org/forge/forged/internal/unsorted.version=$(VERSION)"' -tags 'osusergo netgo static_build' ./forged + CGO_ENABLED=0 go build -o forge -ldflags '-extldflags "-f no-PIC -static" -X "go.lindenii.runxiyu.org/forge/forged/internal/server.version=$(VERSION)"' -tags 'osusergo netgo static_build' ./forged utils/colb: diff --git a/forged/internal/unsorted/config.go b/forged/internal/config/config.go index 9f07480..6eb36ab 100644 --- a/forged/internal/unsorted/config.go +++ b/forged/internal/config/config.go @@ -1,19 +1,18 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package config import ( "bufio" - "errors" "log/slog" "os" - "go.lindenii.runxiyu.org/forge/forged/internal/database" "go.lindenii.runxiyu.org/forge/forged/internal/irc" "go.lindenii.runxiyu.org/forge/forged/internal/scfg" ) +// Config holds runtime configuration for the Forge server. type Config struct { HTTP struct { Net string `scfg:"net"` @@ -61,34 +60,24 @@ type Config struct { } `scfg:"pprof"` } -// LoadConfig loads a configuration file from the specified path and unmarshals -// it to the global [config] struct. This may race with concurrent reads from -// [config]; additional synchronization is necessary if the configuration is to -// be made reloadable. -func (s *Server) loadConfig(path string) (err error) { - var configFile *os.File - if configFile, err = os.Open(path); err != nil { - return err +// Load reads the configuration file from the given path and unmarshals it into +// a Config value. +func Load(path string) (Config, error) { + var cfg Config + + f, err := os.Open(path) + if err != nil { + return cfg, err } - defer configFile.Close() + defer f.Close() - decoder := scfg.NewDecoder(bufio.NewReader(configFile)) - if err = decoder.Decode(&s.config); err != nil { - return err + decoder := scfg.NewDecoder(bufio.NewReader(f)) + if err = decoder.Decode(&cfg); err != nil { + return cfg, err } for _, u := range decoder.UnknownDirectives() { slog.Warn("unknown configuration directive", "directive", u) } - if s.config.DB.Type != "postgres" { - return errors.New("unsupported database type") - } - - if s.database, err = database.Open(s.config.DB.Conn); err != nil { - return err - } - - s.globalData["forge_title"] = s.config.General.Title - - return nil + return cfg, nil } diff --git a/forged/internal/unsorted/users.go b/forged/internal/models/users.go index 0f72eed..61c77a3 100644 --- a/forged/internal/unsorted/users.go +++ b/forged/internal/models/users.go @@ -1,21 +1,22 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package models import ( "context" "github.com/jackc/pgx/v5" + "go.lindenii.runxiyu.org/forge/forged/internal/database" ) -// addUserSSH adds a new user solely based on their SSH public key. +// AddUserSSH adds a new user solely based on their SSH public key. // // TODO: Audit all users of this function. -func (s *Server) addUserSSH(ctx context.Context, pubkey string) (userID int, err error) { +func AddUserSSH(ctx context.Context, db database.Database, pubkey string) (userID int, err error) { var txn pgx.Tx - if txn, err = s.database.Begin(ctx); err != nil { + if txn, err = db.Begin(ctx); err != nil { return } defer func() { diff --git a/forged/internal/unsorted/acl.go b/forged/internal/repos/acl.go index c2e887d..0727e91 100644 --- a/forged/internal/unsorted/acl.go +++ b/forged/internal/repos/acl.go @@ -1,20 +1,21 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package repos import ( "context" "github.com/jackc/pgx/v5/pgtype" + "go.lindenii.runxiyu.org/forge/forged/internal/database" ) -// getRepoInfo returns the filesystem path and direct access permission for a +// GetInfo returns the filesystem path and direct access permission for a // given repo and a provided ssh public key. // // TODO: Revamp. -func (s *Server) getRepoInfo(ctx context.Context, groupPath []string, repoName, sshPubkey string) (repoID int, fsPath string, access bool, contribReq, userType string, userID int, err error) { - err = s.database.QueryRow(ctx, ` +func GetInfo(ctx context.Context, db database.Database, groupPath []string, repoName, sshPubkey string) (repoID int, fsPath string, access bool, contribReq, userType string, userID int, err error) { + err = db.QueryRow(ctx, ` WITH RECURSIVE group_path_cte AS ( -- Start: match the first name in the path where parent_group IS NULL SELECT diff --git a/forged/internal/unsorted/git_misc.go b/forged/internal/repos/repos.go index dd93726..e2a9467 100644 --- a/forged/internal/unsorted/git_misc.go +++ b/forged/internal/repos/repos.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package repos import ( "context" @@ -12,15 +12,16 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" "github.com/jackc/pgx/v5/pgtype" + "go.lindenii.runxiyu.org/forge/forged/internal/database" ) -// openRepo opens a git repository by group and repo name. +// Open opens a git repository by group and repo name. // // TODO: This should be deprecated in favor of doing it in the relevant // request/router context in the future, as it cannot cover the nuance of // fields needed. -func (s *Server) openRepo(ctx context.Context, groupPath []string, repoName string) (repo *git.Repository, description string, repoID int, fsPath string, err error) { - err = s.database.QueryRow(ctx, ` +func Open(ctx context.Context, db database.Database, groupPath []string, repoName string) (repo *git.Repository, description string, repoID int, fsPath string, err error) { + err = db.QueryRow(ctx, ` WITH RECURSIVE group_path_cte AS ( -- Start: match the first name in the path where parent_group IS NULL SELECT @@ -67,7 +68,7 @@ WHERE g.depth = cardinality($1::text[]) // The pointer to error is guaranteed to be populated with either nil or the // error returned by the commit iterator after the returned iterator is // finished. -func commitIterSeqErr(ctx context.Context, commitIter object.CommitIter) (iter.Seq[*object.Commit], *error) { +func CommitIterSeqErr(ctx context.Context, commitIter object.CommitIter) (iter.Seq[*object.Commit], *error) { var err error return func(yield func(*object.Commit) bool) { for { diff --git a/forged/internal/unsorted/fedauth.go b/forged/internal/server/fedauth.go index f54649b..6948767 100644 --- a/forged/internal/unsorted/fedauth.go +++ b/forged/internal/server/fedauth.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package server import ( "bufio" diff --git a/forged/internal/unsorted/git_hooks_handle_linux.go b/forged/internal/server/git_hooks_handle_linux.go index f904550..e36e12e 100644 --- a/forged/internal/unsorted/git_hooks_handle_linux.go +++ b/forged/internal/server/git_hooks_handle_linux.go @@ -3,7 +3,7 @@ // //go:build linux -package unsorted +package server import ( "bytes" @@ -24,6 +24,7 @@ import ( "github.com/jackc/pgx/v5" "go.lindenii.runxiyu.org/forge/forged/internal/ansiec" "go.lindenii.runxiyu.org/forge/forged/internal/misc" + "go.lindenii.runxiyu.org/forge/forged/internal/ssh" ) var ( @@ -39,7 +40,7 @@ func (s *Server) hooksHandler(conn net.Conn) { var ucred *syscall.Ucred var err error var cookie []byte - var packPass packPass + var packPass ssh.PackPass var sshStderr io.Writer var hookRet byte @@ -53,7 +54,7 @@ func (s *Server) hooksHandler(conn net.Conn) { if _, err = conn.Write([]byte{1}); err != nil { return } - writeRedError(conn, "\nUnable to get peer credentials: %v", err) + ssh.WriteRedError(conn, "\nUnable to get peer credentials: %v", err) return } uint32uid := uint32(os.Getuid()) //#nosec G115 @@ -61,7 +62,7 @@ func (s *Server) hooksHandler(conn net.Conn) { if _, err = conn.Write([]byte{1}); err != nil { return } - writeRedError(conn, "\nUID mismatch") + ssh.WriteRedError(conn, "\nUID mismatch") return } @@ -70,7 +71,7 @@ func (s *Server) hooksHandler(conn net.Conn) { if _, err = conn.Write([]byte{1}); err != nil { return } - writeRedError(conn, "\nFailed to read cookie: %v", err) + ssh.WriteRedError(conn, "\nFailed to read cookie: %v", err) return } @@ -81,7 +82,7 @@ func (s *Server) hooksHandler(conn net.Conn) { if _, err = conn.Write([]byte{1}); err != nil { return } - writeRedError(conn, "\nInvalid handler cookie") + ssh.WriteRedError(conn, "\nInvalid handler cookie") return } } @@ -93,7 +94,7 @@ func (s *Server) hooksHandler(conn net.Conn) { hookRet = func() byte { var argc64 uint64 if err = binary.Read(conn, binary.NativeEndian, &argc64); err != nil { - writeRedError(sshStderr, "Failed to read argc: %v", err) + ssh.WriteRedError(sshStderr, "Failed to read argc: %v", err) return 1 } var args []string @@ -103,7 +104,7 @@ func (s *Server) hooksHandler(conn net.Conn) { nextByte := make([]byte, 1) n, err := conn.Read(nextByte) if err != nil || n != 1 { - writeRedError(sshStderr, "Failed to read arg: %v", err) + ssh.WriteRedError(sshStderr, "Failed to read arg: %v", err) return 1 } if nextByte[0] == 0 { @@ -121,7 +122,7 @@ func (s *Server) hooksHandler(conn net.Conn) { nextByte := make([]byte, 1) n, err := conn.Read(nextByte) if err != nil || n != 1 { - writeRedError(sshStderr, "Failed to read environment variable: %v", err) + ssh.WriteRedError(sshStderr, "Failed to read environment variable: %v", err) return 1 } if nextByte[0] == 0 { @@ -135,7 +136,7 @@ func (s *Server) hooksHandler(conn net.Conn) { kv := envLine.String() parts := strings.SplitN(kv, "=", 2) if len(parts) < 2 { - writeRedError(sshStderr, "Invalid environment variable line: %v", kv) + ssh.WriteRedError(sshStderr, "Invalid environment variable line: %v", kv) return 1 } gitEnv[parts[0]] = parts[1] @@ -143,7 +144,7 @@ func (s *Server) hooksHandler(conn net.Conn) { var stdin bytes.Buffer if _, err = io.Copy(&stdin, conn); err != nil { - writeRedError(conn, "Failed to read to the stdin buffer: %v", err) + ssh.WriteRedError(conn, "Failed to read to the stdin buffer: %v", err) } switch filepath.Base(args[0]) { @@ -161,7 +162,7 @@ func (s *Server) hooksHandler(conn net.Conn) { pushOptCount, err = strconv.Atoi(gitEnv["GIT_PUSH_OPTION_COUNT"]) if err != nil { - writeRedError(sshStderr, "Failed to parse GIT_PUSH_OPTION_COUNT: %v", err) + ssh.WriteRedError(sshStderr, "Failed to parse GIT_PUSH_OPTION_COUNT: %v", err) return 1 } @@ -169,37 +170,37 @@ func (s *Server) hooksHandler(conn net.Conn) { // 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") + ssh.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) + ssh.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) + ssh.WriteRedError(sshStderr, "Invalid federated user identifier %#v does not contain a colon", fedUserID) return 1 } ok, err := s.fedauth(ctx, packPass.userID, service, username, packPass.pubkey) if err != nil { - writeRedError(sshStderr, "Failed to verify federated user identifier %#v: %v", fedUserID, err) + ssh.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) + ssh.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") + ssh.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 } } @@ -209,20 +210,20 @@ func (s *Server) hooksHandler(conn net.Conn) { if errors.Is(err, io.EOF) { break } else if err != nil { - writeRedError(sshStderr, "Failed to read pre-receive line: %v", err) + ssh.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) + ssh.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) + ssh.WriteRedError(sshStderr, "Invalid pre-receive line: %v", line) return 1 } @@ -243,7 +244,7 @@ func (s *Server) hooksHandler(conn net.Conn) { ).Scan(&newMRLocalID) } if err != nil { - writeRedError(sshStderr, "Error creating merge request: %v", err) + ssh.WriteRedError(sshStderr, "Error creating merge request: %v", err) return 1 } mergeRequestWebURL := fmt.Sprintf("%s/contrib/%d/", s.genHTTPRemoteURL(packPass.groupPath, packPass.repoName), newMRLocalID) @@ -260,9 +261,9 @@ func (s *Server) hooksHandler(conn net.Conn) { ).Scan(&existingMRUser) if err != nil { if errors.Is(err, pgx.ErrNoRows) { - writeRedError(sshStderr, "No existing merge request for existing contrib branch: %v", err) + ssh.WriteRedError(sshStderr, "No existing merge request for existing contrib branch: %v", err) } else { - writeRedError(sshStderr, "Error querying for existing merge request: %v", err) + ssh.WriteRedError(sshStderr, "Error querying for existing merge request: %v", err) } return 1 } @@ -281,7 +282,7 @@ func (s *Server) hooksHandler(conn net.Conn) { oldHash = plumbing.NewHash(oldOID) if oldCommit, err = packPass.repo.CommitObject(oldHash); err != nil { - writeRedError(sshStderr, "Daemon failed to get old commit: %v", err) + ssh.WriteRedError(sshStderr, "Daemon failed to get old commit: %v", err) return 1 } @@ -291,12 +292,12 @@ func (s *Server) hooksHandler(conn net.Conn) { // 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) + ssh.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) + ssh.WriteRedError(sshStderr, "Daemon failed to check if old commit is ancestor: %v", err) return 1 } diff --git a/forged/internal/unsorted/git_hooks_handle_other.go b/forged/internal/server/git_hooks_handle_other.go index 70b2072..5849ce6 100644 --- a/forged/internal/unsorted/git_hooks_handle_other.go +++ b/forged/internal/server/git_hooks_handle_other.go @@ -3,7 +3,7 @@ // //go:build !linux -package unsorted +package server import ( "bytes" @@ -22,6 +22,7 @@ import ( "github.com/jackc/pgx/v5" "go.lindenii.runxiyu.org/forge/forged/internal/ansiec" "go.lindenii.runxiyu.org/forge/forged/internal/misc" + "go.lindenii.runxiyu.org/forge/forged/internal/ssh" ) // hooksHandler handles a connection from hookc via the @@ -31,7 +32,7 @@ func (s *Server) hooksHandler(conn net.Conn) { var cancel context.CancelFunc var err error var cookie []byte - var packPass packPass + var packPass ssh.PackPass var sshStderr io.Writer var hookRet byte @@ -46,7 +47,7 @@ func (s *Server) hooksHandler(conn net.Conn) { if _, err = conn.Write([]byte{1}); err != nil { return } - writeRedError(conn, "\nFailed to read cookie: %v", err) + ssh.WriteRedError(conn, "\nFailed to read cookie: %v", err) return } @@ -57,7 +58,7 @@ func (s *Server) hooksHandler(conn net.Conn) { if _, err = conn.Write([]byte{1}); err != nil { return } - writeRedError(conn, "\nInvalid handler cookie") + ssh.WriteRedError(conn, "\nInvalid handler cookie") return } } @@ -69,7 +70,7 @@ func (s *Server) hooksHandler(conn net.Conn) { hookRet = func() byte { var argc64 uint64 if err = binary.Read(conn, binary.NativeEndian, &argc64); err != nil { - writeRedError(sshStderr, "Failed to read argc: %v", err) + ssh.WriteRedError(sshStderr, "Failed to read argc: %v", err) return 1 } var args []string @@ -79,7 +80,7 @@ func (s *Server) hooksHandler(conn net.Conn) { nextByte := make([]byte, 1) n, err := conn.Read(nextByte) if err != nil || n != 1 { - writeRedError(sshStderr, "Failed to read arg: %v", err) + ssh.WriteRedError(sshStderr, "Failed to read arg: %v", err) return 1 } if nextByte[0] == 0 { @@ -97,7 +98,7 @@ func (s *Server) hooksHandler(conn net.Conn) { nextByte := make([]byte, 1) n, err := conn.Read(nextByte) if err != nil || n != 1 { - writeRedError(sshStderr, "Failed to read environment variable: %v", err) + ssh.WriteRedError(sshStderr, "Failed to read environment variable: %v", err) return 1 } if nextByte[0] == 0 { @@ -111,7 +112,7 @@ func (s *Server) hooksHandler(conn net.Conn) { kv := envLine.String() parts := strings.SplitN(kv, "=", 2) if len(parts) < 2 { - writeRedError(sshStderr, "Invalid environment variable line: %v", kv) + ssh.WriteRedError(sshStderr, "Invalid environment variable line: %v", kv) return 1 } gitEnv[parts[0]] = parts[1] @@ -119,7 +120,7 @@ func (s *Server) hooksHandler(conn net.Conn) { var stdin bytes.Buffer if _, err = io.Copy(&stdin, conn); err != nil { - writeRedError(conn, "Failed to read to the stdin buffer: %v", err) + ssh.WriteRedError(conn, "Failed to read to the stdin buffer: %v", err) } switch filepath.Base(args[0]) { @@ -137,7 +138,7 @@ func (s *Server) hooksHandler(conn net.Conn) { pushOptCount, err = strconv.Atoi(gitEnv["GIT_PUSH_OPTION_COUNT"]) if err != nil { - writeRedError(sshStderr, "Failed to parse GIT_PUSH_OPTION_COUNT: %v", err) + ssh.WriteRedError(sshStderr, "Failed to parse GIT_PUSH_OPTION_COUNT: %v", err) return 1 } @@ -145,37 +146,37 @@ func (s *Server) hooksHandler(conn net.Conn) { // 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") + ssh.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) + ssh.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) + ssh.WriteRedError(sshStderr, "Invalid federated user identifier %#v does not contain a colon", fedUserID) return 1 } ok, err := s.fedauth(ctx, packPass.userID, service, username, packPass.pubkey) if err != nil { - writeRedError(sshStderr, "Failed to verify federated user identifier %#v: %v", fedUserID, err) + ssh.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) + ssh.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") + ssh.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 } } @@ -185,20 +186,20 @@ func (s *Server) hooksHandler(conn net.Conn) { if errors.Is(err, io.EOF) { break } else if err != nil { - writeRedError(sshStderr, "Failed to read pre-receive line: %v", err) + ssh.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) + ssh.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) + ssh.WriteRedError(sshStderr, "Invalid pre-receive line: %v", line) return 1 } @@ -219,7 +220,7 @@ func (s *Server) hooksHandler(conn net.Conn) { ).Scan(&newMRLocalID) } if err != nil { - writeRedError(sshStderr, "Error creating merge request: %v", err) + ssh.WriteRedError(sshStderr, "Error creating merge request: %v", err) return 1 } mergeRequestWebURL := fmt.Sprintf("%s/contrib/%d/", s.genHTTPRemoteURL(packPass.groupPath, packPass.repoName), newMRLocalID) @@ -236,9 +237,9 @@ func (s *Server) hooksHandler(conn net.Conn) { ).Scan(&existingMRUser) if err != nil { if errors.Is(err, pgx.ErrNoRows) { - writeRedError(sshStderr, "No existing merge request for existing contrib branch: %v", err) + ssh.WriteRedError(sshStderr, "No existing merge request for existing contrib branch: %v", err) } else { - writeRedError(sshStderr, "Error querying for existing merge request: %v", err) + ssh.WriteRedError(sshStderr, "Error querying for existing merge request: %v", err) } return 1 } @@ -257,7 +258,7 @@ func (s *Server) hooksHandler(conn net.Conn) { oldHash = plumbing.NewHash(oldOID) if oldCommit, err = packPass.repo.CommitObject(oldHash); err != nil { - writeRedError(sshStderr, "Daemon failed to get old commit: %v", err) + ssh.WriteRedError(sshStderr, "Daemon failed to get old commit: %v", err) return 1 } @@ -267,12 +268,12 @@ func (s *Server) hooksHandler(conn net.Conn) { // 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) + ssh.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) + ssh.WriteRedError(sshStderr, "Daemon failed to check if old commit is ancestor: %v", err) return 1 } diff --git a/forged/internal/unsorted/git_init.go b/forged/internal/server/git_init.go index a9bba78..8ac036b 100644 --- a/forged/internal/unsorted/git_init.go +++ b/forged/internal/server/git_init.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package server import ( "github.com/go-git/go-git/v5" diff --git a/forged/internal/unsorted/git_plumbing.go b/forged/internal/server/git_plumbing.go index e7ebe8f..64569d8 100644 --- a/forged/internal/unsorted/git_plumbing.go +++ b/forged/internal/server/git_plumbing.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package server import ( "bytes" diff --git a/forged/internal/unsorted/git_ref.go b/forged/internal/server/git_ref.go index d9735ba..c459247 100644 --- a/forged/internal/unsorted/git_ref.go +++ b/forged/internal/server/git_ref.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package server import ( "github.com/go-git/go-git/v5" diff --git a/forged/internal/unsorted/lmtp_handle_patch.go b/forged/internal/server/lmtp_handle_patch.go index b258bfc..06096e8 100644 --- a/forged/internal/unsorted/lmtp_handle_patch.go +++ b/forged/internal/server/lmtp_handle_patch.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package server import ( "bytes" @@ -17,6 +17,7 @@ import ( "github.com/bluekeyes/go-gitdiff/gitdiff" "github.com/go-git/go-git/v5" "go.lindenii.runxiyu.org/forge/forged/internal/misc" + "go.lindenii.runxiyu.org/forge/forged/internal/repos" ) func (s *Server) lmtpHandlePatch(session *lmtpSession, groupPath []string, repoName string, mbox io.Reader) (err error) { @@ -33,7 +34,7 @@ func (s *Server) lmtpHandlePatch(session *lmtpSession, groupPath []string, repoN var repo *git.Repository var fsPath string - repo, _, _, fsPath, err = s.openRepo(session.ctx, groupPath, repoName) + repo, _, _, fsPath, err = repos.Open(session.ctx, s.database, groupPath, repoName) if err != nil { return fmt.Errorf("failed to open repo: %w", err) } diff --git a/forged/internal/unsorted/lmtp_server.go b/forged/internal/server/lmtp_server.go index 1e94894..83bad1e 100644 --- a/forged/internal/unsorted/lmtp_server.go +++ b/forged/internal/server/lmtp_server.go @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> // SPDX-FileCopyrightText: Copyright (c) 2024 Robin Jarry <robin@jarry.cc> -package unsorted +package server import ( "bytes" diff --git a/forged/internal/unsorted/server.go b/forged/internal/server/server.go index 84379b0..4ec0b5c 100644 --- a/forged/internal/unsorted/server.go +++ b/forged/internal/server/server.go @@ -1,12 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package server import ( "errors" - "html/template" - "io/fs" "log" "log/slog" "net" @@ -19,32 +17,30 @@ import ( "time" "go.lindenii.runxiyu.org/forge/forged/internal/cmap" + "go.lindenii.runxiyu.org/forge/forged/internal/config" "go.lindenii.runxiyu.org/forge/forged/internal/database" "go.lindenii.runxiyu.org/forge/forged/internal/embed" "go.lindenii.runxiyu.org/forge/forged/internal/irc" "go.lindenii.runxiyu.org/forge/forged/internal/misc" + "go.lindenii.runxiyu.org/forge/forged/internal/ssh" + "go.lindenii.runxiyu.org/forge/forged/internal/web" goSSH "golang.org/x/crypto/ssh" ) type Server struct { - config Config + config config.Config database database.Database - sourceHandler http.Handler - staticHandler http.Handler - - // globalData is passed as "global" when rendering HTML templates. - globalData map[string]any - serverPubkeyString string serverPubkeyFP string serverPubkey goSSH.PublicKey // packPasses contains hook cookies mapped to their packPass. - packPasses cmap.Map[string, packPass] + packPasses cmap.Map[string, ssh.PackPass] - templates *template.Template + web *web.Server + ssh *ssh.Server ircBot *irc.Bot @@ -52,31 +48,25 @@ type Server struct { } func NewServer(configPath string) (*Server, error) { - s := &Server{ - globalData: make(map[string]any), - } //exhaustruct:ignore - - s.sourceHandler = http.StripPrefix( - "/-/source/", - http.FileServer(http.FS(embed.Source)), - ) - staticFS, err := fs.Sub(embed.Resources, "forged/static") + s := &Server{} //exhaustruct:ignore + s.packPasses = cmap.Map[string, ssh.PackPass]{} + + cfg, err := config.Load(configPath) if err != nil { return s, err } - s.staticHandler = http.StripPrefix("/-/static/", http.FileServer(http.FS(staticFS))) - s.globalData = map[string]any{ - "server_public_key_string": &s.serverPubkeyString, - "server_public_key_fingerprint": &s.serverPubkeyFP, - "forge_version": version, - // Some other ones are populated after config parsing + if cfg.DB.Type != "postgres" { + return s, errors.New("unsupported database type") } - - if err := s.loadConfig(configPath); err != nil { + s.config = cfg + if s.database, err = database.Open(s.config.DB.Conn); err != nil { return s, err } - - misc.NoneOrPanic(s.loadTemplates()) + s.web, err = web.New(s.config, s.database, &s.serverPubkeyString, &s.serverPubkeyFP, version) + if err != nil { + return s, err + } + s.ssh = ssh.New(s.config, s.database, &s.serverPubkeyString, &s.serverPubkeyFP, &s.packPasses) misc.NoneOrPanic(misc.DeployBinary(misc.FirstOrPanic(embed.Resources.Open("git2d/git2d")), s.config.Git.DaemonPath)) misc.NoneOrPanic(misc.DeployBinary(misc.FirstOrPanic(embed.Resources.Open("hookc/hookc")), filepath.Join(s.config.Hooks.Execs, "pre-receive"))) misc.NoneOrPanic(os.Chmod(filepath.Join(s.config.Hooks.Execs, "pre-receive"), 0o755)) @@ -172,7 +162,7 @@ func (s *Server) Run() error { } slog.Info("listening SSH on", "net", s.config.SSH.Net, "addr", s.config.SSH.Addr) go func() { - if err = s.serveSSH(sshListener); err != nil { + if err = s.ssh.Serve(sshListener); err != nil { slog.Error("serving SSH", "error", err) os.Exit(1) } @@ -197,7 +187,7 @@ func (s *Server) Run() error { os.Exit(1) } server := http.Server{ - Handler: s, + Handler: s.web, ReadTimeout: time.Duration(s.config.HTTP.ReadTimeout) * time.Second, WriteTimeout: time.Duration(s.config.HTTP.ReadTimeout) * time.Second, IdleTimeout: time.Duration(s.config.HTTP.ReadTimeout) * time.Second, diff --git a/forged/internal/unsorted/unsorted.go b/forged/internal/server/unsorted.go index f26b0e4..81ef88a 100644 --- a/forged/internal/unsorted/unsorted.go +++ b/forged/internal/server/unsorted.go @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -// Package unsorted is where unsorted Go files from the old structure are kept. -package unsorted +// Package server contains the core orchestration components of the forge. +package server diff --git a/forged/internal/unsorted/version.go b/forged/internal/server/version.go index 52c0f32..da72389 100644 --- a/forged/internal/unsorted/version.go +++ b/forged/internal/server/version.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package server var version = "unknown" diff --git a/forged/internal/unsorted/ssh_handle_receive_pack.go b/forged/internal/ssh/receive_pack.go index a354273..57b46b0 100644 --- a/forged/internal/unsorted/ssh_handle_receive_pack.go +++ b/forged/internal/ssh/receive_pack.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package ssh import ( "errors" @@ -11,23 +11,12 @@ import ( gliderSSH "github.com/gliderlabs/ssh" "github.com/go-git/go-git/v5" + "go.lindenii.runxiyu.org/forge/forged/internal/models" ) // packPass contains information known when handling incoming SSH connections // that then needs to be used in hook socket connection handlers. See hookc(1). -type packPass struct { - session gliderSSH.Session - repo *git.Repository - pubkey string - directAccess bool - repoPath string - userID int - userType string - repoID int - groupPath []string - repoName string - contribReq string -} +type packPass = PackPass // sshHandleRecvPack handles attempts to push to repos. func (s *Server) sshHandleRecvPack(session gliderSSH.Session, pubkey, repoIdentifier string) (err error) { @@ -72,7 +61,7 @@ func (s *Server) sshHandleRecvPack(session gliderSSH.Session, pubkey, repoIdenti return errors.New("you need to have an SSH public key to push to this repo") } if userType == "" { - userID, err = s.addUserSSH(session.Context(), pubkey) + userID, err = models.AddUserSSH(session.Context(), s.database, pubkey) if err != nil { return err } diff --git a/forged/internal/unsorted/ssh_server.go b/forged/internal/ssh/server.go index 43cc0c4..b0b702d 100644 --- a/forged/internal/unsorted/ssh_server.go +++ b/forged/internal/ssh/server.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package ssh import ( "fmt" @@ -11,15 +11,50 @@ import ( "strings" gliderSSH "github.com/gliderlabs/ssh" + "github.com/go-git/go-git/v5" "go.lindenii.runxiyu.org/forge/forged/internal/ansiec" + "go.lindenii.runxiyu.org/forge/forged/internal/cmap" + "go.lindenii.runxiyu.org/forge/forged/internal/config" + "go.lindenii.runxiyu.org/forge/forged/internal/database" "go.lindenii.runxiyu.org/forge/forged/internal/misc" goSSH "golang.org/x/crypto/ssh" ) -// serveSSH serves SSH on a [net.Listener]. The listener should generally be a +// PackPass contains information known when handling incoming SSH connections +// that then needs to be used in hook socket connection handlers. See hookc(1). +type PackPass struct { + session gliderSSH.Session + repo *git.Repository + pubkey string + directAccess bool + repoPath string + userID int + userType string + repoID int + groupPath []string + repoName string + contribReq string +} + +// Server handles SSH connections. +type Server struct { + config config.Config + database database.Database + serverPubkey goSSH.PublicKey + serverPubkeyString *string + serverPubkeyFP *string + packPasses *cmap.Map[string, PackPass] +} + +// New creates a new SSH server. +func New(cfg config.Config, db database.Database, pubkeyStr, pubkeyFP *string, passes *cmap.Map[string, PackPass]) *Server { + return &Server{config: cfg, database: db, serverPubkeyString: pubkeyStr, serverPubkeyFP: pubkeyFP, packPasses: passes} +} + +// Serve 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 (s *Server) serveSSH(listener net.Listener) error { +func (s *Server) Serve(listener net.Listener) error { var hostKeyBytes []byte var hostKey goSSH.Signer var err error @@ -34,8 +69,12 @@ func (s *Server) serveSSH(listener net.Listener) error { } s.serverPubkey = hostKey.PublicKey() - s.serverPubkeyString = misc.BytesToString(goSSH.MarshalAuthorizedKey(s.serverPubkey)) - s.serverPubkeyFP = goSSH.FingerprintSHA256(s.serverPubkey) + if s.serverPubkeyString != nil { + *s.serverPubkeyString = misc.BytesToString(goSSH.MarshalAuthorizedKey(s.serverPubkey)) + } + if s.serverPubkeyFP != nil { + *s.serverPubkeyFP = goSSH.FingerprintSHA256(s.serverPubkey) + } server = &gliderSSH.Server{ Handler: func(session gliderSSH.Session) { diff --git a/forged/internal/unsorted/ssh_handle_upload_pack.go b/forged/internal/ssh/upload_pack.go index 735a053..92589e0 100644 --- a/forged/internal/unsorted/ssh_handle_upload_pack.go +++ b/forged/internal/ssh/upload_pack.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package ssh import ( "fmt" diff --git a/forged/internal/unsorted/ssh_utils.go b/forged/internal/ssh/utils.go index 6f50a87..0153dd7 100644 --- a/forged/internal/unsorted/ssh_utils.go +++ b/forged/internal/ssh/utils.go @@ -1,10 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package ssh import ( "context" + "crypto/rand" + "encoding/base64" "errors" "fmt" "io" @@ -12,6 +14,7 @@ import ( "go.lindenii.runxiyu.org/forge/forged/internal/ansiec" "go.lindenii.runxiyu.org/forge/forged/internal/misc" + "go.lindenii.runxiyu.org/forge/forged/internal/repos" ) var errIllegalSSHRepoPath = errors.New("illegal SSH repo path") @@ -64,16 +67,28 @@ func (s *Server) getRepoInfo2(ctx context.Context, sshPath, sshPubkey string) (g repoName = moduleName switch moduleType { case "repos": - _1, _2, _3, _4, _5, _6, _7 := s.getRepoInfo(ctx, groupPath, moduleName, sshPubkey) + _1, _2, _3, _4, _5, _6, _7 := repos.GetInfo(ctx, s.database, groupPath, moduleName, sshPubkey) return groupPath, repoName, _1, _2, _3, _4, _5, _6, _7 default: return []string{}, "", 0, "", false, "", "", 0, errIllegalSSHRepoPath } } -// writeRedError is a helper function that basically does a Fprintf but makes +// WriteRedError is a helper function that basically does a Fprintf but makes // the entire thing red, in terms of ANSI escape sequences. It's useful when // producing error messages on SSH connections. -func writeRedError(w io.Writer, format string, args ...any) { +// WriteRedError writes a formatted error in red ANSI color. +func WriteRedError(w io.Writer, format string, args ...any) { fmt.Fprintln(w, ansiec.Red+fmt.Sprintf(format, args...)+ansiec.Reset) } + +// randomUrlsafeStr generates a random string of the given entropic size +// using the URL-safe base64 encoding. The actual size of the string returned +// will be 4*sz. +func randomUrlsafeStr(sz int) (string, error) { + r := make([]byte, 3*sz) + if _, err := rand.Read(r); err != nil { + return "", fmt.Errorf("error generating random string: %w", err) + } + return base64.RawURLEncoding.EncodeToString(r), nil +} diff --git a/forged/internal/unsorted/database.go b/forged/internal/web/database.go index 222b0c4..aa3936d 100644 --- a/forged/internal/unsorted/database.go +++ b/forged/internal/web/database.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package web import ( "context" diff --git a/forged/internal/unsorted/http_auth.go b/forged/internal/web/http_auth.go index b0afa05..6946570 100644 --- a/forged/internal/unsorted/http_auth.go +++ b/forged/internal/web/http_auth.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package web import ( "net/http" diff --git a/forged/internal/unsorted/http_handle_branches.go b/forged/internal/web/http_handle_branches.go index 704e1d8..280daee 100644 --- a/forged/internal/unsorted/http_handle_branches.go +++ b/forged/internal/web/http_handle_branches.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package web import ( "net/http" diff --git a/forged/internal/unsorted/http_handle_group_index.go b/forged/internal/web/http_handle_group_index.go index ce28a1c..45f62bd 100644 --- a/forged/internal/unsorted/http_handle_group_index.go +++ b/forged/internal/web/http_handle_group_index.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package web import ( "errors" @@ -12,7 +12,6 @@ import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "go.lindenii.runxiyu.org/forge/forged/internal/misc" - "go.lindenii.runxiyu.org/forge/forged/internal/web" ) // httpHandleGroupIndex provides index pages for groups, which includes a list @@ -61,10 +60,10 @@ func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http. ).Scan(&groupID, &groupDesc) if errors.Is(err, pgx.ErrNoRows) { - web.ErrorPage404(s.templates, writer, params) + ErrorPage404(s.templates, writer, params) return } else if err != nil { - web.ErrorPage500(s.templates, writer, params, "Error getting group: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error getting group: "+err.Error()) return } @@ -77,14 +76,14 @@ func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http. AND group_id = $2 `, params["user_id"].(int), groupID).Scan(&count) if err != nil { - web.ErrorPage500(s.templates, writer, params, "Error checking access: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error checking access: "+err.Error()) return } directAccess := (count > 0) if request.Method == http.MethodPost { if !directAccess { - web.ErrorPage403(s.templates, writer, params, "You do not have direct access to this group") + ErrorPage403(s.templates, writer, params, "You do not have direct access to this group") return } @@ -92,7 +91,7 @@ func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http. repoDesc := request.FormValue("repo_desc") contribReq := request.FormValue("repo_contrib") if repoName == "" { - web.ErrorPage400(s.templates, writer, params, "Repo name is required") + ErrorPage400(s.templates, writer, params, "Repo name is required") return } @@ -108,7 +107,7 @@ func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http. contribReq, ).Scan(&newRepoID) if err != nil { - web.ErrorPage500(s.templates, writer, params, "Error creating repo: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error creating repo: "+err.Error()) return } @@ -123,12 +122,12 @@ func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http. newRepoID, ) if err != nil { - web.ErrorPage500(s.templates, writer, params, "Error updating repo path: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error updating repo path: "+err.Error()) return } if err = s.gitInit(filePath); err != nil { - web.ErrorPage500(s.templates, writer, params, "Error initializing repo: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error initializing repo: "+err.Error()) return } @@ -144,7 +143,7 @@ func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http. WHERE group_id = $1 `, groupID) if err != nil { - web.ErrorPage500(s.templates, writer, params, "Error getting repos: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error getting repos: "+err.Error()) return } defer rows.Close() @@ -152,13 +151,13 @@ func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http. for rows.Next() { var name, description string if err = rows.Scan(&name, &description); err != nil { - web.ErrorPage500(s.templates, writer, params, "Error getting repos: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error getting repos: "+err.Error()) return } repos = append(repos, nameDesc{name, description}) } if err = rows.Err(); err != nil { - web.ErrorPage500(s.templates, writer, params, "Error getting repos: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error getting repos: "+err.Error()) return } @@ -169,7 +168,7 @@ func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http. WHERE parent_group = $1 `, groupID) if err != nil { - web.ErrorPage500(s.templates, writer, params, "Error getting subgroups: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error getting subgroups: "+err.Error()) return } defer rows.Close() @@ -177,13 +176,13 @@ func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http. for rows.Next() { var name, description string if err = rows.Scan(&name, &description); err != nil { - web.ErrorPage500(s.templates, writer, params, "Error getting subgroups: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error getting subgroups: "+err.Error()) return } subgroups = append(subgroups, nameDesc{name, description}) } if err = rows.Err(); err != nil { - web.ErrorPage500(s.templates, writer, params, "Error getting subgroups: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error getting subgroups: "+err.Error()) return } diff --git a/forged/internal/unsorted/http_handle_index.go b/forged/internal/web/http_handle_index.go index a3141f4..e77155b 100644 --- a/forged/internal/unsorted/http_handle_index.go +++ b/forged/internal/web/http_handle_index.go @@ -1,12 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package web import ( "net/http" - - "go.lindenii.runxiyu.org/forge/forged/internal/web" ) // httpHandleIndex provides the main index page which includes a list of groups @@ -17,7 +15,7 @@ func (s *Server) httpHandleIndex(writer http.ResponseWriter, request *http.Reque groups, err = s.queryNameDesc(request.Context(), "SELECT name, COALESCE(description, '') FROM groups WHERE parent_group IS NULL") if err != nil { - web.ErrorPage500(s.templates, writer, params, "Error querying groups: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error querying groups: "+err.Error()) return } params["groups"] = groups diff --git a/forged/internal/unsorted/http_handle_login.go b/forged/internal/web/http_handle_login.go index 8adbe17..a18025e 100644 --- a/forged/internal/unsorted/http_handle_login.go +++ b/forged/internal/web/http_handle_login.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package web import ( "crypto/rand" @@ -13,7 +13,6 @@ import ( "github.com/jackc/pgx/v5" "go.lindenii.runxiyu.org/forge/forged/internal/argon2id" - "go.lindenii.runxiyu.org/forge/forged/internal/web" ) // httpHandleLogin provides the login page for local users. @@ -46,7 +45,7 @@ func (s *Server) httpHandleLogin(writer http.ResponseWriter, request *http.Reque s.renderTemplate(writer, "login", params) return } - web.ErrorPage500(s.templates, writer, params, "Error querying user information: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error querying user information: "+err.Error()) return } if passwordHash == "" { @@ -56,7 +55,7 @@ func (s *Server) httpHandleLogin(writer http.ResponseWriter, request *http.Reque } if passwordMatches, err = argon2id.ComparePasswordAndHash(password, passwordHash); err != nil { - web.ErrorPage500(s.templates, writer, params, "Error comparing password and hash: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error comparing password and hash: "+err.Error()) return } @@ -67,7 +66,7 @@ func (s *Server) httpHandleLogin(writer http.ResponseWriter, request *http.Reque } if cookieValue, err = randomUrlsafeStr(16); err != nil { - web.ErrorPage500(s.templates, writer, params, "Error getting random string: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error getting random string: "+err.Error()) return } @@ -88,7 +87,7 @@ func (s *Server) httpHandleLogin(writer http.ResponseWriter, request *http.Reque _, err = s.database.Exec(request.Context(), "INSERT INTO sessions (user_id, session_id) VALUES ($1, $2)", userID, cookieValue) if err != nil { - web.ErrorPage500(s.templates, writer, params, "Error inserting session: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error inserting session: "+err.Error()) return } diff --git a/forged/internal/unsorted/http_handle_repo_commit.go b/forged/internal/web/http_handle_repo_commit.go index 2afdf3a..ce3e799 100644 --- a/forged/internal/unsorted/http_handle_repo_commit.go +++ b/forged/internal/web/http_handle_repo_commit.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package web import ( "fmt" @@ -15,7 +15,6 @@ import ( "github.com/go-git/go-git/v5/plumbing/object" "go.lindenii.runxiyu.org/forge/forged/internal/misc" "go.lindenii.runxiyu.org/forge/forged/internal/oldgit" - "go.lindenii.runxiyu.org/forge/forged/internal/web" ) // usableFilePatch is a [diff.FilePatch] that is structured in a way more @@ -48,13 +47,13 @@ func (s *Server) httpHandleRepoCommit(writer http.ResponseWriter, request *http. commitIDStrSpecNoSuffix = strings.TrimSuffix(commitIDStrSpec, ".patch") commitID = plumbing.NewHash(commitIDStrSpecNoSuffix) if commitObj, err = repo.CommitObject(commitID); err != nil { - web.ErrorPage500(s.templates, writer, params, "Error getting commit object: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error getting commit object: "+err.Error()) return } if commitIDStrSpecNoSuffix != commitIDStrSpec { var patchStr string if patchStr, err = oldgit.FmtCommitPatch(commitObj); err != nil { - web.ErrorPage500(s.templates, writer, params, "Error formatting patch: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error formatting patch: "+err.Error()) return } fmt.Fprintln(writer, patchStr) @@ -72,7 +71,7 @@ func (s *Server) httpHandleRepoCommit(writer http.ResponseWriter, request *http. parentCommitHash, patch, err = oldgit.CommitToPatch(commitObj) if err != nil { - web.ErrorPage500(s.templates, writer, params, "Error getting patch from commit: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error getting patch from commit: "+err.Error()) return } params["parent_commit_hash"] = parentCommitHash.String() diff --git a/forged/internal/unsorted/http_handle_repo_contrib_index.go b/forged/internal/web/http_handle_repo_contrib_index.go index 5c68c08..218e800 100644 --- a/forged/internal/unsorted/http_handle_repo_contrib_index.go +++ b/forged/internal/web/http_handle_repo_contrib_index.go @@ -1,13 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package web import ( "net/http" "github.com/jackc/pgx/v5" - "go.lindenii.runxiyu.org/forge/forged/internal/web" ) // idTitleStatus describes properties of a merge request that needs to be @@ -28,7 +27,7 @@ func (s *Server) httpHandleRepoContribIndex(writer http.ResponseWriter, request "SELECT repo_local_id, COALESCE(title, 'Untitled'), status FROM merge_requests WHERE repo_id = $1", params["repo_id"], ); err != nil { - web.ErrorPage500(s.templates, writer, params, "Error querying merge requests: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error querying merge requests: "+err.Error()) return } defer rows.Close() @@ -37,13 +36,13 @@ func (s *Server) httpHandleRepoContribIndex(writer http.ResponseWriter, request var mrID int var mrTitle, mrStatus string if err = rows.Scan(&mrID, &mrTitle, &mrStatus); err != nil { - web.ErrorPage500(s.templates, writer, params, "Error scanning merge request: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error scanning merge request: "+err.Error()) return } result = append(result, idTitleStatus{mrID, mrTitle, mrStatus}) } if err = rows.Err(); err != nil { - web.ErrorPage500(s.templates, writer, params, "Error ranging over merge requests: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error ranging over merge requests: "+err.Error()) return } params["merge_requests"] = result diff --git a/forged/internal/unsorted/http_handle_repo_contrib_one.go b/forged/internal/web/http_handle_repo_contrib_one.go index 1d733b0..5178b29 100644 --- a/forged/internal/unsorted/http_handle_repo_contrib_one.go +++ b/forged/internal/web/http_handle_repo_contrib_one.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package web import ( "net/http" @@ -10,7 +10,6 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" - "go.lindenii.runxiyu.org/forge/forged/internal/web" ) // httpHandleRepoContribOne provides an interface to each merge request of a @@ -29,7 +28,7 @@ func (s *Server) httpHandleRepoContribOne(writer http.ResponseWriter, request *h mrIDStr = params["mr_id"].(string) mrIDInt64, err := strconv.ParseInt(mrIDStr, 10, strconv.IntSize) if err != nil { - web.ErrorPage400(s.templates, writer, params, "Merge request ID not an integer") + ErrorPage400(s.templates, writer, params, "Merge request ID not an integer") return } mrIDInt = int(mrIDInt64) @@ -38,18 +37,18 @@ func (s *Server) httpHandleRepoContribOne(writer http.ResponseWriter, request *h "SELECT COALESCE(title, ''), status, source_ref, COALESCE(destination_branch, '') FROM merge_requests WHERE repo_id = $1 AND repo_local_id = $2", params["repo_id"], mrIDInt, ).Scan(&title, &status, &srcRefStr, &dstBranchStr); err != nil { - web.ErrorPage500(s.templates, writer, params, "Error querying merge request: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error querying merge request: "+err.Error()) return } repo = params["repo"].(*git.Repository) if srcRefHash, err = getRefHash(repo, "branch", srcRefStr); err != nil { - web.ErrorPage500(s.templates, writer, params, "Error getting source ref hash: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error getting source ref hash: "+err.Error()) return } if srcCommit, err = repo.CommitObject(srcRefHash); err != nil { - web.ErrorPage500(s.templates, writer, params, "Error getting source commit: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error getting source commit: "+err.Error()) return } params["source_commit"] = srcCommit @@ -61,23 +60,23 @@ func (s *Server) httpHandleRepoContribOne(writer http.ResponseWriter, request *h dstBranchHash, err = getRefHash(repo, "branch", dstBranchStr) } if err != nil { - web.ErrorPage500(s.templates, writer, params, "Error getting destination branch hash: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error getting destination branch hash: "+err.Error()) return } if dstCommit, err = repo.CommitObject(dstBranchHash); err != nil { - web.ErrorPage500(s.templates, writer, params, "Error getting destination commit: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error getting destination commit: "+err.Error()) return } params["destination_commit"] = dstCommit if mergeBases, err = srcCommit.MergeBase(dstCommit); err != nil { - web.ErrorPage500(s.templates, writer, params, "Error getting merge base: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error getting merge base: "+err.Error()) return } if len(mergeBases) < 1 { - web.ErrorPage500(s.templates, writer, params, "No merge base found for this merge request; these two branches do not share any common history") + ErrorPage500(s.templates, writer, params, "No merge base found for this merge request; these two branches do not share any common history") // TODO return } @@ -87,7 +86,7 @@ func (s *Server) httpHandleRepoContribOne(writer http.ResponseWriter, request *h patch, err := mergeBaseCommit.Patch(srcCommit) if err != nil { - web.ErrorPage500(s.templates, writer, params, "Error getting patch: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error getting patch: "+err.Error()) return } params["file_patches"] = makeUsableFilePatches(patch) diff --git a/forged/internal/unsorted/http_handle_repo_index.go b/forged/internal/web/http_handle_repo_index.go index dd46dfe..1c9a625 100644 --- a/forged/internal/unsorted/http_handle_repo_index.go +++ b/forged/internal/web/http_handle_repo_index.go @@ -1,14 +1,14 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package web import ( "net/http" "go.lindenii.runxiyu.org/forge/forged/internal/git2c" "go.lindenii.runxiyu.org/forge/forged/internal/render" - "go.lindenii.runxiyu.org/forge/forged/internal/web" + "go.lindenii.runxiyu.org/forge/forged/internal/repos" ) // httpHandleRepoIndex provides the front page of a repo using git2d. @@ -16,18 +16,18 @@ func (s *Server) httpHandleRepoIndex(w http.ResponseWriter, req *http.Request, p repoName := params["repo_name"].(string) groupPath := params["group_path"].([]string) - _, repoPath, _, _, _, _, _ := s.getRepoInfo(req.Context(), groupPath, repoName, "") // TODO: Don't use getRepoInfo + _, repoPath, _, _, _, _, _, _ := repos.GetInfo(req.Context(), s.database, groupPath, repoName, "") // TODO: Don't use getRepoInfo client, err := git2c.NewClient(s.config.Git.Socket) if err != nil { - web.ErrorPage500(s.templates, w, params, err.Error()) + ErrorPage500(s.templates, w, params, err.Error()) return } defer client.Close() commits, readme, err := client.CmdIndex(repoPath) if err != nil { - web.ErrorPage500(s.templates, w, params, err.Error()) + ErrorPage500(s.templates, w, params, err.Error()) return } diff --git a/forged/internal/unsorted/http_handle_repo_info.go b/forged/internal/web/http_handle_repo_info.go index e23b1d2..d608466 100644 --- a/forged/internal/unsorted/http_handle_repo_info.go +++ b/forged/internal/web/http_handle_repo_info.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package web import ( "fmt" diff --git a/forged/internal/unsorted/http_handle_repo_log.go b/forged/internal/web/http_handle_repo_log.go index 5d90871..f50d233 100644 --- a/forged/internal/unsorted/http_handle_repo_log.go +++ b/forged/internal/web/http_handle_repo_log.go @@ -1,14 +1,14 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package web import ( "net/http" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" - "go.lindenii.runxiyu.org/forge/forged/internal/web" + "go.lindenii.runxiyu.org/forge/forged/internal/repos" ) // httpHandleRepoLog provides a page with a complete Git log. @@ -23,17 +23,17 @@ func (s *Server) httpHandleRepoLog(writer http.ResponseWriter, req *http.Request repo = params["repo"].(*git.Repository) if refHash, err = getRefHash(repo, params["ref_type"].(string), params["ref_name"].(string)); err != nil { - web.ErrorPage500(s.templates, writer, params, "Error getting ref hash: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error getting ref hash: "+err.Error()) return } logOptions := git.LogOptions{From: refHash} //exhaustruct:ignore commitIter, err := repo.Log(&logOptions) if err != nil { - web.ErrorPage500(s.templates, writer, params, "Error getting recent commits: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error getting recent commits: "+err.Error()) return } - params["commits"], params["commits_err"] = commitIterSeqErr(req.Context(), commitIter) + params["commits"], params["commits_err"] = repos.CommitIterSeqErr(req.Context(), commitIter) s.renderTemplate(writer, "repo_log", params) } diff --git a/forged/internal/unsorted/http_handle_repo_raw.go b/forged/internal/web/http_handle_repo_raw.go index 1127284..c496c99 100644 --- a/forged/internal/unsorted/http_handle_repo_raw.go +++ b/forged/internal/web/http_handle_repo_raw.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package web import ( "fmt" @@ -11,7 +11,7 @@ import ( "go.lindenii.runxiyu.org/forge/forged/internal/git2c" "go.lindenii.runxiyu.org/forge/forged/internal/misc" - "go.lindenii.runxiyu.org/forge/forged/internal/web" + "go.lindenii.runxiyu.org/forge/forged/internal/repos" ) // httpHandleRepoRaw serves raw files, or directory listings that point to raw @@ -23,18 +23,18 @@ func (s *Server) httpHandleRepoRaw(writer http.ResponseWriter, request *http.Req pathSpec := strings.TrimSuffix(rawPathSpec, "/") params["path_spec"] = pathSpec - _, repoPath, _, _, _, _, _ := s.getRepoInfo(request.Context(), groupPath, repoName, "") + _, repoPath, _, _, _, _, _, _ := repos.GetInfo(request.Context(), s.database, groupPath, repoName, "") client, err := git2c.NewClient(s.config.Git.Socket) if err != nil { - web.ErrorPage500(s.templates, writer, params, err.Error()) + ErrorPage500(s.templates, writer, params, err.Error()) return } defer client.Close() files, content, err := client.CmdTreeRaw(repoPath, pathSpec) if err != nil { - web.ErrorPage500(s.templates, writer, params, err.Error()) + ErrorPage500(s.templates, writer, params, err.Error()) return } @@ -51,6 +51,6 @@ func (s *Server) httpHandleRepoRaw(writer http.ResponseWriter, request *http.Req writer.Header().Set("Content-Type", "application/octet-stream") fmt.Fprint(writer, content) default: - web.ErrorPage500(s.templates, writer, params, "Unknown error fetching repo raw data") + ErrorPage500(s.templates, writer, params, "Unknown error fetching repo raw data") } } diff --git a/forged/internal/unsorted/http_handle_repo_tree.go b/forged/internal/web/http_handle_repo_tree.go index 4799ccb..73da431 100644 --- a/forged/internal/unsorted/http_handle_repo_tree.go +++ b/forged/internal/web/http_handle_repo_tree.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package web import ( "html/template" @@ -10,7 +10,7 @@ import ( "go.lindenii.runxiyu.org/forge/forged/internal/git2c" "go.lindenii.runxiyu.org/forge/forged/internal/render" - "go.lindenii.runxiyu.org/forge/forged/internal/web" + "go.lindenii.runxiyu.org/forge/forged/internal/repos" ) // httpHandleRepoTree provides a friendly, syntax-highlighted view of @@ -24,18 +24,18 @@ func (s *Server) httpHandleRepoTree(writer http.ResponseWriter, request *http.Re pathSpec := strings.TrimSuffix(rawPathSpec, "/") params["path_spec"] = pathSpec - _, repoPath, _, _, _, _, _ := s.getRepoInfo(request.Context(), groupPath, repoName, "") + _, repoPath, _, _, _, _, _, _ := repos.GetInfo(request.Context(), s.database, groupPath, repoName, "") client, err := git2c.NewClient(s.config.Git.Socket) if err != nil { - web.ErrorPage500(s.templates, writer, params, err.Error()) + ErrorPage500(s.templates, writer, params, err.Error()) return } defer client.Close() files, content, err := client.CmdTreeRaw(repoPath, pathSpec) if err != nil { - web.ErrorPage500(s.templates, writer, params, err.Error()) + ErrorPage500(s.templates, writer, params, err.Error()) return } @@ -50,6 +50,6 @@ func (s *Server) httpHandleRepoTree(writer http.ResponseWriter, request *http.Re params["file_contents"] = rendered s.renderTemplate(writer, "repo_tree_file", params) default: - web.ErrorPage500(s.templates, writer, params, "Unknown object type, something is seriously wrong") + ErrorPage500(s.templates, writer, params, "Unknown object type, something is seriously wrong") } } diff --git a/forged/internal/unsorted/http_handle_repo_upload_pack.go b/forged/internal/web/http_handle_repo_upload_pack.go index 914c9cc..cbb133b 100644 --- a/forged/internal/unsorted/http_handle_repo_upload_pack.go +++ b/forged/internal/web/http_handle_repo_upload_pack.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package web import ( "bytes" @@ -108,11 +108,15 @@ func decodeBody(r *http.Request) (io.ReadCloser, error) { return r.Body, nil case "gzip": zr, err := gzip.NewReader(r.Body) - if err != nil { return nil, err } + if err != nil { + return nil, err + } return zr, nil case "deflate": zr, err := zlib.NewReader(r.Body) - if err != nil { return nil, err } + if err != nil { + return nil, err + } return zr, nil default: return nil, fmt.Errorf("unsupported Content-Encoding: %q", ce) diff --git a/forged/internal/unsorted/http_handle_users.go b/forged/internal/web/http_handle_users.go index b41ee44..9d90553 100644 --- a/forged/internal/unsorted/http_handle_users.go +++ b/forged/internal/web/http_handle_users.go @@ -1,15 +1,13 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package web import ( "net/http" - - "go.lindenii.runxiyu.org/forge/forged/internal/web" ) // httpHandleUsers is a useless stub. func (s *Server) httpHandleUsers(writer http.ResponseWriter, _ *http.Request, params map[string]any) { - web.ErrorPage501(s.templates, writer, params) + ErrorPage501(s.templates, writer, params) } diff --git a/forged/internal/unsorted/remote_url.go b/forged/internal/web/remote_url.go index f4d4c58..4650695 100644 --- a/forged/internal/unsorted/remote_url.go +++ b/forged/internal/web/remote_url.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package web import ( "net/url" diff --git a/forged/internal/unsorted/resources.go b/forged/internal/web/resources.go index 692b454..a0dd3e3 100644 --- a/forged/internal/unsorted/resources.go +++ b/forged/internal/web/resources.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package web import ( "html/template" diff --git a/forged/internal/unsorted/http_server.go b/forged/internal/web/server.go index f6a1794..3e43cb0 100644 --- a/forged/internal/unsorted/http_server.go +++ b/forged/internal/web/server.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package web import ( "errors" @@ -13,7 +13,7 @@ import ( "github.com/jackc/pgx/v5" "go.lindenii.runxiyu.org/forge/forged/internal/misc" - "go.lindenii.runxiyu.org/forge/forged/internal/web" + "go.lindenii.runxiyu.org/forge/forged/internal/repos" ) // ServeHTTP handles all incoming HTTP requests and routes them to the correct @@ -40,7 +40,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { params := make(map[string]any) if segments, _, err = misc.ParseReqURI(request.RequestURI); err != nil { - web.ErrorPage400(s.templates, writer, params, "Error parsing request URI: "+err.Error()) + ErrorPage400(s.templates, writer, params, "Error parsing request URI: "+err.Error()) return } dirMode := false @@ -56,7 +56,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { userID, params["username"], err = s.getUserFromRequest(request) params["user_id"] = userID if err != nil && !errors.Is(err, http.ErrNoCookie) && !errors.Is(err, pgx.ErrNoRows) { - web.ErrorPage500(s.templates, writer, params, "Error getting user info from request: "+err.Error()) + ErrorPage500(s.templates, writer, params, "Error getting user info from request: "+err.Error()) return } @@ -68,7 +68,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { for _, v := range segments { if strings.Contains(v, ":") { - web.ErrorPage400Colon(s.templates, writer, params) + ErrorPage400Colon(s.templates, writer, params) return } } @@ -80,7 +80,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { if segments[0] == "-" { if len(segments) < 2 { - web.ErrorPage404(s.templates, writer, params) + ErrorPage404(s.templates, writer, params) return } else if len(segments) == 2 && misc.RedirectDir(writer, request) { return @@ -105,7 +105,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { s.httpHandleUsers(writer, request, params) return default: - web.ErrorPage404(s.templates, writer, params) + ErrorPage404(s.templates, writer, params) return } } @@ -138,10 +138,10 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { } s.httpHandleGroupIndex(writer, request, params) case len(segments) == sepIndex+1: - web.ErrorPage404(s.templates, writer, params) + ErrorPage404(s.templates, writer, params) return case len(segments) == sepIndex+2: - web.ErrorPage404(s.templates, writer, params) + ErrorPage404(s.templates, writer, params) return default: moduleType = segments[sepIndex+1] @@ -154,12 +154,12 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { switch segments[sepIndex+3] { case "info": if err = s.httpHandleRepoInfo(writer, request, params); err != nil { - web.ErrorPage500(s.templates, writer, params, err.Error()) + ErrorPage500(s.templates, writer, params, err.Error()) } return case "git-upload-pack": if err = s.httpHandleUploadPack(writer, request, params); err != nil { - web.ErrorPage500(s.templates, writer, params, err.Error()) + ErrorPage500(s.templates, writer, params, err.Error()) } return } @@ -169,13 +169,13 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { if errors.Is(err, misc.ErrNoRefSpec) { params["ref_type"] = "" } else { - web.ErrorPage400(s.templates, writer, params, "Error querying ref type: "+err.Error()) + ErrorPage400(s.templates, writer, params, "Error querying ref type: "+err.Error()) return } } - if params["repo"], params["repo_description"], params["repo_id"], _, err = s.openRepo(request.Context(), groupPath, moduleName); err != nil { - web.ErrorPage500(s.templates, writer, params, "Error opening repo: "+err.Error()) + if params["repo"], params["repo_description"], params["repo_id"], _, err = repos.Open(request.Context(), s.database, groupPath, moduleName); err != nil { + ErrorPage500(s.templates, writer, params, "Error opening repo: "+err.Error()) return } @@ -200,7 +200,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { switch repoFeature { case "tree": if misc.AnyContain(segments[sepIndex+4:], "/") { - web.ErrorPage400(s.templates, writer, params, "Repo tree paths may not contain slashes in any segments") + ErrorPage400(s.templates, writer, params, "Repo tree paths may not contain slashes in any segments") return } if dirMode { @@ -220,7 +220,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { return case "raw": if misc.AnyContain(segments[sepIndex+4:], "/") { - web.ErrorPage400(s.templates, writer, params, "Repo tree paths may not contain slashes in any segments") + ErrorPage400(s.templates, writer, params, "Repo tree paths may not contain slashes in any segments") return } if dirMode { @@ -234,7 +234,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { s.httpHandleRepoRaw(writer, request, params) case "log": if len(segments) > sepIndex+4 { - web.ErrorPage400(s.templates, writer, params, "Too many parameters") + ErrorPage400(s.templates, writer, params, "Too many parameters") return } if misc.RedirectDir(writer, request) { @@ -243,7 +243,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { s.httpHandleRepoLog(writer, request, params) case "commit": if len(segments) != sepIndex+5 { - web.ErrorPage400(s.templates, writer, params, "Incorrect number of parameters") + ErrorPage400(s.templates, writer, params, "Incorrect number of parameters") return } if misc.RedirectNoDir(writer, request) { @@ -262,14 +262,14 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { params["mr_id"] = segments[sepIndex+4] s.httpHandleRepoContribOne(writer, request, params) default: - web.ErrorPage400(s.templates, writer, params, "Too many parameters") + ErrorPage400(s.templates, writer, params, "Too many parameters") } default: - web.ErrorPage404(s.templates, writer, params) + ErrorPage404(s.templates, writer, params) return } default: - web.ErrorPage404(s.templates, writer, params) + ErrorPage404(s.templates, writer, params) return } } diff --git a/forged/internal/unsorted/http_template.go b/forged/internal/web/template.go index db44e4c..1f4372a 100644 --- a/forged/internal/unsorted/http_template.go +++ b/forged/internal/web/template.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package unsorted +package web import ( "log/slog" diff --git a/forged/internal/web/web.go b/forged/internal/web/web.go index f4d15f8..b0f02cc 100644 --- a/forged/internal/web/web.go +++ b/forged/internal/web/web.go @@ -3,3 +3,51 @@ // Package web provides web-facing components of the forge. package web + +import ( + "html/template" + "io/fs" + "net/http" + + "go.lindenii.runxiyu.org/forge/forged/internal/config" + "go.lindenii.runxiyu.org/forge/forged/internal/database" + "go.lindenii.runxiyu.org/forge/forged/internal/embed" + "go.lindenii.runxiyu.org/forge/forged/internal/misc" +) + +// Server handles HTTP requests for the forge. +type Server struct { + config config.Config + database database.Database + sourceHandler http.Handler + staticHandler http.Handler + templates *template.Template + globalData map[string]any +} + +// New creates a new web server. +func New(cfg config.Config, db database.Database, pubkeyStr, pubkeyFP *string, version string) (*Server, error) { + s := &Server{config: cfg, database: db} + s.globalData = map[string]any{ + "server_public_key_string": pubkeyStr, + "server_public_key_fingerprint": pubkeyFP, + "forge_version": version, + "forge_title": cfg.General.Title, + } + + s.sourceHandler = http.StripPrefix( + "/-/source/", + http.FileServer(http.FS(embed.Source)), + ) + staticFS, err := fs.Sub(embed.Resources, "forged/static") + if err != nil { + return s, err + } + s.staticHandler = http.StripPrefix("/-/static/", http.FileServer(http.FS(staticFS))) + + if err = s.loadTemplates(); err != nil { + return s, err + } + + return s, nil +} diff --git a/forged/main.go b/forged/main.go index fde15d1..d729e63 100644 --- a/forged/main.go +++ b/forged/main.go @@ -7,7 +7,7 @@ package main import ( "flag" - "go.lindenii.runxiyu.org/forge/forged/internal/unsorted" + "go.lindenii.runxiyu.org/forge/forged/internal/server" ) func main() { @@ -18,7 +18,7 @@ func main() { ) flag.Parse() - s, err := unsorted.NewServer(*configPath) + s, err := server.NewServer(*configPath) if err != nil { panic(err) } |