Gitfed
bastien-mrq/gitfed / cmd / gitfed-server / main.go
// 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()
}