Harden nuget per-request index rewrite (#149)
NuGet index rewrites embed the client Host and X-Forwarded-Proto into per-request JSON, so a shared cache could store one host's URLs, a forged header could inject content, HEAD skipped the rewrite, and immutable flat-container blobs were re-validated as mutable. - set `no-store`, `Vary: Host, X-Forwarded-Proto` and the rewritten Content-Length on GET and HEAD - share `v1.Scheme()` (first X-Forwarded-Proto value, `http`/`https` only) across nuget, cargo, virtual redirects and the terraform registry - reject non host[:port] Host headers and JSON-escape the injected base URL - classify `.snupkg`, `readme` and `icon` flat-container blobs as immutable requires unkin/artifactapi#148 merged first Reviewed-on: #149 Co-authored-by: unkin-agent <unkin-agent@unkin.net> Co-committed-by: unkin-agent <unkin-agent@unkin.net>
This commit was merged in pull request #149.
This commit is contained in:
@@ -4,4 +4,4 @@ Shared classification, TTL and eviction rules: [caching](../common/caching.md).
|
||||
|
||||
| Type | Mutable (auto-detected) | Immutable (auto-detected) |
|
||||
|---|---|---|
|
||||
| `nuget` | `index.json` (service index), registration and flat container indexes | `.nupkg`, `.nuspec` |
|
||||
| `nuget` | `index.json` (service index), registration and flat container indexes | `.nupkg`, `.snupkg`, `.nuspec`, `readme`, `icon` |
|
||||
|
||||
+17
-3
@@ -40,8 +40,10 @@ func assertHeadMatchesGet(t *testing.T, url string) {
|
||||
if len(hb) != 0 {
|
||||
t.Errorf("HEAD returned a %d-byte body", len(hb))
|
||||
}
|
||||
if head.Header.Get("Content-Type") != get.Header.Get("Content-Type") {
|
||||
t.Errorf("HEAD Content-Type %q, GET %q", head.Header.Get("Content-Type"), get.Header.Get("Content-Type"))
|
||||
for _, h := range []string{"Content-Type", "Cache-Control", "Vary"} {
|
||||
if head.Header.Get(h) != get.Header.Get(h) {
|
||||
t.Errorf("HEAD %s %q, GET %q", h, head.Header.Get(h), get.Header.Get(h))
|
||||
}
|
||||
}
|
||||
if head.ContentLength != int64(len(body)) {
|
||||
t.Errorf("HEAD Content-Length %d, GET body %d bytes", head.ContentLength, len(body))
|
||||
@@ -61,7 +63,19 @@ func TestProxyHeadNuGetRewritten(t *testing.T) {
|
||||
createRemote(t, mustFormat(`{"name": "head-nuget", "package_type": "nuget", "base_url": %q}`, upstream.URL+"/v3/index.json"))
|
||||
defer deleteRemote(t, "head-nuget")
|
||||
|
||||
assertHeadMatchesGet(t, apiURL("/api/v1/remote/head-nuget/index.json"))
|
||||
url := apiURL("/api/v1/remote/head-nuget/index.json")
|
||||
assertHeadMatchesGet(t, url)
|
||||
|
||||
req, _ := http.NewRequest(http.MethodHead, url, nil)
|
||||
req.Host = "evil.example:"
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("HEAD with invalid Host: got %d, want 400", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyHeadGitHubRPMRepodata(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func getForwardedHTTPS(t *testing.T, url string) *http.Response {
|
||||
t.Helper()
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
req.Header.Set("X-Forwarded-Proto", "https")
|
||||
client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("GET %s: %v", url, err)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func TestForwardedProtoCargoConfig(t *testing.T) {
|
||||
createRemote(t, `{"name": "scheme-cargo", "package_type": "cargo", "base_url": "https://index.crates.io"}`)
|
||||
defer deleteRemote(t, "scheme-cargo")
|
||||
|
||||
resp := getForwardedHTTPS(t, apiURL("/api/v1/remote/scheme-cargo/config.json"))
|
||||
defer resp.Body.Close()
|
||||
var cfg map[string]string
|
||||
if err := json.NewDecoder(resp.Body).Decode(&cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := "https://" + strings.TrimPrefix(baseURL, "http://") + "/api/v1/remote/scheme-cargo/"; !strings.HasPrefix(cfg["dl"], want) {
|
||||
t.Errorf("dl = %q, want prefix %q", cfg["dl"], want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestForwardedProtoVirtualRedirect(t *testing.T) {
|
||||
createRemote(t, `{"name": "scheme-rpm", "package_type": "rpm", "base_url": "https://rpm.example.com"}`)
|
||||
defer deleteRemote(t, "scheme-rpm")
|
||||
resp, err := http.Post(apiURL("/api/v2/virtuals"), "application/json",
|
||||
strings.NewReader(`{"name": "scheme-virt", "package_type": "rpm", "members": ["scheme-rpm"]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
t.Fatalf("create virtual: status %d", resp.StatusCode)
|
||||
}
|
||||
defer deleteRequest(t, apiURL("/api/v2/virtuals/scheme-virt"))
|
||||
|
||||
resp = getForwardedHTTPS(t, apiURL("/api/v1/virtual/scheme-virt/scheme-rpm/Packages/a.rpm"))
|
||||
resp.Body.Close()
|
||||
loc := resp.Header.Get("Location")
|
||||
if want := "https://" + strings.TrimPrefix(baseURL, "http://") + "/api/v1/remote/scheme-rpm/Packages/a.rpm"; resp.StatusCode != http.StatusFound || loc != want {
|
||||
t.Errorf("redirect = %d %q, want 302 %q", resp.StatusCode, loc, want)
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
v1 "git.unkin.net/unkin/artifactapi/internal/api/v1"
|
||||
"git.unkin.net/unkin/artifactapi/internal/database"
|
||||
tfprov "git.unkin.net/unkin/artifactapi/internal/provider/terraform"
|
||||
"git.unkin.net/unkin/artifactapi/internal/tfsign"
|
||||
@@ -290,12 +291,5 @@ func writeJSON(w http.ResponseWriter, v any) {
|
||||
}
|
||||
|
||||
func baseURL(r *http.Request) string {
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
if fwd := r.Header.Get("X-Forwarded-Proto"); fwd != "" {
|
||||
scheme = fwd
|
||||
}
|
||||
return scheme + "://" + r.Host
|
||||
return v1.Scheme(r) + "://" + r.Host
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ func (h *ProxyHandler) serveNPMPackument(w http.ResponseWriter, r *http.Request,
|
||||
return
|
||||
}
|
||||
read := func(hash string) ([]byte, error) { return h.readLocalBlob(r.Context(), hash) }
|
||||
tarballBase := fmt.Sprintf("%s://%s/api/v1/local/%s/", scheme(r), r.Host, remote.Name)
|
||||
tarballBase := fmt.Sprintf("%s://%s/api/v1/local/%s/", Scheme(r), r.Host, remote.Name)
|
||||
doc, err := buildNPMPackument(pkg, files, read, tarballBase)
|
||||
if err != nil {
|
||||
npmError(w, http.StatusInternalServerError, err.Error())
|
||||
|
||||
+65
-16
@@ -6,7 +6,10 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
@@ -44,7 +47,6 @@ func (h *ProxyHandler) Routes() chi.Router {
|
||||
r.Get("/remote/{remoteName}/*", h.handleProxy)
|
||||
r.Head("/remote/{remoteName}/*", h.handleProxyHead)
|
||||
r.Get("/local/{localName}/*", h.handleLocal)
|
||||
r.Put("/local/{localName}/*", h.handleLocalPut)
|
||||
r.Get("/virtual/{virtualName}/*", h.handleVirtual)
|
||||
return r
|
||||
}
|
||||
@@ -92,7 +94,7 @@ func (h *ProxyHandler) remoteFor(w http.ResponseWriter, r *http.Request) (*model
|
||||
// responses and redirect package downloads instead of proxying bytes.
|
||||
func (h *ProxyHandler) serveRemote(w http.ResponseWriter, r *http.Request, remote *models.Remote, prov provider.Provider, path string) bool {
|
||||
rs, ok := prov.(provider.RemoteServer)
|
||||
return ok && rs.ServeRemote(w, r, *remote, path, fmt.Sprintf("%s://%s", scheme(r), r.Host), h.db)
|
||||
return ok && rs.ServeRemote(w, r, *remote, path, fmt.Sprintf("%s://%s", Scheme(r), r.Host), h.db)
|
||||
}
|
||||
|
||||
func (h *ProxyHandler) handleProxy(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -123,12 +125,10 @@ func (h *ProxyHandler) serveFetch(w http.ResponseWriter, r *http.Request, remote
|
||||
|
||||
var body io.Reader = result.Reader
|
||||
if sr, ok := prov.(provider.ServeRewriter); ok && prov.Classify(path) == provider.Mutable {
|
||||
raw, err := io.ReadAll(result.Reader)
|
||||
if err != nil {
|
||||
http.Error(w, "bad gateway", http.StatusBadGateway)
|
||||
raw, ok := rewriteServed(w, r, sr, *remote, result.Reader)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
raw = sr.RewriteServed(raw, *remote, fmt.Sprintf("%s://%s", scheme(r), r.Host))
|
||||
body, result.Size = bytes.NewReader(raw), int64(len(raw))
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ func (h *ProxyHandler) handleVirtual(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
proxyBaseURL := fmt.Sprintf("%s://%s", scheme(r), r.Host)
|
||||
proxyBaseURL := fmt.Sprintf("%s://%s", Scheme(r), r.Host)
|
||||
|
||||
loc, ok, err := h.virtualEngine.MemberRedirect(r.Context(), *virt, path, r.URL.RawQuery, proxyBaseURL)
|
||||
if err != nil {
|
||||
@@ -234,13 +234,6 @@ func (h *ProxyHandler) handleLocal(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if remote.PackageType == models.PackageNPM {
|
||||
if pkg := npmPackageName(path); pkg != "" {
|
||||
h.serveNPMPackument(w, r, remote, pkg)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
prov, _ := provider.Get(remote.PackageType)
|
||||
if indexer, ok := prov.(provider.LocalIndexer); ok {
|
||||
if indexer.ServeLocalIndex(w, r, h.db, remote.Name, path) {
|
||||
@@ -279,12 +272,68 @@ func (h *ProxyHandler) serveLocalFile(w http.ResponseWriter, r *http.Request, re
|
||||
io.Copy(w, reader)
|
||||
}
|
||||
|
||||
func scheme(r *http.Request) string {
|
||||
// Scheme returns the client-facing scheme: TLS, else the first
|
||||
// X-Forwarded-Proto value when it is http or https, else http.
|
||||
func Scheme(r *http.Request) string {
|
||||
if r.TLS != nil {
|
||||
return "https"
|
||||
}
|
||||
if fwd := r.Header.Get("X-Forwarded-Proto"); fwd != "" {
|
||||
fwd, _, _ := strings.Cut(r.Header.Get("X-Forwarded-Proto"), ",")
|
||||
if fwd = strings.ToLower(strings.TrimSpace(fwd)); fwd == "https" {
|
||||
return fwd
|
||||
}
|
||||
return "http"
|
||||
}
|
||||
|
||||
// rewriteServed rewrites a cached body for the requesting host and marks the
|
||||
// response uncacheable by shared caches, since it varies per Host.
|
||||
func rewriteServed(w http.ResponseWriter, r *http.Request, sr provider.ServeRewriter, remote models.Remote, src io.Reader) ([]byte, bool) {
|
||||
if !validHost(r.Host) {
|
||||
http.Error(w, "invalid host header", http.StatusBadRequest)
|
||||
return nil, false
|
||||
}
|
||||
raw, err := io.ReadAll(src)
|
||||
if err != nil {
|
||||
http.Error(w, "bad gateway", http.StatusBadGateway)
|
||||
return nil, false
|
||||
}
|
||||
raw = sr.RewriteServed(raw, remote, fmt.Sprintf("%s://%s", Scheme(r), r.Host))
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("Vary", "Host, X-Forwarded-Proto")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(raw)))
|
||||
return raw, true
|
||||
}
|
||||
|
||||
// validHost accepts host[:port] where host is a DNS name or IP literal.
|
||||
func validHost(hostport string) bool {
|
||||
host, port := hostport, ""
|
||||
if h, p, err := net.SplitHostPort(hostport); err == nil {
|
||||
if p == "" {
|
||||
return false
|
||||
}
|
||||
host, port = h, p
|
||||
} else if strings.HasPrefix(hostport, "[") {
|
||||
return strings.HasSuffix(hostport, "]") && net.ParseIP(hostport[1:len(hostport)-1]) != nil
|
||||
}
|
||||
if port != "" {
|
||||
if n, err := strconv.Atoi(port); err != nil || n < 1 || n > 65535 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if net.ParseIP(host) != nil {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(hostport, "[") {
|
||||
return false
|
||||
}
|
||||
if host == "" || len(host) > 253 {
|
||||
return false
|
||||
}
|
||||
for _, label := range strings.Split(strings.TrimSuffix(host, "."), ".") {
|
||||
if label == "" || len(label) > 63 || label[0] == '-' || label[len(label)-1] == '-' ||
|
||||
strings.Trim(strings.ToLower(label), "abcdefghijklmnopqrstuvwxyz0123456789-_") != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/internal/provider/nuget"
|
||||
"git.unkin.net/unkin/artifactapi/pkg/models"
|
||||
)
|
||||
|
||||
func TestValidHost(t *testing.T) {
|
||||
for _, h := range []string{"aa.example", "aa.example:8443", "artifactapi.k8s.syd1.au.unkin.net", "10.0.0.1:80", "[::1]:8080", "[::1]", "::1", "localhost", "my_host.local"} {
|
||||
if !validHost(h) {
|
||||
t.Errorf("validHost(%q) = false, want true", h)
|
||||
}
|
||||
}
|
||||
for _, h := range []string{"", "evil\".example", "a b", "aa.example:0", "aa.example:99999", "aa.example:x", "aa.example:", ":8080", "-bad.example", "a..b", "[::1", "[nope]:80", "a/b", "a<script>", strings.Repeat("a", 64) + ".example"} {
|
||||
if validHost(h) {
|
||||
t.Errorf("validHost(%q) = true, want false", h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var nugetOrg = models.Remote{Name: "nuget-org", BaseURL: "https://api.nuget.org/v3/index.json"}
|
||||
|
||||
const serviceIndex = `{"resources":[{"@id":"https://api.nuget.org/v3-flatcontainer/"}]}`
|
||||
|
||||
func rewrite(t *testing.T, host, proto string) (*httptest.ResponseRecorder, string, bool) {
|
||||
t.Helper()
|
||||
r := httptest.NewRequest(http.MethodGet, "/api/v1/remote/nuget-org/index.json", nil)
|
||||
r.Host = host
|
||||
if proto != "" {
|
||||
r.Header.Set("X-Forwarded-Proto", proto)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
raw, ok := rewriteServed(w, r, &nuget.Provider{}, nugetOrg, strings.NewReader(serviceIndex))
|
||||
return w, string(raw), ok
|
||||
}
|
||||
|
||||
func TestRewriteServedHeaders(t *testing.T) {
|
||||
w, body, ok := rewrite(t, "aa.example:8443", "https")
|
||||
if !ok {
|
||||
t.Fatal("valid request rejected")
|
||||
}
|
||||
if want := `{"resources":[{"@id":"https://aa.example:8443/api/v1/remote/nuget-org/v3-flatcontainer/"}]}`; body != want {
|
||||
t.Errorf("body = %s, want %s", body, want)
|
||||
}
|
||||
if got := w.Header().Get("Cache-Control"); got != "no-store" {
|
||||
t.Errorf("Cache-Control = %q, want no-store", got)
|
||||
}
|
||||
if got := w.Header().Get("Vary"); got != "Host, X-Forwarded-Proto" {
|
||||
t.Errorf("Vary = %q", got)
|
||||
}
|
||||
if got := w.Header().Get("Content-Length"); got != "91" || len(body) != 91 {
|
||||
t.Errorf("Content-Length = %q, body len %d", got, len(body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteServedIgnoresBogusProto(t *testing.T) {
|
||||
_, plain, _ := rewrite(t, "aa.example", "")
|
||||
_, bogus, _ := rewrite(t, "aa.example", "javascript")
|
||||
if bogus != plain || !strings.Contains(bogus, `"http://aa.example/`) {
|
||||
t.Errorf("bogus X-Forwarded-Proto changed the body: %s", bogus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteServedRejectsInvalidHost(t *testing.T) {
|
||||
w, _, ok := rewrite(t, `evil"},{"x":"`, "")
|
||||
if ok || w.Code != http.StatusBadRequest {
|
||||
t.Errorf("invalid host: ok=%v code=%d, want rejected with 400", ok, w.Code)
|
||||
}
|
||||
}
|
||||
@@ -7,14 +7,28 @@ import (
|
||||
)
|
||||
|
||||
func TestScheme(t *testing.T) {
|
||||
if got := scheme(&http.Request{TLS: &tls.ConnectionState{}}); got != "https" {
|
||||
if got := Scheme(&http.Request{TLS: &tls.ConnectionState{}}); got != "https" {
|
||||
t.Errorf("TLS request scheme = %q, want https", got)
|
||||
}
|
||||
r := &http.Request{Header: http.Header{"X-Forwarded-Proto": {"https"}}}
|
||||
if got := scheme(r); got != "https" {
|
||||
if got := Scheme(r); got != "https" {
|
||||
t.Errorf("X-Forwarded-Proto scheme = %q, want https", got)
|
||||
}
|
||||
if got := scheme(&http.Request{Header: http.Header{}}); got != "http" {
|
||||
for fwd, want := range map[string]string{
|
||||
"https, http": "https",
|
||||
"http, https": "http",
|
||||
"HTTPS": "https",
|
||||
" https ": "https",
|
||||
"javascript": "http",
|
||||
"ftp": "http",
|
||||
"https;evil": "http",
|
||||
} {
|
||||
r := &http.Request{Header: http.Header{"X-Forwarded-Proto": {fwd}}}
|
||||
if got := Scheme(r); got != want {
|
||||
t.Errorf("X-Forwarded-Proto %q scheme = %q, want %s", fwd, got, want)
|
||||
}
|
||||
}
|
||||
if got := Scheme(&http.Request{Header: http.Header{}}); got != "http" {
|
||||
t.Errorf("default scheme = %q, want http", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Package nuget proxies a NuGet v3 feed: the service index, registration pages
|
||||
// and flat container indexes are mutable, .nupkg/.nuspec downloads immutable.
|
||||
// and flat container indexes are mutable; package, symbol, nuspec, readme and
|
||||
// icon blobs are immutable.
|
||||
package nuget
|
||||
|
||||
import (
|
||||
@@ -7,6 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
pathpkg "path"
|
||||
"strings"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/internal/auth"
|
||||
@@ -34,10 +36,12 @@ func (p *Provider) Classify(path string) provider.Mutability {
|
||||
|
||||
func (p *Provider) ContentType(path string) string {
|
||||
switch {
|
||||
case strings.HasSuffix(path, ".nupkg"):
|
||||
case strings.HasSuffix(path, ".nupkg"), strings.HasSuffix(path, ".snupkg"), pathpkg.Base(path) == "icon":
|
||||
return "application/octet-stream"
|
||||
case strings.HasSuffix(path, ".nuspec"):
|
||||
return "application/xml"
|
||||
case pathpkg.Base(path) == "readme":
|
||||
return "text/markdown"
|
||||
}
|
||||
return "application/json"
|
||||
}
|
||||
@@ -64,8 +68,8 @@ func (p *Provider) RewriteServed(body []byte, remote models.Remote, proxyBaseURL
|
||||
if from == "" || !json.Valid(body) {
|
||||
return body
|
||||
}
|
||||
to := strings.TrimRight(proxyBaseURL, "/") + "/api/v1/remote/" + remote.Name
|
||||
return []byte(strings.ReplaceAll(string(body), from, to))
|
||||
to, _ := json.Marshal(strings.TrimRight(proxyBaseURL, "/") + "/api/v1/remote/" + remote.Name)
|
||||
return []byte(strings.ReplaceAll(string(body), from, string(to[1:len(to)-1])))
|
||||
}
|
||||
|
||||
func (p *Provider) AuthHeaders(_ context.Context, remote models.Remote) (http.Header, error) {
|
||||
@@ -80,6 +84,14 @@ func origin(baseURL string) string {
|
||||
return u.Scheme + "://" + u.Host
|
||||
}
|
||||
|
||||
// isPackage matches flat container blobs: {id}.{ver}.(s)nupkg, {id}.nuspec and
|
||||
// the {id}/{ver}/readme and {id}/{ver}/icon files.
|
||||
func isPackage(path string) bool {
|
||||
return strings.HasSuffix(path, ".nupkg") || strings.HasSuffix(path, ".nuspec")
|
||||
for _, ext := range []string{".nupkg", ".snupkg", ".nuspec"} {
|
||||
if strings.HasSuffix(path, ext) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
base := pathpkg.Base(path)
|
||||
return (base == "readme" || base == "icon") && strings.Count(strings.Trim(path, "/"), "/") >= 2
|
||||
}
|
||||
|
||||
@@ -19,7 +19,11 @@ func TestClassify(t *testing.T) {
|
||||
}{
|
||||
{"v3-flatcontainer/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg", provider.Immutable},
|
||||
{"v3-flatcontainer/newtonsoft.json/13.0.3/newtonsoft.json.nuspec", provider.Immutable},
|
||||
{"v3-flatcontainer/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.snupkg", provider.Immutable},
|
||||
{"v3-flatcontainer/newtonsoft.json/13.0.3/readme", provider.Immutable},
|
||||
{"v3-flatcontainer/newtonsoft.json/13.0.3/icon", provider.Immutable},
|
||||
{"v3-flatcontainer/newtonsoft.json/index.json", provider.Mutable},
|
||||
{"readme", provider.Mutable},
|
||||
{"v3/registration5-gz-semver2/newtonsoft.json/index.json", provider.Mutable},
|
||||
{"index.json", provider.Mutable},
|
||||
}
|
||||
@@ -30,6 +34,28 @@ func TestClassify(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentType(t *testing.T) {
|
||||
p := &Provider{}
|
||||
for path, want := range map[string]string{
|
||||
"v3-flatcontainer/a/1.0.0/a.1.0.0.snupkg": "application/octet-stream",
|
||||
"v3-flatcontainer/a/1.0.0/icon": "application/octet-stream",
|
||||
"v3-flatcontainer/a/1.0.0/readme": "text/markdown",
|
||||
"v3-flatcontainer/a/index.json": "application/json",
|
||||
} {
|
||||
if got := p.ContentType(path); got != want {
|
||||
t.Errorf("ContentType(%q) = %q, want %q", path, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteServedEscapesJSON(t *testing.T) {
|
||||
body := `{"@id":"https://api.nuget.org/v3-flatcontainer/"}`
|
||||
got := (&Provider{}).RewriteServed([]byte(body), nugetOrg, `http://evil"<x>`)
|
||||
if want := `{"@id":"http://evil\"\u003cx\u003e/api/v1/remote/nuget-org/v3-flatcontainer/"}`; string(got) != want {
|
||||
t.Errorf("RewriteServed = %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpstreamURL(t *testing.T) {
|
||||
p := &Provider{}
|
||||
tests := []struct {
|
||||
|
||||
Reference in New Issue
Block a user