Gitfed
bastien-mrq/gitfed / cmd / gitfed-web / main.go
// gitfed-web is gitfed's web UI: a public repo browser (README/LICENSE/tags,
// public repos, no login needed) plus, behind a real username/password
// login, self-service (manage your own keys, repos, collaborators) and an
// admin section for instance-admin accounts. This is DESIGN.md §9 Phase 4
// ("UI web plus tard, branchée sur le même noyau identité/ACL") — it drives
// the same admin.Ops interface as gitfed-tui (live socket or offline store,
// via internal/opsconnect), not a second implementation of the repo/ACL
// logic.
//
// SSH keys remain the only way to actually push/pull git — the password
// login here only ever grants access to this web UI.
package main

import (
	"flag"
	"fmt"
	"net/http"
	"os"
	"time"

	"git.neuromancer.ovh/bastien-mrq/gitfed/internal/admin"
	"git.neuromancer.ovh/bastien-mrq/gitfed/internal/config"
	"git.neuromancer.ovh/bastien-mrq/gitfed/internal/opsconnect"
)

type server struct {
	ops    admin.Ops
	domain string

	loginByIP   *rateLimiter
	loginByUser *rateLimiter
	gitHTTPByIP *rateLimiter
}

func main() {
	configPath := flag.String("config", "gitfed.json", "path to instance config file")
	listen := flag.String("listen", ":8088", "address to serve the web UI on")
	flag.Parse()

	cfg, err := config.Load(*configPath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "gitfed-web: load config: %v\n", err)
		fmt.Fprintln(os.Stderr, "hint: run gitfed-server -init <domain> first")
		os.Exit(1)
	}

	ops, mode, closeFn, err := opsconnect.Connect(cfg)
	if err != nil {
		fmt.Fprintf(os.Stderr, "gitfed-web: %v\n", err)
		os.Exit(1)
	}
	defer closeFn()

	s := &server{
		ops:    ops,
		domain: cfg.Domain,
		// Per-account is the tighter bound (an attacker targeting one login);
		// per-IP is looser but catches spraying across many usernames from
		// one source. Either tripping blocks the attempt.
		loginByUser: newRateLimiter(5, 15*time.Minute),
		loginByIP:   newRateLimiter(20, 15*time.Minute),
		// The anonymous git-clone endpoints (handlers_git_http.go) have no
		// login to fail, so this counts every request rather than just
		// failures — generous enough for a real clone (which is a handful
		// of requests: one info/refs, one or more upload-pack), tight
		// enough to blunt someone hammering it for disk/CPU/bandwidth.
		gitHTTPByIP: newRateLimiter(60, time.Minute),
	}

	mux := http.NewServeMux()
	s.routes(mux)

	// Security middleware wraps the whole mux: same-origin enforcement on
	// state-changing requests (CSRF defense-in-depth on top of SameSite
	// cookies) and the standard hardening response headers.
	handler := securityHeaders(s.sameOriginPOST(mux))

	srv := &http.Server{
		Addr:              *listen,
		Handler:           handler,
		ReadHeaderTimeout: 5 * time.Second,
		ReadTimeout:       15 * time.Second,
		WriteTimeout:      30 * time.Second,
		IdleTimeout:       60 * time.Second,
	}

	fmt.Printf("gitfed-web serving on %s (mode: %s)\n", *listen, mode)
	if err := srv.ListenAndServe(); err != nil {
		fmt.Fprintln(os.Stderr, "gitfed-web:", err)
		os.Exit(1)
	}
}