enhance(runner): add extra HTTP headers (#1269)

Add `runner.extra_headers` for reverse proxies that require request headers on the runner's API requests to Gitea, like registration, polling and log and status reports.

Headers the runner sets itself (authentication, User-Agent, protocol headers) stay authoritative. Extra headers are dropped when a redirect changes the hostname or leaves HTTPS, while the runner's own authentication headers follow redirects as before. Invalid or case-insensitively duplicated header names are rejected at config load.

Fixes: https://gitea.com/gitea/runner/issues/171
Co-authored-by: silverwind <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1269
Co-authored-by: bircni <[email protected]>
This commit is contained in:
bircniandsilverwind authored and silverwind committed 2026-10-08 17:22:37 +00:00
1 parent 3802feb3fe
commit ff30f39b1b
8 files changed
+131 -11

No files matched your search

+20
View File
@@ -18,6 +18,7 @@ import (
"github.com/joho/godotenv"
log "github.com/sirupsen/logrus"
"go.yaml.in/yaml/v4"
"golang.org/x/net/http/httpguts"
)
// RequestTimeout bounds every RPC to Gitea, and with it runner.fetch_timeout.
@@ -60,6 +61,7 @@ type Runner struct {
Timeout time.Duration `yaml:"timeout"` // Timeout specifies the duration for runner timeout.
ShutdownTimeout time.Duration `yaml:"shutdown_timeout"` // ShutdownTimeout specifies the duration to wait for running jobs to complete during a shutdown of the runner.
Insecure bool `yaml:"insecure"` // Insecure indicates whether the runner operates in an insecure mode.
ExtraHeaders map[string]string `yaml:"extra_headers"` // ExtraHeaders adds HTTP headers to runner API requests to Gitea.
FetchTimeout time.Duration `yaml:"fetch_timeout"` // FetchTimeout specifies the timeout duration for fetching resources.
FetchInterval time.Duration `yaml:"fetch_interval"` // FetchInterval specifies the interval duration for fetching resources.
FetchIntervalMax time.Duration `yaml:"fetch_interval_max"` // FetchIntervalMax specifies the maximum backoff interval when idle.
@@ -320,6 +322,9 @@ func LoadDefault(file string) (*Config, error) {
if !slices.Contains(ToolCacheModes, cfg.Runner.ToolCacheMode) {
return nil, fmt.Errorf("invalid runner.tool_cache_mode %q: must be one of %q", cfg.Runner.ToolCacheMode, ToolCacheModes)
}
if err := validateExtraHeaders(cfg.Runner.ExtraHeaders); err != nil {
return nil, err
}
if cfg.Host.WorkdirParent == "" {
home, err := os.UserHomeDir()
if err != nil {
@@ -453,6 +458,21 @@ func definedRunnerConfigKeys(content []byte) (map[string]bool, error) {
return defined, nil
}
func validateExtraHeaders(headers map[string]string) error {
seen := make(map[string]string, len(headers))
for name, value := range headers {
if !httpguts.ValidHeaderFieldName(name) || !httpguts.ValidHeaderFieldValue(value) {
return fmt.Errorf("invalid runner.extra_headers entry %q", name)
}
key := strings.ToLower(name)
if other, ok := seen[key]; ok {
return fmt.Errorf("runner.extra_headers sets %q and %q, which name the same header", other, name)
}
seen[key] = name
}
return nil
}
// resolveSecretFile reads key from key_file, so deployments can mount a secret instead of committing it.
func resolveSecretFile(key string, secret *string, file string) error {
if file == "" {