git@cosmocr.at/gitweb.git @ HEAD · summary refs log
HEAD / cmd / git-credential-cosmocr / main.go /
cmd/git-credential-cosmocr/main.go · 2.8 KiB · raw
view: SourceMarkdown ☑ force

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 <get|store|erase>") 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() }