Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ The proxy can be configured via:
```bash
PROXY_LISTEN=:8080
PROXY_BASE_URL=http://localhost:8080
PROXY_UI_URL=http://localhost:8080 # Optional; defaults to PROXY_BASE_URL
PROXY_STORAGE_URL=file:///var/cache/proxy/artifacts
PROXY_DATABASE_DRIVER=sqlite
PROXY_DATABASE_PATH=./cache/proxy.db
Expand Down Expand Up @@ -934,6 +935,15 @@ When running behind nginx, Apache, or another reverse proxy, set `base_url` to y
base_url: "https://proxy.example.com"
```

If the UI is reached on a different hostname than the package endpoints — for example, the UI exposed publicly on a domain while build machines hit a Docker network alias — set `ui_base_url` separately. `base_url` is the URL package managers and metadata rewriting use; `ui_base_url` is the URL advertised to humans visiting the web UI (canonical/`og:url` tags and the install guide banner):

```yaml
base_url: "http://pkg-proxy:8080" # internal alias for build machines
ui_base_url: "https://proxy.example.com/ui" # public UI URL
```

When unset, `ui_base_url` defaults to `base_url`.

nginx example:

```nginx
Expand Down
11 changes: 9 additions & 2 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@
# Server listen address
listen: ":8080"

# Public URL where this proxy is accessible
# Used for rewriting package metadata URLs
# Public URL where package endpoints are reachable.
# Used for rewriting package metadata URLs and shown in install guide snippets
# so users know what to point their package manager at.
base_url: "http://localhost:8080"

# Public URL where the web UI is reached. Defaults to base_url when unset.
# Set this separately when the UI is served on a different hostname than the
# package endpoints — for example, the UI on a public domain behind auth while
# build machines hit a Docker network alias for the package endpoints.
# ui_base_url: "https://proxy.example.com/ui"

# Artifact storage configuration
storage:
# Storage backend URL
Expand Down
3 changes: 2 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ See `config.example.yaml` in the repository root for a complete example.
| Config | Environment | Flag | Default | Description |
|--------|-------------|------|---------|-------------|
| `listen` | `PROXY_LISTEN` | `-listen` | `:8080` | Address to listen on |
| `base_url` | `PROXY_BASE_URL` | `-base-url` | `http://localhost:8080` | Public URL for the proxy |
| `base_url` | `PROXY_BASE_URL` | `-base-url` | `http://localhost:8080` | Public URL package managers use to reach this proxy |
| `ui_base_url` | `PROXY_UI_URL` | - | (defaults to `base_url`) | Public URL where the web UI is reached. Set separately when the UI lives behind a different hostname than package endpoints (e.g. public domain vs Docker network alias). Used for canonical/og:url tags and the install guide banner. |

## Storage

Expand Down
37 changes: 32 additions & 5 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,20 @@ type Config struct {
// Listen is the address to listen on (e.g., ":8080", "127.0.0.1:8080").
Listen string `json:"listen" yaml:"listen"`

// BaseURL is the public URL where this proxy is accessible.
// Used for rewriting package metadata URLs.
// BaseURL is the public URL where package endpoints are reachable.
// Used for rewriting package metadata URLs and shown to humans on the
// install guide so they know what to point their package manager at.
// Example: "https://proxy.example.com" or "http://localhost:8080"
BaseURL string `json:"base_url" yaml:"base_url"`

// UIBaseURL is the public URL where the web UI is reachable. Defaults to
// BaseURL when unset. Set this separately when the UI is served on a
// different hostname than the package endpoints — for example, the UI on a
// public domain behind auth while build machines hit a Docker network alias
// for the package endpoints.
// Example: "https://proxy.example.com/ui"
UIBaseURL string `json:"ui_base_url" yaml:"ui_base_url"`

// Storage configures artifact storage.
Storage StorageConfig `json:"storage" yaml:"storage"`

Expand Down Expand Up @@ -360,6 +369,7 @@ func Load(path string) (*Config, error) {
// Environment variables use the PROXY_ prefix:
// - PROXY_LISTEN
// - PROXY_BASE_URL
// - PROXY_UI_URL
// - PROXY_STORAGE_PATH
// - PROXY_STORAGE_MAX_SIZE
// - PROXY_DATABASE_PATH
Expand All @@ -373,6 +383,9 @@ func (c *Config) LoadFromEnv() {
if v := os.Getenv("PROXY_BASE_URL"); v != "" {
c.BaseURL = v
}
if v := os.Getenv("PROXY_UI_URL"); v != "" {
c.UIBaseURL = v
}
if v := os.Getenv("PROXY_STORAGE_URL"); v != "" {
c.Storage.URL = v
}
Expand Down Expand Up @@ -444,6 +457,16 @@ func (c *Config) LoadFromEnv() {
}
}

// validateAbsoluteURL returns an error if value is not a parseable URL with
// both a scheme and host. fieldName is used in the error message.
func validateAbsoluteURL(fieldName, value string) error {
u, err := url.Parse(value)
if err != nil || u.Scheme == "" || u.Host == "" {
return fmt.Errorf("invalid %s %q: must be an absolute URL", fieldName, value)
}
return nil
}

// Validate checks the configuration for errors.
func (c *Config) Validate() error {
if c.Listen == "" {
Expand All @@ -452,6 +475,11 @@ func (c *Config) Validate() error {
if c.BaseURL == "" {
return fmt.Errorf("base_url is required")
}
if c.UIBaseURL == "" {
c.UIBaseURL = c.BaseURL
} else if err := validateAbsoluteURL("ui_base_url", c.UIBaseURL); err != nil {
return err
}
if c.Storage.URL == "" && c.Storage.Path == "" {
return fmt.Errorf("storage.url or storage.path is required")
}
Expand Down Expand Up @@ -500,9 +528,8 @@ func (c *Config) Validate() error {

// Validate direct serve base URL if specified
if c.Storage.DirectServeBaseURL != "" {
u, err := url.Parse(c.Storage.DirectServeBaseURL)
if err != nil || u.Scheme == "" || u.Host == "" {
return fmt.Errorf("invalid storage.direct_serve_base_url %q: must be an absolute URL", c.Storage.DirectServeBaseURL)
if err := validateAbsoluteURL("storage.direct_serve_base_url", c.Storage.DirectServeBaseURL); err != nil {
return err
}
}

Expand Down
38 changes: 38 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ func TestLoadFromEnv(t *testing.T) {

t.Setenv("PROXY_LISTEN", ":9000")
t.Setenv("PROXY_BASE_URL", "https://env.example.com")
t.Setenv("PROXY_UI_URL", "https://ui.env.example.com/ui")
t.Setenv("PROXY_STORAGE_PATH", "/env/cache")
t.Setenv("PROXY_LOG_LEVEL", testLevelDebug)
t.Setenv("PROXY_UPSTREAM_MAVEN", "https://maven.example.com/repository/maven-public")
Expand All @@ -286,6 +287,9 @@ func TestLoadFromEnv(t *testing.T) {
if cfg.BaseURL != "https://env.example.com" {
t.Errorf("BaseURL = %q, want %q", cfg.BaseURL, "https://env.example.com")
}
if cfg.UIBaseURL != "https://ui.env.example.com/ui" {
t.Errorf("UIBaseURL = %q, want %q", cfg.UIBaseURL, "https://ui.env.example.com/ui")
}
if cfg.Storage.Path != "/env/cache" {
t.Errorf("Storage.Path = %q, want %q", cfg.Storage.Path, "/env/cache")
}
Expand Down Expand Up @@ -574,6 +578,40 @@ func TestLoadDirectServeFromEnv(t *testing.T) {
}
}

func TestValidateUIBaseURLDefaultsToBaseURL(t *testing.T) {
cfg := Default()
cfg.BaseURL = "https://proxy.example.com"
cfg.UIBaseURL = ""

if err := cfg.Validate(); err != nil {
t.Fatalf("unexpected validation error: %v", err)
}
if cfg.UIBaseURL != "https://proxy.example.com" {
t.Errorf("UIBaseURL = %q, want it to default to BaseURL %q", cfg.UIBaseURL, "https://proxy.example.com")
}
}

func TestValidateUIBaseURL(t *testing.T) {
cfg := Default()

cfg.UIBaseURL = "not a url"
if err := cfg.Validate(); err == nil {
t.Error("expected validation error for relative ui_base_url")
}

cfg = Default()
cfg.UIBaseURL = "://bad"
if err := cfg.Validate(); err == nil {
t.Error("expected validation error for unparseable ui_base_url")
}

cfg = Default()
cfg.UIBaseURL = "https://ui.example.com/ui"
if err := cfg.Validate(); err != nil {
t.Errorf("unexpected error for valid ui_base_url: %v", err)
}
}

func TestValidateDirectServeBaseURL(t *testing.T) {
cfg := Default()

Expand Down
2 changes: 2 additions & 0 deletions internal/server/browse.go
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ func isLikelyText(filename string) bool {

// BrowseSourceData contains data for the browse source page.
type BrowseSourceData struct {
Layout
Ecosystem string
PackageName string
Version string
Expand Down Expand Up @@ -583,6 +584,7 @@ func (s *Server) compareDiff(w http.ResponseWriter, r *http.Request, ecosystem,

// ComparePageData contains data for the version comparison page.
type ComparePageData struct {
Layout
Ecosystem string
PackageName string
FromVersion string
Expand Down
5 changes: 5 additions & 0 deletions internal/server/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

// DashboardData contains data for rendering the dashboard.
type DashboardData struct {
Layout
Stats DashboardStats
EnrichmentStats EnrichmentStatsView
RecentPackages []PackageInfo
Expand Down Expand Up @@ -60,6 +61,7 @@ type RegistryConfig struct {

// PackageShowData contains data for rendering the package show page.
type PackageShowData struct {
Layout
Package *database.Package
Versions []database.Version
Vulnerabilities []database.Vulnerability
Expand All @@ -68,6 +70,7 @@ type PackageShowData struct {

// VersionShowData contains data for rendering the version show page.
type VersionShowData struct {
Layout
Package *database.Package
Version *database.Version
Artifacts []database.Artifact
Expand All @@ -79,6 +82,7 @@ type VersionShowData struct {

// SearchPageData contains data for rendering the search results page.
type SearchPageData struct {
Layout
Query string
Ecosystem string
Results []SearchResultItem
Expand All @@ -104,6 +108,7 @@ type SearchResultItem struct {

// PackagesListPageData contains data for rendering the packages list page.
type PackagesListPageData struct {
Layout
Ecosystem string
SortBy string
Results []SearchResultItem
Expand Down
19 changes: 19 additions & 0 deletions internal/server/layout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package server

import "net/http"

// Layout carries per-request fields consumed by the shared base template
// (canonical URL, og:url). It is embedded in every page data struct so that
// templates can reference {{.UIBaseURL}} and {{.CanonicalPath}} alongside the
// page's own fields.
type Layout struct {
UIBaseURL string
CanonicalPath string
}

func (s *Server) layoutFor(r *http.Request) Layout {
return Layout{
UIBaseURL: s.cfg.UIBaseURL,
CanonicalPath: r.URL.Path,
}
}
Loading