proxy

package
v0.0.0-...-66da7ee Latest Latest
Warning

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

Go to latest
Published: May 12, 2026 License: MIT Imports: 33 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func EndpointScope

func EndpointScope(method, path string) (permission, level string)

EndpointScope returns the permission and level required for a given method and path. Returns empty strings if the endpoint is not recognized.

func ExtractRepoFromPath

func ExtractRepoFromPath(path string) string

ExtractRepoFromPath extracts the owner/repo from a /repos/{owner}/{repo}/... path. Returns empty string if the path doesn't match.

func GetCacheState

func GetCacheState(r *http.Request) string

GetCacheState returns the cache result state from the request's context slot, or "" if no state was set.

func GetUserID

func GetUserID(r *http.Request) string

GetUserID returns the internal user ID stored in the request context, or "" if none has been set.

func GetUsername

func GetUsername(r *http.Request) string

GetUsername returns the GitHub username stored in the request context, or "" if none has been set.

func GitSmartHTTPScope

func GitSmartHTTPScope(method, path, query string) (repo, permission, level string)

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

func NewReleasesHandler(inner http.Handler, cfg *config.Config, logger *slog.Logger) http.Handler

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

func PrepareUsernameSlot(r *http.Request) (*http.Request, *string)

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

func SetCacheRepo(r *http.Request, repo string)

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

func SetCacheState(r *http.Request, state string)

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

func SetUserID(r *http.Request, userID string)

SetUserID stores the internal user ID in the request's context slot. It is a no-op if no slot was prepared.

func SetUsername

func SetUsername(r *http.Request, username string)

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

type HTTPHeadDoer interface {
	Do(req *http.Request) (*http.Response, error)
}

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.

Jump to

Keyboard shortcuts

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