From a149edd90ac69b217cf28bbd42e647a6e5614a5d Mon Sep 17 00:00:00 2001 From: Super User Date: Mon, 24 Aug 2026 22:51:04 -0400 Subject: [PATCH 1/2] Added in branding for the landing page via pkgproxy config file --- README.md | 41 ++++++++++++++++++++++ cmd/serve.go | 6 ++-- configs/pkgproxy.yaml | 4 +++ pkg/pkgproxy/landing.go | 55 +++++++++++++++++++++++++---- pkg/pkgproxy/landing_test.go | 62 +++++++++++++++++++++++++++++++++ pkg/pkgproxy/repository.go | 7 ++++ pkg/pkgproxy/repository_test.go | 41 ++++++++++++++++++++++ 7 files changed, 206 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c0be5b3..734e70f 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,27 @@ podman run --rm -p 8080:8080 -e PKGPROXY_HOST=0.0.0.0 --volume ./cache:/ko-app/c Any flag with an env variable listed above can be set via the environment instead of passing the flag. +### Landing page hostname + +The config snippets shown on the landing page (`GET /`) need pkgproxy's own +address, e.g. `baseurl=http:///fedora/...`. Rather than relying on a +server-side setting, this is filled in automatically, with no configuration +needed: + +- **Server-side, from the request's `Host` header.** Every response — including + `curl` and other non-browser clients — already contains a working address + built from the `Host` header the request itself carried (the same header a + reverse proxy forwards by default). No JavaScript required. +- **Client-side, from the page's own URL.** In a browser, a small inline script + additionally corrects the address to `window.location.origin` if it differs + from the server-rendered one — which matters behind a reverse proxy that + changes the scheme (e.g. TLS termination), since the `Host` header alone + can't reveal that. + +If a reverse proxy in front of pkgproxy does not forward the original `Host` +header, `curl` (or a browser with JavaScript disabled) will see whatever host +pkgproxy itself observed instead. + ### Trusting X-Forwarded-For By default pkgproxy ignores the `X-Forwarded-For` header and uses the direct connecting IP address for the `remote_ip` access-log field. This is the safe behavior when pkgproxy faces the internet directly or runs in a container without a reverse proxy in front of it. @@ -69,6 +90,26 @@ Each repository supports the following options: | `mirrors` | yes | Ordered list of upstream mirror URLs | | `retries` | no | Number of attempts per mirror before moving to the next one (default: `1`) | +### Landing page branding + +The top-level `branding` key customizes the title and description shown on the +landing page (and the HTML ``) served at `/`: + +```yaml +branding: + title: Acme Package Mirror + description: Internal package cache for Acme Corp. + +repositories: + ... +``` + +Both fields are optional and independent — omitting `branding` entirely, or +leaving one of the two fields unset, falls back to the default "pkgproxy" title +and "Caching forward proxy for Linux package repositories." description. The +landing page also always shows the running pkgproxy version below the +description. + ### Mirror retries Some upstream mirrors (e.g. `download.fedoraproject.org`) act as redirectors that diff --git a/cmd/serve.go b/cmd/serve.go index 82cbaba..b99c393 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -165,7 +165,7 @@ func parseTrustProxy(value string) (echo.IPExtractor, error) { // newEchoApp wires up the Echo application: IP extraction, middleware chain, // the landing page route and the caching forward proxy. -func newEchoApp(cacheBasePath string, config *pkgproxy.RepoConfig, publicAddr string, extractor echo.IPExtractor) *echo.Echo { +func newEchoApp(cacheBasePath string, config *pkgproxy.RepoConfig, publicAddr string, extractor echo.IPExtractor, version string) *echo.Echo { app := pkgproxy.NewEcho() // Extract client IP from X-Forwarded-For only when a trusted proxy is explicitly configured // via --trust-proxy. By default, XFF is ignored and the direct connecting IP is used. @@ -210,7 +210,7 @@ func newEchoApp(cacheBasePath string, config *pkgproxy.RepoConfig, publicAddr st CacheBasePath: cacheBasePath, RepositoryConfig: config, }) - app.GET("/", pkgproxy.LandingHandler(config, publicAddr)) + app.GET("/", pkgproxy.LandingHandler(config, publicAddr, version)) app.Use(pkgProxy.Cache) app.Use(pkgProxy.ForwardProxy) @@ -236,7 +236,7 @@ func startServer(_ *cobra.Command, _ []string) error { slog.Info("trust-proxy", "value", trustProxyLog) publicAddr := resolvePublicAddr(publicHost, listenAddress, listenPort) - app := newEchoApp(cacheDir, &repoConfig, publicAddr, ipExtractor) + app := newEchoApp(cacheDir, &repoConfig, publicAddr, ipExtractor, Version) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() diff --git a/configs/pkgproxy.yaml b/configs/pkgproxy.yaml index f94aeff..bb1c718 100644 --- a/configs/pkgproxy.yaml +++ b/configs/pkgproxy.yaml @@ -1,4 +1,8 @@ --- +branding: + title: Pkgproxy Application + description: Caching forward proxy for Linux package repositories + repositories: almalinux: suffixes: diff --git a/pkg/pkgproxy/landing.go b/pkg/pkgproxy/landing.go index e4724d8..e39dbad 100644 --- a/pkg/pkgproxy/landing.go +++ b/pkg/pkgproxy/landing.go @@ -16,7 +16,7 @@ const landingTemplate = `<!DOCTYPE html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> -<title>pkgproxy +{{.Title}} -

pkgproxy

-

Caching forward proxy for Linux package repositories.

-{{range .}} +

{{.Title}}

+

{{.Description}}

+

pkgproxy {{.Version}}

+{{range .Repos}}

{{.Name}}

-

Mirrors:

+

Mirrors:

s {{with repoSnippet .Name}}

Configuration snippet:

@@ -41,6 +42,13 @@ ul { padding-left: 1.4em; } ` +// defaultTitle and defaultDescription are used when the config's 'branding' +// block is absent or leaves a field empty. +const ( + defaultTitle = "pkgproxy" + defaultDescription = "Caching forward proxy for Linux package repositories." +) + // snippetFuncs maps known repository names to functions that generate // package manager configuration snippets for the landing page. // Each function takes the public address (host or host:port) and returns @@ -110,6 +118,29 @@ type repoEntry struct { Mirrors []string } +type landingData struct { + Title string + Description string + Version string + Repos []repoEntry +} + +// brandingOrDefault returns the configured title and description, falling +// back to the built-in pkgproxy defaults for whichever field is unset. +func brandingOrDefault(branding *BrandingConfig) (title string, description string) { + title, description = defaultTitle, defaultDescription + if branding == nil { + return title, description + } + if branding.Title != "" { + title = branding.Title + } + if branding.Description != "" { + description = branding.Description + } + return title, description +} + // sortedRepos returns repository entries sorted alphabetically by name. func sortedRepos(config *RepoConfig) []repoEntry { names := make([]string, 0, len(config.Repositories)) @@ -128,7 +159,7 @@ func sortedRepos(config *RepoConfig) []repoEntry { // LandingHandler returns an Echo handler that renders an HTML overview page // listing all configured repositories, their mirrors, and package manager snippets. // publicAddr is the address (host or host:port) rendered in config snippets. -func LandingHandler(config *RepoConfig, publicAddr string) echo.HandlerFunc { +func LandingHandler(config *RepoConfig, publicAddr string, version string) echo.HandlerFunc { funcMap := template.FuncMap{ "repoSnippet": func(name string) string { fn, ok := snippetFuncs[name] @@ -140,9 +171,19 @@ func LandingHandler(config *RepoConfig, publicAddr string) echo.HandlerFunc { } tmpl := template.Must(template.New("landing").Funcs(funcMap).Parse(landingTemplate)) + title, description := brandingOrDefault(config.Branding) + repos := sortedRepos(config) + return func(c *echo.Context) error { + data := landingData{ + Title: title, + Description: description, + Version: version, + Repos: repos, + } + var buf bytes.Buffer - if err := tmpl.Execute(&buf, sortedRepos(config)); err != nil { + if err := tmpl.Execute(&buf, data); err != nil { return err } c.Response().Header().Set(echo.HeaderContentType, "text/html; charset=UTF-8") diff --git a/pkg/pkgproxy/landing_test.go b/pkg/pkgproxy/landing_test.go index 683a358..54a3ebf 100644 --- a/pkg/pkgproxy/landing_test.go +++ b/pkg/pkgproxy/landing_test.go @@ -17,6 +17,12 @@ func newLandingApp(config *RepoConfig, publicAddr string) *echo.Echo { return app } +func newLandingAppWithVersion(config *RepoConfig, publicAddr string, version string) *echo.Echo { + app := echo.New() + app.GET("/", LandingHandler(config, publicAddr, version)) + return app +} + func getLandingBody(t *testing.T, app *echo.Echo) string { t.Helper() req := httptest.NewRequest(http.MethodGet, "/", nil) @@ -67,6 +73,62 @@ func TestLandingHandlerMirrorLinks(t *testing.T) { assert.Contains(t, body, `https://mirror.example.com/fedora/`) } +func TestLandingHandlerDefaultBranding(t *testing.T) { + config := &RepoConfig{ + Repositories: map[string]Repository{ + "fedora": {CacheSuffixes: []string{".rpm"}, Mirrors: []string{"https://mirror.example.com/"}}, + }, + } + body := getLandingBody(t, newLandingApp(config)) + + assert.Contains(t, body, "pkgproxy") + assert.Contains(t, body, "

pkgproxy

") + assert.Contains(t, body, "

Caching forward proxy for Linux package repositories.

") +} + +func TestLandingHandlerCustomBranding(t *testing.T) { + config := &RepoConfig{ + Branding: &BrandingConfig{ + Title: "Acme Package Mirror", + Description: "Internal package cache for Acme Corp.", + }, + Repositories: map[string]Repository{ + "fedora": {CacheSuffixes: []string{".rpm"}, Mirrors: []string{"https://mirror.example.com/"}}, + }, + } + body := getLandingBody(t, newLandingApp(config)) + + assert.Contains(t, body, "Acme Package Mirror") + assert.Contains(t, body, "

Acme Package Mirror

") + assert.Contains(t, body, "

Internal package cache for Acme Corp.

") + assert.NotContains(t, body, "pkgproxy") +} + +func TestLandingHandlerBrandingPartialOverride(t *testing.T) { + config := &RepoConfig{ + Branding: &BrandingConfig{Title: "Acme Package Mirror"}, + Repositories: map[string]Repository{ + "fedora": {CacheSuffixes: []string{".rpm"}, Mirrors: []string{"https://mirror.example.com/"}}, + }, + } + body := getLandingBody(t, newLandingApp(config)) + + assert.Contains(t, body, "

Acme Package Mirror

") + // Description falls back to the default when only the title is customized. + assert.Contains(t, body, "

Caching forward proxy for Linux package repositories.

") +} + +func TestLandingHandlerVersion(t *testing.T) { + config := &RepoConfig{ + Repositories: map[string]Repository{ + "fedora": {CacheSuffixes: []string{".rpm"}, Mirrors: []string{"https://mirror.example.com/"}}, + }, + } + body := getLandingBody(t, newLandingAppWithVersion(config, "localhost:8080", "v0.3.1")) + + assert.Contains(t, body, "

pkgproxy v0.3.1

") +} + func TestLandingHandlerKnownSnippets(t *testing.T) { tests := []struct { repo string diff --git a/pkg/pkgproxy/repository.go b/pkg/pkgproxy/repository.go index a292916..b2f1e57 100644 --- a/pkg/pkgproxy/repository.go +++ b/pkg/pkgproxy/repository.go @@ -18,9 +18,16 @@ var repoHandleRegexp = regexp.MustCompile("^[a-zA-Z0-9_~.-]*$") // RepoConfig defines the upstream package repositories type RepoConfig struct { + Branding *BrandingConfig `yaml:"branding,omitempty"` Repositories map[string]Repository `yaml:"repositories"` } +// BrandingConfig customizes the title and description shown on the landing page. +type BrandingConfig struct { + Title string `yaml:"title,omitempty"` + Description string `yaml:"description,omitempty"` +} + type Repository struct { CacheSuffixes []string `yaml:"suffixes"` Exclude []string `yaml:"exclude,omitempty"` diff --git a/pkg/pkgproxy/repository_test.go b/pkg/pkgproxy/repository_test.go index 124d3ad..0375bd4 100644 --- a/pkg/pkgproxy/repository_test.go +++ b/pkg/pkgproxy/repository_test.go @@ -60,3 +60,44 @@ func TestValidateConfigWildcardAloneNoWarning(t *testing.T) { assert.Empty(t, buf.String()) } + +func TestLoadConfigBranding(t *testing.T) { + dir := t.TempDir() + configPath := filepath.Join(dir, "pkgproxy.yaml") + require.NoError(t, os.WriteFile(configPath, []byte(`--- +branding: + title: Acme Package Mirror + description: Internal package cache for Acme Corp. +repositories: + fedora: + suffixes: + - .rpm + mirrors: + - https://mirror.example.com/ +`), 0o600)) + + var config RepoConfig + require.NoError(t, LoadConfig(&config, configPath)) + + require.NotNil(t, config.Branding) + assert.Equal(t, "Acme Package Mirror", config.Branding.Title) + assert.Equal(t, "Internal package cache for Acme Corp.", config.Branding.Description) +} + +func TestLoadConfigNoBranding(t *testing.T) { + dir := t.TempDir() + configPath := filepath.Join(dir, "pkgproxy.yaml") + require.NoError(t, os.WriteFile(configPath, []byte(`--- +repositories: + fedora: + suffixes: + - .rpm + mirrors: + - https://mirror.example.com/ +`), 0o600)) + + var config RepoConfig + require.NoError(t, LoadConfig(&config, configPath)) + + assert.Nil(t, config.Branding) +} From 70350a36ce23bb1c575fcf3b905501e2eac90e48 Mon Sep 17 00:00:00 2001 From: codeyschoettle Date: Mon, 24 Aug 2026 23:03:41 -0400 Subject: [PATCH 2/2] Removed accidental 's' in html code --- pkg/pkgproxy/landing.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/pkgproxy/landing.go b/pkg/pkgproxy/landing.go index e39dbad..bf393a7 100644 --- a/pkg/pkgproxy/landing.go +++ b/pkg/pkgproxy/landing.go @@ -31,7 +31,7 @@ ul { padding-left: 1.4em; }

pkgproxy {{.Version}}

{{range .Repos}}

{{.Name}}

-

Mirrors:

s +

Mirrors:

    {{range .Mirrors}}
  • {{.}}
  • {{end}}
{{with repoSnippet .Name}}

Configuration snippet: