Hi… I am well aware that this diff view is very suboptimal. It will be fixed when the refactored server comes along!
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