mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-08-23 12:24:57 -04:00
146 lines
5.3 KiB
Go
146 lines
5.3 KiB
Go
package server
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/git-pkgs/proxy/internal/database"
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// maxPackagePathLen bounds the wildcard portion of package routes (name plus
|
|
// version and any suffix). npm caps names at 214 and Maven coordinates can be
|
|
// longer, so 512 leaves room without admitting pathological inputs.
|
|
const maxPackagePathLen = 512
|
|
|
|
// packagePathSegments validates the wildcard portion of a package route and
|
|
// splits it into decoded path segments.
|
|
func packagePathSegments(r *http.Request) ([]string, error) {
|
|
wildcard := chi.URLParam(r, "*")
|
|
encoded := wildcardIsEncoded(r)
|
|
if err := validatePackagePath(wildcard, encoded); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return splitWildcardPath(wildcard, encoded), nil
|
|
}
|
|
|
|
// wildcardIsEncoded reports whether the chi wildcard for this request is still
|
|
// percent-encoded.
|
|
//
|
|
// chi routes on r.URL.RawPath when it is set and on r.URL.Path otherwise, and
|
|
// net/url only sets RawPath when the request's escaping differs from the
|
|
// canonical encoding of the decoded path. A version such as "release%2F1" is
|
|
// therefore routed raw, while "1.0%252B" (a version whose text contains a
|
|
// literal "%2B") encodes canonically and arrives already decoded once. The
|
|
// distinction decides whether the segments still need decoding: decoding the
|
|
// second case again would turn it into "1.0+" and resolve a different version.
|
|
func wildcardIsEncoded(r *http.Request) bool {
|
|
return r.URL.RawPath != ""
|
|
}
|
|
|
|
// validatePackagePath rejects wildcard package paths that cannot be valid in
|
|
// any supported ecosystem. It is a coarse filter applied before database or
|
|
// enrichment lookups; ecosystem-specific name rules are layered on top.
|
|
//
|
|
// encoded has the meaning described on wildcardIsEncoded.
|
|
func validatePackagePath(path string, encoded bool) error {
|
|
if path == "" {
|
|
return fmt.Errorf("package name required")
|
|
}
|
|
if len(path) > maxPackagePathLen {
|
|
return fmt.Errorf("package path exceeds %d bytes", maxPackagePathLen)
|
|
}
|
|
// Validate the decoded segments: the handlers work with decoded values, so
|
|
// an escape such as "%00" or "%2E%2E" must not slip past these checks.
|
|
for _, seg := range splitWildcardPath(path, encoded) {
|
|
// Each segment is checked both as the handlers see it and decoded once
|
|
// more: a segment can reach a handler with escapes intact, and the
|
|
// upstream registry is then the one that decodes them.
|
|
for _, value := range []string{seg, decodePathSegment(seg)} {
|
|
// A decoded segment can itself contain slashes (from "%2F"), and
|
|
// the segments are later rejoined into a package name that
|
|
// registries interpolate straight into an upstream URL. Check every
|
|
// path element, not just the segment as a whole, or
|
|
// "a%2F..%2F..%2Fb" traverses.
|
|
for _, elem := range strings.Split(value, "/") {
|
|
if elem == ".." {
|
|
return fmt.Errorf("package path contains parent directory segment")
|
|
}
|
|
}
|
|
for _, r := range value {
|
|
if r == 0 {
|
|
return fmt.Errorf("package path contains null byte")
|
|
}
|
|
if unicode.IsControl(r) {
|
|
return fmt.Errorf("package path contains control character %#U", r)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// resolvePackageName determines the package name from a wildcard path by
|
|
// checking the database. This handles namespaced packages like Composer's
|
|
// vendor/name format where the package name contains a slash.
|
|
//
|
|
// It tries the full path as a package name first. If not found, it splits
|
|
// off the last segment as a non-name suffix (version, action, etc.) and
|
|
// tries again, working backwards until a match is found or segments run out.
|
|
//
|
|
// Returns the package name and the remaining path segments after the name.
|
|
// If no package is found, returns empty name and the original segments.
|
|
func resolvePackageName(db *database.DB, ecosystem string, segments []string) (name string, rest []string) {
|
|
// Try increasingly longer prefixes as the package name.
|
|
// Start with the longest possible name (all segments) and work down.
|
|
for i := len(segments); i >= 1; i-- {
|
|
candidate := strings.Join(segments[:i], "/")
|
|
pkg, err := db.GetPackageByEcosystemName(ecosystem, candidate)
|
|
if err == nil && pkg != nil {
|
|
return candidate, segments[i:]
|
|
}
|
|
}
|
|
|
|
return "", segments
|
|
}
|
|
|
|
// splitWildcardPath splits a chi wildcard path value into segments,
|
|
// trimming any leading/trailing slashes.
|
|
//
|
|
// When encoded is set the value is still percent-encoded (see
|
|
// wildcardIsEncoded), so each segment is decoded after splitting. Splitting
|
|
// first keeps an encoded "%2F" inside a name from being mistaken for a
|
|
// separator. Decoding matters for versions such as "1.0%2Bbuild1", which must
|
|
// reach the handlers as "1.0+build1" so that rebuilding the PURL yields the
|
|
// value that was stored rather than a double-encoded one.
|
|
func splitWildcardPath(path string, encoded bool) []string {
|
|
path = strings.Trim(path, "/")
|
|
if path == "" {
|
|
return nil
|
|
}
|
|
segments := strings.Split(path, "/")
|
|
if !encoded {
|
|
return segments
|
|
}
|
|
for i, seg := range segments {
|
|
segments[i] = decodePathSegment(seg)
|
|
}
|
|
return segments
|
|
}
|
|
|
|
// decodePathSegment percent-decodes a single URL path segment, returning it
|
|
// unchanged if it is not valid percent-encoding.
|
|
func decodePathSegment(seg string) string {
|
|
if !strings.Contains(seg, "%") {
|
|
return seg
|
|
}
|
|
decoded, err := url.PathUnescape(seg)
|
|
if err != nil {
|
|
return seg
|
|
}
|
|
return decoded
|
|
}
|