// gitfed-server is the gitfed daemon: SSH git server + federation
// well-known HTTP endpoint, per DESIGN.md §4/§8.
package main
import (
"errors"
"flag"
"fmt"
"log"
"net/http"
"os"
"time"
"git.neuromancer.ovh/bastien-mrq/gitfed/internal/admin"
"git.neuromancer.ovh/bastien-mrq/gitfed/internal/adminrpc"
"git.neuromancer.ovh/bastien-mrq/gitfed/internal/ca"
"git.neuromancer.ovh/bastien-mrq/gitfed/internal/config"
"git.neuromancer.ovh/bastien-mrq/gitfed/internal/federation"
"git.neuromancer.ovh/bastien-mrq/gitfed/internal/ssh"
"git.neuromancer.ovh/bastien-mrq/gitfed/internal/store"
"git.neuromancer.ovh/bastien-mrq/gitfed/internal/version"
)
func main() {
configPath := flag.String("config", "gitfed.json", "path to instance config file")
initDomain := flag.String("init", "", "initialize a new instance for this domain and exit")
dataDir := flag.String("data-dir", "data", "data directory to use with -init")
showVersion := flag.Bool("version", false, "print the version and exit")
flag.Parse()
if *showVersion {
fmt.Println("gitfed-server " + version.Version)
return
}
if *initDomain != "" {
if err := initInstance(*configPath, *initDomain, *dataDir); err != nil {
log.Fatalf("init: %v", err)
}
return
}
if err := run(*configPath); err != nil {
log.Fatalf("gitfed-server: %v", err)
}
}
func initInstance(configPath, domain, dataDir string) error {
if _, err := os.Stat(configPath); err == nil {
return fmt.Errorf("%s already exists", configPath)
}
cfg := config.Default(domain, dataDir)
if err := os.MkdirAll(cfg.ReposDir, 0755); err != nil {
return err
}
if _, err := ca.LoadOrCreate(cfg.CADir()); err != nil {
return err
}
st, err := store.Open(cfg.DBPath())
if err != nil {
return err
}
defer st.Close()
if err := st.PutInstanceMeta(store.InstanceMeta{
Domain: cfg.Domain,
TrustPolicy: cfg.TrustPolicy,
CertTTLHours: cfg.CertTTLHours,
}); err != nil {
return err
}
if err := config.Save(configPath, cfg); err != nil {
return err
}
fmt.Printf("gitfed instance initialized for domain %q\n", domain)
fmt.Printf("config: %s\n", configPath)
fmt.Printf("data dir: %s\n", cfg.DataDir)
fmt.Println("edit the config file to adjust listen addresses, then run: gitfed-server -config " + configPath)
return nil
}
func run(configPath string) error {
log.Printf("gitfed-server %s starting", version.Version)
cfg, err := config.Load(configPath)
if err != nil {
return fmt.Errorf("load config (did you run -init first?): %w", err)
}
st, err := store.Open(cfg.DBPath())
if err != nil {
return err
}
defer st.Close()
localCA, err := ca.LoadOrCreate(cfg.CADir())
if err != nil {
return err
}
// Instance metadata in the store is the live source of truth for trust
// policy (editable from the TUI); seed it from the config on first run.
if _, err := st.GetInstanceMeta(); errors.Is(err, store.ErrNotFound) {
if err := st.PutInstanceMeta(store.InstanceMeta{
Domain: cfg.Domain,
TrustPolicy: cfg.TrustPolicy,
CertTTLHours: cfg.CertTTLHours,
}); err != nil {
return err
}
}
// Sweep expired web sessions periodically — GetSession only evicts them
// lazily on access, so ones never looked up again would otherwise linger.
go func() {
for range time.Tick(time.Hour) {
if n, err := st.PurgeExpiredSessions(); err != nil {
log.Printf("session purge: %v", err)
} else if n > 0 {
log.Printf("purged %d expired session(s)", n)
}
}
}()
resolver := federation.NewResolver(st, cfg.Domain, cfg.InsecureFederation)
adminOps := admin.New(st, resolver, cfg.Domain, cfg.ReposDir, localCA)
go func() {
rpcServer := adminrpc.NewServer(adminOps, cfg.AdminSocketPath())
if err := rpcServer.ListenAndServe(); err != nil {
log.Printf("admin socket server: %v", err)
}
}()
if cfg.ListenHTTP != "" {
go func() {
handler := federation.Handler(cfg.Domain, cfg.Contact, version.Version, localCA)
mux := http.NewServeMux()
mux.Handle("/.well-known/gitfed.json", handler)
mux.Handle("POST /.well-known/gitfed-notify", federation.NotifyHandler(st, cfg.Domain, cfg.InsecureFederation))
srv := &http.Server{
Addr: cfg.ListenHTTP,
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second,
}
log.Printf("gitfed well-known endpoint listening on %s", cfg.ListenHTTP)
if err := srv.ListenAndServe(); err != nil {
log.Printf("well-known http server: %v", err)
}
}()
}
srv, err := ssh.New(cfg, st, localCA, resolver)
if err != nil {
return err
}
return srv.ListenAndServe()
}