git@cosmocr.at/gitweb.git @ main · summary refs log
main / main.go /
main.go · 5.5 KiB · raw
view: SourceMarkdown ☑ force

// Command gitweb serves git repositories stored in upspin over HTTPS: a browse // UI plus the git smart-HTTP protocol for clone and push. It runs as a single // upspin user (git@cosmocr.at); a repo owner publishes a path by granting that // user access in an upspin Access file. The upspin path is the URL path, so a // repo lives at https://git.cosmocr.at/user@domain/. // // Public vs private is derived from the repo's governing Access file: a repo // readable by all is public (browse and clone anonymously); otherwise callers // must authenticate with HTTP Basic auth, supplying either a vault-issued SSO // token (see cmd/git-credential-cosmocr) or a personal access token. Push // requires write access in the Access file. See README.md. package main

import ( "crypto/tls" "embed" "flag" "io/fs" "log" "net/http" "os" "sync" "time"

"gitweb/internal/auth"
"gitweb/internal/server"
"gitweb/internal/store"

"upspin.io/config"
"upspin.io/rpc"
"upspin.io/transports"
"upspin.io/upspin"

// Packers used by the upspin client when reading packed files.
_ "upspin.io/pack/ee"
_ "upspin.io/pack/eeintegrity"
_ "upspin.io/pack/plain"

)

//go:embed all:web var embeddedWeb embed.FS

func main() { var ( addr = flag.String("addr", ":8446", "listen address") certFile = flag.String("tls_cert", "", "TLS certificate (PEM)") keyFile = flag.String("tls_key", "", "TLS key (PEM)") upspinConf = flag.String("upspin_config", "/gitweb/config", "upspin config file for the gitweb user") webDir = flag.String("web", "", "serve static assets from this dir (default: embedded)") devDir = flag.String("dev", "", "if set, read files from this local dir instead of upspin (dev only)") ssoIss = flag.String("sso_issuer", "vault@cosmocr.at", "upspin user that issues SSO tokens; its keyserver public key is trusted ('' disables SSO auth)") ssoAud = flag.String("sso_audience", "gitweb", "expected audience of accepted SSO tokens") ssoAuthURL = flag.String("sso_authorize_url", "https://vault.cosmocr.at/authorize", "issuer's browser login endpoint; the browser SSO 'Sign in' flow is enabled when set") baseURL = flag.String("base_url", "https://git.cosmocr.at", "this service's public base URL, used to build the SSO redirect_uri") cacheDir = flag.String("cache_dir", "", "repo cache directory — ALL its contents are DELETED at startup (default: fresh dir under the system temp dir)") cacheMB = flag.Int64("cache_budget_mb", 512, "target total size of the on-disk repo cache, in MiB") maxRepoMB = flag.Int64("max_repo_mb", 256, "reject repos larger than this when materializing, in MiB") ) flag.Parse()

var st store.WriteStore
var vault *auth.VaultVerifier
if *devDir != "" {
	st = store.NewLocalStore(*devDir)
	log.Printf("gitweb: DEV MODE — local file store at %s", *devDir)
} else {
	cfg, err := config.FromFile(*upspinConf)
	if err != nil {
		log.Fatalf("gitweb: load upspin config: %v", err)
	}
	transports.Init(cfg)
	st = store.NewUpspinStore(cfg)
	if *ssoIss != "" {
		// Verify SSO tokens by looking up the issuer's public key through
		// the upspin keyserver — no runtime call to the issuer service.
		lookup := rpc.PublicUserKeyService(cfg)
		vault = auth.NewVaultVerifier(upspin.UserName(*ssoIss), *ssoAud,
			auth.KeyLookup(lookup))
		log.Printf("gitweb: accepting SSO tokens from %s (aud=%s)", *ssoIss, *ssoAud)
	}
}

var webFS http.FileSystem
if *webDir != "" {
	webFS = http.Dir(*webDir)
} else {
	sub, err := fs.Sub(embeddedWeb, "web")
	if err != nil {
		log.Fatalf("gitweb: embed web: %v", err)
	}
	webFS = http.FS(sub)
}

srv, err := server.New(server.Config{
	Store:            st,
	Web:              webFS,
	Vault:            vault,
	SSOAuthorizeURL:  *ssoAuthURL,
	SSOAudience:      *ssoAud,
	BaseURL:          *baseURL,
	CacheDir:         *cacheDir,
	CacheBudgetBytes: *cacheMB << 20,
	MaxRepoBytes:     *maxRepoMB << 20,
})
if err != nil {
	log.Fatalf("gitweb: init server: %v", err)
}

httpSrv := &http.Server{
	Addr:              *addr,
	Handler:           srv.Handler(),
	ReadHeaderTimeout: 10 * time.Second,
}
log.Printf("gitweb: serving on %s", *addr)
if *certFile == "" || *keyFile == "" {
	log.Fatal(httpSrv.ListenAndServe()) // dev only (e.g. localhost)
}
httpSrv.TLSConfig = &tls.Config{
	MinVersion:     tls.VersionTLS12,
	GetCertificate: (&certReloader{certFile: *certFile, keyFile: *keyFile}).get,
}
log.Fatal(httpSrv.ListenAndServeTLS("", ""))

}

// certReloader serves a file-based TLS certificate, reloading it when the files // change so certbot renewals are picked up without a restart. (Copied from the // vault/markdown services, which share this deployment model.) type certReloader struct { certFile, keyFile string mu sync.RWMutex cert *tls.Certificate modTime time.Time }

func (r *certReloader) get(*tls.ClientHelloInfo) (*tls.Certificate, error) { mt := newest(r.certFile, r.keyFile) r.mu.RLock() cert, loaded := r.cert, r.modTime r.mu.RUnlock() if cert == nil || mt.After(loaded) { c, err := tls.LoadX509KeyPair(r.certFile, r.keyFile) if err != nil { if cert != nil { log.Printf("gitweb: cert reload failed, keeping previous: %v", err) return cert, nil } return nil, err } r.mu.Lock() r.cert, r.modTime = &c, mt r.mu.Unlock() cert = &c } return cert, nil }

func newest(files ...string) time.Time { var t time.Time for _, f := range files { if fi, err := os.Stat(f); err == nil && fi.ModTime().After(t) { t = fi.ModTime() } } return t }