package main import ( "fmt" "net/http" "os" "os/exec" "runtime" "time" ) func main() { if len(os.Args) < 2 { fmt.Fprintln(os.Stderr, "usage: git-credential-cosmocr ") os.Exit(2) } op := os.Args[1] attrs := parseAttrs(os.Stdin) c := cache{path: defaultCachePath()} host := attrs["host"] switch op { case "get": runGet(c, host, os.Stdout) case "erase": // git calls erase after the credential was rejected (e.g. token expired // server-side); drop it so the next get re-authenticates. _ = c.del(host) case "store": // The helper manages its own cache; nothing to do. default: fmt.Fprintf(os.Stderr, "git-credential-cosmocr: unknown operation %q\n", op) os.Exit(2) } } func runGet(c cache, host string, out *os.File) { // Serve a cached, still-valid token without a round trip. if tok, ok := c.get(host); ok && !expired(tok, time.Now()) { emit(out, tok) return } cfg := configFromEnv() tok, err := cfg.login(useDeviceFlow()) if err != nil { fmt.Fprintf(cfg.stderr, "git-credential-cosmocr: %v\n", err) os.Exit(1) } if err := c.set(host, tok); err != nil { fmt.Fprintf(cfg.stderr, "git-credential-cosmocr: warning: could not cache token: %v\n", err) } emit(out, tok) } // emit writes the token to git as Basic-auth credentials. The username is the // token's subject; git ignores it for our server but including it keeps the // credential record meaningful. func emit(out *os.File, tok string) { info, _ := decodeToken(tok) attrs := map[string]string{"username": info.Sub, "password": tok} if info.Exp > 0 { attrs["password_expiry_utc"] = fmt.Sprintf("%d", info.Exp) } writeAttrs(out, attrs) } func configFromEnv() config { vaultURL := os.Getenv("COSMOCR_VAULT_URL") if vaultURL == "" { vaultURL = "https://vault.cosmocr.at" } aud := os.Getenv("COSMOCR_AUDIENCE") if aud == "" { aud = "gitweb" } return config{ vaultURL: vaultURL, audience: aud, client: &http.Client{Timeout: 30 * time.Second}, openURL: openBrowser, stderr: os.Stderr, timeout: 3 * time.Minute, } } // useDeviceFlow reports whether to skip the browser and use the device-code // flow: when explicitly requested, or on a headless machine with no display. func useDeviceFlow() bool { if os.Getenv("COSMOCR_DEVICE") == "1" { return true } switch runtime.GOOS { case "linux": return os.Getenv("DISPLAY") == "" && os.Getenv("WAYLAND_DISPLAY") == "" default: return false } } // openBrowser best-effort launches the user's browser at url. func openBrowser(url string) { var cmd string var args []string switch runtime.GOOS { case "darwin": cmd = "open" case "windows": cmd, args = "rundll32", []string{"url.dll,FileProtocolHandler"} default: cmd = "xdg-open" } _ = exec.Command(cmd, append(args, url)...).Start() }