bf8228e9f2
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>
75 lines
2.6 KiB
Go
75 lines
2.6 KiB
Go
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)
|
|
}
|
|
}
|