Documentation
¶
Overview ¶
Package proxy implements the core reverse proxy that sits between coding agents and GitHub. It is responsible for:
- Extracting ghx_/gha_ tokens from the Authorization header
- Resolving tokens to their database records and checking expiry/revocation
- Enforcing repository and permission scopes against the requested API path
- Swapping the ghp token for the real GitHub credential (decrypted or obtained from the GitHub App installation token provider)
- Forwarding the request to the real GitHub API and streaming the response
- Emitting structured JSON audit log entries for API proxy requests and Prometheus metrics for all requests
The proxy handles three distinct traffic patterns through separate handlers:
- Handler: the API proxy for api.github.com traffic (REST and GraphQL)
- ScopedPassthroughHandler: the github.com passthrough for git operations
- CopilotPassthroughHandler: transparent forwarding for *.githubcopilot.com
Each stage of the request pipeline is individually timed and recorded in the ghp_proxy_decision_duration_seconds histogram, enabling operators to identify exactly where overhead is introduced.
Package proxy implements the GitHub API reverse proxy with scope enforcement.
Index ¶
- func EndpointScope(method, path string) (permission, level string)
- func ExtractRepoFromPath(path string) string
- func GetCacheState(r *http.Request) string
- func GetUserID(r *http.Request) string
- func GetUsername(r *http.Request) string
- func GitSmartHTTPScope(method, path, query string) (repo, permission, level string)
- func NewCodeloadHandler(cfg *config.Config, logger *slog.Logger, transport http.RoundTripper) http.Handler
- func NewCopilotPassthroughHandler(upstream string, enterpriseSlug string, logger *slog.Logger, ...) http.Handler
- func NewPassthroughHandler(upstream string, resolver TokenResolver, enterpriseSlug string, ...) http.Handler
- func NewReleasesHandler(inner http.Handler, cfg *config.Config, logger *slog.Logger) http.Handler
- func NewReleasesHandlerWithClient(inner http.Handler, cfg *config.Config, logger *slog.Logger, ...) http.Handler
- func NewScopedPassthroughHandler(inner http.Handler, enforcer ScopeEnforcer, resolver TokenResolver, ...) http.Handler
- func PrepareUsernameSlot(r *http.Request) (*http.Request, *string)deprecated
- func SetCacheRepo(r *http.Request, repo string)
- func SetCacheState(r *http.Request, state string)
- func SetUserID(r *http.Request, userID string)
- func SetUsername(r *http.Request, username string)
- func WithGraphQLURL(url string) func(*UsernameResolver)
- type AccessLogSlots
- type AppTokenProvider
- type AuditLogEntry
- type AuditLogWriter
- type GitHubTokenResolver
- type HTTPHeadDoer
- type Handler
- type MultiAppTokenProvider
- type ProxyTokenResolver
- type ScopeEnforcer
- type TokenResolver
- type UsernameResolver
- func (u *UsernameResolver) CheckCache(rawToken string) string
- func (u *UsernameResolver) ResolveFromGitHubToken(ctx context.Context, rawToken string) string
- func (u *UsernameResolver) ResolveFromUserID(ctx context.Context, userID string) string
- func (u *UsernameResolver) WarmCache(ctx context.Context, resolver GitHubTokenResolver)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func EndpointScope ¶
EndpointScope returns the permission and level required for a given method and path. Returns empty strings if the endpoint is not recognized.
func ExtractRepoFromPath ¶
ExtractRepoFromPath extracts the owner/repo from a /repos/{owner}/{repo}/... path. Returns empty string if the path doesn't match.
func GetCacheState ¶
GetCacheState returns the cache result state from the request's context slot, or "" if no state was set.
func GetUserID ¶
GetUserID returns the internal user ID stored in the request context, or "" if none has been set.
func GetUsername ¶
GetUsername returns the GitHub username stored in the request context, or "" if none has been set.
func GitSmartHTTPScope ¶
GitSmartHTTPScope extracts the repository, permission, and level from a git smart HTTP request path. Returns empty strings for non-git paths.
Git smart HTTP paths:
- GET /{owner}/{repo}.git/info/refs?service=git-upload-pack → contents:read
- POST /{owner}/{repo}.git/git-upload-pack → contents:read
- GET /{owner}/{repo}.git/info/refs?service=git-receive-pack → contents:write
- POST /{owner}/{repo}.git/git-receive-pack → contents:write
func NewCodeloadHandler ¶
func NewCodeloadHandler(cfg *config.Config, logger *slog.Logger, transport http.RoundTripper) http.Handler
NewCodeloadHandler returns an http.Handler for codeload.github.com requests.
When cfg.Codeload.RedirectTo is set to an absolute URL, archive download requests matching /{owner}/{repo}/(tar.gz|zip|legacy.tar.gz|legacy.zip)/{ref} are answered with a 302 to RedirectTo + the original path (and query string). Requests for orgs or org/repo pairs in cfg.Codeload.Allow bypass the redirect and are forwarded to the upstream codeload service. Non-archive paths are always forwarded. When RedirectTo is empty, every archive request is still counted as result="passthrough" before forwarding so operators can see total archive volume even before a mirror is configured.
cfg is read on every request so SIGUSR1 hot-reload of codeload.redirect_to and codeload.allow takes effect without a server restart. transport is optional; when nil the default RoundTripper is used. It exists to allow tests to intercept upstream requests without making real network calls.
func NewCopilotPassthroughHandler ¶
func NewCopilotPassthroughHandler(upstream string, enterpriseSlug string, logger *slog.Logger, transport http.RoundTripper) http.Handler
NewCopilotPassthroughHandler creates a transparent reverse proxy for *.githubcopilot.com traffic. The original Host header is preserved so the correct subdomain reaches the real Copilot service. No token interception or scope enforcement is performed; credentials in the Authorization header are forwarded verbatim. Caddy-compatible access logging and Prometheus metrics are applied consistently by the server layer (accessLogHandler), so this handler only concerns itself with proxying. The upstream parameter sets the network destination (scheme + host:port). The transport parameter allows callers to supply a custom RoundTripper; pass nil to use http.DefaultTransport.
func NewPassthroughHandler ¶
func NewPassthroughHandler(upstream string, resolver TokenResolver, enterpriseSlug string, logger *slog.Logger, transport http.RoundTripper) http.Handler
NewPassthroughHandler creates a transparent reverse proxy to the given upstream URL. If a client token (ghx_/gha_) is found in the Authorization header, it is resolved and replaced with the real GitHub credential. If enterpriseSlug is non-empty, the sec-GitHub-allowed-enterprise header is injected on every request. The transport parameter allows callers to supply a custom RoundTripper (e.g. for test TLS); pass nil to use http.DefaultTransport.
func NewReleasesHandler ¶
NewReleasesHandler wraps inner with a policy handler for github.com release download requests. Paths matching /{org}/{repo}/releases/download/** are intercepted when cfg.Releases.Mode is non-empty:
- "block": Returns 403 unless the org or org/repo is in the allow list.
- "redirect": Returns a 302 redirect to cfg.Releases.RedirectTo + the original path (and query string) unless the org or org/repo is in the allow list. When cfg.Releases.RedirectHeadCheck is true, a HEAD request is made to the target URL first; if the upstream returns 404, a friendly HTML page is returned instead of a redirect.
When cfg.Releases.RedirectHeadCheckNetrc is set, credentials from the specified netrc file are sent as Basic auth on HEAD availability probes over HTTPS only (plain http:// requests are never authenticated to avoid sending credentials in cleartext). Existing Authorization headers on a request are preserved and never overwritten.
Any other mode value or an empty mode passes all requests through to inner unchanged. The allow list check is always performed before applying the policy, so explicitly listed orgs and repos are never affected.
func NewReleasesHandlerWithClient ¶
func NewReleasesHandlerWithClient(inner http.Handler, cfg *config.Config, logger *slog.Logger, client HTTPHeadDoer, creds *netrcCreds) http.Handler
NewReleasesHandlerWithClient is like NewReleasesHandler but accepts a custom HTTP client for HEAD requests and optional pre-parsed netrc credentials, enabling testing without real network calls.
func NewScopedPassthroughHandler ¶
func NewScopedPassthroughHandler(inner http.Handler, enforcer ScopeEnforcer, resolver TokenResolver, ur *UsernameResolver, logger *slog.Logger, cfg ...*config.Config) http.Handler
NewScopedPassthroughHandler wraps a passthrough reverse proxy with git smart HTTP scope enforcement. For requests carrying a client token (ghx_/gha_) that match a git smart HTTP path, the token's repository and permission scopes are verified before the request is forwarded. Non-git paths and non-client tokens pass through unchanged.
When cfg is non-nil, the token type border policy (cfg.Block) is enforced: requests bearing a raw GitHub token whose type prefix is blocked are rejected with 403 before they reach the upstream.
The optional usernameResolver is used to resolve GitHub usernames for both client tokens (via the database) and raw GitHub tokens (via the GitHub API) so that they appear in metrics and access logs.
func PrepareUsernameSlot
deprecated
PrepareUsernameSlot returns a new request whose context carries a mutable string slot. Inner handlers call SetUsername to populate it; outer handlers call GetUsername to read the result. This pattern allows the access-log middleware to learn the username that a downstream handler resolved.
Deprecated: use PrepareAccessLogSlots for new code.
func SetCacheRepo ¶
SetCacheRepo stores the cached repository identifier (owner/repo) in the request's context slot. It is a no-op if no slot was prepared.
func SetCacheState ¶
SetCacheState stores the cache result state in the request's context slot. Valid values: "hit", "miss", "rejected", "error". It is a no-op if no slot was prepared.
func SetUserID ¶
SetUserID stores the internal user ID in the request's context slot. It is a no-op if no slot was prepared.
func SetUsername ¶
SetUsername stores the resolved GitHub username in the request's context slot. It is a no-op if no slot was prepared.
func WithGraphQLURL ¶
func WithGraphQLURL(url string) func(*UsernameResolver)
WithGraphQLURL returns an option that overrides the GitHub GraphQL endpoint URL. This is primarily intended for testing with mock servers.
Types ¶
type AccessLogSlots ¶
type AccessLogSlots struct {
Username *string
UserID *string
CacheState *string // "hit", "miss", "rejected", "error", or "" for non-cached
CacheRepo *string // "owner/repo" if request hit a cached repository
}
AccessLogSlots holds the mutable string slots that downstream handlers populate so the access-log middleware can read them after the request.
func PrepareAccessLogSlots ¶
func PrepareAccessLogSlots(r *http.Request) (*http.Request, *AccessLogSlots)
PrepareAccessLogSlots returns a new request whose context carries mutable string slots for both the GitHub username and the internal user ID. Inner handlers call SetUsername / SetUserID to populate them; the access-log middleware reads the results after the request completes.
type AppTokenProvider ¶
type AppTokenProvider interface {
GetInstallationToken(ctx context.Context, installationID int64, repos []string, permissions map[string]string) (string, error)
}
AppTokenProvider generates installation tokens for agent (gha_) tokens.
type AuditLogEntry ¶
type AuditLogEntry struct {
Action string
UserID string
Username string
TokenID string
TokenType string
SessionID string
Method string
Path string
Repository string
StatusCode int
DurationMS int
}
AuditLogEntry holds the fields for a structured JSON audit event.
type AuditLogWriter ¶
type AuditLogWriter interface {
WriteAuditEntry(entry AuditLogEntry)
}
AuditLogWriter is the interface used by the proxy handler to write structured JSON audit log entries.
type GitHubTokenResolver ¶
type GitHubTokenResolver interface {
ResolveProxyTokenToGitHub(ctx context.Context, pt *database.ProxyToken) (string, error)
}
GitHubTokenResolver resolves a proxy token database record to the underlying plaintext GitHub credential. This interface is satisfied by ProxyTokenResolver and enables cache warming without depending on the full proxy handler.
type HTTPHeadDoer ¶
HTTPHeadDoer is the interface used by the releases handler to perform HEAD requests. This allows injecting a test client.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler is the reverse proxy HTTP handler.
func NewHandler ¶
func NewHandler(cfg *config.Config, ts *token.Service, store database.Store, enc *crypto.Encryptor, atp AppTokenProvider, ur *UsernameResolver, logger *slog.Logger) *Handler
NewHandler creates a new reverse proxy handler.
func (*Handler) ServeHTTP ¶
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP handles proxied requests.
func (*Handler) SetAuditLogWriter ¶
func (h *Handler) SetAuditLogWriter(w AuditLogWriter)
SetAuditLogWriter sets the audit log writer for the handler.
type MultiAppTokenProvider ¶
type MultiAppTokenProvider interface {
AppTokenProvider
// GetInstallationTokenForApp returns a token using the provider for the specified app.
// If appID is empty, the default provider is used.
GetInstallationTokenForApp(ctx context.Context, appID string, installationID int64, repos []string, permissions map[string]string) (string, error)
}
MultiAppTokenProvider generates installation tokens with app-specific dispatch.
type ProxyTokenResolver ¶
type ProxyTokenResolver struct {
// contains filtered or unexported fields
}
ProxyTokenResolver resolves client tokens (ghx_/gha_) to real GitHub access tokens.
func NewProxyTokenResolver ¶
func NewProxyTokenResolver(ts *token.Service, store database.Store, enc *crypto.Encryptor, atp AppTokenProvider) *ProxyTokenResolver
NewProxyTokenResolver creates a new resolver.
func (*ProxyTokenResolver) ResolveProxyTokenToGitHub ¶
func (r *ProxyTokenResolver) ResolveProxyTokenToGitHub(ctx context.Context, pt *database.ProxyToken) (string, error)
ResolveProxyTokenToGitHub resolves a ProxyToken database record to the underlying plaintext GitHub credential. This is used for cache warming where the caller already has the database record and does not need the full Resolve-from-plaintext-token flow.
func (*ProxyTokenResolver) ResolveToGitHubToken ¶
func (r *ProxyTokenResolver) ResolveToGitHubToken(ctx context.Context, clientToken string) (string, error)
ResolveToGitHubToken resolves a client token to a plaintext GitHub access token.
type ScopeEnforcer ¶
type ScopeEnforcer interface {
Resolve(ctx context.Context, clientToken string) (*database.ProxyToken, error)
}
ScopeEnforcer resolves a client token string to the full ProxyToken record so that repository and permission scopes can be checked.
type TokenResolver ¶
type TokenResolver interface {
ResolveToGitHubToken(ctx context.Context, clientToken string) (string, error)
}
TokenResolver resolves a client token (ghx_/gha_) to a real GitHub access token.
type UsernameResolver ¶
type UsernameResolver struct {
// contains filtered or unexported fields
}
UsernameResolver resolves GitHub usernames from proxy token user IDs (via the database) and from raw GitHub tokens (via the GitHub GraphQL API). Results are kept in a long-lived in-memory cache keyed by a one-way SHA-256 hash of the token so the actual credential is never stored.
func NewUsernameResolver ¶
func NewUsernameResolver(store database.Store, logger *slog.Logger, opts ...func(*UsernameResolver)) *UsernameResolver
NewUsernameResolver creates a resolver backed by store for database lookups and an LRU cache for GitHub GraphQL API lookups. Optional functional options (e.g. WithGraphQLURL) may be applied for customisation.
func (*UsernameResolver) CheckCache ¶
func (u *UsernameResolver) CheckCache(rawToken string) string
CheckCache returns the cached GitHub username for the given raw token without triggering a background lookup. Returns "" if the token is not yet cached. Use this after an upstream roundtrip to pick up usernames that an in-flight async lookup (started earlier in the same request) may have resolved by then.
func (*UsernameResolver) ResolveFromGitHubToken ¶
func (u *UsernameResolver) ResolveFromGitHubToken(ctx context.Context, rawToken string) string
ResolveFromGitHubToken determines the GitHub username that owns the given raw GitHub token (e.g. gho_, ghp_, ghu_, ghs_ prefixed). The result is cached with a SHA-256 hash of the token as key. On a cache miss the lookup is performed asynchronously so GitHub API latency does not block the caller; empty string is returned for that first request. Only one in-flight lookup is allowed per token to prevent goroutine storms under load. On any error the empty string is returned silently so callers can treat this as best-effort.
func (*UsernameResolver) ResolveFromUserID ¶
func (u *UsernameResolver) ResolveFromUserID(ctx context.Context, userID string) string
ResolveFromUserID looks up the GitHub username for an internal user ID via the database. Returns "" if the user cannot be found.
func (*UsernameResolver) WarmCache ¶
func (u *UsernameResolver) WarmCache(ctx context.Context, resolver GitHubTokenResolver)
WarmCache loads all unexpired, non-revoked proxy tokens from the database, resolves each to its underlying GitHub credential, and triggers an async GraphQL viewer lookup to populate the username cache. This runs in a background goroutine so server startup is not blocked. It is safe to call with a nil resolver or on a resolver with no store — in those cases the warm is silently skipped. ctx is threaded into the warm goroutine so it is cancelled when the caller (e.g. Server.Run) shuts down or returns early due to a startup failure.