mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-10 08:09:17 -05:00
Package files outside Hashed/, such as All/{name}-{version}.pkg from
repositories built without pkg repo --hash and the bootstrap
Latest/pkg.pkg, were relayed without any checks. They are still passed
through uncached, since their names can be reused for rebuilds, but a
versioned name is now checked against the denylist first, and while a
scanner covers freebsd they are refused, because only cached artifacts
can be scanned.
256 lines
10 KiB
Go
256 lines
10 KiB
Go
package handler
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/git-pkgs/proxy/internal/retention"
|
|
"github.com/git-pkgs/purl"
|
|
)
|
|
|
|
const (
|
|
freebsdEcosystem = "freebsd"
|
|
// defaultFreeBSDRepositoryName is the repository name used when no
|
|
// upstream.freebsd repositories are configured.
|
|
defaultFreeBSDRepositoryName = "freebsd"
|
|
// defaultFreeBSDUpstream is the official FreeBSD package mirror.
|
|
defaultFreeBSDUpstream = "https://pkg.freebsd.org"
|
|
freebsdNamespace = "freebsd"
|
|
freebsdMatchCount = 3 // full match + name + version
|
|
freebsdHashedDir = "Hashed"
|
|
freebsdPackageMedia = "application/octet-stream"
|
|
)
|
|
|
|
// Packages are stored as pkg:bsd/freebsd/{name}, whichever configured
|
|
// repository they came from.
|
|
func init() {
|
|
retention.Default.Register(retention.Spec{Key: freebsdEcosystem, CanonicalPackage: freebsdRetentionKey})
|
|
}
|
|
|
|
// freebsdRetentionKey maps pkg:bsd/freebsd/{name} to the stored package PURL.
|
|
// Another namespace, or a name with a slash, can never match a stored package
|
|
// and is refused.
|
|
func freebsdRetentionKey(p *purl.PURL) (string, bool) {
|
|
if p == nil || p.Type != "bsd" || p.Namespace != freebsdNamespace || strings.Contains(p.Name, "/") {
|
|
return "", false
|
|
}
|
|
canonical := purl.MakePURLString(freebsdEcosystem, p.Name, "")
|
|
return canonical, canonical != ""
|
|
}
|
|
|
|
// FreeBSDHandler serves FreeBSD pkg repositories, ports and pkgbase alike.
|
|
// Each configured upstream is mounted at /freebsd/{repository}/ and the rest
|
|
// of the path mirrors the upstream layout ({ABI}/{repo}/{file}).
|
|
//
|
|
// Repository catalogues (meta.conf, packagesite.pkg, data.pkg) carry their
|
|
// signature inside the archive, so they are served byte for byte through the
|
|
// metadata cache. Packages under Hashed/ have the content hash in their name
|
|
// and never change, so they go to the artifact cache. Other package files,
|
|
// unhashed ones and the bootstrap package under Latest/, can be rebuilt under
|
|
// the same name and are passed through after the denylist; while a scanner
|
|
// covers freebsd they are refused, since only cached artifacts are scanned.
|
|
// Everything else, such as signatures and keys, is passed through.
|
|
type FreeBSDHandler struct {
|
|
proxy *Proxy
|
|
proxyURL string
|
|
repositories map[string]string
|
|
}
|
|
|
|
// NewFreeBSDHandler creates a FreeBSD pkg repository handler. Without
|
|
// repositories it serves the official mirror as "freebsd".
|
|
func NewFreeBSDHandler(proxy *Proxy, proxyURL string, repositories map[string]string) *FreeBSDHandler {
|
|
h := &FreeBSDHandler{
|
|
proxy: proxy,
|
|
proxyURL: strings.TrimSuffix(proxyURL, "/"),
|
|
repositories: make(map[string]string, len(repositories)),
|
|
}
|
|
for name, repositoryURL := range repositories {
|
|
h.repositories[name] = strings.TrimSuffix(repositoryURL, "/")
|
|
}
|
|
if len(h.repositories) == 0 {
|
|
h.repositories[defaultFreeBSDRepositoryName] = defaultFreeBSDUpstream
|
|
}
|
|
return h
|
|
}
|
|
|
|
// Routes returns the HTTP handler for pkg requests.
|
|
// Mount this at /freebsd on your router.
|
|
func (h *FreeBSDHandler) 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, "/")
|
|
|
|
// A decoded '?' or '#' would cut the upstream path short and cache one
|
|
// file under another file's name.
|
|
if containsPathTraversal(path) || strings.ContainsAny(path, "?#") {
|
|
http.Error(w, "invalid path", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
repository, rest, ok := strings.Cut(path, "/")
|
|
upstreamURL, found := h.repositories[repository]
|
|
if !ok || rest == "" || !found {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
switch {
|
|
case isFreeBSDCatalogue(rest):
|
|
h.handleMetadata(w, r, repository, upstreamURL, rest)
|
|
case isFreeBSDHashedPackage(rest):
|
|
h.handlePackageDownload(w, r, repository, upstreamURL, rest)
|
|
case isFreeBSDPackageFile(rest):
|
|
h.handleUncachedPackage(w, r, upstreamURL, rest)
|
|
default:
|
|
h.proxyFile(w, r, upstreamURL, rest)
|
|
}
|
|
})
|
|
}
|
|
|
|
// freebsdCataloguePattern matches the files pkg reads to update a repository:
|
|
// meta and meta.conf, and the packagesite and data catalogues in the current
|
|
// (.pkg) and older (.txz, .tzst) packing formats.
|
|
var freebsdCataloguePattern = regexp.MustCompile(`^(?:meta(?:\.conf|\.txz|\.tzst|\.pkg)?|(?:packagesite|data)\.(?:pkg|txz|tzst))$`)
|
|
|
|
func freebsdBase(path string) string {
|
|
return path[strings.LastIndex(path, "/")+1:]
|
|
}
|
|
|
|
// isFreeBSDCatalogue reports whether path names a repository catalogue. The
|
|
// depth isn't checked, so repositories built with poudriere and served under
|
|
// another layout work too; package files are always {name}-{version}.pkg and
|
|
// can't take these names.
|
|
func isFreeBSDCatalogue(path string) bool {
|
|
return freebsdCataloguePattern.MatchString(freebsdBase(path))
|
|
}
|
|
|
|
// freebsdHashedPattern matches {name}-{version}~{hash}.pkg. Port versions
|
|
// never contain '-', so the version is what follows the last one, e.g.
|
|
// zsh-navigation-tools-2.2.7~2$imxi7k7w.pkg or
|
|
// FreeBSD-syslogd-man-14.snap20261019150821~2$k6xtent4.pkg.
|
|
var freebsdHashedPattern = regexp.MustCompile(`^(.+)-([^-~/]+)~[^~/]+\.pkg$`)
|
|
|
|
// isFreeBSDHashedPackage reports whether path names a package in a Hashed/
|
|
// directory: All/Hashed/ for ports, Hashed/ for pkgbase.
|
|
func isFreeBSDHashedPackage(path string) bool {
|
|
dir := strings.TrimSuffix(path, "/"+freebsdBase(path))
|
|
return freebsdBase(dir) == freebsdHashedDir && freebsdHashedPattern.MatchString(freebsdBase(path))
|
|
}
|
|
|
|
// parseFreeBSDPackage returns the name and version of a hashed package file.
|
|
func parseFreeBSDPackage(filename string) (name, version string) {
|
|
matches := freebsdHashedPattern.FindStringSubmatch(filename)
|
|
if len(matches) != freebsdMatchCount {
|
|
return "", ""
|
|
}
|
|
return matches[1], matches[2]
|
|
}
|
|
|
|
// freebsdUnhashedPattern matches {name}-{version} package files in the current
|
|
// (.pkg) and older (.txz, .tzst) packing formats, split at the last '-' like
|
|
// freebsdHashedPattern.
|
|
var freebsdUnhashedPattern = regexp.MustCompile(`^(.+)-([^-~/]+)\.(?:pkg|txz|tzst)$`)
|
|
|
|
// freebsdPackageFilePattern matches every package file name, including
|
|
// Latest/pkg.pkg, which has no version. Catalogues match it too and must be
|
|
// routed first.
|
|
var freebsdPackageFilePattern = regexp.MustCompile(`\.(?:pkg|txz|tzst)$`)
|
|
|
|
// isFreeBSDPackageFile reports whether path names a package file.
|
|
func isFreeBSDPackageFile(path string) bool {
|
|
return freebsdPackageFilePattern.MatchString(freebsdBase(path))
|
|
}
|
|
|
|
// parseFreeBSDPackageFile returns the name and version of any package file,
|
|
// hashed or not, or empty strings for a file without a version such as
|
|
// Latest/pkg.pkg.
|
|
func parseFreeBSDPackageFile(filename string) (name, version string) {
|
|
if name, version = parseFreeBSDPackage(filename); name != "" {
|
|
return name, version
|
|
}
|
|
matches := freebsdUnhashedPattern.FindStringSubmatch(filename)
|
|
if len(matches) != freebsdMatchCount {
|
|
return "", ""
|
|
}
|
|
return matches[1], matches[2]
|
|
}
|
|
|
|
// errFreeBSDUnscannable refuses package files that can't be cached while
|
|
// scanning covers freebsd.
|
|
var errFreeBSDUnscannable = errors.New("package file can't be scanned: only packages under Hashed/ are cached and scanned; build the repository with pkg repo --hash")
|
|
|
|
// handleUncachedPackage passes a package file outside Hashed/ through. Its
|
|
// name may be reused for a rebuild with other bytes, which pkg would then
|
|
// reject against the catalogue checksum, so it is never cached. The denylist
|
|
// applies when the name carries a version. Scanners only see cached
|
|
// artifacts, so the file is refused while one covers freebsd.
|
|
func (h *FreeBSDHandler) handleUncachedPackage(w http.ResponseWriter, r *http.Request, upstreamURL, path string) {
|
|
name, version := parseFreeBSDPackageFile(freebsdBase(path))
|
|
if name != "" && h.proxy.versionDenied(freebsdEcosystem, name, version) {
|
|
h.proxy.serveArtifactError(w, fmt.Errorf("%w: %s", ErrVersionDenied, canonicalVersionPURL(freebsdEcosystem, name, version)), "")
|
|
return
|
|
}
|
|
if h.proxy.Scanners.Covers(freebsdEcosystem) {
|
|
h.proxy.Logger.Warn("refusing unscannable freebsd package", "path", path)
|
|
JSONError(w, http.StatusForbidden, errFreeBSDUnscannable.Error())
|
|
return
|
|
}
|
|
h.proxyFile(w, r, upstreamURL, path)
|
|
}
|
|
|
|
// handlePackageDownload caches hashed packages. The cache key holds the
|
|
// repository and full path, since every ABI and repository has its own
|
|
// builds.
|
|
func (h *FreeBSDHandler) handlePackageDownload(w http.ResponseWriter, r *http.Request, repository, upstreamURL, path string) {
|
|
name, version := parseFreeBSDPackage(freebsdBase(path))
|
|
if name == "" {
|
|
h.proxyFile(w, r, upstreamURL, path)
|
|
return
|
|
}
|
|
|
|
downloadURL := upstreamURL + "/" + path
|
|
cacheFilename := repository + "/" + path
|
|
|
|
h.proxy.Logger.Info("freebsd package download",
|
|
"repository", repository, "name", name, "version", version)
|
|
|
|
result, err := h.proxy.GetOrFetchArtifactFromURL(
|
|
r.Context(), freebsdEcosystem, name, version, cacheFilename, downloadURL)
|
|
if err != nil {
|
|
h.proxy.serveArtifactError(w, err, "failed to fetch package")
|
|
return
|
|
}
|
|
|
|
if result.Artifact.MediaType == "" {
|
|
result.Artifact.MediaType = freebsdPackageMedia
|
|
}
|
|
ServeArtifactRequest(w, r, result)
|
|
}
|
|
|
|
// handleMetadata serves catalogues unchanged through the metadata cache,
|
|
// which keeps their embedded signatures valid.
|
|
func (h *FreeBSDHandler) handleMetadata(w http.ResponseWriter, r *http.Request, repository, upstreamURL, path string) {
|
|
h.proxy.ProxyCached(w, r, upstreamURL+"/"+path, freebsdEcosystem,
|
|
h.metadataCacheKey(repository, upstreamURL, path), "*/*")
|
|
}
|
|
|
|
// metadataCacheKey hashes repository, upstream URL and path, as APKHandler does.
|
|
func (h *FreeBSDHandler) metadataCacheKey(repository, upstreamURL, path string) string {
|
|
identity := repository + "\x00" + upstreamURL + "\x00" + path
|
|
digest := sha256.Sum256([]byte(identity))
|
|
return hex.EncodeToString(digest[:])
|
|
}
|
|
|
|
// proxyFile passes other files through uncached.
|
|
func (h *FreeBSDHandler) proxyFile(w http.ResponseWriter, r *http.Request, upstreamURL, path string) {
|
|
h.proxy.ProxyFile(w, r, upstreamURL+"/"+path)
|
|
}
|