diff options
48 files changed, 372 insertions, 360 deletions
@@ -16,7 +16,7 @@ VERSION = $(shell git describe --tags --always --dirty) SOURCE_FILES = $(shell git ls-files) forge: source.tar.gz hookc/hookc git2d/git2d $(SOURCE_FILES) - CGO_ENABLED=0 go build -o forge -ldflags '-extldflags "-f no-PIC -static" -X "main.VERSION=$(VERSION)"' -tags 'osusergo netgo static_build' + CGO_ENABLED=0 go build -o forge -ldflags '-extldflags "-f no-PIC -static" -X "main.VERSION=$(VERSION)"' -tags 'osusergo netgo static_build' ./cmd/forge utils/colb: @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "context" @@ -13,8 +13,8 @@ import ( // 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 (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, ` WITH RECURSIVE group_path_cte AS ( -- Start: match the first name in the path where parent_group IS NULL SELECT diff --git a/cmd/forge/main.go b/cmd/forge/main.go new file mode 100644 index 0000000..102f4da --- /dev/null +++ b/cmd/forge/main.go @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> + +package main + +import ( + "flag" + "log/slog" + "os" + + "go.lindenii.runxiyu.org/forge" +) + +func main() { + configPath := flag.String( + "config", + "/etc/lindenii/forge.scfg", + "path to configuration file", + ) + flag.Parse() + + s := forge.Server{} + + s.Setup() + + if err := s.LoadConfig(*configPath); err != nil { + slog.Error("loading configuration", "error", err) + os.Exit(1) + } + + s.Run() +} @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "bufio" @@ -67,7 +67,7 @@ type Config struct { } `scfg:"db"` } -// loadConfig loads a configuration file from the specified path and unmarshals +// 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. @@ -76,7 +76,7 @@ type Config struct { // configuration patterns, but silently ignores fields in the [config] struct // that is not present in the user's configuration file. We would prefer the // exact opposite behavior. -func (s *server) loadConfig(path string) (err error) { +func (s *Server) LoadConfig(path string) (err error) { var configFile *os.File if configFile, err = os.Open(path); err != nil { return err @@ -84,19 +84,19 @@ func (s *server) loadConfig(path string) (err error) { defer configFile.Close() decoder := scfg.NewDecoder(bufio.NewReader(configFile)) - if err = decoder.Decode(&s.config); err != nil { + if err = decoder.Decode(&s.Config); err != nil { return err } - if s.config.DB.Type != "postgres" { + if s.Config.DB.Type != "postgres" { return errors.New("unsupported database type") } - if s.database, err = pgxpool.New(context.Background(), s.config.DB.Conn); err != nil { + if s.Database, err = pgxpool.New(context.Background(), s.Config.DB.Conn); err != nil { return err } - s.globalData["forge_title"] = s.config.General.Title + s.GlobalData["forge_title"] = s.Config.General.Title return nil } diff --git a/database.go b/database.go index 1ea0753..eafad33 100644 --- a/database.go +++ b/database.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "context" @@ -18,10 +18,10 @@ import ( // queryNameDesc is a helper function that executes a query and returns a // list of nameDesc results. The query must return two string arguments, i.e. a // name and a description. -func (s *server) queryNameDesc(ctx context.Context, query string, args ...any) (result []nameDesc, err error) { +func (s *Server) queryNameDesc(ctx context.Context, query string, args ...any) (result []nameDesc, err error) { var rows pgx.Rows - if rows, err = s.database.Query(ctx, query, args...); err != nil { + if rows, err = s.Database.Query(ctx, query, args...); err != nil { return nil, err } defer rows.Close() @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "bufio" @@ -17,7 +17,7 @@ import ( // fedauth checks whether a user's SSH public key matches the remote username // they claim to have on the service. If so, the association is recorded. -func (s *server) fedauth(ctx context.Context, userID int, service, remoteUsername, pubkey string) (bool, error) { +func (s *Server) fedauth(ctx context.Context, userID int, service, remoteUsername, pubkey string) (bool, error) { var err error matched := false @@ -77,7 +77,7 @@ func (s *server) fedauth(ctx context.Context, userID int, service, remoteUsernam } var txn pgx.Tx - if txn, err = s.database.Begin(ctx); err != nil { + if txn, err = s.Database.Begin(ctx); err != nil { return false, err } defer func() { diff --git a/git2d_deploy.go b/git2d_deploy.go index ba63a1b..5a5f336 100644 --- a/git2d_deploy.go +++ b/git2d_deploy.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "io" @@ -9,7 +9,7 @@ import ( "os" ) -func (s *server) deployGit2D() (err error) { +func (s *Server) deployGit2D() (err error) { var srcFD fs.File var dstFD *os.File @@ -18,7 +18,7 @@ func (s *server) deployGit2D() (err error) { } defer srcFD.Close() - if dstFD, err = os.OpenFile(s.config.Git.DaemonPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755); err != nil { + if dstFD, err = os.OpenFile(s.Config.Git.DaemonPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755); err != nil { return err } defer dstFD.Close() diff --git a/git_format_patch.go b/git_format_patch.go index 79a7474..5628ce1 100644 --- a/git_format_patch.go +++ b/git_format_patch.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "bytes" diff --git a/git_hooks_deploy.go b/git_hooks_deploy.go index 0cfb4f9..eeda995 100644 --- a/git_hooks_deploy.go +++ b/git_hooks_deploy.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "errors" @@ -14,7 +14,7 @@ import ( // deployHooks deploys the git hooks client to the filesystem. The git hooks // client is expected to be embedded in resourcesFS and must be pre-compiled // during the build process; see the Makefile. -func (s *server) deployHooks() (err error) { +func (s *Server) deployHooks() (err error) { err = func() (err error) { var srcFD fs.File var dstFD *os.File @@ -24,7 +24,7 @@ func (s *server) deployHooks() (err error) { } defer srcFD.Close() - if dstFD, err = os.OpenFile(filepath.Join(s.config.Hooks.Execs, "hookc"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755); err != nil { + if dstFD, err = os.OpenFile(filepath.Join(s.Config.Hooks.Execs, "hookc"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755); err != nil { return err } defer dstFD.Close() @@ -41,14 +41,14 @@ func (s *server) deployHooks() (err error) { // Go's embed filesystems do not store permissions; but in any case, // they would need to be 0o755: - if err = os.Chmod(filepath.Join(s.config.Hooks.Execs, "hookc"), 0o755); err != nil { + if err = os.Chmod(filepath.Join(s.Config.Hooks.Execs, "hookc"), 0o755); err != nil { return err } for _, hookName := range []string{ "pre-receive", } { - if err = os.Symlink(filepath.Join(s.config.Hooks.Execs, "hookc"), filepath.Join(s.config.Hooks.Execs, hookName)); err != nil { + if err = os.Symlink(filepath.Join(s.Config.Hooks.Execs, "hookc"), filepath.Join(s.Config.Hooks.Execs, hookName)); err != nil { if !errors.Is(err, fs.ErrExist) { return err } diff --git a/git_hooks_handle_linux.go b/git_hooks_handle_linux.go index 097f236..1101302 100644 --- a/git_hooks_handle_linux.go +++ b/git_hooks_handle_linux.go @@ -3,7 +3,7 @@ // //go:build linux -package main +package forge import ( "bytes" @@ -34,7 +34,7 @@ var ( // hooksHandler handles a connection from hookc via the // unix socket. -func (s *server) hooksHandler(conn net.Conn) { +func (s *Server) hooksHandler(conn net.Conn) { var ctx context.Context var cancel context.CancelFunc var ucred *syscall.Ucred @@ -77,7 +77,7 @@ func (s *server) hooksHandler(conn net.Conn) { { var ok bool - packPass, ok = s.packPasses.Load(misc.BytesToString(cookie)) + packPass, ok = s.PackPasses.Load(misc.BytesToString(cookie)) if !ok { if _, err = conn.Write([]byte{1}); err != nil { return @@ -233,12 +233,12 @@ func (s *server) hooksHandler(conn net.Conn) { var newMRLocalID int if packPass.userID != 0 { - err = s.database.QueryRow(ctx, + err = s.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 = s.database.QueryRow(ctx, + err = s.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) @@ -251,7 +251,7 @@ func (s *server) hooksHandler(conn net.Conn) { fmt.Fprintln(sshStderr, ansiec.Blue+"Created merge request at", mergeRequestWebURL+ansiec.Reset) select { - case s.ircSendBuffered <- "PRIVMSG #chat :New merge request at " + mergeRequestWebURL: + case s.IrcSendBuffered <- "PRIVMSG #chat :New merge request at " + mergeRequestWebURL: default: slog.Error("IRC SendQ exceeded") } @@ -259,7 +259,7 @@ func (s *server) hooksHandler(conn net.Conn) { var existingMRUser int var isAncestor bool - err = s.database.QueryRow(ctx, + err = s.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) @@ -342,7 +342,7 @@ func (s *server) hooksHandler(conn net.Conn) { // 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 (s *server) serveGitHooks(listener net.Listener) error { +func (s *Server) serveGitHooks(listener net.Listener) error { for { conn, err := listener.Accept() if err != nil { diff --git a/git_hooks_handle_other.go b/git_hooks_handle_other.go index 687bd8f..4a4328f 100644 --- a/git_hooks_handle_other.go +++ b/git_hooks_handle_other.go @@ -3,7 +3,7 @@ // //go:build !linux -package main +package forge import ( "bytes" diff --git a/git_init.go b/git_init.go index 1800c5a..b448451 100644 --- a/git_init.go +++ b/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 main +package forge import ( "github.com/go-git/go-git/v5" @@ -11,7 +11,7 @@ import ( // gitInit initializes a bare git repository with the forge-deployed hooks // directory as the hooksPath. -func (s *server) gitInit(repoPath string) (err error) { +func (s *Server) gitInit(repoPath string) (err error) { var repo *git.Repository var gitConf *gitConfig.Config @@ -23,7 +23,7 @@ func (s *server) gitInit(repoPath string) (err error) { return err } - gitConf.Raw.SetOption("core", gitFmtConfig.NoSubsection, "hooksPath", s.config.Hooks.Execs) + gitConf.Raw.SetOption("core", gitFmtConfig.NoSubsection, "hooksPath", s.Config.Hooks.Execs) gitConf.Raw.SetOption("receive", gitFmtConfig.NoSubsection, "advertisePushOptions", "true") if err = repo.SetConfig(gitConf); err != nil { diff --git a/git_misc.go b/git_misc.go index 17f834c..8ba10e1 100644 --- a/git_misc.go +++ b/git_misc.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "context" @@ -22,8 +22,8 @@ import ( // 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 (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, ` WITH RECURSIVE group_path_cte AS ( -- Start: match the first name in the path where parent_group IS NULL SELECT diff --git a/git_plumbing.go b/git_plumbing.go index 74c80ac..440de7c 100644 --- a/git_plumbing.go +++ b/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 main +package forge import ( "bytes" @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "github.com/go-git/go-git/v5" diff --git a/http_auth.go b/http_auth.go index 5f0dc66..5ba278b 100644 --- a/http_auth.go +++ b/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 main +package forge import ( "net/http" @@ -9,14 +9,14 @@ import ( // getUserFromRequest returns the user ID and username associated with the // session cookie in a given [http.Request]. -func (s *server) getUserFromRequest(request *http.Request) (id int, username string, err error) { +func (s *Server) getUserFromRequest(request *http.Request) (id int, username string, err error) { var sessionCookie *http.Cookie if sessionCookie, err = request.Cookie("session"); err != nil { return } - err = s.database.QueryRow( + err = s.Database.QueryRow( request.Context(), "SELECT user_id, COALESCE(username, '') FROM users u JOIN sessions s ON u.id = s.user_id WHERE s.session_id = $1;", sessionCookie.Value, diff --git a/http_error_page.go b/http_error_page.go index 00ef04b..0cce72e 100644 --- a/http_error_page.go +++ b/http_error_page.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "net/http" diff --git a/http_handle_branches.go b/http_handle_branches.go index 01a162a..96c4ac7 100644 --- a/http_handle_branches.go +++ b/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 main +package forge import ( "net/http" @@ -14,7 +14,7 @@ import ( ) // httpHandleRepoBranches provides the branches page in repos. -func (s *server) httpHandleRepoBranches(writer http.ResponseWriter, _ *http.Request, params map[string]any) { +func (s *Server) httpHandleRepoBranches(writer http.ResponseWriter, _ *http.Request, params map[string]any) { var repo *git.Repository var repoName string var groupPath []string diff --git a/http_handle_group_index.go b/http_handle_group_index.go index 46f1f6a..cc33860 100644 --- a/http_handle_group_index.go +++ b/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 main +package forge import ( "errors" @@ -17,7 +17,7 @@ import ( // httpHandleGroupIndex provides index pages for groups, which includes a list // of its subgroups and repos, as well as a form for group maintainers to // create repos. -func (s *server) httpHandleGroupIndex(writer http.ResponseWriter, request *http.Request, params map[string]any) { +func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http.Request, params map[string]any) { var groupPath []string var repos []nameDesc var subgroups []nameDesc @@ -28,7 +28,7 @@ func (s *server) httpHandleGroupIndex(writer http.ResponseWriter, request *http. groupPath = params["group_path"].([]string) // The group itself - err = s.database.QueryRow(request.Context(), ` + err = s.Database.QueryRow(request.Context(), ` WITH RECURSIVE group_path_cte AS ( SELECT id, @@ -69,7 +69,7 @@ func (s *server) httpHandleGroupIndex(writer http.ResponseWriter, request *http. // ACL var count int - err = s.database.QueryRow(request.Context(), ` + err = s.Database.QueryRow(request.Context(), ` SELECT COUNT(*) FROM user_group_roles WHERE user_id = $1 @@ -96,7 +96,7 @@ func (s *server) httpHandleGroupIndex(writer http.ResponseWriter, request *http. } var newRepoID int - err := s.database.QueryRow( + err := s.Database.QueryRow( request.Context(), `INSERT INTO repos (name, description, group_id, contrib_requirements) VALUES ($1, $2, $3, $4) @@ -111,9 +111,9 @@ func (s *server) httpHandleGroupIndex(writer http.ResponseWriter, request *http. return } - filePath := filepath.Join(s.config.Git.RepoDir, strconv.Itoa(newRepoID)+".git") + filePath := filepath.Join(s.Config.Git.RepoDir, strconv.Itoa(newRepoID)+".git") - _, err = s.database.Exec( + _, err = s.Database.Exec( request.Context(), `UPDATE repos SET filesystem_path = $1 @@ -137,7 +137,7 @@ func (s *server) httpHandleGroupIndex(writer http.ResponseWriter, request *http. // Repos var rows pgx.Rows - rows, err = s.database.Query(request.Context(), ` + rows, err = s.Database.Query(request.Context(), ` SELECT name, COALESCE(description, '') FROM repos WHERE group_id = $1 @@ -162,7 +162,7 @@ func (s *server) httpHandleGroupIndex(writer http.ResponseWriter, request *http. } // Subgroups - rows, err = s.database.Query(request.Context(), ` + rows, err = s.Database.Query(request.Context(), ` SELECT name, COALESCE(description, '') FROM groups WHERE parent_group = $1 diff --git a/http_handle_index.go b/http_handle_index.go index 755e7c4..a519a5a 100644 --- a/http_handle_index.go +++ b/http_handle_index.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "net/http" @@ -12,7 +12,7 @@ import ( // httpHandleIndex provides the main index page which includes a list of groups // and some global information such as SSH keys. -func (s *server) httpHandleIndex(writer http.ResponseWriter, request *http.Request, params map[string]any) { +func (s *Server) httpHandleIndex(writer http.ResponseWriter, request *http.Request, params map[string]any) { var err error var groups []nameDesc diff --git a/http_handle_login.go b/http_handle_login.go index 10bfdcd..e02ba10 100644 --- a/http_handle_login.go +++ b/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 main +package forge import ( "crypto/rand" @@ -16,7 +16,7 @@ import ( ) // httpHandleLogin provides the login page for local users. -func (s *server) httpHandleLogin(writer http.ResponseWriter, request *http.Request, params map[string]any) { +func (s *Server) httpHandleLogin(writer http.ResponseWriter, request *http.Request, params map[string]any) { var username, password string var userID int var passwordHash string @@ -35,7 +35,7 @@ func (s *server) httpHandleLogin(writer http.ResponseWriter, request *http.Reque username = request.PostFormValue("username") password = request.PostFormValue("password") - err = s.database.QueryRow(request.Context(), + err = s.Database.QueryRow(request.Context(), "SELECT id, COALESCE(password, '') FROM users WHERE username = $1", username, ).Scan(&userID, &passwordHash) @@ -71,7 +71,7 @@ func (s *server) httpHandleLogin(writer http.ResponseWriter, request *http.Reque } now = time.Now() - expiry = now.Add(time.Duration(s.config.HTTP.CookieExpiry) * time.Second) + expiry = now.Add(time.Duration(s.Config.HTTP.CookieExpiry) * time.Second) cookie = http.Cookie{ Name: "session", @@ -85,7 +85,7 @@ func (s *server) httpHandleLogin(writer http.ResponseWriter, request *http.Reque http.SetCookie(writer, &cookie) - _, err = s.database.Exec(request.Context(), "INSERT INTO sessions (user_id, session_id) VALUES ($1, $2)", userID, cookieValue) + _, err = s.Database.Exec(request.Context(), "INSERT INTO sessions (user_id, session_id) VALUES ($1, $2)", userID, cookieValue) if err != nil { errorPage500(writer, params, "Error inserting session: "+err.Error()) return diff --git a/http_handle_repo_commit.go b/http_handle_repo_commit.go index a398dc2..88ade4b 100644 --- a/http_handle_repo_commit.go +++ b/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 main +package forge import ( "fmt" diff --git a/http_handle_repo_contrib_index.go b/http_handle_repo_contrib_index.go index e0c8478..f729cbe 100644 --- a/http_handle_repo_contrib_index.go +++ b/http_handle_repo_contrib_index.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "net/http" @@ -18,12 +18,12 @@ type idTitleStatus struct { } // httpHandleRepoContribIndex provides an index to merge requests of a repo. -func (s *server) httpHandleRepoContribIndex(writer http.ResponseWriter, request *http.Request, params map[string]any) { +func (s *Server) httpHandleRepoContribIndex(writer http.ResponseWriter, request *http.Request, params map[string]any) { var rows pgx.Rows var result []idTitleStatus var err error - if rows, err = s.database.Query(request.Context(), + if rows, err = s.Database.Query(request.Context(), "SELECT repo_local_id, COALESCE(title, 'Untitled'), status FROM merge_requests WHERE repo_id = $1", params["repo_id"], ); err != nil { diff --git a/http_handle_repo_contrib_one.go b/http_handle_repo_contrib_one.go index 0df7491..9a261a4 100644 --- a/http_handle_repo_contrib_one.go +++ b/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 main +package forge import ( "net/http" @@ -14,7 +14,7 @@ import ( // httpHandleRepoContribOne provides an interface to each merge request of a // repo. -func (s *server) httpHandleRepoContribOne(writer http.ResponseWriter, request *http.Request, params map[string]any) { +func (s *Server) httpHandleRepoContribOne(writer http.ResponseWriter, request *http.Request, params map[string]any) { var mrIDStr string var mrIDInt int var err error @@ -33,7 +33,7 @@ func (s *server) httpHandleRepoContribOne(writer http.ResponseWriter, request *h } mrIDInt = int(mrIDInt64) - if err = s.database.QueryRow(request.Context(), + if err = s.Database.QueryRow(request.Context(), "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 { diff --git a/http_handle_repo_index.go b/http_handle_repo_index.go index c6338cf..edba57b 100644 --- a/http_handle_repo_index.go +++ b/http_handle_repo_index.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "net/http" @@ -19,13 +19,13 @@ type commitDisplay struct { } // httpHandleRepoIndex provides the front page of a repo using git2d. -func (s *server) httpHandleRepoIndex(w http.ResponseWriter, req *http.Request, params map[string]any) { +func (s *Server) httpHandleRepoIndex(w http.ResponseWriter, req *http.Request, params map[string]any) { repoName := params["repo_name"].(string) groupPath := params["group_path"].([]string) _, repoPath, _, _, _, _, _ := s.getRepoInfo(req.Context(), groupPath, repoName, "") // TODO: Don't use getRepoInfo - client, err := git2c.NewClient(s.config.Git.Socket) + client, err := git2c.NewClient(s.Config.Git.Socket) if err != nil { errorPage500(w, params, err.Error()) return diff --git a/http_handle_repo_info.go b/http_handle_repo_info.go index b7b7438..e2080ac 100644 --- a/http_handle_repo_info.go +++ b/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 main +package forge import ( "fmt" @@ -16,12 +16,12 @@ import ( // HTTP protocol. // // TODO: Reject access from web browsers. -func (s *server) httpHandleRepoInfo(writer http.ResponseWriter, request *http.Request, params map[string]any) (err error) { +func (s *Server) httpHandleRepoInfo(writer http.ResponseWriter, request *http.Request, params map[string]any) (err error) { groupPath := params["group_path"].([]string) repoName := params["repo_name"].(string) var repoPath string - if err := s.database.QueryRow(request.Context(), ` + if err := s.Database.QueryRow(request.Context(), ` WITH RECURSIVE group_path_cte AS ( -- Start: match the first name in the path where parent_group IS NULL SELECT diff --git a/http_handle_repo_log.go b/http_handle_repo_log.go index 5c69836..b104491 100644 --- a/http_handle_repo_log.go +++ b/http_handle_repo_log.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "net/http" diff --git a/http_handle_repo_raw.go b/http_handle_repo_raw.go index 570030f..7e19e02 100644 --- a/http_handle_repo_raw.go +++ b/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 main +package forge import ( "fmt" @@ -15,7 +15,7 @@ import ( // httpHandleRepoRaw serves raw files, or directory listings that point to raw // files. -func (s *server) httpHandleRepoRaw(writer http.ResponseWriter, request *http.Request, params map[string]any) { +func (s *Server) httpHandleRepoRaw(writer http.ResponseWriter, request *http.Request, params map[string]any) { repoName := params["repo_name"].(string) groupPath := params["group_path"].([]string) rawPathSpec := params["rest"].(string) @@ -24,7 +24,7 @@ func (s *server) httpHandleRepoRaw(writer http.ResponseWriter, request *http.Req _, repoPath, _, _, _, _, _ := s.getRepoInfo(request.Context(), groupPath, repoName, "") - client, err := git2c.NewClient(s.config.Git.Socket) + client, err := git2c.NewClient(s.Config.Git.Socket) if err != nil { errorPage500(writer, params, err.Error()) return diff --git a/http_handle_repo_tree.go b/http_handle_repo_tree.go index 7af6e3e..e8e5ff8 100644 --- a/http_handle_repo_tree.go +++ b/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 main +package forge import ( "html/template" @@ -16,7 +16,7 @@ import ( // individual files, and provides directory views that link to these files. // // TODO: Do not highlight files that are too large. -func (s *server) httpHandleRepoTree(writer http.ResponseWriter, request *http.Request, params map[string]any) { +func (s *Server) 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) @@ -25,7 +25,7 @@ func (s *server) httpHandleRepoTree(writer http.ResponseWriter, request *http.Re _, repoPath, _, _, _, _, _ := s.getRepoInfo(request.Context(), groupPath, repoName, "") - client, err := git2c.NewClient(s.config.Git.Socket) + client, err := git2c.NewClient(s.Config.Git.Socket) if err != nil { errorPage500(writer, params, err.Error()) return diff --git a/http_handle_repo_upload_pack.go b/http_handle_repo_upload_pack.go index a6580a7..4c7291b 100644 --- a/http_handle_repo_upload_pack.go +++ b/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 main +package forge import ( "io" @@ -14,7 +14,7 @@ import ( // httpHandleUploadPack handles incoming Git fetch/pull/clone's over the Smart // HTTP protocol. -func (s *server) httpHandleUploadPack(writer http.ResponseWriter, request *http.Request, params map[string]any) (err error) { +func (s *Server) httpHandleUploadPack(writer http.ResponseWriter, request *http.Request, params map[string]any) (err error) { var groupPath []string var repoName string var repoPath string @@ -24,7 +24,7 @@ func (s *server) httpHandleUploadPack(writer http.ResponseWriter, request *http. groupPath, repoName = params["group_path"].([]string), params["repo_name"].(string) - if err := s.database.QueryRow(request.Context(), ` + if err := s.Database.QueryRow(request.Context(), ` WITH RECURSIVE group_path_cte AS ( -- Start: match the first name in the path where parent_group IS NULL SELECT @@ -67,7 +67,7 @@ func (s *server) httpHandleUploadPack(writer http.ResponseWriter, request *http. writer.WriteHeader(http.StatusOK) cmd = exec.Command("git", "upload-pack", "--stateless-rpc", repoPath) - cmd.Env = append(os.Environ(), "LINDENII_FORGE_HOOKS_SOCKET_PATH="+s.config.Hooks.Socket) + cmd.Env = append(os.Environ(), "LINDENII_FORGE_HOOKS_SOCKET_PATH="+s.Config.Hooks.Socket) if stdout, err = cmd.StdoutPipe(); err != nil { return err } diff --git a/http_handle_users.go b/http_handle_users.go index e02d4b2..d379624 100644 --- a/http_handle_users.go +++ b/http_handle_users.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "net/http" diff --git a/http_server.go b/http_server.go index 13ee492..fdb55b4 100644 --- a/http_server.go +++ b/http_server.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "errors" @@ -19,9 +19,9 @@ import ( // location. // // TODO: This function is way too large. -func (s *server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { +func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { var remoteAddr string - if s.config.HTTP.ReverseProxy { + if s.Config.HTTP.ReverseProxy { remoteAddrs, ok := request.Header["X-Forwarded-For"] if ok && len(remoteAddrs) == 1 { remoteAddr = remoteAddrs[0] @@ -50,7 +50,7 @@ func (s *server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { params["url_segments"] = segments params["dir_mode"] = dirMode - params["global"] = s.globalData + params["global"] = s.GlobalData var userID int // 0 for none userID, params["username"], err = s.getUserFromRequest(request) params["user_id"] = userID @@ -87,10 +87,10 @@ func (s *server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { switch segments[1] { case "static": - s.staticHandler.ServeHTTP(writer, request) + s.StaticHandler.ServeHTTP(writer, request) return case "source": - s.sourceHandler.ServeHTTP(writer, request) + s.SourceHandler.ServeHTTP(writer, request) return } } @@ -183,7 +183,7 @@ func (s *server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { repoURLRoot = repoURLRoot + url.PathEscape(part) + "/" } params["repo_url_root"] = repoURLRoot - params["repo_patch_mailing_list"] = repoURLRoot[1:len(repoURLRoot)-1] + "@" + s.config.LMTP.Domain + params["repo_patch_mailing_list"] = repoURLRoot[1:len(repoURLRoot)-1] + "@" + s.Config.LMTP.Domain params["http_clone_url"] = s.genHTTPRemoteURL(groupPath, moduleName) params["ssh_clone_url"] = s.genSSHRemoteURL(groupPath, moduleName) diff --git a/http_template.go b/http_template.go index 9aa15cb..f60f026 100644 --- a/http_template.go +++ b/http_template.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "log/slog" diff --git a/http_template_funcs.go b/http_template_funcs.go index 526841f..616afe2 100644 --- a/http_template_funcs.go +++ b/http_template_funcs.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "net/url" @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "crypto/tls" @@ -16,13 +16,13 @@ type errorBack[T any] struct { errorBack chan error } -func (s *server) ircBotSession() error { +func (s *Server) ircBotSession() error { var err error var underlyingConn net.Conn - if s.config.IRC.TLS { - underlyingConn, err = tls.Dial(s.config.IRC.Net, s.config.IRC.Addr, nil) + if s.Config.IRC.TLS { + underlyingConn, err = tls.Dial(s.Config.IRC.Net, s.Config.IRC.Addr, nil) } else { - underlyingConn, err = net.Dial(s.config.IRC.Net, s.config.IRC.Addr) + underlyingConn, err = net.Dial(s.Config.IRC.Net, s.Config.IRC.Addr) } if err != nil { return err @@ -36,11 +36,11 @@ func (s *server) ircBotSession() error { return conn.WriteString(s + "\r\n") } - _, err = logAndWriteLn("NICK " + s.config.IRC.Nick) + _, err = logAndWriteLn("NICK " + s.Config.IRC.Nick) if err != nil { return err } - _, err = logAndWriteLn("USER " + s.config.IRC.User + " 0 * :" + s.config.IRC.Gecos) + _, err = logAndWriteLn("USER " + s.Config.IRC.User + " 0 * :" + s.Config.IRC.Gecos) if err != nil { return err } @@ -81,7 +81,7 @@ func (s *server) ircBotSession() error { if !ok { slog.Error("unable to convert source of JOIN to client") } - if c.Nick != s.config.IRC.Nick { + if c.Nick != s.Config.IRC.Nick { continue } default: @@ -93,18 +93,18 @@ func (s *server) ircBotSession() error { select { case err = <-readLoopError: return err - case line := <-s.ircSendBuffered: + case line := <-s.IrcSendBuffered: _, err = logAndWriteLn(line) if err != nil { select { - case s.ircSendBuffered <- line: + case s.IrcSendBuffered <- line: default: slog.Error("unable to requeue message", "line", line) } writeLoopAbort <- struct{}{} return err } - case lineErrorBack := <-s.ircSendDirectChan: + case lineErrorBack := <-s.IrcSendDirectChan: _, err = logAndWriteLn(lineErrorBack.content) lineErrorBack.errorBack <- err if err != nil { @@ -117,10 +117,10 @@ func (s *server) ircBotSession() error { // ircSendDirect sends an IRC message directly to the connection and bypasses // the buffering system. -func (s *server) ircSendDirect(line string) error { +func (s *Server) ircSendDirect(line string) error { ech := make(chan error, 1) - s.ircSendDirectChan <- errorBack[string]{ + s.IrcSendDirectChan <- errorBack[string]{ content: line, errorBack: ech, } @@ -129,9 +129,9 @@ func (s *server) ircSendDirect(line string) error { } // TODO: Delay and warnings? -func (s *server) ircBotLoop() { - s.ircSendBuffered = make(chan string, s.config.IRC.SendQ) - s.ircSendDirectChan = make(chan errorBack[string]) +func (s *Server) ircBotLoop() { + s.IrcSendBuffered = make(chan string, s.Config.IRC.SendQ) + s.IrcSendDirectChan = make(chan errorBack[string]) for { err := s.ircBotSession() @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import "iter" diff --git a/lmtp_handle_patch.go b/lmtp_handle_patch.go index ab846aa..bf1b94c 100644 --- a/lmtp_handle_patch.go +++ b/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 main +package forge import ( "bytes" @@ -19,7 +19,7 @@ import ( "go.lindenii.runxiyu.org/forge/misc" ) -func (s *server) lmtpHandlePatch(session *lmtpSession, groupPath []string, repoName string, mbox io.Reader) (err error) { +func (s *Server) 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 { diff --git a/lmtp_server.go b/lmtp_server.go index 8191766..863a5c0 100644 --- a/lmtp_server.go +++ b/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 main +package forge import ( "bytes" @@ -27,7 +27,7 @@ type lmtpSession struct { to []string ctx context.Context cancel context.CancelFunc - s server + s Server } func (session *lmtpSession) Reset() { @@ -63,13 +63,13 @@ func (*lmtpHandler) NewSession(_ *smtp.Conn) (smtp.Session, error) { return session, nil } -func (s *server) serveLMTP(listener net.Listener) error { +func (s *Server) serveLMTP(listener net.Listener) error { smtpServer := smtp.NewServer(&lmtpHandler{}) smtpServer.LMTP = true - smtpServer.Domain = s.config.LMTP.Domain - smtpServer.Addr = s.config.LMTP.Socket - smtpServer.WriteTimeout = time.Duration(s.config.LMTP.WriteTimeout) * time.Second - smtpServer.ReadTimeout = time.Duration(s.config.LMTP.ReadTimeout) * time.Second + smtpServer.Domain = s.Config.LMTP.Domain + smtpServer.Addr = s.Config.LMTP.Socket + smtpServer.WriteTimeout = time.Duration(s.Config.LMTP.WriteTimeout) * time.Second + smtpServer.ReadTimeout = time.Duration(s.Config.LMTP.ReadTimeout) * time.Second smtpServer.EnableSMTPUTF8 = true return smtpServer.Serve(listener) } @@ -85,9 +85,9 @@ func (session *lmtpSession) Data(r io.Reader) error { n int64 ) - n, err = io.CopyN(&buf, r, session.s.config.LMTP.MaxSize) + n, err = io.CopyN(&buf, r, session.s.Config.LMTP.MaxSize) switch { - case n == session.s.config.LMTP.MaxSize: + case n == session.s.Config.LMTP.MaxSize: err = errors.New("Message too big.") // drain whatever is left in the pipe _, _ = io.Copy(io.Discard, r) @@ -133,10 +133,10 @@ func (session *lmtpSession) Data(r io.Reader) error { _ = from for _, to := range to { - if !strings.HasSuffix(to, "@"+session.s.config.LMTP.Domain) { + if !strings.HasSuffix(to, "@"+session.s.Config.LMTP.Domain) { continue } - localPart := to[:len(to)-len("@"+session.s.config.LMTP.Domain)] + localPart := to[:len(to)-len("@"+session.s.Config.LMTP.Domain)] var segments []string segments, err = misc.PathToSegments(localPart) if err != nil { diff --git a/main.go b/main.go deleted file mode 100644 index 2e1e094..0000000 --- a/main.go +++ /dev/null @@ -1,187 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> - -package main - -import ( - "errors" - "flag" - "io/fs" - "log" - "log/slog" - "net" - "net/http" - "os" - "os/exec" - "syscall" - "time" -) - -func main() { - configPath := flag.String( - "config", - "/etc/lindenii/forge.scfg", - "path to configuration file", - ) - flag.Parse() - - s := server{} - - s.sourceHandler = http.StripPrefix( - "/-/source/", - http.FileServer(http.FS(embeddedSourceFS)), - ) - staticFS, err := fs.Sub(embeddedResourcesFS, "static") - if err != nil { - panic(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 err := s.loadConfig(*configPath); err != nil { - slog.Error("loading configuration", "error", err) - os.Exit(1) - } - if err := s.deployHooks(); err != nil { - slog.Error("deploying hooks", "error", err) - os.Exit(1) - } - if err := loadTemplates(); err != nil { - slog.Error("loading templates", "error", err) - os.Exit(1) - } - if err := s.deployGit2D(); err != nil { - slog.Error("deploying git2d", "error", err) - os.Exit(1) - } - - // Launch Git2D - go func() { - cmd := exec.Command(s.config.Git.DaemonPath, s.config.Git.Socket) //#nosec G204 - cmd.Stderr = log.Writer() - cmd.Stdout = log.Writer() - if err := cmd.Run(); err != nil { - panic(err) - } - }() - - // UNIX socket listener for hooks - { - hooksListener, err := net.Listen("unix", s.config.Hooks.Socket) - if errors.Is(err, syscall.EADDRINUSE) { - slog.Warn("removing existing socket", "path", s.config.Hooks.Socket) - if err = syscall.Unlink(s.config.Hooks.Socket); err != nil { - slog.Error("removing existing socket", "path", s.config.Hooks.Socket, "error", err) - os.Exit(1) - } - if hooksListener, err = net.Listen("unix", s.config.Hooks.Socket); err != nil { - slog.Error("listening hooks", "error", err) - os.Exit(1) - } - } else if err != nil { - slog.Error("listening hooks", "error", err) - os.Exit(1) - } - slog.Info("listening hooks on unix", "path", s.config.Hooks.Socket) - go func() { - if err = s.serveGitHooks(hooksListener); err != nil { - slog.Error("serving hooks", "error", err) - os.Exit(1) - } - }() - } - - // UNIX socket listener for LMTP - { - lmtpListener, err := net.Listen("unix", s.config.LMTP.Socket) - if errors.Is(err, syscall.EADDRINUSE) { - slog.Warn("removing existing socket", "path", s.config.LMTP.Socket) - if err = syscall.Unlink(s.config.LMTP.Socket); err != nil { - slog.Error("removing existing socket", "path", s.config.LMTP.Socket, "error", err) - os.Exit(1) - } - if lmtpListener, err = net.Listen("unix", s.config.LMTP.Socket); err != nil { - slog.Error("listening LMTP", "error", err) - os.Exit(1) - } - } else if err != nil { - slog.Error("listening LMTP", "error", err) - os.Exit(1) - } - slog.Info("listening LMTP on unix", "path", s.config.LMTP.Socket) - go func() { - if err = s.serveLMTP(lmtpListener); err != nil { - slog.Error("serving LMTP", "error", err) - os.Exit(1) - } - }() - } - - // SSH listener - { - sshListener, err := net.Listen(s.config.SSH.Net, s.config.SSH.Addr) - if errors.Is(err, syscall.EADDRINUSE) && s.config.SSH.Net == "unix" { - slog.Warn("removing existing socket", "path", s.config.SSH.Addr) - if err = syscall.Unlink(s.config.SSH.Addr); err != nil { - slog.Error("removing existing socket", "path", s.config.SSH.Addr, "error", err) - os.Exit(1) - } - if sshListener, err = net.Listen(s.config.SSH.Net, s.config.SSH.Addr); err != nil { - slog.Error("listening SSH", "error", err) - os.Exit(1) - } - } else if err != nil { - slog.Error("listening SSH", "error", err) - os.Exit(1) - } - slog.Info("listening SSH on", "net", s.config.SSH.Net, "addr", s.config.SSH.Addr) - go func() { - if err = s.serveSSH(sshListener); err != nil { - slog.Error("serving SSH", "error", err) - os.Exit(1) - } - }() - } - - // HTTP listener - { - httpListener, err := net.Listen(s.config.HTTP.Net, s.config.HTTP.Addr) - if errors.Is(err, syscall.EADDRINUSE) && s.config.HTTP.Net == "unix" { - slog.Warn("removing existing socket", "path", s.config.HTTP.Addr) - if err = syscall.Unlink(s.config.HTTP.Addr); err != nil { - slog.Error("removing existing socket", "path", s.config.HTTP.Addr, "error", err) - os.Exit(1) - } - if httpListener, err = net.Listen(s.config.HTTP.Net, s.config.HTTP.Addr); err != nil { - slog.Error("listening HTTP", "error", err) - os.Exit(1) - } - } else if err != nil { - slog.Error("listening HTTP", "error", err) - os.Exit(1) - } - server := http.Server{ - Handler: &s, - 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, - } //exhaustruct:ignore - slog.Info("listening HTTP on", "net", s.config.HTTP.Net, "addr", s.config.HTTP.Addr) - go func() { - if err = server.Serve(httpListener); err != nil && !errors.Is(err, http.ErrServerClosed) { - slog.Error("serving HTTP", "error", err) - os.Exit(1) - } - }() - } - - // IRC bot - go s.ircBotLoop() - - select {} -} diff --git a/remote_url.go b/remote_url.go index 9f30993..453ddeb 100644 --- a/remote_url.go +++ b/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 main +package forge import ( "net/url" @@ -14,12 +14,12 @@ import ( // genSSHRemoteURL generates SSH remote URLs from a given group path and repo // name. -func (s *server) genSSHRemoteURL(groupPath []string, repoName string) string { - return strings.TrimSuffix(s.config.SSH.Root, "/") + "/" + misc.SegmentsToURL(groupPath) + "/-/repos/" + url.PathEscape(repoName) +func (s *Server) genSSHRemoteURL(groupPath []string, repoName string) string { + return strings.TrimSuffix(s.Config.SSH.Root, "/") + "/" + misc.SegmentsToURL(groupPath) + "/-/repos/" + url.PathEscape(repoName) } // genHTTPRemoteURL generates HTTP remote URLs from a given group path and repo // name. -func (s *server) genHTTPRemoteURL(groupPath []string, repoName string) string { - return strings.TrimSuffix(s.config.HTTP.Root, "/") + "/" + misc.SegmentsToURL(groupPath) + "/-/repos/" + url.PathEscape(repoName) +func (s *Server) genHTTPRemoteURL(groupPath []string, repoName string) string { + return strings.TrimSuffix(s.Config.HTTP.Root, "/") + "/" + misc.SegmentsToURL(groupPath) + "/-/repos/" + url.PathEscape(repoName) } diff --git a/resources.go b/resources.go index 5ecb218..ffe1008 100644 --- a/resources.go +++ b/resources.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "embed" @@ -1,34 +1,201 @@ -package main +package forge import ( + "errors" + "io/fs" + "log" + "log/slog" + "net" "net/http" + "os" + "os/exec" + "syscall" + "time" "github.com/jackc/pgx/v5/pgxpool" "go.lindenii.runxiyu.org/lindenii-common/cmap" goSSH "golang.org/x/crypto/ssh" ) -type server struct { - config Config +type Server struct { + Config Config - // database serves as the primary database handle for this entire application. + // Database serves as the primary Database handle for this entire application. // Transactions or single reads may be used from it. A [pgxpool.Pool] is // necessary to safely use pgx concurrently; pgx.Conn, etc. are insufficient. - database *pgxpool.Pool + Database *pgxpool.Pool - sourceHandler http.Handler - staticHandler http.Handler + SourceHandler http.Handler + StaticHandler http.Handler - ircSendBuffered chan string - ircSendDirectChan chan errorBack[string] + IrcSendBuffered chan string + IrcSendDirectChan chan errorBack[string] - // globalData is passed as "global" when rendering HTML templates. - globalData map[string]any + // GlobalData is passed as "global" when rendering HTML templates. + GlobalData map[string]any - serverPubkeyString string - serverPubkeyFP string - serverPubkey goSSH.PublicKey + ServerPubkeyString string + ServerPubkeyFP string + ServerPubkey goSSH.PublicKey - // packPasses contains hook cookies mapped to their packPass. - packPasses cmap.Map[string, packPass] + // PackPasses contains hook cookies mapped to their packPass. + PackPasses cmap.Map[string, packPass] +} + +func (s *Server) Setup() { + s.SourceHandler = http.StripPrefix( + "/-/source/", + http.FileServer(http.FS(embeddedSourceFS)), + ) + staticFS, err := fs.Sub(embeddedResourcesFS, "static") + if err != nil { + panic(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 + } +} + +func (s *Server) Run() { + if err := s.deployHooks(); err != nil { + slog.Error("deploying hooks", "error", err) + os.Exit(1) + } + if err := loadTemplates(); err != nil { + slog.Error("loading templates", "error", err) + os.Exit(1) + } + if err := s.deployGit2D(); err != nil { + slog.Error("deploying git2d", "error", err) + os.Exit(1) + } + + // Launch Git2D + go func() { + cmd := exec.Command(s.Config.Git.DaemonPath, s.Config.Git.Socket) //#nosec G204 + cmd.Stderr = log.Writer() + cmd.Stdout = log.Writer() + if err := cmd.Run(); err != nil { + panic(err) + } + }() + + // UNIX socket listener for hooks + { + hooksListener, err := net.Listen("unix", s.Config.Hooks.Socket) + if errors.Is(err, syscall.EADDRINUSE) { + slog.Warn("removing existing socket", "path", s.Config.Hooks.Socket) + if err = syscall.Unlink(s.Config.Hooks.Socket); err != nil { + slog.Error("removing existing socket", "path", s.Config.Hooks.Socket, "error", err) + os.Exit(1) + } + if hooksListener, err = net.Listen("unix", s.Config.Hooks.Socket); err != nil { + slog.Error("listening hooks", "error", err) + os.Exit(1) + } + } else if err != nil { + slog.Error("listening hooks", "error", err) + os.Exit(1) + } + slog.Info("listening hooks on unix", "path", s.Config.Hooks.Socket) + go func() { + if err = s.serveGitHooks(hooksListener); err != nil { + slog.Error("serving hooks", "error", err) + os.Exit(1) + } + }() + } + + // UNIX socket listener for LMTP + { + lmtpListener, err := net.Listen("unix", s.Config.LMTP.Socket) + if errors.Is(err, syscall.EADDRINUSE) { + slog.Warn("removing existing socket", "path", s.Config.LMTP.Socket) + if err = syscall.Unlink(s.Config.LMTP.Socket); err != nil { + slog.Error("removing existing socket", "path", s.Config.LMTP.Socket, "error", err) + os.Exit(1) + } + if lmtpListener, err = net.Listen("unix", s.Config.LMTP.Socket); err != nil { + slog.Error("listening LMTP", "error", err) + os.Exit(1) + } + } else if err != nil { + slog.Error("listening LMTP", "error", err) + os.Exit(1) + } + slog.Info("listening LMTP on unix", "path", s.Config.LMTP.Socket) + go func() { + if err = s.serveLMTP(lmtpListener); err != nil { + slog.Error("serving LMTP", "error", err) + os.Exit(1) + } + }() + } + + // SSH listener + { + sshListener, err := net.Listen(s.Config.SSH.Net, s.Config.SSH.Addr) + if errors.Is(err, syscall.EADDRINUSE) && s.Config.SSH.Net == "unix" { + slog.Warn("removing existing socket", "path", s.Config.SSH.Addr) + if err = syscall.Unlink(s.Config.SSH.Addr); err != nil { + slog.Error("removing existing socket", "path", s.Config.SSH.Addr, "error", err) + os.Exit(1) + } + if sshListener, err = net.Listen(s.Config.SSH.Net, s.Config.SSH.Addr); err != nil { + slog.Error("listening SSH", "error", err) + os.Exit(1) + } + } else if err != nil { + slog.Error("listening SSH", "error", err) + os.Exit(1) + } + slog.Info("listening SSH on", "net", s.Config.SSH.Net, "addr", s.Config.SSH.Addr) + go func() { + if err = s.serveSSH(sshListener); err != nil { + slog.Error("serving SSH", "error", err) + os.Exit(1) + } + }() + } + + // HTTP listener + { + httpListener, err := net.Listen(s.Config.HTTP.Net, s.Config.HTTP.Addr) + if errors.Is(err, syscall.EADDRINUSE) && s.Config.HTTP.Net == "unix" { + slog.Warn("removing existing socket", "path", s.Config.HTTP.Addr) + if err = syscall.Unlink(s.Config.HTTP.Addr); err != nil { + slog.Error("removing existing socket", "path", s.Config.HTTP.Addr, "error", err) + os.Exit(1) + } + if httpListener, err = net.Listen(s.Config.HTTP.Net, s.Config.HTTP.Addr); err != nil { + slog.Error("listening HTTP", "error", err) + os.Exit(1) + } + } else if err != nil { + slog.Error("listening HTTP", "error", err) + os.Exit(1) + } + server := http.Server{ + Handler: s, + 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, + } //exhaustruct:ignore + slog.Info("listening HTTP on", "net", s.Config.HTTP.Net, "addr", s.Config.HTTP.Addr) + go func() { + if err = server.Serve(httpListener); err != nil && !errors.Is(err, http.ErrServerClosed) { + slog.Error("serving HTTP", "error", err) + os.Exit(1) + } + }() + } + + // IRC bot + go s.ircBotLoop() + + select {} } diff --git a/ssh_handle_receive_pack.go b/ssh_handle_receive_pack.go index 33262e4..724c3fd 100644 --- a/ssh_handle_receive_pack.go +++ b/ssh_handle_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 main +package forge import ( "errors" @@ -30,7 +30,7 @@ type packPass struct { } // sshHandleRecvPack handles attempts to push to repos. -func (s *server) sshHandleRecvPack(session gliderSSH.Session, pubkey, repoIdentifier string) (err error) { +func (s *Server) sshHandleRecvPack(session gliderSSH.Session, pubkey, repoIdentifier string) (err error) { groupPath, repoName, repoID, repoPath, directAccess, contribReq, userType, userID, err := s.getRepoInfo2(session.Context(), repoIdentifier, pubkey) if err != nil { return err @@ -51,7 +51,7 @@ func (s *server) sshHandleRecvPack(session gliderSSH.Session, pubkey, repoIdenti } hooksPath := repoConfCore.OptionAll("hooksPath") - if len(hooksPath) != 1 || hooksPath[0] != s.config.Hooks.Execs { + if len(hooksPath) != 1 || hooksPath[0] != s.Config.Hooks.Execs { return errors.New("repository has hooksPath set to an unexpected value") } @@ -91,7 +91,7 @@ func (s *server) sshHandleRecvPack(session gliderSSH.Session, pubkey, repoIdenti fmt.Fprintln(session.Stderr(), "Error while generating cookie:", err) } - s.packPasses.Store(cookie, packPass{ + s.PackPasses.Store(cookie, packPass{ session: session, pubkey: pubkey, directAccess: directAccess, @@ -104,13 +104,13 @@ func (s *server) sshHandleRecvPack(session gliderSSH.Session, pubkey, repoIdenti contribReq: contribReq, userType: userType, }) - defer s.packPasses.Delete(cookie) + defer s.PackPasses.Delete(cookie) // The Delete won't execute until proc.Wait returns unless something // horribly wrong such as a panic occurs. proc := exec.CommandContext(session.Context(), "git-receive-pack", repoPath) proc.Env = append(os.Environ(), - "LINDENII_FORGE_HOOKS_SOCKET_PATH="+s.config.Hooks.Socket, + "LINDENII_FORGE_HOOKS_SOCKET_PATH="+s.Config.Hooks.Socket, "LINDENII_FORGE_HOOKS_COOKIE="+cookie, ) proc.Stdin = session diff --git a/ssh_handle_upload_pack.go b/ssh_handle_upload_pack.go index 7f2a52c..45ecfd8 100644 --- a/ssh_handle_upload_pack.go +++ b/ssh_handle_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 main +package forge import ( "fmt" @@ -13,14 +13,14 @@ import ( // sshHandleUploadPack handles clones/fetches. It just uses git-upload-pack // and has no ACL checks. -func (s *server) sshHandleUploadPack(session glider_ssh.Session, pubkey, repoIdentifier string) (err error) { +func (s *Server) sshHandleUploadPack(session glider_ssh.Session, pubkey, repoIdentifier string) (err error) { var repoPath string if _, _, _, repoPath, _, _, _, _, err = s.getRepoInfo2(session.Context(), repoIdentifier, pubkey); err != nil { return err } proc := exec.CommandContext(session.Context(), "git-upload-pack", repoPath) - proc.Env = append(os.Environ(), "LINDENII_FORGE_HOOKS_SOCKET_PATH="+s.config.Hooks.Socket) + proc.Env = append(os.Environ(), "LINDENII_FORGE_HOOKS_SOCKET_PATH="+s.Config.Hooks.Socket) proc.Stdin = session proc.Stdout = session proc.Stderr = session.Stderr() diff --git a/ssh_server.go b/ssh_server.go index afb0d95..ed303b9 100644 --- a/ssh_server.go +++ b/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 main +package forge import ( "fmt" @@ -19,13 +19,13 @@ import ( // 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 (s *server) serveSSH(listener net.Listener) error { +func (s *Server) serveSSH(listener net.Listener) error { var hostKeyBytes []byte var hostKey goSSH.Signer var err error var server *gliderSSH.Server - if hostKeyBytes, err = os.ReadFile(s.config.SSH.Key); err != nil { + if hostKeyBytes, err = os.ReadFile(s.Config.SSH.Key); err != nil { return err } @@ -33,9 +33,9 @@ func (s *server) serveSSH(listener net.Listener) error { return err } - s.serverPubkey = hostKey.PublicKey() - s.serverPubkeyString = misc.BytesToString(goSSH.MarshalAuthorizedKey(s.serverPubkey)) - s.serverPubkeyFP = goSSH.FingerprintSHA256(s.serverPubkey) + s.ServerPubkey = hostKey.PublicKey() + s.ServerPubkeyString = misc.BytesToString(goSSH.MarshalAuthorizedKey(s.ServerPubkey)) + s.ServerPubkeyFP = goSSH.FingerprintSHA256(s.ServerPubkey) server = &gliderSSH.Server{ Handler: func(session gliderSSH.Session) { @@ -46,7 +46,7 @@ func (s *server) serveSSH(listener net.Listener) error { } slog.Info("incoming ssh", "addr", session.RemoteAddr().String(), "key", clientPubkeyStr, "command", session.RawCommand()) - fmt.Fprintln(session.Stderr(), ansiec.Blue+"Lindenii Forge "+VERSION+", source at "+strings.TrimSuffix(s.config.HTTP.Root, "/")+"/-/source/"+ansiec.Reset+"\r") + fmt.Fprintln(session.Stderr(), ansiec.Blue+"Lindenii Forge "+VERSION+", source at "+strings.TrimSuffix(s.Config.HTTP.Root, "/")+"/-/source/"+ansiec.Reset+"\r") cmd := session.Command() diff --git a/ssh_utils.go b/ssh_utils.go index 02069dd..8f04209 100644 --- a/ssh_utils.go +++ b/ssh_utils.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "context" @@ -18,7 +18,7 @@ var errIllegalSSHRepoPath = errors.New("illegal SSH repo path") // getRepoInfo2 also fetches repo information... it should be deprecated and // implemented in individual handlers. -func (s *server) getRepoInfo2(ctx context.Context, sshPath, sshPubkey string) (groupPath []string, repoName string, repoID int, repoPath string, directAccess bool, contribReq, userType string, userID int, err error) { +func (s *Server) getRepoInfo2(ctx context.Context, sshPath, sshPubkey string) (groupPath []string, repoName string, repoID int, repoPath string, directAccess bool, contribReq, userType string, userID int, err error) { var segments []string var sepIndex int var moduleType, moduleName string @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> -package main +package forge import ( "context" @@ -12,10 +12,10 @@ import ( // 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 (s *Server) addUserSSH(ctx context.Context, pubkey string) (userID int, err error) { var txn pgx.Tx - if txn, err = s.database.Begin(ctx); err != nil { + if txn, err = s.Database.Begin(ctx); err != nil { return } defer func() { @@ -1,3 +1,3 @@ -package main +package forge var VERSION = "unknown" //nolint:gochecknoglobals |