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/freebsd.go
Simon Chrzanowski d9d3bd2ff1 Check unhashed FreeBSD packages against the denylist
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.
2026-10-23 09:57:49 +02:00

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)
}