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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/git-pkgs/cooldown v0.1.1
github.com/git-pkgs/enrichment v0.6.0
github.com/git-pkgs/purl v0.1.14
github.com/git-pkgs/registries v0.6.2
github.com/git-pkgs/registries v0.6.3
github.com/git-pkgs/spdx v0.1.4
github.com/git-pkgs/vers v0.2.6
github.com/git-pkgs/vulns v0.1.6
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,8 @@ github.com/git-pkgs/pom v0.1.5 h1:TGT8Az2OMxGWsXnSagtUMGzZm7Oax8HrSCteA+mi0qY=
github.com/git-pkgs/pom v0.1.5/go.mod h1:ufdMBe1lKzqOeP9IUb9NPZ458xKV8E8NvuyBMxOfwIk=
github.com/git-pkgs/purl v0.1.14 h1:GgqwiBNS0eKJqJ/gabUBEC10Xhpj6UX12kfNzujaeYc=
github.com/git-pkgs/purl v0.1.14/go.mod h1:8oCcdcYZA/e1B33e7Ylju6azboTKjdqf3ybcbQj6I/o=
github.com/git-pkgs/registries v0.6.2 h1:26G5zW6Q7x1CSfNkaEqEjRMJiA4JwfdKOCJ7Qm+u0a8=
github.com/git-pkgs/registries v0.6.2/go.mod h1:GR0Bu6nC3NQe6f7lfDoEVqAnoQkMocf4M98B12a7B3E=
github.com/git-pkgs/registries v0.6.3 h1:7sveeeMS2lgXtcqNYAA3bwaT7H9CQs0uhy6wQSxz3Js=
github.com/git-pkgs/registries v0.6.3/go.mod h1:j4o50ii/vD9Z42/nFeQASt15BPolADIejeFmmqK4njo=
github.com/git-pkgs/spdx v0.1.4 h1:eQ0waEV3uUeItpWAOvdN1K1rL9hTgsU7fF74r1mDXMs=
github.com/git-pkgs/spdx v0.1.4/go.mod h1:cqRoZcvl530s/W+oGNvwjt4ODN8T1W6D/20MUZEFdto=
github.com/git-pkgs/vers v0.2.6 h1:IelZd7BP/JhzTloUTDY67nehUgoYva3g9viqAMCHJg8=
Expand Down
3 changes: 1 addition & 2 deletions internal/handler/cargo.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,7 @@ func (h *CargoHandler) handleDownload(w http.ResponseWriter, r *http.Request) {

result, err := h.proxy.GetOrFetchArtifact(r.Context(), "cargo", name, version, filename)
if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err)
http.Error(w, "failed to fetch crate", http.StatusBadGateway)
h.proxy.serveArtifactError(w, err, "failed to fetch crate")
return
}

Expand Down
3 changes: 1 addition & 2 deletions internal/handler/composer.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,7 @@ func (h *ComposerHandler) handleDownload(w http.ResponseWriter, r *http.Request)

result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "composer", packageName, version, filename, downloadURL)
if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err)
http.Error(w, "failed to fetch package", http.StatusBadGateway)
h.proxy.serveArtifactError(w, err, "failed to fetch package")
return
}

Expand Down
6 changes: 2 additions & 4 deletions internal/handler/conan.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,7 @@ func (h *ConanHandler) handleRecipeFile(w http.ResponseWriter, r *http.Request)

result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "conan", packageName, storageVersion, storageFilename, upstreamURL)
if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err)
http.Error(w, "failed to fetch file", http.StatusBadGateway)
h.proxy.serveArtifactError(w, err, "failed to fetch file")
return
}

Expand Down Expand Up @@ -122,8 +121,7 @@ func (h *ConanHandler) handlePackageFile(w http.ResponseWriter, r *http.Request)

result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "conan", packageName, storageVersion, storageFilename, upstreamURL)
if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err)
http.Error(w, "failed to fetch file", http.StatusBadGateway)
h.proxy.serveArtifactError(w, err, "failed to fetch file")
return
}

Expand Down
3 changes: 1 addition & 2 deletions internal/handler/conda.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@ func (h *CondaHandler) handleDownload(w http.ResponseWriter, r *http.Request) {

result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "conda", packageName, version, filename, upstreamURL)
if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err)
http.Error(w, "failed to fetch package", http.StatusBadGateway)
h.proxy.serveArtifactError(w, err, "failed to fetch package")
return
}

Expand Down
7 changes: 6 additions & 1 deletion internal/handler/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package handler

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -117,8 +118,12 @@ func (h *ContainerHandler) handleBlobDownload(w http.ResponseWriter, r *http.Req
)

if err != nil {
if errors.Is(err, ErrUpstreamNotFound) {
h.containerError(w, http.StatusNotFound, "BLOB_UNKNOWN", "blob unknown to registry")
return
}
h.proxy.Logger.Error("failed to fetch blob", "error", err)
h.containerError(w, http.StatusBadGateway, "BLOB_UNKNOWN", "failed to fetch blob")
h.containerError(w, http.StatusBadGateway, "INTERNAL_ERROR", "failed to fetch blob")
return
}

Expand Down
6 changes: 2 additions & 4 deletions internal/handler/cran.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@ func (h *CRANHandler) handleSourceDownload(w http.ResponseWriter, r *http.Reques

result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "cran", name, version, filename, upstreamURL)
if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err)
http.Error(w, "failed to fetch package", http.StatusBadGateway)
h.proxy.serveArtifactError(w, err, "failed to fetch package")
return
}

Expand Down Expand Up @@ -107,8 +106,7 @@ func (h *CRANHandler) handleBinaryDownload(w http.ResponseWriter, r *http.Reques

result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "cran", name, storageVersion, filename, upstreamURL)
if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err)
http.Error(w, "failed to fetch package", http.StatusBadGateway)
h.proxy.serveArtifactError(w, err, "failed to fetch package")
return
}

Expand Down
3 changes: 1 addition & 2 deletions internal/handler/debian.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,7 @@ func (h *DebianHandler) handlePackageDownload(w http.ResponseWriter, r *http.Req
result, err := h.proxy.GetOrFetchArtifactFromURL(
r.Context(), "deb", name, version, filename, downloadURL)
if err != nil {
h.proxy.Logger.Error("failed to get debian package", "error", err)
http.Error(w, "failed to fetch package", http.StatusBadGateway)
h.proxy.serveArtifactError(w, err, "failed to fetch package")
return
}

Expand Down
39 changes: 39 additions & 0 deletions internal/handler/filename_download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package handler

import (
"net/http"
"strings"
)

type filenameDownload struct {
ecosystem string
suffix string
parseErr string
fetchErr string
parse func(string) (name, version string)
}

func (p *Proxy) handleFilenameDownload(w http.ResponseWriter, r *http.Request, d filenameDownload) {
filename := r.PathValue("filename")
if filename == "" || !strings.HasSuffix(filename, d.suffix) {
http.Error(w, "invalid filename", http.StatusBadRequest)
return
}

name, version := d.parse(filename)
if name == "" || version == "" {
http.Error(w, d.parseErr, http.StatusBadRequest)
return
}

p.Logger.Info(d.ecosystem+" download request",
"name", name, "version", version, "filename", filename)

result, err := p.GetOrFetchArtifact(r.Context(), d.ecosystem, name, version, filename)
if err != nil {
p.serveArtifactError(w, err, d.fetchErr)
return
}

ServeArtifact(w, result)
}
31 changes: 7 additions & 24 deletions internal/handler/gem.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,30 +58,13 @@ func (h *GemHandler) Routes() http.Handler {

// handleDownload serves a gem file, fetching and caching from upstream if needed.
func (h *GemHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
filename := r.PathValue("filename")
if filename == "" || !strings.HasSuffix(filename, ".gem") {
http.Error(w, "invalid filename", http.StatusBadRequest)
return
}

// Extract name and version from filename (e.g., "rails-7.1.0.gem")
name, version := h.parseGemFilename(filename)
if name == "" || version == "" {
http.Error(w, "could not parse gem filename", http.StatusBadRequest)
return
}

h.proxy.Logger.Info("gem download request",
"name", name, "version", version, "filename", filename)

result, err := h.proxy.GetOrFetchArtifact(r.Context(), "gem", name, version, filename)
if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err)
http.Error(w, "failed to fetch gem", http.StatusBadGateway)
return
}

ServeArtifact(w, result)
h.proxy.handleFilenameDownload(w, r, filenameDownload{
ecosystem: "gem",
suffix: ".gem",
parseErr: "could not parse gem filename",
fetchErr: "failed to fetch gem",
parse: h.parseGemFilename,
})
}

// parseGemFilename extracts name and version from a gem filename.
Expand Down
22 changes: 21 additions & 1 deletion internal/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,9 @@ func (p *Proxy) fetchAndCache(ctx context.Context, ecosystem, name, version, fil
// Resolve download URL
info, err := p.Resolver.Resolve(ctx, ecosystem, name, version)
if err != nil {
if errors.Is(err, fetch.ErrNotFound) {
return nil, ErrUpstreamNotFound
}
return nil, fmt.Errorf("resolving download URL: %w", err)
}

Expand All @@ -270,6 +273,9 @@ func (p *Proxy) fetchAndCache(ctx context.Context, ecosystem, name, version, fil
if err != nil {
metrics.RecordUpstreamFetch(ecosystem, fetchDuration)
metrics.RecordUpstreamError(ecosystem, "fetch_failed")
if errors.Is(err, fetch.ErrNotFound) {
return nil, ErrUpstreamNotFound
}
return nil, fmt.Errorf("fetching from upstream: %w", err)
}
metrics.RecordUpstreamFetch(ecosystem, fetchDuration)
Expand Down Expand Up @@ -451,7 +457,18 @@ func JSONError(w http.ResponseWriter, status int, message string) {
}

// ErrUpstreamNotFound indicates the upstream returned 404.
var ErrUpstreamNotFound = fmt.Errorf("upstream: not found")
var ErrUpstreamNotFound = fmt.Errorf("upstream: %w", fetch.ErrNotFound)

// serveArtifactError writes response for a failed fetch:
// 404 when upstream reports artifact missing, 502 otherwise.
func (p *Proxy) serveArtifactError(w http.ResponseWriter, err error, clientMsg string) {
if errors.Is(err, ErrUpstreamNotFound) {
http.Error(w, "not found", http.StatusNotFound)
return
}
p.Logger.Error("failed to get artifact", "error", err)
http.Error(w, clientMsg, http.StatusBadGateway)
}

// errStale304 is returned when upstream sends 304 but the cached file is missing.
var errStale304 = fmt.Errorf("upstream returned 304 but cached file is missing")
Expand Down Expand Up @@ -795,6 +812,9 @@ func (p *Proxy) fetchAndCacheFromURL(ctx context.Context, ecosystem, name, versi

artifact, err := p.Fetcher.FetchWithHeaders(ctx, downloadURL, headers)
if err != nil {
if errors.Is(err, fetch.ErrNotFound) {
return nil, ErrUpstreamNotFound
}
return nil, fmt.Errorf("fetching from upstream: %w", err)
}

Expand Down
31 changes: 7 additions & 24 deletions internal/handler/hex.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,30 +53,13 @@ func (h *HexHandler) Routes() http.Handler {

// handleDownload serves a package tarball, fetching and caching from upstream if needed.
func (h *HexHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
filename := r.PathValue("filename")
if filename == "" || !strings.HasSuffix(filename, ".tar") {
http.Error(w, "invalid filename", http.StatusBadRequest)
return
}

// Extract name and version from filename (e.g., "phoenix-1.7.10.tar")
name, version := h.parseTarballFilename(filename)
if name == "" || version == "" {
http.Error(w, "could not parse tarball filename", http.StatusBadRequest)
return
}

h.proxy.Logger.Info("hex download request",
"name", name, "version", version, "filename", filename)

result, err := h.proxy.GetOrFetchArtifact(r.Context(), "hex", name, version, filename)
if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err)
http.Error(w, "failed to fetch package", http.StatusBadGateway)
return
}

ServeArtifact(w, result)
h.proxy.handleFilenameDownload(w, r, filenameDownload{
ecosystem: "hex",
suffix: ".tar",
parseErr: "could not parse tarball filename",
fetchErr: "failed to fetch package",
parse: h.parseTarballFilename,
})
}

// parseTarballFilename extracts name and version from a hex tarball filename.
Expand Down
9 changes: 3 additions & 6 deletions internal/handler/julia.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,7 @@ func (h *JuliaHandler) handleRegistry(w http.ResponseWriter, r *http.Request) {
upstreamURL := h.upstreamURL + r.URL.Path
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "julia", juliaRegistryName, hash, hash+".tar.gz", upstreamURL)
if err != nil {
h.proxy.Logger.Error("failed to get registry", "error", err)
http.Error(w, "failed to fetch registry", http.StatusBadGateway)
h.proxy.serveArtifactError(w, err, "failed to fetch registry")
return
}

Expand Down Expand Up @@ -119,8 +118,7 @@ func (h *JuliaHandler) handlePackage(w http.ResponseWriter, r *http.Request) {
upstreamURL := h.upstreamURL + r.URL.Path
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "julia", name, hash, hash+".tar.gz", upstreamURL)
if err != nil {
h.proxy.Logger.Error("failed to get package", "error", err)
http.Error(w, "failed to fetch package", http.StatusBadGateway)
h.proxy.serveArtifactError(w, err, "failed to fetch package")
return
}

Expand All @@ -141,8 +139,7 @@ func (h *JuliaHandler) handleArtifact(w http.ResponseWriter, r *http.Request) {
upstreamURL := h.upstreamURL + r.URL.Path
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "julia", juliaArtifactName, hash, hash+".tar.gz", upstreamURL)
if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err)
http.Error(w, "failed to fetch artifact", http.StatusBadGateway)
h.proxy.serveArtifactError(w, err, "failed to fetch artifact")
return
}

Expand Down
7 changes: 1 addition & 6 deletions internal/handler/maven.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,7 @@ func (h *MavenHandler) handleDownload(w http.ResponseWriter, r *http.Request, ur
}
}
if err != nil {
if errors.Is(err, ErrUpstreamNotFound) {
http.Error(w, "not found", http.StatusNotFound)
return
}
h.proxy.Logger.Error("failed to get artifact", "error", err)
http.Error(w, "failed to fetch artifact", http.StatusBadGateway)
h.proxy.serveArtifactError(w, err, "failed to fetch artifact")
return
}

Expand Down
Loading