Lindenii Project Forge
Do not export version
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> 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 // 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 sourceHandler http.Handler staticHandler http.Handler ircSendBuffered chan string ircSendDirectChan chan errorBack[string] // 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] } 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,
"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 {} }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package forge import ( "fmt" "log/slog" "net" "os" "strings" gliderSSH "github.com/gliderlabs/ssh" "go.lindenii.runxiyu.org/forge/internal/ansiec" "go.lindenii.runxiyu.org/forge/internal/misc" goSSH "golang.org/x/crypto/ssh" ) // 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 { 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 { return err } if hostKey, err = goSSH.ParsePrivateKey(hostKeyBytes); err != nil { return err } 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) { clientPubkey := session.PublicKey() var clientPubkeyStr string if clientPubkey != nil { clientPubkeyStr = strings.TrimSuffix(misc.BytesToString(goSSH.MarshalAuthorizedKey(clientPubkey)), "\n") } 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() if len(cmd) < 2 { fmt.Fprintln(session.Stderr(), "Insufficient arguments\r") return } switch cmd[0] { case "git-upload-pack": if len(cmd) > 2 { fmt.Fprintln(session.Stderr(), "Too many arguments\r") return } err = s.sshHandleUploadPack(session, clientPubkeyStr, cmd[1]) case "git-receive-pack": if len(cmd) > 2 { fmt.Fprintln(session.Stderr(), "Too many arguments\r") return } err = s.sshHandleRecvPack(session, clientPubkeyStr, cmd[1]) default: fmt.Fprintln(session.Stderr(), "Unsupported command: "+cmd[0]+"\r") return } if err != nil { fmt.Fprintln(session.Stderr(), err.Error()) return } }, PublicKeyHandler: func(_ gliderSSH.Context, _ gliderSSH.PublicKey) bool { return true }, KeyboardInteractiveHandler: func(_ gliderSSH.Context, _ goSSH.KeyboardInteractiveChallenge) bool { return true }, // It is intentional that we do not check any credentials and accept all connections. // This allows all users to connect and clone repositories. However, the public key // is passed to handlers, so e.g. the push handler could check the key and reject the // push if it needs to. } //exhaustruct:ignore server.AddHostKey(hostKey) if err = server.Serve(listener); err != nil { slog.Error("error serving SSH", "error", err.Error()) os.Exit(1) } return nil }
package forge
var VERSION = "unknown" //nolint:gochecknoglobals
var version = "unknown" //nolint:gochecknoglobals