Lindenii Project Forge
Stub LMTP listener
linters: enable-all: true disable: - tenv - depguard - err113 # dynamically defined errors are fine for our purposes - forcetypeassert # type assertion failures are usually programming errors - gochecknoglobals # doesn't matter since this isn't a library - gochecknoinits # we use inits sparingly for good reasons - godox # they're just used as markers for where needs improvements - ireturn # doesn't work well with how we use generics - lll # long lines are acceptable - mnd # it's a bit ridiculous to replace all of them - nakedret # patterns should be consistent - nonamedreturns # i like named returns - wrapcheck # wrapping all errors is just not necessary - maintidx # e - nestif # e - gocognit # e - gocyclo # e
- dupl # e
- cyclop # e - goconst # e - funlen # e - wsl # e - nlreturn # e - unused # e - exhaustruct # e issues: max-issues-per-linter: 0 max-same-issues: 0
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package main import ( "bufio" "context" "errors" "os" "github.com/jackc/pgx/v5/pgxpool" "go.lindenii.runxiyu.org/lindenii-common/scfg" ) // config holds the global configuration used by this instance. There is // currently no synchronization mechanism, so it must not be modified after // request handlers are spawned. var 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"` } `scfg:"lmtp"`
Git struct { RepoDir string `scfg:"repo_dir"` } `scfg:"git"` SSH struct { Net string `scfg:"net"` Addr string `scfg:"addr"` Key string `scfg:"key"` Root string `scfg:"root"` } `scfg:"ssh"` IRC struct { Net string `scfg:"net"` Addr string `scfg:"addr"` TLS bool `scfg:"tls"` SendQ uint `scfg:"sendq"` Nick string `scfg:"nick"` User string `scfg:"user"` Gecos string `scfg:"gecos"` } `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. // // TODO: Currently, it returns an error when the user specifies any unknown // 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 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(&config); err != nil { return err } if config.DB.Type != "postgres" { return errors.New("unsupported database type") } if database, err = pgxpool.New(context.Background(), config.DB.Conn); err != nil { return err } globalData["forge_title"] = config.General.Title return nil }
http { # What network transport should we listen on? # Examples: tcp tcp4 tcp6 unix net tcp # What address to listen on? # Examples for net tcp*: 127.0.0.1:8080 :80 # Example for unix: /var/run/lindenii/forge/http.sock addr /var/run/lindenii/forge/http.sock # How many seconds should cookies be remembered before they are purged? cookie_expiry 604800 # What is the canonical URL of the web root? root https://forge.example.org # General HTTP server context timeout settings. It's recommended to # set them slightly higher than usual as Git operations over large # repos may take a long time. read_timeout 120 write_timeout 1800 idle_timeout 120 # Are we running behind a reverse proxy? If so, we will trust # X-Forwarded-For headers. reverse_proxy true } irc { tls true net tcp addr irc.runxiyu.org:6697 sendq 6000 nick forge-test user forge gecos "Lindenii Forge Test" } git { repo_dir /var/lib/lindenii/forge/repos } ssh { # What network transport should we listen on? # This should be "tcp" in almost all cases. net tcp # What address to listen on? addr :22 # What is the path to the SSH host key? Generate it with ssh-keygen. # The key must have an empty password. key /etc/lindenii/ssh_host_ed25519_key # What is the canonical SSH URL? root ssh://forge.example.org } general { title "Test Forge" } db { # What type of database are we connecting to? # Currently only "postgres" is supported. type postgres # What is the connection string? conn postgresql:///lindenii-forge?host=/var/run/postgresql } hooks { # On which UNIX domain socket should we listen for hook callbacks on? socket /var/run/lindenii/forge/hooks.sock # Where should hook executables be put? execs /usr/libexec/lindenii/forge/hooks }
lmtp { # On which UNIX domain socket should we listen for LMTP on? socket /var/run/lindenii/forge/lmtp.sock }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package main import "net" func serveLMTP(_ net.Listener) error { return nil }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package main import ( "errors" "flag" "net" "net/http" "syscall" "time" "go.lindenii.runxiyu.org/lindenii-common/clog" ) func main() { configPath := flag.String( "config", "/etc/lindenii/forge.scfg", "path to configuration file", ) flag.Parse() if err := loadConfig(*configPath); err != nil { clog.Fatal(1, "Loading configuration: "+err.Error()) } if err := deployHooks(); err != nil { clog.Fatal(1, "Deploying hooks to filesystem: "+err.Error()) } if err := loadTemplates(); err != nil { clog.Fatal(1, "Loading templates: "+err.Error()) } // UNIX socket listener for hooks
var hooksListener net.Listener var err error hooksListener, err = net.Listen("unix", config.Hooks.Socket) if errors.Is(err, syscall.EADDRINUSE) { clog.Warn("Removing existing socket " + config.Hooks.Socket) if err = syscall.Unlink(config.Hooks.Socket); err != nil { clog.Fatal(1, "Removing existing socket: "+err.Error()) } if hooksListener, err = net.Listen("unix", config.Hooks.Socket); err != nil {
{ hooksListener, err := net.Listen("unix", config.Hooks.Socket) if errors.Is(err, syscall.EADDRINUSE) { clog.Warn("Removing existing socket " + config.Hooks.Socket) if err = syscall.Unlink(config.Hooks.Socket); err != nil { clog.Fatal(1, "Removing existing socket: "+err.Error()) } if hooksListener, err = net.Listen("unix", config.Hooks.Socket); err != nil { clog.Fatal(1, "Listening hooks: "+err.Error()) } } else if err != nil {
clog.Fatal(1, "Listening hooks: "+err.Error()) }
} else if err != nil { clog.Fatal(1, "Listening hooks: "+err.Error())
clog.Info("Listening hooks on unix " + config.Hooks.Socket) go func() { if err = serveGitHooks(hooksListener); err != nil { clog.Fatal(1, "Serving hooks: "+err.Error()) } }()
}
clog.Info("Listening hooks on unix " + config.Hooks.Socket) go func() { if err = serveGitHooks(hooksListener); err != nil { clog.Fatal(1, "Serving hooks: "+err.Error())
// UNIX socket listener for LMTP { lmtpListener, err := net.Listen("unix", config.LMTP.Socket) if errors.Is(err, syscall.EADDRINUSE) { clog.Warn("Removing existing socket " + config.LMTP.Socket) if err = syscall.Unlink(config.LMTP.Socket); err != nil { clog.Fatal(1, "Removing existing socket: "+err.Error()) } if lmtpListener, err = net.Listen("unix", config.LMTP.Socket); err != nil { clog.Fatal(1, "Listening LMTP: "+err.Error()) } } else if err != nil { clog.Fatal(1, "Listening LMTP: "+err.Error())
}
}()
clog.Info("Listening LMTP on unix " + config.LMTP.Socket) go func() { if err = serveLMTP(lmtpListener); err != nil { clog.Fatal(1, "Serving LMTP: "+err.Error()) } }() }
// SSH listener
sshListener, err := net.Listen(config.SSH.Net, config.SSH.Addr) if errors.Is(err, syscall.EADDRINUSE) && config.SSH.Net == "unix" { clog.Warn("Removing existing socket " + config.SSH.Addr) if err = syscall.Unlink(config.SSH.Addr); err != nil { clog.Fatal(1, "Removing existing socket: "+err.Error()) } if sshListener, err = net.Listen(config.SSH.Net, config.SSH.Addr); err != nil {
{ sshListener, err := net.Listen(config.SSH.Net, config.SSH.Addr) if errors.Is(err, syscall.EADDRINUSE) && config.SSH.Net == "unix" { clog.Warn("Removing existing socket " + config.SSH.Addr) if err = syscall.Unlink(config.SSH.Addr); err != nil { clog.Fatal(1, "Removing existing socket: "+err.Error()) } if sshListener, err = net.Listen(config.SSH.Net, config.SSH.Addr); err != nil { clog.Fatal(1, "Listening SSH: "+err.Error()) } } else if err != nil {
clog.Fatal(1, "Listening SSH: "+err.Error()) }
} else if err != nil { clog.Fatal(1, "Listening SSH: "+err.Error())
clog.Info("Listening SSH on " + config.SSH.Net + " " + config.SSH.Addr) go func() { if err = serveSSH(sshListener); err != nil { clog.Fatal(1, "Serving SSH: "+err.Error()) } }()
}
clog.Info("Listening SSH on " + config.SSH.Net + " " + config.SSH.Addr) go func() { if err = serveSSH(sshListener); err != nil { clog.Fatal(1, "Serving SSH: "+err.Error()) } }()
// HTTP listener
httpListener, err := net.Listen(config.HTTP.Net, config.HTTP.Addr) if errors.Is(err, syscall.EADDRINUSE) && config.HTTP.Net == "unix" { clog.Warn("Removing existing socket " + config.HTTP.Addr) if err = syscall.Unlink(config.HTTP.Addr); err != nil { clog.Fatal(1, "Removing existing socket: "+err.Error()) } if httpListener, err = net.Listen(config.HTTP.Net, config.HTTP.Addr); err != nil {
{ httpListener, err := net.Listen(config.HTTP.Net, config.HTTP.Addr) if errors.Is(err, syscall.EADDRINUSE) && config.HTTP.Net == "unix" { clog.Warn("Removing existing socket " + config.HTTP.Addr) if err = syscall.Unlink(config.HTTP.Addr); err != nil { clog.Fatal(1, "Removing existing socket: "+err.Error()) } if httpListener, err = net.Listen(config.HTTP.Net, config.HTTP.Addr); err != nil { clog.Fatal(1, "Listening HTTP: "+err.Error()) } } else if err != nil {
clog.Fatal(1, "Listening HTTP: "+err.Error()) }
} else if err != nil { clog.Fatal(1, "Listening HTTP: "+err.Error())
server := http.Server{ Handler: &forgeHTTPRouter{}, ReadTimeout: time.Duration(config.HTTP.ReadTimeout) * time.Second, WriteTimeout: time.Duration(config.HTTP.ReadTimeout) * time.Second, IdleTimeout: time.Duration(config.HTTP.ReadTimeout) * time.Second, } //exhaustruct:ignore clog.Info("Listening HTTP on " + config.HTTP.Net + " " + config.HTTP.Addr) go func() { if err = server.Serve(httpListener); err != nil && !errors.Is(err, http.ErrServerClosed) { clog.Fatal(1, "Serving HTTP: "+err.Error()) } }()
}
server := http.Server{ Handler: &forgeHTTPRouter{}, ReadTimeout: time.Duration(config.HTTP.ReadTimeout) * time.Second, WriteTimeout: time.Duration(config.HTTP.ReadTimeout) * time.Second, IdleTimeout: time.Duration(config.HTTP.ReadTimeout) * time.Second, } //exhaustruct:ignore clog.Info("Listening HTTP on " + config.HTTP.Net + " " + config.HTTP.Addr) go func() { if err = server.Serve(httpListener); err != nil && !errors.Is(err, http.ErrServerClosed) { clog.Fatal(1, "Serving HTTP: "+err.Error()) } }()
// IRC bot go ircBotLoop() select {} }