Documentation
¶
Overview ¶
Package gitcorsproxy is a small, sovereign reverse proxy that lets a browser reach git smart-HTTP remotes that do not send CORS headers.
A browser-hosted git client (for example the go-tex playground's wasm git-worker, github.com/go-tex/go-tex.github.io playground/internal/browsergit) speaks the git smart-HTTP protocol over the Fetch API. GitHub and most ghcr-style hosts answer that protocol correctly but send no Access-Control-Allow-Origin, so the browser blocks the response. This proxy sits in front: the browser talks to it same-origin-friendly (with an explicit allowed-origin list), and the proxy streams the git bytes to and from the real upstream, adding the CORS headers the browser needs.
Route ¶
The proxy accepts exactly the git smart-HTTP endpoint shape, with the upstream host carried as the first path segment:
GET /<host>/<owner>/<repo>.git/info/refs?service=git-upload-pack GET /<host>/<owner>/<repo>.git/info/refs?service=git-receive-pack POST /<host>/<owner>/<repo>.git/git-upload-pack POST /<host>/<owner>/<repo>.git/git-receive-pack
and forwards them to https://<host>/<owner>/<repo>.git/… . The <owner>/<repo> portion may contain nested groups (Forgejo/GitLab subgroups). Any path that does not match this shape is rejected 400.
Security posture ¶
- CORS is scoped to a CONFIGURED explicit origin list. It is never "*", because the client's Authorization header (a PAT) is forwarded upstream; a wildcard origin with credential-bearing requests would let any web page drive the user's token.
- An upstream-host ALLOWLIST is the primary SSRF control: only the exact hosts an operator lists (github.com, the sovereign Forgejo host, …) are reachable. Everything else is 403.
- As defence in depth the target host is resolved and every IP is checked against a private/loopback/link-local/cloud-metadata denylist (ported from the loom server's checkRemoteIP), so an allowlisted host that resolves into an internal range is still refused.
- The Authorization header is forwarded upstream but NEVER logged. Nothing in this package writes a token, a header dump, or a request body to the logger.
- Because a browser CORS proxy is publicly reachable and cannot source-restrict, it defends itself against abuse/DoS: a per-client-IP token-bucket rate limit (429 + Retry-After), a cap on the size of a single relayed response, and a per-request timeout. The client IP is read from X-Forwarded-For counting from the right by a trusted-hops count, so a client-forged leftmost entry cannot spoof the limiter key. These paths never log the token either. See Config for the knobs; all default to off in the library (the command sets production defaults).
The proxy stores nothing: it is a pure auth-passthrough. The browser holds the user's PAT and sends it on each request; the proxy relays it and forgets it.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// AllowedOrigins is the explicit list of browser origins permitted to read
// proxied responses, e.g. []string{"https://go-tex.github.io"}. It must be
// non-empty and must not contain "*" — see the package doc for why a
// wildcard is refused when Authorization is forwarded.
AllowedOrigins []string
// UpstreamHosts is the allowlist of upstream hosts the proxy will reach,
// matched case-insensitively against the first path segment exactly as it
// appears in the URL (including any :port), e.g.
// []string{"github.com", "sources.example.net"}. It must be non-empty.
UpstreamHosts []string
// UpstreamScheme is the scheme used to reach the upstream. It defaults to
// "https" and should stay that way in production; tests set "http" to reach
// a local httptest server.
UpstreamScheme string
// Logger receives request/decision logs. Defaults to slog.Default(). The
// proxy never logs the Authorization header, any token, or a request body.
Logger *slog.Logger
// Transport performs the upstream round-trip. Defaults to an SSRF-agnostic
// http.Transport with compression passthrough (DisableCompression) so git
// bytes are relayed verbatim. Primarily an injection seam for tests; the
// SSRF decision itself lives in the ServeHTTP pre-flight, not here.
Transport http.RoundTripper
// LookupIP resolves a host to its IPs for the SSRF pre-flight. Defaults to
// net.LookupIP. Injectable for tests.
LookupIP func(host string) ([]net.IP, error)
// RatePerMinute is the sustained request budget per client IP. When it is
// <= 0 rate limiting is disabled (the library default); the command sets a
// production default. On exceed the proxy answers 429 with Retry-After.
RatePerMinute int
// Burst is the token-bucket ceiling: the number of requests a single client
// IP may make back-to-back before the per-minute rate throttles it. When
// <= 0 it falls back to RatePerMinute (a one-minute burst). Ignored when
// rate limiting is disabled.
Burst int
// TrustedProxyHops is the number of trusted reverse proxies (Caddy, plus any
// trusted CDN in front of it) between the client and this process. The real
// client IP is read as the (1+hops)-th entry from the right of the
// [X-Forwarded-For…, RemoteAddr] chain, so a client-forged LEFTMOST
// X-Forwarded-For entry cannot spoof the limiter key. Default 0 means "trust
// nothing, key on RemoteAddr"; the command defaults it to 1 for the
// behind-Caddy deployment.
TrustedProxyHops int
// MaxResponseBytes caps the size of a single relayed upstream response so
// the proxy cannot be used to shift unbounded bandwidth. 0 means unlimited
// (the library default); the command sets a production default. A response
// whose declared Content-Length exceeds the cap is refused 502 before any
// body is streamed; an undeclared (chunked) body is truncated at the cap.
MaxResponseBytes int64
// Timeout bounds the whole proxied request (upstream round-trip plus body
// relay) via a context deadline, so a slow-loris or hung upstream cannot pin
// resources. 0 means no deadline (the library default); the command sets a
// production default.
Timeout time.Duration
// Now is the limiter's clock. Defaults to time.Now; injectable for tests.
Now func() time.Time
// MaxTrackedIPs bounds the number of live per-IP buckets. Defaults to 65536.
// Injectable for tests.
MaxTrackedIPs int
// IdleEvictAfter is how long a per-IP bucket may go unseen before it is
// swept. Defaults to 10 minutes. Injectable for tests.
IdleEvictAfter time.Duration
}
Config configures a Proxy. AllowedOrigins and UpstreamHosts are required; the rest have production-safe defaults.
type Proxy ¶
type Proxy struct {
// contains filtered or unexported fields
}
Proxy is an http.Handler implementing the CORS git smart-HTTP proxy. It is safe for concurrent use.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
gitcorsproxy
command
Command gitcorsproxy runs the sovereign CORS git smart-HTTP proxy.
|
Command gitcorsproxy runs the sovereign CORS git smart-HTTP proxy. |
