package imgproxy
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
)
var png1x1 = []byte("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89")
func upstream(t *testing.T, ctype string, body []byte) (*httptest.Server, *int) {
t.Helper()
hits := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
hits++
w.Header().Set("Content-Type", ctype)
w.Write(body)
}))
t.Cleanup(srv.Close)
return srv, &hits
}
func get(t *testing.T, p *Proxy, path string) *httptest.ResponseRecorder {
t.Helper()
rec := httptest.NewRecorder()
p.ServeHTTP(rec, httptest.NewRequest("GET", path, nil))
return rec
}
// Only what the renderer signed is fetched: a URL with no signature, a
// wrong one, or one signed for a different image is refused.
func TestUnsignedImagesAreRefused(t *testing.T) {
p, err := New(t.TempDir(), true)
if err != nil {
t.Fatal(err)
}
src, hits := upstream(t, "image/png", png1x1)
for _, path := range []string{
"/img?u=" + url.QueryEscape(src.URL+"/a.png"),
"/img?u=" + url.QueryEscape(src.URL+"/a.png") + "&s=forged",
strings.Replace(p.URL(src.URL+"/b.png"), url.QueryEscape("/b.png"), url.QueryEscape("/a.png"), 1),
} {
if rec := get(t, p, path); rec.Code != http.StatusForbidden {
t.Errorf("%s: %d, want 403", path, rec.Code)
}
}
if *hits != 0 {
t.Fatalf("a refused request reached the upstream %d times", *hits)
}
}
// The guard runs at connect time: a correctly signed URL that points at a
// loopback address is still not fetched.
func TestPrivateAddressesAreNotFetched(t *testing.T) {
p, err := New(t.TempDir(), false)
if err != nil {
t.Fatal(err)
}
src, hits := upstream(t, "image/png", png1x1)
if rec := get(t, p, p.URL(src.URL+"/a.png")); rec.Code != http.StatusBadGateway {
t.Fatalf("a loopback image answered %d", rec.Code)
}
if *hits != 0 {
t.Fatal("the guard let a loopback fetch through")
}
}
// An image is served with the headers that keep it an image, and cached:
// the second request does not reach the upstream, and a stale copy is
// served when the upstream has gone.
func TestImagesAreServedAndCached(t *testing.T) {
p, err := New(t.TempDir(), true)
if err != nil {
t.Fatal(err)
}
src, hits := upstream(t, "image/png", png1x1)
path := p.URL(src.URL + "/badge.png?branch=main")
rec := get(t, p, path)
if rec.Code != http.StatusOK || rec.Body.String() != string(png1x1) {
t.Fatalf("first request: %d", rec.Code)
}
if rec.Header().Get("Content-Type") != "image/png" || rec.Header().Get("X-Content-Type-Options") != "nosniff" {
t.Errorf("headers: %v", rec.Header())
}
get(t, p, path)
if *hits != 1 {
t.Fatalf("a fresh cached image was fetched again (%d hits)", *hits)
}
p.now = func() time.Time { return time.Now().Add(2 * TTL) }
src.Close()
if rec := get(t, p, path); rec.Code != http.StatusOK {
t.Fatalf("a stale copy was not served when the upstream was down: %d", rec.Code)
}
}
// Anything that is not an image is refused, and so is an image past the
// size cap.
func TestNonImagesAndOversizeAreRefused(t *testing.T) {
p, err := New(t.TempDir(), true)
if err != nil {
t.Fatal(err)
}
html, _ := upstream(t, "text/html", []byte("<script>alert(1)</script>"))
if rec := get(t, p, p.URL(html.URL+"/x")); rec.Code != http.StatusBadGateway {
t.Errorf("html answered %d", rec.Code)
}
big, _ := upstream(t, "image/png", make([]byte, MaxBytes+1))
if rec := get(t, p, p.URL(big.URL+"/big.png")); rec.Code != http.StatusBadGateway {
t.Errorf("an oversize image answered %d", rec.Code)
}
}
// SVG can carry script; served here it is sandboxed.
func TestSVGIsSandboxed(t *testing.T) {
p, err := New(t.TempDir(), true)
if err != nil {
t.Fatal(err)
}
svg, _ := upstream(t, "image/svg+xml; charset=utf-8", []byte(`<svg xmlns="http://www.w3.org/2000/svg"/>`))
rec := get(t, p, p.URL(svg.URL+"/badge.svg"))
if rec.Code != http.StatusOK {
t.Fatalf("svg: %d", rec.Code)
}
if csp := rec.Header().Get("Content-Security-Policy"); !strings.Contains(csp, "sandbox") || !strings.Contains(csp, "default-src 'none'") {
t.Errorf("svg csp = %q", csp)
}
}
// The key survives a restart, so a page rendered before it still loads.
func TestTheKeyPersists(t *testing.T) {
dir := t.TempDir()
a, err := New(dir, false)
if err != nil {
t.Fatal(err)
}
b, err := New(dir, false)
if err != nil {
t.Fatal(err)
}
if a.URL("https://example.org/x.png") != b.URL("https://example.org/x.png") {
t.Fatal("a second start signs differently")
}
if a.URL("javascript:alert(1)") != "" || a.URL("/local.png") != "" {
t.Fatal("a non-http target was given a proxy URL")
}
}