From 78325b6bf1eed44e49652948e5e0df6a8386418a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:16:24 +0000 Subject: [PATCH 1/6] Bump docker/login-action from 4.0.0 to 4.1.0 Bumps [docker/login-action](https://github.com/docker/login-action) from 4.0.0 to 4.1.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/b45d80f862d83dbcd57f89517bcf500b2ab88fb2...4907a6ddec9925e35a0a9e82d7399ccc52663121) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 4.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 554d98e..76f2541 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -25,7 +25,7 @@ jobs: persist-credentials: false - name: Log in to the Container registry - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 with: registry: ghcr.io username: ${{ github.actor }} From a1d028696daecfa8ffffc256d8d268a93443e264 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:16:37 +0000 Subject: [PATCH 2/6] Bump docker/build-push-action from 7.0.0 to 7.1.0 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.0.0 to 7.1.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/d08e5c354a6adb9ed34480a06d141179aa583294...bcafcacb16a39f128d818304e6c9c0c18556b85f) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: 7.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 554d98e..a9328ed 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -38,7 +38,7 @@ jobs: images: ghcr.io/${{ github.repository }} - name: Build and push Docker image - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f with: context: . push: true From 35f26f464502a59ba8dbdce4e01e1a0c80aaa67c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:16:42 +0000 Subject: [PATCH 3/6] Bump modernc.org/sqlite from 1.48.0 to 1.48.2 Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.48.0 to 1.48.2. - [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md) - [Commits](https://gitlab.com/cznic/sqlite/compare/v1.48.0...v1.48.2) --- updated-dependencies: - dependency-name: modernc.org/sqlite dependency-version: 1.48.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 85869db..8f5d8f4 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( golang.org/x/sync v0.20.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 - modernc.org/sqlite v1.48.0 + modernc.org/sqlite v1.48.2 ) require ( diff --git a/go.sum b/go.sum index dda4a35..12eff48 100644 --- a/go.sum +++ b/go.sum @@ -873,8 +873,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.48.0 h1:ElZyLop3Q2mHYk5IFPPXADejZrlHu7APbpB0sF78bq4= -modernc.org/sqlite v1.48.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= +modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c= +modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= From 59a510f3f57ebd685c8aac19f6ceaf21ed4a1ae5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:16:53 +0000 Subject: [PATCH 4/6] Bump github.com/lib/pq from 1.12.2 to 1.12.3 Bumps [github.com/lib/pq](https://github.com/lib/pq) from 1.12.2 to 1.12.3. - [Release notes](https://github.com/lib/pq/releases) - [Changelog](https://github.com/lib/pq/blob/master/CHANGELOG.md) - [Commits](https://github.com/lib/pq/compare/v1.12.2...v1.12.3) --- updated-dependencies: - dependency-name: github.com/lib/pq dependency-version: 1.12.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 85869db..13423f5 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/git-pkgs/vulns v0.1.4 github.com/go-chi/chi/v5 v5.2.5 github.com/jmoiron/sqlx v1.4.0 - github.com/lib/pq v1.12.2 + github.com/lib/pq v1.12.3 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 github.com/spdx/tools-golang v0.5.7 diff --git a/go.sum b/go.sum index dda4a35..a13e52b 100644 --- a/go.sum +++ b/go.sum @@ -432,8 +432,8 @@ github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3 github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.12.2 h1:ajJNv84limnK3aPbDIhLtcjrUbqAw/5XNdkuI6KNe/Q= -github.com/lib/pq v1.12.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE= From e59798cb1fbcf59ed02cb592235c16f80ac12676 Mon Sep 17 00:00:00 2001 From: "Kevin P. Fleming" Date: Sat, 18 Apr 2026 10:47:43 -0400 Subject: [PATCH 5/6] WIP --- config.example.yaml | 139 +++++++++++++++++++++++++++++++++ internal/config/cargo/cargo.go | 78 ++++++++++++++++++ internal/config/config.go | 18 +++++ internal/config/ecosystem.go | 10 +++ internal/handler/cargo.go | 18 +++-- internal/handler/cargo_test.go | 1 + internal/server/server.go | 8 +- internal/server/server_test.go | 5 +- 8 files changed, 266 insertions(+), 11 deletions(-) create mode 100644 internal/config/cargo/cargo.go create mode 100644 internal/config/ecosystem.go diff --git a/config.example.yaml b/config.example.yaml index ea17d15..2bdfd1e 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -49,6 +49,145 @@ log: # Log format: "text" or "json" format: "text" +# Ecosystem support - routes and upstream repositories +# +# This section is optional, since 'include_default' in each section +# defaults to 'true' and the route map will be populated with all of +# the default routes if no configuration is provided. +ecosystem: + cargo: + include_default: true + # the default route for crates.io + # route: + # - path: /cargo + # upstream: + # - name: crates.io + # index: https://index.crates.io + # crates: https://static.crates.io/crates + composer: + include_default: true + # the default route for packagist.org + # route: + # - name: composer + # upstream: + # - name: packagist.org + # upstream: https://packagist.org + # repository: https://repo.packagist.org + conan: + include_default: true + # the default route for conan.io + # route: + # - name: conan + # upstream: + # - name: conan.io + # upstream: https://center.conan.io + conda: + include_default: true + # the default route for anaconda.org + # route: + # - name: conda + # upstream: + # - name: anaconda.org + # upstream: https://conda.anaconda.org + cran: + include_default: true + # the default route for r-project.org + # route: + # - name: cran + # upstream: + # - name: r-project.org + # upstream: https://cloud.r-project.org + debian: + include_default: true + # the default route for debian.org + # route: + # - name: debian + # upstream: + # - name: debian.org + # upstream: http://deb.debian.org/debian + gem: + include_default: true + # the default route for rubygems.org + # route: + # - name: gem + # upstream: + # - name: rubygems.org + # upstream: https://rubygems.org + go: + include_default: true + # the default route for golang.org + # route: + # - name: go + # upstream: + # - name: golang.org + # upstream: https://proxy.golang.org + hex: + include_default: true + # the default route for hex.pm + # route: + # - name: hex + # upstream: + # - name: hex.pm + # upstream: https://repo.hex.pm + maven: + include_default: true + # the default route for maven.org + # route: + # - name: maven + # upstream: + # - name: maven.org + # upstream: https://repo1.maven.org/maven2 + npm: + include_default: true + # the default route for npmjs.org + # route: + # - name: npm + # upstream: + # - name: npmjs.org + # upstream: https://registry.npmjs.org + nuget: + include_default: true + # the default route for nuget.org + # route: + # - name: nuget + # upstream: + # - name: nuget.org + # upstream: https://api.nuget.org + oci: + include_default: true + # the default route for docker.io + # route: + # - name: v2 + # upstream: + # - name: docker.io + # registry: https://registry-1.docker.io + # auth: https://auth.docker.io + pub: + include_default: true + # the default route for pub.dev + # route: + # - name: pub + # upstream: + # - name: pub.dev + # upstream: https://pub.dev + pypi: + include_default: true + # the default route for pypi.org + # route: + # - name: pypi + # upstream: + # - name: pypi.org + # index: https://pypi.org + # files_host: files.pythonhosted.org + rpm: + include_default: true + # the default route for fedoraproject.org + # route: + # - name: rpm + # upstream: + # - name: fedoraproject.org + # upstream: https://dl.fedoraproject.org/pub/fedora/linux + # Upstream registry URLs and authentication upstream: # npm registry URL diff --git a/internal/config/cargo/cargo.go b/internal/config/cargo/cargo.go new file mode 100644 index 0000000..3e9c3cf --- /dev/null +++ b/internal/config/cargo/cargo.go @@ -0,0 +1,78 @@ +package cargo + +import ( + "fmt" + "net/url" +) + +// Config configures routes +type Config struct { + IncludeDefault bool `json:"include_default" yaml:"include_default"` + Route []RouteConfig `json:"route" yaml:"route"` +} + +// RouteConfig configures a route +type RouteConfig struct { + Path string `json:"path" yaml:"path"` + Upstream []UpstreamConfig `json:"upstream" yaml:"upstream"` +} + +// UpstreamConfig configures an upstream (source) +type UpstreamConfig struct { + Name string `json:"name" yaml:"name"` + Index string `json:"index" yaml:"index"` + Crates string `json:"crates" yaml:"crates"` +} + +// RouteDefault is the default route +var RouteDefault = RouteConfig{ + Path: "/cargo", + Upstream: []UpstreamConfig{ + { + Name: "crates.io", + Index: "https://index.crates.io", + Crates: "https://static.crates.io/crates", + }, + }, +} + +func (c *Config) Validate() error { + for _, route := range c.Route { + if err := route.Validate(); err != nil { + return err + } + } + + return nil +} + +func (r *RouteConfig) Validate() error { + // TODO: validate Path + + if len(r.Upstream) == 0 { + return fmt.Errorf("cargo route %q does not have any upstreams", r.Path) + } + if len(r.Upstream) > 1 { + return fmt.Errorf("cargo route %q has multiple upstreams; this is not yet supported", r.Path) + } + + for _, upstream := range r.Upstream { + if err := upstream.Validate(); err != nil { + return err + } + } + + return nil +} + +func (u *UpstreamConfig) Validate() error { + if _, err := url.Parse(u.Index); err != nil { + return fmt.Errorf("cargo upstream index %q is not a valid URL", u.Index) + } + + if _, err := url.Parse(u.Crates); err != nil { + return fmt.Errorf("cargo upstream crates %q is not a valid URL", u.Crates) + } + + return nil +} diff --git a/internal/config/config.go b/internal/config/config.go index ad0acc0..a82840f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -57,6 +57,7 @@ import ( "strings" "time" + "github.com/git-pkgs/proxy/internal/config/cargo" "gopkg.in/yaml.v3" ) @@ -82,6 +83,9 @@ type Config struct { // Upstream configures upstream registry URLs (optional overrides). Upstream UpstreamConfig `json:"upstream" yaml:"upstream"` + // Ecosystem configures ecosystem routes and upstreams + Ecosystem EcosystemConfig `json:"ecosystem" yaml:"ecosystem"` + // Cooldown configures version age filtering to mitigate supply chain attacks. Cooldown CooldownConfig `json:"cooldown" yaml:"cooldown"` @@ -239,6 +243,11 @@ func Default() *Config { Level: "info", Format: "text", }, + Ecosystem: EcosystemConfig{ + Cargo: cargo.Config{ + IncludeDefault: true, + }, + }, Upstream: UpstreamConfig{ NPM: "https://registry.npmjs.org", Cargo: "https://index.crates.io", @@ -334,6 +343,11 @@ func (c *Config) LoadFromEnv() { // Validate checks the configuration for errors. func (c *Config) Validate() error { + // finalize the configuration by injecting default routes if requested + if c.Ecosystem.Cargo.IncludeDefault { + c.Ecosystem.Cargo.Route = append(c.Ecosystem.Cargo.Route, cargo.RouteDefault) + } + if c.Listen == "" { return fmt.Errorf("listen address is required") } @@ -386,6 +400,10 @@ func (c *Config) Validate() error { } } + if err := c.Ecosystem.Cargo.Validate(); err != nil { + return err + } + return nil } diff --git a/internal/config/ecosystem.go b/internal/config/ecosystem.go new file mode 100644 index 0000000..a542698 --- /dev/null +++ b/internal/config/ecosystem.go @@ -0,0 +1,10 @@ +package config + +import ( + "github.com/git-pkgs/proxy/internal/config/cargo" +) + +// Ecosystem configuration (routes and upstreams) +type EcosystemConfig struct { + Cargo cargo.Config `json:"cargo" yaml:"cargo"` +} diff --git a/internal/handler/cargo.go b/internal/handler/cargo.go index 5d7810c..4c6f195 100644 --- a/internal/handler/cargo.go +++ b/internal/handler/cargo.go @@ -9,13 +9,11 @@ import ( "strings" "time" + "github.com/git-pkgs/proxy/internal/config/cargo" "github.com/git-pkgs/purl" ) const ( - cargoUpstream = "https://index.crates.io" - cargoDownloadBase = "https://static.crates.io/crates" - cargoIndexLen1 = 1 cargoIndexLen2 = 2 cargoIndexLen3 = 3 @@ -24,21 +22,27 @@ const ( // CargoHandler handles cargo registry protocol requests. type CargoHandler struct { proxy *Proxy + path string indexURL string downloadURL string proxyURL string } // NewCargoHandler creates a new cargo protocol handler. -func NewCargoHandler(proxy *Proxy, proxyURL string) *CargoHandler { +func NewCargoHandler(proxy *Proxy, proxyURL string, cfg cargo.RouteConfig) *CargoHandler { return &CargoHandler{ proxy: proxy, - indexURL: cargoUpstream, - downloadURL: cargoDownloadBase, + path: cfg.Path, + indexURL: cfg.Upstream[0].Index, + downloadURL: cfg.Upstream[0].Crates, proxyURL: strings.TrimSuffix(proxyURL, "/"), } } +func (h *CargoHandler) Path() string { + return h.path +} + // Routes returns the HTTP handler for cargo requests. // Mount this at /cargo on your router. func (h *CargoHandler) Routes() http.Handler { @@ -71,7 +75,7 @@ type CargoConfig struct { // handleConfig returns the registry configuration. func (h *CargoHandler) handleConfig(w http.ResponseWriter, r *http.Request) { config := CargoConfig{ - DL: h.proxyURL + "/cargo/crates/{crate}/{version}/download", + DL: h.proxyURL + h.path + "/crates/{crate}/{version}/download", } w.Header().Set("Content-Type", "application/json") diff --git a/internal/handler/cargo_test.go b/internal/handler/cargo_test.go index 5ce81b6..d80634f 100644 --- a/internal/handler/cargo_test.go +++ b/internal/handler/cargo_test.go @@ -48,6 +48,7 @@ func TestCargoBuildIndexPath(t *testing.T) { func TestCargoConfigEndpoint(t *testing.T) { h := &CargoHandler{ proxyURL: "http://localhost:8080", + path: "/cargo", } req := httptest.NewRequest(http.MethodGet, "/config.json", nil) diff --git a/internal/server/server.go b/internal/server/server.go index 5d544a2..657d0ca 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -170,7 +170,6 @@ func (s *Server) Start() error { // Mount protocol handlers npmHandler := handler.NewNPMHandler(proxy, s.cfg.BaseURL) - cargoHandler := handler.NewCargoHandler(proxy, s.cfg.BaseURL) gemHandler := handler.NewGemHandler(proxy, s.cfg.BaseURL) goHandler := handler.NewGoHandler(proxy, s.cfg.BaseURL) hexHandler := handler.NewHexHandler(proxy, s.cfg.BaseURL) @@ -186,8 +185,13 @@ func (s *Server) Start() error { debianHandler := handler.NewDebianHandler(proxy, s.cfg.BaseURL) rpmHandler := handler.NewRPMHandler(proxy, s.cfg.BaseURL) + for _, route := range s.cfg.Ecosystem.Cargo.Route { + routeHandler := handler.NewCargoHandler(proxy, s.cfg.BaseURL, route) + r.Mount(routeHandler.Path(), http.StripPrefix(routeHandler.Path(), routeHandler.Routes())) + s.logger.Info("mounted handler", "ecosystem", "cargo", "path", routeHandler.Path()) + } + r.Mount("/npm", http.StripPrefix("/npm", npmHandler.Routes())) - r.Mount("/cargo", http.StripPrefix("/cargo", cargoHandler.Routes())) r.Mount("/gem", http.StripPrefix("/gem", gemHandler.Routes())) r.Mount("/go", http.StripPrefix("/go", goHandler.Routes())) r.Mount("/hex", http.StripPrefix("/hex", hexHandler.Routes())) diff --git a/internal/server/server_test.go b/internal/server/server_test.go index be88bf6..0155437 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -15,6 +15,7 @@ import ( "time" "github.com/git-pkgs/proxy/internal/config" + "github.com/git-pkgs/proxy/internal/config/cargo" "github.com/git-pkgs/proxy/internal/database" "github.com/git-pkgs/proxy/internal/handler" "github.com/git-pkgs/proxy/internal/storage" @@ -68,13 +69,13 @@ func newTestServer(t *testing.T) *testServer { // Mount handlers npmHandler := handler.NewNPMHandler(proxy, cfg.BaseURL) - cargoHandler := handler.NewCargoHandler(proxy, cfg.BaseURL) + cargoHandler := handler.NewCargoHandler(proxy, cfg.BaseURL, cargo.RouteDefault) gemHandler := handler.NewGemHandler(proxy, cfg.BaseURL) goHandler := handler.NewGoHandler(proxy, cfg.BaseURL) pypiHandler := handler.NewPyPIHandler(proxy, cfg.BaseURL) r.Mount("/npm", http.StripPrefix("/npm", npmHandler.Routes())) - r.Mount("/cargo", http.StripPrefix("/cargo", cargoHandler.Routes())) + r.Mount(cargoHandler.Path(), http.StripPrefix(cargoHandler.Path(), cargoHandler.Routes())) r.Mount("/gem", http.StripPrefix("/gem", gemHandler.Routes())) r.Mount("/go", http.StripPrefix("/go", goHandler.Routes())) r.Mount("/pypi", http.StripPrefix("/pypi", pypiHandler.Routes())) From 081c30287df451808566de41cac7b0345b7775d6 Mon Sep 17 00:00:00 2001 From: "Kevin P. Fleming" Date: Sun, 19 Apr 2026 07:16:43 -0400 Subject: [PATCH 6/6] WIP --- config.example.yaml | 139 ++++++++++++++++++++++++++++++ internal/config/cargo/cargo.go | 78 +++++++++++++++++ internal/config/config.go | 28 ++++++ internal/config/debian/debian.go | 72 ++++++++++++++++ internal/config/ecosystem.go | 12 +++ internal/handler/cargo.go | 18 ++-- internal/handler/cargo_test.go | 3 +- internal/handler/debian.go | 14 ++- internal/handler/debian_test.go | 3 +- internal/handler/download_test.go | 3 +- internal/server/server.go | 16 +++- internal/server/server_test.go | 5 +- 12 files changed, 371 insertions(+), 20 deletions(-) create mode 100644 internal/config/cargo/cargo.go create mode 100644 internal/config/debian/debian.go create mode 100644 internal/config/ecosystem.go diff --git a/config.example.yaml b/config.example.yaml index ea17d15..2bdfd1e 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -49,6 +49,145 @@ log: # Log format: "text" or "json" format: "text" +# Ecosystem support - routes and upstream repositories +# +# This section is optional, since 'include_default' in each section +# defaults to 'true' and the route map will be populated with all of +# the default routes if no configuration is provided. +ecosystem: + cargo: + include_default: true + # the default route for crates.io + # route: + # - path: /cargo + # upstream: + # - name: crates.io + # index: https://index.crates.io + # crates: https://static.crates.io/crates + composer: + include_default: true + # the default route for packagist.org + # route: + # - name: composer + # upstream: + # - name: packagist.org + # upstream: https://packagist.org + # repository: https://repo.packagist.org + conan: + include_default: true + # the default route for conan.io + # route: + # - name: conan + # upstream: + # - name: conan.io + # upstream: https://center.conan.io + conda: + include_default: true + # the default route for anaconda.org + # route: + # - name: conda + # upstream: + # - name: anaconda.org + # upstream: https://conda.anaconda.org + cran: + include_default: true + # the default route for r-project.org + # route: + # - name: cran + # upstream: + # - name: r-project.org + # upstream: https://cloud.r-project.org + debian: + include_default: true + # the default route for debian.org + # route: + # - name: debian + # upstream: + # - name: debian.org + # upstream: http://deb.debian.org/debian + gem: + include_default: true + # the default route for rubygems.org + # route: + # - name: gem + # upstream: + # - name: rubygems.org + # upstream: https://rubygems.org + go: + include_default: true + # the default route for golang.org + # route: + # - name: go + # upstream: + # - name: golang.org + # upstream: https://proxy.golang.org + hex: + include_default: true + # the default route for hex.pm + # route: + # - name: hex + # upstream: + # - name: hex.pm + # upstream: https://repo.hex.pm + maven: + include_default: true + # the default route for maven.org + # route: + # - name: maven + # upstream: + # - name: maven.org + # upstream: https://repo1.maven.org/maven2 + npm: + include_default: true + # the default route for npmjs.org + # route: + # - name: npm + # upstream: + # - name: npmjs.org + # upstream: https://registry.npmjs.org + nuget: + include_default: true + # the default route for nuget.org + # route: + # - name: nuget + # upstream: + # - name: nuget.org + # upstream: https://api.nuget.org + oci: + include_default: true + # the default route for docker.io + # route: + # - name: v2 + # upstream: + # - name: docker.io + # registry: https://registry-1.docker.io + # auth: https://auth.docker.io + pub: + include_default: true + # the default route for pub.dev + # route: + # - name: pub + # upstream: + # - name: pub.dev + # upstream: https://pub.dev + pypi: + include_default: true + # the default route for pypi.org + # route: + # - name: pypi + # upstream: + # - name: pypi.org + # index: https://pypi.org + # files_host: files.pythonhosted.org + rpm: + include_default: true + # the default route for fedoraproject.org + # route: + # - name: rpm + # upstream: + # - name: fedoraproject.org + # upstream: https://dl.fedoraproject.org/pub/fedora/linux + # Upstream registry URLs and authentication upstream: # npm registry URL diff --git a/internal/config/cargo/cargo.go b/internal/config/cargo/cargo.go new file mode 100644 index 0000000..3e9c3cf --- /dev/null +++ b/internal/config/cargo/cargo.go @@ -0,0 +1,78 @@ +package cargo + +import ( + "fmt" + "net/url" +) + +// Config configures routes +type Config struct { + IncludeDefault bool `json:"include_default" yaml:"include_default"` + Route []RouteConfig `json:"route" yaml:"route"` +} + +// RouteConfig configures a route +type RouteConfig struct { + Path string `json:"path" yaml:"path"` + Upstream []UpstreamConfig `json:"upstream" yaml:"upstream"` +} + +// UpstreamConfig configures an upstream (source) +type UpstreamConfig struct { + Name string `json:"name" yaml:"name"` + Index string `json:"index" yaml:"index"` + Crates string `json:"crates" yaml:"crates"` +} + +// RouteDefault is the default route +var RouteDefault = RouteConfig{ + Path: "/cargo", + Upstream: []UpstreamConfig{ + { + Name: "crates.io", + Index: "https://index.crates.io", + Crates: "https://static.crates.io/crates", + }, + }, +} + +func (c *Config) Validate() error { + for _, route := range c.Route { + if err := route.Validate(); err != nil { + return err + } + } + + return nil +} + +func (r *RouteConfig) Validate() error { + // TODO: validate Path + + if len(r.Upstream) == 0 { + return fmt.Errorf("cargo route %q does not have any upstreams", r.Path) + } + if len(r.Upstream) > 1 { + return fmt.Errorf("cargo route %q has multiple upstreams; this is not yet supported", r.Path) + } + + for _, upstream := range r.Upstream { + if err := upstream.Validate(); err != nil { + return err + } + } + + return nil +} + +func (u *UpstreamConfig) Validate() error { + if _, err := url.Parse(u.Index); err != nil { + return fmt.Errorf("cargo upstream index %q is not a valid URL", u.Index) + } + + if _, err := url.Parse(u.Crates); err != nil { + return fmt.Errorf("cargo upstream crates %q is not a valid URL", u.Crates) + } + + return nil +} diff --git a/internal/config/config.go b/internal/config/config.go index ad0acc0..f5aebdf 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -57,6 +57,8 @@ import ( "strings" "time" + "github.com/git-pkgs/proxy/internal/config/cargo" + "github.com/git-pkgs/proxy/internal/config/debian" "gopkg.in/yaml.v3" ) @@ -82,6 +84,9 @@ type Config struct { // Upstream configures upstream registry URLs (optional overrides). Upstream UpstreamConfig `json:"upstream" yaml:"upstream"` + // Ecosystem configures ecosystem routes and upstreams + Ecosystem EcosystemConfig `json:"ecosystem" yaml:"ecosystem"` + // Cooldown configures version age filtering to mitigate supply chain attacks. Cooldown CooldownConfig `json:"cooldown" yaml:"cooldown"` @@ -239,6 +244,14 @@ func Default() *Config { Level: "info", Format: "text", }, + Ecosystem: EcosystemConfig{ + Cargo: cargo.Config{ + IncludeDefault: true, + }, + Debian: debian.Config{ + IncludeDefault: true, + }, + }, Upstream: UpstreamConfig{ NPM: "https://registry.npmjs.org", Cargo: "https://index.crates.io", @@ -334,6 +347,14 @@ func (c *Config) LoadFromEnv() { // Validate checks the configuration for errors. func (c *Config) Validate() error { + // finalize the configuration by injecting default routes if requested + if c.Ecosystem.Cargo.IncludeDefault { + c.Ecosystem.Cargo.Route = append(c.Ecosystem.Cargo.Route, cargo.RouteDefault) + } + if c.Ecosystem.Debian.IncludeDefault { + c.Ecosystem.Debian.Route = append(c.Ecosystem.Debian.Route, debian.RouteDefault) + } + if c.Listen == "" { return fmt.Errorf("listen address is required") } @@ -386,6 +407,13 @@ func (c *Config) Validate() error { } } + if err := c.Ecosystem.Cargo.Validate(); err != nil { + return err + } + if err := c.Ecosystem.Debian.Validate(); err != nil { + return err + } + return nil } diff --git a/internal/config/debian/debian.go b/internal/config/debian/debian.go new file mode 100644 index 0000000..06c5f97 --- /dev/null +++ b/internal/config/debian/debian.go @@ -0,0 +1,72 @@ +package debian + +import ( + "fmt" + "net/url" +) + +// Config configures routes +type Config struct { + IncludeDefault bool `json:"include_default" yaml:"include_default"` + Route []RouteConfig `json:"route" yaml:"route"` +} + +// RouteConfig configures a route +type RouteConfig struct { + Path string `json:"path" yaml:"path"` + Upstream []UpstreamConfig `json:"upstream" yaml:"upstream"` +} + +// UpstreamConfig configures an upstream (source) +type UpstreamConfig struct { + Name string `json:"name" yaml:"name"` + Upstream string `json:"upstream" yaml:"upstream"` +} + +// RouteDefault is the default route +var RouteDefault = RouteConfig{ + Path: "/debian", + Upstream: []UpstreamConfig{ + { + Name: "debian.org", + Upstream: "http://deb.debian.org/debian", + }, + }, +} + +func (c *Config) Validate() error { + for _, route := range c.Route { + if err := route.Validate(); err != nil { + return err + } + } + + return nil +} + +func (r *RouteConfig) Validate() error { + // TODO: validate Path + + if len(r.Upstream) == 0 { + return fmt.Errorf("debian route %q does not have any upstreams", r.Path) + } + if len(r.Upstream) > 1 { + return fmt.Errorf("debian route %q has multiple upstreams; this is not yet supported", r.Path) + } + + for _, upstream := range r.Upstream { + if err := upstream.Validate(); err != nil { + return err + } + } + + return nil +} + +func (u *UpstreamConfig) Validate() error { + if _, err := url.Parse(u.Upstream); err != nil { + return fmt.Errorf("debian upstream upstream %q is not a valid URL", u.Upstream) + } + + return nil +} diff --git a/internal/config/ecosystem.go b/internal/config/ecosystem.go new file mode 100644 index 0000000..bc0bc62 --- /dev/null +++ b/internal/config/ecosystem.go @@ -0,0 +1,12 @@ +package config + +import ( + "github.com/git-pkgs/proxy/internal/config/cargo" + "github.com/git-pkgs/proxy/internal/config/debian" +) + +// Ecosystem configuration (routes and upstreams) +type EcosystemConfig struct { + Cargo cargo.Config `json:"cargo" yaml:"cargo"` + Debian debian.Config `json:"debian" yaml:"debian"` +} diff --git a/internal/handler/cargo.go b/internal/handler/cargo.go index 5d7810c..4c6f195 100644 --- a/internal/handler/cargo.go +++ b/internal/handler/cargo.go @@ -9,13 +9,11 @@ import ( "strings" "time" + "github.com/git-pkgs/proxy/internal/config/cargo" "github.com/git-pkgs/purl" ) const ( - cargoUpstream = "https://index.crates.io" - cargoDownloadBase = "https://static.crates.io/crates" - cargoIndexLen1 = 1 cargoIndexLen2 = 2 cargoIndexLen3 = 3 @@ -24,21 +22,27 @@ const ( // CargoHandler handles cargo registry protocol requests. type CargoHandler struct { proxy *Proxy + path string indexURL string downloadURL string proxyURL string } // NewCargoHandler creates a new cargo protocol handler. -func NewCargoHandler(proxy *Proxy, proxyURL string) *CargoHandler { +func NewCargoHandler(proxy *Proxy, proxyURL string, cfg cargo.RouteConfig) *CargoHandler { return &CargoHandler{ proxy: proxy, - indexURL: cargoUpstream, - downloadURL: cargoDownloadBase, + path: cfg.Path, + indexURL: cfg.Upstream[0].Index, + downloadURL: cfg.Upstream[0].Crates, proxyURL: strings.TrimSuffix(proxyURL, "/"), } } +func (h *CargoHandler) Path() string { + return h.path +} + // Routes returns the HTTP handler for cargo requests. // Mount this at /cargo on your router. func (h *CargoHandler) Routes() http.Handler { @@ -71,7 +75,7 @@ type CargoConfig struct { // handleConfig returns the registry configuration. func (h *CargoHandler) handleConfig(w http.ResponseWriter, r *http.Request) { config := CargoConfig{ - DL: h.proxyURL + "/cargo/crates/{crate}/{version}/download", + DL: h.proxyURL + h.path + "/crates/{crate}/{version}/download", } w.Header().Set("Content-Type", "application/json") diff --git a/internal/handler/cargo_test.go b/internal/handler/cargo_test.go index 5ce81b6..9f76a9c 100644 --- a/internal/handler/cargo_test.go +++ b/internal/handler/cargo_test.go @@ -48,6 +48,7 @@ func TestCargoBuildIndexPath(t *testing.T) { func TestCargoConfigEndpoint(t *testing.T) { h := &CargoHandler{ proxyURL: "http://localhost:8080", + path: "/xyzzy", } req := httptest.NewRequest(http.MethodGet, "/config.json", nil) @@ -64,7 +65,7 @@ func TestCargoConfigEndpoint(t *testing.T) { t.Fatalf("failed to parse config: %v", err) } - expectedDL := "http://localhost:8080/cargo/crates/{crate}/{version}/download" + expectedDL := "http://localhost:8080/xyzzy/crates/{crate}/{version}/download" if config.DL != expectedDL { t.Errorf("DL = %q, want %q", config.DL, expectedDL) } diff --git a/internal/handler/debian.go b/internal/handler/debian.go index b767f6d..8b313dd 100644 --- a/internal/handler/debian.go +++ b/internal/handler/debian.go @@ -2,33 +2,39 @@ package handler import ( "fmt" + "github.com/git-pkgs/proxy/internal/config/debian" "net/http" "regexp" "strings" ) const ( - debianUpstream = "http://deb.debian.org/debian" - debMatchCount = 4 // full match + name + version + arch + debMatchCount = 4 // full match + name + version + arch ) // DebianHandler handles APT/Debian repository protocol requests. // It proxies requests to upstream Debian/Ubuntu repositories and caches .deb packages. type DebianHandler struct { proxy *Proxy + path string upstreamURL string proxyURL string } // NewDebianHandler creates a new Debian/APT protocol handler. -func NewDebianHandler(proxy *Proxy, proxyURL string) *DebianHandler { +func NewDebianHandler(proxy *Proxy, proxyURL string, cfg debian.RouteConfig) *DebianHandler { return &DebianHandler{ proxy: proxy, - upstreamURL: debianUpstream, + path: cfg.Path, + upstreamURL: cfg.Upstream[0].Upstream, proxyURL: strings.TrimSuffix(proxyURL, "/"), } } +func (h *DebianHandler) Path() string { + return h.path +} + // Routes returns the HTTP handler for Debian requests. // Mount this at /debian on your router. func (h *DebianHandler) Routes() http.Handler { diff --git a/internal/handler/debian_test.go b/internal/handler/debian_test.go index dfdd326..6a26573 100644 --- a/internal/handler/debian_test.go +++ b/internal/handler/debian_test.go @@ -2,6 +2,7 @@ package handler import ( "testing" + "github.com/git-pkgs/proxy/internal/config/debian" ) func TestDebianHandler_parsePoolPath(t *testing.T) { @@ -18,6 +19,6 @@ func TestDebianHandler_parsePoolPath(t *testing.T) { } func TestDebianHandler_Routes(t *testing.T) { - h := NewDebianHandler(nil, "http://localhost:8080") + h := NewDebianHandler(nil, "http://localhost:8080", debian.RouteDefault) assertRoutesBasics(t, h.Routes(), "/dists/stable/Release", "/pool/../../../etc/passwd") } diff --git a/internal/handler/download_test.go b/internal/handler/download_test.go index 639e976..620b022 100644 --- a/internal/handler/download_test.go +++ b/internal/handler/download_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/git-pkgs/proxy/internal/config/debian" "github.com/git-pkgs/proxy/internal/database" "github.com/git-pkgs/proxy/internal/storage" "github.com/git-pkgs/purl" @@ -897,7 +898,7 @@ func TestDebianHandler_DownloadCacheMiss(t *testing.T) { ContentType: "application/vnd.debian.binary-package", } - h := NewDebianHandler(proxy, "http://localhost") + h := NewDebianHandler(proxy, "http://localhost", debian.RouteDefault) srv := httptest.NewServer(h.Routes()) defer srv.Close() diff --git a/internal/server/server.go b/internal/server/server.go index 5d544a2..9196b19 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -170,7 +170,6 @@ func (s *Server) Start() error { // Mount protocol handlers npmHandler := handler.NewNPMHandler(proxy, s.cfg.BaseURL) - cargoHandler := handler.NewCargoHandler(proxy, s.cfg.BaseURL) gemHandler := handler.NewGemHandler(proxy, s.cfg.BaseURL) goHandler := handler.NewGoHandler(proxy, s.cfg.BaseURL) hexHandler := handler.NewHexHandler(proxy, s.cfg.BaseURL) @@ -183,11 +182,21 @@ func (s *Server) Start() error { condaHandler := handler.NewCondaHandler(proxy, s.cfg.BaseURL) cranHandler := handler.NewCRANHandler(proxy, s.cfg.BaseURL) containerHandler := handler.NewContainerHandler(proxy, s.cfg.BaseURL) - debianHandler := handler.NewDebianHandler(proxy, s.cfg.BaseURL) rpmHandler := handler.NewRPMHandler(proxy, s.cfg.BaseURL) + for _, route := range s.cfg.Ecosystem.Cargo.Route { + routeHandler := handler.NewCargoHandler(proxy, s.cfg.BaseURL, route) + r.Mount(routeHandler.Path(), http.StripPrefix(routeHandler.Path(), routeHandler.Routes())) + s.logger.Info("mounted handler", "ecosystem", "cargo", "path", routeHandler.Path()) + } + + for _, route := range s.cfg.Ecosystem.Debian.Route { + routeHandler := handler.NewDebianHandler(proxy, s.cfg.BaseURL, route) + r.Mount(routeHandler.Path(), http.StripPrefix(routeHandler.Path(), routeHandler.Routes())) + s.logger.Info("mounted handler", "ecosystem", "debian", "path", routeHandler.Path()) + } + r.Mount("/npm", http.StripPrefix("/npm", npmHandler.Routes())) - r.Mount("/cargo", http.StripPrefix("/cargo", cargoHandler.Routes())) r.Mount("/gem", http.StripPrefix("/gem", gemHandler.Routes())) r.Mount("/go", http.StripPrefix("/go", goHandler.Routes())) r.Mount("/hex", http.StripPrefix("/hex", hexHandler.Routes())) @@ -200,7 +209,6 @@ func (s *Server) Start() error { r.Mount("/conda", http.StripPrefix("/conda", condaHandler.Routes())) r.Mount("/cran", http.StripPrefix("/cran", cranHandler.Routes())) r.Mount("/v2", http.StripPrefix("/v2", containerHandler.Routes())) - r.Mount("/debian", http.StripPrefix("/debian", debianHandler.Routes())) r.Mount("/rpm", http.StripPrefix("/rpm", rpmHandler.Routes())) // Health, stats, and static endpoints diff --git a/internal/server/server_test.go b/internal/server/server_test.go index be88bf6..0155437 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -15,6 +15,7 @@ import ( "time" "github.com/git-pkgs/proxy/internal/config" + "github.com/git-pkgs/proxy/internal/config/cargo" "github.com/git-pkgs/proxy/internal/database" "github.com/git-pkgs/proxy/internal/handler" "github.com/git-pkgs/proxy/internal/storage" @@ -68,13 +69,13 @@ func newTestServer(t *testing.T) *testServer { // Mount handlers npmHandler := handler.NewNPMHandler(proxy, cfg.BaseURL) - cargoHandler := handler.NewCargoHandler(proxy, cfg.BaseURL) + cargoHandler := handler.NewCargoHandler(proxy, cfg.BaseURL, cargo.RouteDefault) gemHandler := handler.NewGemHandler(proxy, cfg.BaseURL) goHandler := handler.NewGoHandler(proxy, cfg.BaseURL) pypiHandler := handler.NewPyPIHandler(proxy, cfg.BaseURL) r.Mount("/npm", http.StripPrefix("/npm", npmHandler.Routes())) - r.Mount("/cargo", http.StripPrefix("/cargo", cargoHandler.Routes())) + r.Mount(cargoHandler.Path(), http.StripPrefix(cargoHandler.Path(), cargoHandler.Routes())) r.Mount("/gem", http.StripPrefix("/gem", gemHandler.Routes())) r.Mount("/go", http.StripPrefix("/go", goHandler.Routes())) r.Mount("/pypi", http.StripPrefix("/pypi", pypiHandler.Routes()))