gitcorsproxy

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2026 License: BSD-3-Clause Imports: 8 Imported by: 0

README

go-browserhttp/gitcorsproxy

go-browserhttp / gitcorsproxy

CI Go Reference

A tiny, sovereign CORS reverse proxy for git smart-HTTP. It lets a browser — specifically a Go/wasm git client such as the go-tex playground's git-worker.wasm — reach git remotes that answer the smart-HTTP protocol correctly but send no Access-Control-Allow-Origin (GitHub, ghcr-style hosts, a bare Forgejo). The proxy forwards the git bytes both ways and adds the CORS headers the browser needs, scoped to an explicit origin list.

Pure-Go, CGO=0, zero dependencies (standard library only). It stores nothing: the browser holds the user's PAT and sends it on each request; the proxy relays it and forgets it (auth passthrough).

browser (wasm go-git) ──▶ gitcorsproxy ──▶ https://github.com/owner/repo.git/…
        Fetch + PAT          + CORS               real smart-HTTP

Route

The upstream host is carried as the first path segment; the rest is the git smart-HTTP endpoint verbatim:

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

is proxied to https://<host>/<owner>/<repo>.git/…. The <owner>/<repo> part may contain nested groups (Forgejo/GitLab subgroups). Anything that does not match this shape is rejected 400. info/refs must be a GET with a valid service query; the pack endpoints must be POST.

This is exactly the client contract of the go-tex playground's browsergit package, whose panel accepts a proxy-prefixed base URL like https://gitproxy.<host>/github.com/owner/repo.git.

CORS

  • Access-Control-Allow-Origin echoes the request Origin only when it is in the configured allow-list — it is never *, because the client's Authorization (a PAT) is forwarded upstream and a wildcard origin with credential-bearing requests would let any web page drive the user's token.
  • Access-Control-Allow-Methods: GET,POST,OPTIONS
  • Access-Control-Allow-Headers: Content-Type,Git-Protocol,Authorization
  • Access-Control-Expose-Headers: Content-Type,Content-Length
  • OPTIONS preflight is answered 204 with the headers above.

Request bodies and response bodies are streamed (never fully buffered — git packs are large). Content-Type (application/x-git-*), Git-Protocol, Authorization, Content-Encoding, Accept-Encoding and User-Agent are forwarded upstream; the relevant response headers and the upstream status code are preserved on the way back.

Security: SSRF guard + no token logging

  • An upstream-host allow-list is the primary control: only the exact hosts an operator lists (e.g. 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/multicast/unspecified/cloud-metadata/ IPv6-ULA deny-list (ported from the loom server's checkRemoteIP), so an allow-listed host that resolves into an internal range is still refused.
  • The Authorization header is never logged. Nothing in this package writes a token, a header dump, or a request body to the logger.

Library

proxy, err := gitcorsproxy.New(gitcorsproxy.Config{
    AllowedOrigins: []string{"https://go-tex.github.io"},
    UpstreamHosts:  []string{"github.com", "sources.example.net"},
})
if err != nil {
    log.Fatal(err)
}
log.Fatal(http.ListenAndServe(":8181", proxy))

*Proxy is an http.Handler, so it drops into any server or middleware chain.

Command

go run ./cmd/gitcorsproxy -origins https://go-tex.github.io -hosts github.com

Each flag falls back to an environment variable:

Flag Env Default Meaning
-listen GITCORSPROXY_LISTEN :8181 listen address
-origins GITCORSPROXY_ORIGINS — comma-separated allowed origins
-hosts GITCORSPROXY_HOSTS — comma-separated upstream host allow-list
-tls-cert GITCORSPROXY_TLS_CERT — optional TLS certificate file
-tls-key GITCORSPROXY_TLS_KEY — optional TLS key file

-origins and -hosts are required.

Deployment (behind the sovereign EU Caddy)

Run the proxy behind Caddy, which terminates Let's Encrypt TLS, so the two TLS flags stay unused in production:

gitproxy.example.net {
    reverse_proxy 127.0.0.1:8181
}
gitcorsproxy \
  -listen 127.0.0.1:8181 \
  -origins https://go-tex.github.io \
  -hosts github.com

The go-tex playground is then configured with a proxy-prefixed remote, e.g. https://gitproxy.example.net/github.com/owner/repo.git. The proxy is auth-passthrough: the browser sends the user's PAT on each request; the proxy relays it upstream and never stores it. Keep the allow-lists tight — one or two origins, one or two upstream hosts.

License

BSD-3-Clause — see LICENSE. Copyright (c) 2026, the go-browserhttp/gitcorsproxy authors.

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.

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)
}

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.

func New

func New(cfg Config) (*Proxy, error)

New validates cfg and returns a ready Proxy. It errors when the required allowlists are empty or when AllowedOrigins contains a wildcard.

func (*Proxy) ServeHTTP

func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP routes, guards and proxies a single request.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL