2026-10-22 12:13:20 +02:00
package handler
import (
"crypto/sha256"
"encoding/hex"
2026-10-23 09:50:22 +02:00
"errors"
"fmt"
2026-10-22 12:13:20 +02:00
"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
2026-10-23 09:50:22 +02:00
// 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.
2026-10-22 12:13:20 +02:00
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 )
2026-10-23 09:50:22 +02:00
case isFreeBSDPackageFile ( rest ) :
h . handleUncachedPackage ( w , r , upstreamURL , rest )
2026-10-22 12:13:20 +02:00
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 ]
}
2026-10-23 09:50:22 +02:00
// 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 )
}
2026-10-22 12:13:20 +02:00
// 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 )
}