Hi… I am well aware that this diff view is very suboptimal. It will be fixed when the refactored server comes along!
Slight refactor on NewServer
// 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 := forge.NewServer(*configPath)
s.Run() }
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package forge
import (
"bufio"
"errors"
"log/slog"
"os"
"go.lindenii.runxiyu.org/forge/internal/database"
"go.lindenii.runxiyu.org/forge/internal/irc"
"go.lindenii.runxiyu.org/forge/internal/scfg"
)
type Config struct {
HTTP struct {
Net string `scfg:"net"`
Addr string `scfg:"addr"`
CookieExpiry int `scfg:"cookie_expiry"`
Root string `scfg:"root"`
ReadTimeout uint32 `scfg:"read_timeout"`
WriteTimeout uint32 `scfg:"write_timeout"`
IdleTimeout uint32 `scfg:"idle_timeout"`
ReverseProxy bool `scfg:"reverse_proxy"`
} `scfg:"http"`
Hooks struct {
Socket string `scfg:"socket"`
Execs string `scfg:"execs"`
} `scfg:"hooks"`
LMTP struct {
Socket string `scfg:"socket"`
Domain string `scfg:"domain"`
MaxSize int64 `scfg:"max_size"`
WriteTimeout uint32 `scfg:"write_timeout"`
ReadTimeout uint32 `scfg:"read_timeout"`
} `scfg:"lmtp"`
Git struct {
RepoDir string `scfg:"repo_dir"`
Socket string `scfg:"socket"`
DaemonPath string `scfg:"daemon_path"`
} `scfg:"git"`
SSH struct {
Net string `scfg:"net"`
Addr string `scfg:"addr"`
Key string `scfg:"key"`
Root string `scfg:"root"`
} `scfg:"ssh"`
IRC irc.Config `scfg:"irc"`
General struct {
Title string `scfg:"title"`
} `scfg:"general"`
DB struct {
Type string `scfg:"type"`
Conn string `scfg:"conn"`
} `scfg:"db"`
}
// 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) {
func (s *Server) loadConfig(path string) (err error) {
var configFile *os.File
if configFile, err = os.Open(path); err != nil {
return err
}
defer configFile.Close()
decoder := scfg.NewDecoder(bufio.NewReader(configFile))
if err = decoder.Decode(&s.config); err != nil {
return 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
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package forge
import (
"errors"
"html/template"
"io/fs"
"log"
"log/slog"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"syscall"
"time"
"go.lindenii.runxiyu.org/forge/internal/database"
"go.lindenii.runxiyu.org/forge/internal/irc"
"go.lindenii.runxiyu.org/forge/internal/misc"
"go.lindenii.runxiyu.org/lindenii-common/cmap"
goSSH "golang.org/x/crypto/ssh"
)
type Server struct {
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]
templates *template.Template
ircBot *irc.Bot
ready bool
}
func (s *Server) Setup() {
func (s *Server) NewServer(configPath string) error {
if err := s.loadConfig(configPath); err != nil {
return err
}
s.sourceHandler = http.StripPrefix(
"/-/source/",
http.FileServer(http.FS(embeddedSourceFS)),
)
staticFS, err := fs.Sub(embeddedResourcesFS, "static")
if err != nil {
panic(err)
return 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
}
misc.NoneOrPanic(s.loadTemplates())
misc.NoneOrPanic(misc.DeployBinary(misc.FirstOrPanic(embeddedResourcesFS.Open("git2d/git2d")), s.config.Git.DaemonPath))
misc.NoneOrPanic(misc.DeployBinary(misc.FirstOrPanic(embeddedResourcesFS.Open("hookc/hookc")), filepath.Join(s.config.Hooks.Execs, "pre-receive")))
misc.NoneOrPanic(os.Chmod(filepath.Join(s.config.Hooks.Execs, "pre-receive"), 0o755))
s.ready = true
return nil
}
func (s *Server) Run() error {
if !s.ready {
return errors.New("not ready")
}
// 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.ircBot.ConnectLoop()
select {}
}