Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-10 08:09:17 -05:00
pkg-proxy/internal/handler/rpm.go

158 lines
5.1 KiB
Go
Raw Permalink Normal View History

package handler
import (
"fmt"
"net/http"
"regexp"
"strings"
"github.com/git-pkgs/proxy/internal/retention"
"github.com/git-pkgs/purl"
)
const (
// Default upstream for Fedora packages
defaultRPMUpstream = "https://dl.fedoraproject.org/pub/fedora/linux"
rpmMatchCount = 5 // full match + name + version + release + arch
)
// Packages are stored as pkg:rpm/{name}, without a vendor and with the name's
// case kept. Package rules may name a vendor the way PURLs usually do,
// pkg:rpm/fedora/curl; it is dropped.
func init() {
retention.Default.Register(retention.Spec{Key: "rpm", CanonicalPackage: rpmRetentionKey})
}
// rpmRetentionKey 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 rpmRetentionKey(p *purl.PURL) (string, bool) {
if p == nil || p.Type != "rpm" || strings.Contains(p.Namespace, "/") || strings.Contains(p.Name, "/") {
return "", false
}
canonical := purl.MakePURLString("rpm", p.Name, "")
return canonical, canonical != ""
}
// RPMHandler handles RPM/Yum repository protocol requests.
// It proxies requests to upstream RPM repositories and caches .rpm packages.
type RPMHandler struct {
proxy *Proxy
upstreamURL string
proxyURL string
}
// NewRPMHandler creates a new RPM/Yum protocol handler.
func NewRPMHandler(proxy *Proxy, proxyURL string) *RPMHandler {
return &RPMHandler{
proxy: proxy,
upstreamURL: defaultRPMUpstream,
proxyURL: strings.TrimSuffix(proxyURL, "/"),
}
}
// NewRPMHandlerWithUpstream creates an RPM handler with a custom upstream.
func NewRPMHandlerWithUpstream(proxy *Proxy, proxyURL, upstreamURL string) *RPMHandler {
h := NewRPMHandler(proxy, proxyURL)
h.upstreamURL = configuredUpstreamURL(upstreamURL, defaultRPMUpstream)
return h
}
// Routes returns the HTTP handler for RPM requests.
// Mount this at /rpm on your router.
func (h *RPMHandler) 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, "/")
if containsPathTraversal(path) {
http.Error(w, "invalid path", http.StatusBadRequest)
return
}
// Route based on path type
switch {
case strings.HasSuffix(path, ".rpm"):
// Package downloads - cache these
h.handlePackageDownload(w, r, path)
case strings.Contains(path, "/repodata/"):
// Repository metadata - proxy without caching (changes frequently)
h.handleMetadata(w, r, path)
default:
// Other files - proxy directly
h.proxyFile(w, r, path)
}
})
}
// handlePackageDownload fetches and caches .rpm packages.
// Path format varies by repo structure, e.g.:
// - releases/39/Everything/x86_64/os/Packages/n/nginx-1.24.0-1.fc39.x86_64.rpm
// - updates/39/Everything/x86_64/Packages/n/nginx-1.24.0-2.fc39.x86_64.rpm
func (h *RPMHandler) handlePackageDownload(w http.ResponseWriter, r *http.Request, path string) {
// Parse the path to extract package info
name, version, arch := h.parseRPMPath(path)
if name == "" {
// Can't parse, just proxy directly
h.proxyFile(w, r, path)
return
}
filename := path[strings.LastIndex(path, "/")+1:]
downloadURL := fmt.Sprintf("%s/%s", h.upstreamURL, path)
h.proxy.Logger.Info("rpm package download",
"name", name, "version", version, "arch", arch, "filename", filename)
result, err := h.proxy.GetOrFetchArtifactFromURL(
r.Context(), "rpm", name, version, filename, downloadURL)
if err != nil {
h.proxy.serveArtifactError(w, err, "failed to fetch package")
return
}
w.Header().Set(headerContentType, "application/x-rpm")
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)
}
// handleMetadata proxies repository metadata files (repomd.xml, primary.xml.gz, etc.).
// These change frequently so we don't cache them.
func (h *RPMHandler) handleMetadata(w http.ResponseWriter, r *http.Request, path string) {
cacheKey := strings.ReplaceAll(path, "/", "_")
h.proxy.ProxyCached(w, r, fmt.Sprintf("%s/%s", h.upstreamURL, path), "rpm", cacheKey, "*/*")
}
// proxyFile proxies any file directly without caching.
func (h *RPMHandler) proxyFile(w http.ResponseWriter, r *http.Request, path string) {
h.proxy.ProxyFile(w, r, fmt.Sprintf("%s/%s", h.upstreamURL, path))
}
// rpmPackagePattern matches .rpm filenames to extract name, version, release, and arch.
// Format: {name}-{version}-{release}.{arch}.rpm
// Examples:
// - nginx-1.24.0-1.fc39.x86_64.rpm
// - kernel-core-6.5.5-200.fc38.x86_64.rpm
var rpmPackagePattern = regexp.MustCompile(`^(.+)-([^-]+)-([^-]+)\.([^.]+)\.rpm$`)
// parseRPMPath extracts package info from a path containing an RPM filename.
func (h *RPMHandler) parseRPMPath(path string) (name, version, arch string) {
// Get the filename
idx := strings.LastIndex(path, "/")
filename := path
if idx >= 0 {
filename = path[idx+1:]
}
// Parse the filename
matches := rpmPackagePattern.FindStringSubmatch(filename)
if len(matches) != rpmMatchCount {
return "", "", ""
}
// name, version-release, arch
return matches[1], matches[2] + "-" + matches[3], matches[4]
}