2026-01-29 19:35:15 +00:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/hex"
|
2026-01-29 19:35:15 +00:00
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
|
|
|
|
"regexp"
|
2026-09-18 16:48:18 -07:00
|
|
|
"sort"
|
2026-01-29 19:35:15 +00:00
|
|
|
"strings"
|
2026-10-11 01:12:41 +02:00
|
|
|
|
|
|
|
|
"github.com/git-pkgs/proxy/internal/retention"
|
|
|
|
|
"github.com/git-pkgs/purl"
|
2026-01-29 19:35:15 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
debianUpstream = "http://deb.debian.org/debian"
|
2026-03-18 10:59:29 +00:00
|
|
|
debMatchCount = 4 // full match + name + version + arch
|
2026-01-29 19:35:15 +00:00
|
|
|
)
|
|
|
|
|
|
2026-10-11 01:12:41 +02:00
|
|
|
// Packages are stored as pkg:deb/{name}, without a vendor and whichever
|
|
|
|
|
// configured archive they came from. Package rules may name a vendor the way
|
|
|
|
|
// PURLs usually do, pkg:deb/debian/curl or pkg:deb/ubuntu/curl; it is dropped,
|
|
|
|
|
// so the rule covers the package from every archive.
|
|
|
|
|
func init() {
|
|
|
|
|
retention.Default.Register(retention.Spec{Key: "deb", CanonicalPackage: debRetentionKey})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// debRetentionKey maps a package rule to the stored package PURL. A name with
|
|
|
|
|
// a slash, or a namespace with more than one segment, can never match a stored
|
|
|
|
|
// package and is refused.
|
|
|
|
|
func debRetentionKey(p *purl.PURL) (string, bool) {
|
|
|
|
|
if p == nil || p.Type != "deb" || strings.Contains(p.Namespace, "/") || strings.Contains(p.Name, "/") {
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
canonical := purl.MakePURLString("deb", p.Name, "")
|
|
|
|
|
return canonical, canonical != ""
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-29 19:35:15 +00:00
|
|
|
// DebianHandler handles APT/Debian repository protocol requests.
|
|
|
|
|
// It proxies requests to upstream Debian/Ubuntu repositories and caches .deb packages.
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
//
|
2026-09-16 10:15:59 -07:00
|
|
|
// The main archive is served at /debian/. Additional archives are mounted at
|
|
|
|
|
// /debian/{repository}/ and the remaining path mirrors the upstream layout.
|
2026-01-29 19:35:15 +00:00
|
|
|
type DebianHandler struct {
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
proxy *Proxy
|
|
|
|
|
upstreamURL string
|
|
|
|
|
proxyURL string
|
|
|
|
|
repositories map[string]string
|
2026-01-29 19:35:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewDebianHandler creates a new Debian/APT protocol handler.
|
2026-09-16 10:15:59 -07:00
|
|
|
// When repositories is empty, only the main archive is reachable.
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
func NewDebianHandler(
|
|
|
|
|
proxy *Proxy,
|
|
|
|
|
proxyURL string,
|
|
|
|
|
upstreamURL string,
|
|
|
|
|
repositories map[string]string,
|
|
|
|
|
) *DebianHandler {
|
2026-08-05 18:48:00 +02:00
|
|
|
if upstreamURL == "" {
|
|
|
|
|
upstreamURL = debianUpstream
|
|
|
|
|
}
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
h := &DebianHandler{
|
|
|
|
|
proxy: proxy,
|
|
|
|
|
upstreamURL: strings.TrimSuffix(upstreamURL, "/"),
|
|
|
|
|
proxyURL: strings.TrimSuffix(proxyURL, "/"),
|
|
|
|
|
repositories: make(map[string]string, len(repositories)),
|
2026-01-29 19:35:15 +00:00
|
|
|
}
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
for name, repositoryURL := range repositories {
|
|
|
|
|
h.repositories[name] = strings.TrimSuffix(repositoryURL, "/")
|
|
|
|
|
}
|
|
|
|
|
return h
|
2026-01-29 19:35:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Routes returns the HTTP handler for Debian requests.
|
|
|
|
|
// Mount this at /debian on your router.
|
|
|
|
|
func (h *DebianHandler) Routes() http.Handler {
|
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
|
|
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
path := strings.TrimPrefix(r.URL.Path, "/")
|
|
|
|
|
|
2026-03-12 12:05:52 +00:00
|
|
|
if containsPathTraversal(path) {
|
|
|
|
|
http.Error(w, "invalid path", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-16 10:15:59 -07:00
|
|
|
// Unlike the APK handler, an unconfigured first segment is not a 404:
|
|
|
|
|
// the main archive is unnamed and serves paths of its own at the root
|
|
|
|
|
// (README, indices/, project/), so an unknown name stays a
|
|
|
|
|
// main-archive path.
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
upstreamURL, repository, rest := h.upstreamURL, "", path
|
|
|
|
|
if !strings.HasPrefix(path, "pool/") && !strings.HasPrefix(path, "dists/") {
|
|
|
|
|
if name, tail, ok := strings.Cut(path, "/"); ok && tail != "" {
|
|
|
|
|
if named, found := h.repositories[name]; found {
|
|
|
|
|
upstreamURL, repository, rest = named, name, tail
|
2026-09-18 16:48:18 -07:00
|
|
|
} else if strings.HasPrefix(tail, "dists/") || strings.HasPrefix(tail, "pool/") {
|
|
|
|
|
// The main archive has no {name}/dists/ or {name}/pool/ of
|
|
|
|
|
// its own, so this is a misspelled repository rather than a
|
|
|
|
|
// main-archive path. Answering here keeps a typo from
|
|
|
|
|
// surfacing as the upstream's opaque HTML 404.
|
|
|
|
|
h.repositoryNotFound(w, name)
|
|
|
|
|
return
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-29 19:35:15 +00:00
|
|
|
// Route based on path type
|
|
|
|
|
switch {
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
case strings.HasPrefix(rest, "pool/"):
|
2026-01-29 19:35:15 +00:00
|
|
|
// Package downloads - cache these
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
h.handlePackageDownload(w, r, repository, upstreamURL, rest)
|
|
|
|
|
case strings.HasPrefix(rest, "dists/"):
|
|
|
|
|
// Repository metadata - served through the metadata cache
|
|
|
|
|
h.handleMetadata(w, r, repository, upstreamURL, rest)
|
2026-01-29 19:35:15 +00:00
|
|
|
default:
|
|
|
|
|
// Other files (like README, etc.) - proxy directly
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
h.proxyFile(w, r, upstreamURL, rest)
|
2026-01-29 19:35:15 +00:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-18 16:48:18 -07:00
|
|
|
// repositoryNotFound answers a request addressed to a repository that is not
|
|
|
|
|
// configured, naming the ones that are so a misspelling is self-evident.
|
|
|
|
|
func (h *DebianHandler) repositoryNotFound(w http.ResponseWriter, name string) {
|
|
|
|
|
if len(h.repositories) == 0 {
|
|
|
|
|
http.Error(w,
|
|
|
|
|
fmt.Sprintf("unknown debian repository %q: none are configured", name),
|
|
|
|
|
http.StatusNotFound)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
configured := make([]string, 0, len(h.repositories))
|
|
|
|
|
for repository := range h.repositories {
|
|
|
|
|
configured = append(configured, repository)
|
|
|
|
|
}
|
|
|
|
|
sort.Strings(configured)
|
|
|
|
|
|
|
|
|
|
http.Error(w,
|
|
|
|
|
fmt.Sprintf("unknown debian repository %q: configured repositories are %s",
|
|
|
|
|
name, strings.Join(configured, ", ")),
|
|
|
|
|
http.StatusNotFound)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-29 19:35:15 +00:00
|
|
|
// handlePackageDownload fetches and caches .deb packages from the pool.
|
|
|
|
|
// Pool path format: pool/{component}/{prefix}/{name}/{filename}
|
|
|
|
|
// Example: pool/main/n/nginx/nginx_1.18.0-6_amd64.deb
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
func (h *DebianHandler) handlePackageDownload(
|
|
|
|
|
w http.ResponseWriter,
|
|
|
|
|
r *http.Request,
|
|
|
|
|
repository, upstreamURL, path string,
|
|
|
|
|
) {
|
2026-01-29 19:35:15 +00:00
|
|
|
// Parse the path to extract package info
|
|
|
|
|
name, version, arch := h.parsePoolPath(path)
|
|
|
|
|
if name == "" {
|
|
|
|
|
// Can't parse, just proxy directly
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
h.proxyFile(w, r, upstreamURL, path)
|
2026-01-29 19:35:15 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
filename := path[strings.LastIndex(path, "/")+1:]
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
downloadURL := fmt.Sprintf("%s/%s", upstreamURL, path)
|
|
|
|
|
|
|
|
|
|
cacheFilename := h.artifactCacheFilename(repository, filename)
|
2026-01-29 19:35:15 +00:00
|
|
|
|
|
|
|
|
h.proxy.Logger.Info("debian package download",
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
"repository", repository,
|
2026-01-29 19:35:15 +00:00
|
|
|
"name", name, "version", version, "arch", arch, "filename", filename)
|
|
|
|
|
|
|
|
|
|
result, err := h.proxy.GetOrFetchArtifactFromURL(
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
r.Context(), "deb", name, version, cacheFilename, downloadURL)
|
2026-01-29 19:35:15 +00:00
|
|
|
if err != nil {
|
2026-07-26 20:07:46 +02:00
|
|
|
h.proxy.serveArtifactError(w, err, "failed to fetch package")
|
2026-01-29 19:35:15 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-02 13:22:02 +01:00
|
|
|
w.Header().Set(headerContentType, "application/vnd.debian.binary-package")
|
Support HTTP byte-range requests for local file cached artifact downloads (#404)
* feat(storage): expose efficient seek for local files
Open file-backed artifacts with os.Open so local readers support
efficient seeking. Leave cloud storage readers unchanged.
Keep the legacy sidecar mapping test focused on fileblob's reader:
Blob.Open now reads local files directly and no longer consults the
sidecar. Continue checking sidecar cleanup and post-store reads.
* feat(handler): preserve efficient seek through integrity checks
Keep the cache result's single Reader field and preserve io.Seeker
when the underlying storage reader supports efficient seeking.
Sequential reads continue through whole-object integrity verification.
A successful seek switches subsequent reads to the source for range
responses, where full-object verification is not possible.
Test both modes and verify the source is closed once.
* feat(handler): add request-aware byte range responses
Add a request-aware artifact serving helper for bounded, open-ended,
and suffix byte ranges, including If-Range handling, 206 responses,
and 416 responses for valid unsatisfiable ranges.
Ignore malformed and multi-range requests by serving the full artifact.
Advertise byte-range support only for seekable readers, preserving HEAD,
redirect, and non-seekable behavior. Clarify that net/http recovers
ErrAbortHandler per request and keeps the server running.
* feat(handler): enable byte ranges across artifact downloads
Forward each artifact request to the shared range-aware response helper,
including cached generic release assets and OCI blobs. This lets the
handler honor ranges only when its reader supports efficient seeking,
without changing direct-storage redirect behavior.
Add warm-cache endpoint tests that assert partial response headers and
bytes without an upstream refetch. Document supported range behavior,
unsupported reader fallback, and the integrity limitation of partial reads.
2026-10-04 09:48:41 -07:00
|
|
|
ServeArtifactRequest(w, r, result)
|
2026-01-29 19:35:15 +00:00
|
|
|
}
|
|
|
|
|
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
// handleMetadata serves repository metadata files through the metadata cache,
|
|
|
|
|
// so they keep resolving while the upstream archive is unreachable.
|
|
|
|
|
func (h *DebianHandler) handleMetadata(
|
|
|
|
|
w http.ResponseWriter,
|
|
|
|
|
r *http.Request,
|
|
|
|
|
repository, upstreamURL, path string,
|
|
|
|
|
) {
|
|
|
|
|
cacheKey := h.metadataCacheKeyFor(repository, upstreamURL, path)
|
|
|
|
|
h.proxy.ProxyCached(w, r, fmt.Sprintf("%s/%s", upstreamURL, path), "debian", cacheKey, "*/*")
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-16 10:15:59 -07:00
|
|
|
// artifactCacheFilename scopes a package by repository, since the same
|
|
|
|
|
// filename can hold different bytes in different archives. The main archive
|
|
|
|
|
// keeps the unscoped filename its existing cache entries were stored under.
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
func (h *DebianHandler) artifactCacheFilename(repository, filename string) string {
|
|
|
|
|
if repository == "" {
|
|
|
|
|
return filename
|
|
|
|
|
}
|
|
|
|
|
return repository + "/" + filename
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-16 10:15:59 -07:00
|
|
|
// metadataCacheKeyFor returns the metadata cache key for a request. The main
|
|
|
|
|
// archive keeps its separator-based key so existing cache entries stay valid;
|
|
|
|
|
// a named repository hashes its identity as APKHandler.metadataCacheKey does.
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
func (h *DebianHandler) metadataCacheKeyFor(repository, upstreamURL, path string) string {
|
|
|
|
|
if repository == "" {
|
|
|
|
|
return strings.ReplaceAll(path, "/", "_")
|
|
|
|
|
}
|
|
|
|
|
identity := repository + "\x00" + upstreamURL + "\x00" + path
|
|
|
|
|
digest := sha256.Sum256([]byte(identity))
|
|
|
|
|
return hex.EncodeToString(digest[:])
|
2026-01-29 19:35:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// proxyFile proxies any file directly without caching.
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
func (h *DebianHandler) proxyFile(w http.ResponseWriter, r *http.Request, upstreamURL, path string) {
|
|
|
|
|
h.proxy.ProxyFile(w, r, fmt.Sprintf("%s/%s", upstreamURL, path))
|
2026-01-29 19:35:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// debPackagePattern matches .deb filenames to extract name, version, and arch.
|
|
|
|
|
// Format: {name}_{version}_{arch}.deb
|
|
|
|
|
var debPackagePattern = regexp.MustCompile(`^(.+)_([^_]+)_([^_]+)\.deb$`)
|
|
|
|
|
|
|
|
|
|
// parsePoolPath extracts package info from a pool path.
|
|
|
|
|
// Example: pool/main/n/nginx/nginx_1.18.0-6_amd64.deb
|
|
|
|
|
func (h *DebianHandler) parsePoolPath(path string) (name, version, arch string) {
|
|
|
|
|
// Get the filename
|
|
|
|
|
idx := strings.LastIndex(path, "/")
|
|
|
|
|
if idx < 0 {
|
|
|
|
|
return "", "", ""
|
|
|
|
|
}
|
|
|
|
|
filename := path[idx+1:]
|
|
|
|
|
|
|
|
|
|
// Parse the filename
|
|
|
|
|
matches := debPackagePattern.FindStringSubmatch(filename)
|
2026-03-18 10:59:29 +00:00
|
|
|
if len(matches) != debMatchCount {
|
2026-01-29 19:35:15 +00:00
|
|
|
return "", "", ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return matches[1], matches[2], matches[3]
|
|
|
|
|
}
|