0
Fork 0
mirror of https://github.com/willnorris/imageproxy.git synced 2024-12-16 21:56:43 -05:00
imageproxy/imageproxy_test.go

588 lines
17 KiB
Go
Raw Normal View History

// Copyright 2013 The imageproxy authors.
// SPDX-License-Identifier: Apache-2.0
package imageproxy
import (
"bufio"
2014-12-04 20:30:01 -05:00
"bytes"
"encoding/base64"
2014-12-04 20:30:01 -05:00
"errors"
"fmt"
"image"
"image/png"
"log"
"net/http"
2014-12-04 20:30:01 -05:00
"net/http/httptest"
"net/url"
"os"
"reflect"
"regexp"
"strconv"
"strings"
"testing"
)
func TestPeekContentType(t *testing.T) {
// 1 pixel png image, base64 encoded
b, _ := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAEUlEQVR4nGJiYGBgAAQAAP//AA8AA/6P688AAAAASUVORK5CYII=")
got := peekContentType(bufio.NewReader(bytes.NewReader(b)))
if want := "image/png"; got != want {
t.Errorf("peekContentType returned %v, want %v", got, want)
}
// single zero byte
got = peekContentType(bufio.NewReader(bytes.NewReader([]byte{0x0})))
if want := "application/octet-stream"; got != want {
t.Errorf("peekContentType returned %v, want %v", got, want)
}
}
func TestCopyHeader(t *testing.T) {
tests := []struct {
dst, src http.Header
keys []string
want http.Header
}{
// empty
{http.Header{}, http.Header{}, nil, http.Header{}},
{http.Header{}, http.Header{}, []string{}, http.Header{}},
{http.Header{}, http.Header{}, []string{"A"}, http.Header{}},
// nothing to copy
{
dst: http.Header{"A": []string{"a1"}},
src: http.Header{},
keys: nil,
want: http.Header{"A": []string{"a1"}},
},
{
dst: http.Header{},
src: http.Header{"A": []string{"a"}},
keys: []string{"B"},
want: http.Header{},
},
// copy headers
{
dst: http.Header{"A": []string{"a"}},
src: http.Header{"B": []string{"b"}, "C": []string{"c"}},
keys: []string{"B"},
want: http.Header{"A": []string{"a"}, "B": []string{"b"}},
},
}
for _, tt := range tests {
// copy dst map
got := make(http.Header)
for k, v := range tt.dst {
got[k] = v
}
copyHeader(got, tt.src, tt.keys...)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("copyHeader(%v, %v, %v) returned %v, want %v", tt.dst, tt.src, tt.keys, got, tt.want)
}
}
}
func TestAllowed(t *testing.T) {
allowHosts := []string{"good"}
key := [][]byte{
[]byte("c0ffee"),
}
multipleKey := [][]byte{
[]byte("c0ffee"),
[]byte("beer"),
}
2015-06-14 03:26:40 -05:00
genRequest := func(headers map[string]string) *http.Request {
req := &http.Request{Header: make(http.Header)}
for key, value := range headers {
req.Header.Set(key, value)
}
return req
}
tests := []struct {
url string
options Options
allowHosts []string
2019-03-21 23:43:10 -05:00
denyHosts []string
referrers []string
keys [][]byte
request *http.Request
allowed bool
}{
// no allowHosts or signature key
2019-03-21 23:43:10 -05:00
{"http://test/image", emptyOptions, nil, nil, nil, nil, nil, true},
2014-12-04 20:30:01 -05:00
// allowHosts
2019-03-21 23:43:10 -05:00
{"http://good/image", emptyOptions, allowHosts, nil, nil, nil, nil, true},
{"http://bad/image", emptyOptions, allowHosts, nil, nil, nil, nil, false},
2015-06-14 03:26:40 -05:00
// referrer
2019-03-21 23:43:10 -05:00
{"http://test/image", emptyOptions, nil, nil, allowHosts, nil, genRequest(map[string]string{"Referer": "http://good/foo"}), true},
{"http://test/image", emptyOptions, nil, nil, allowHosts, nil, genRequest(map[string]string{"Referer": "http://bad/foo"}), false},
{"http://test/image", emptyOptions, nil, nil, allowHosts, nil, genRequest(map[string]string{"Referer": "MALFORMED!!"}), false},
{"http://test/image", emptyOptions, nil, nil, allowHosts, nil, genRequest(map[string]string{}), false},
2015-05-11 21:36:42 -05:00
// signature key
2019-03-21 23:43:10 -05:00
{"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ="}, nil, nil, nil, key, nil, true},
{"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ="}, nil, nil, nil, multipleKey, nil, true}, // signed with key "c0ffee"
{"http://test/image", Options{Signature: "FWIawYV4SEyI4zKJMeGugM-eJM1eI_jXPEQ20ZgRe4A="}, nil, nil, nil, multipleKey, nil, true}, // signed with key "beer"
2019-03-21 23:43:10 -05:00
{"http://test/image", Options{Signature: "deadbeef"}, nil, nil, nil, key, nil, false},
{"http://test/image", Options{Signature: "deadbeef"}, nil, nil, nil, multipleKey, nil, false},
2019-03-21 23:43:10 -05:00
{"http://test/image", emptyOptions, nil, nil, nil, key, nil, false},
2015-05-11 21:36:42 -05:00
// allowHosts and signature
2019-03-21 23:43:10 -05:00
{"http://good/image", emptyOptions, allowHosts, nil, nil, key, nil, true},
{"http://bad/image", Options{Signature: "gWivrPhXBbsYEwpmWAKjbJEiAEgZwbXbltg95O2tgNI="}, nil, nil, nil, key, nil, true},
{"http://bad/image", emptyOptions, allowHosts, nil, nil, key, nil, false},
// deny requests that match denyHosts, even if signature is valid or also matches allowHosts
{"http://test/image", emptyOptions, nil, []string{"test"}, nil, nil, nil, false},
{"http://test:3000/image", emptyOptions, nil, []string{"test"}, nil, nil, nil, false},
2019-03-21 23:43:10 -05:00
{"http://test/image", emptyOptions, []string{"test"}, []string{"test"}, nil, nil, nil, false},
{"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ="}, nil, []string{"test"}, nil, key, nil, false},
{"http://127.0.0.1/image", emptyOptions, nil, []string{"127.0.0.0/8"}, nil, nil, nil, false},
{"http://127.0.0.1:3000/image", emptyOptions, nil, []string{"127.0.0.0/8"}, nil, nil, nil, false},
}
for _, tt := range tests {
2014-12-04 20:30:01 -05:00
p := NewProxy(nil, nil)
p.AllowHosts = tt.allowHosts
2019-03-21 23:43:10 -05:00
p.DenyHosts = tt.denyHosts
p.SignatureKeys = tt.keys
2015-06-14 03:26:40 -05:00
p.Referrers = tt.referrers
2014-12-04 20:30:01 -05:00
u, err := url.Parse(tt.url)
if err != nil {
t.Errorf("error parsing url %q: %v", tt.url, err)
}
2015-06-14 03:26:40 -05:00
req := &Request{u, tt.options, tt.request}
if got, want := p.allowed(req), tt.allowed; (got == nil) != want {
2015-06-14 03:26:40 -05:00
t.Errorf("allowed(%q) returned %v, want %v.\nTest struct: %#v", req, got, want, tt)
}
}
}
func TestHostMatches(t *testing.T) {
hosts := []string{"a.test", "*.b.test", "*c.test"}
tests := []struct {
url string
valid bool
}{
{"http://a.test/image", true},
{"http://x.a.test/image", false},
{"http://b.test/image", true},
{"http://x.b.test/image", true},
{"http://x.y.b.test/image", true},
{"http://c.test/image", false},
{"http://xc.test/image", false},
{"/image", false},
}
for _, tt := range tests {
u, err := url.Parse(tt.url)
if err != nil {
t.Errorf("error parsing url %q: %v", tt.url, err)
}
if got, want := hostMatches(hosts, u), tt.valid; got != want {
t.Errorf("hostMatches(%v, %q) returned %v, want %v", hosts, u, got, want)
}
}
}
2019-03-22 03:46:34 -05:00
func TestReferrerMatches(t *testing.T) {
hosts := []string{"a.test"}
tests := []struct {
referrer string
valid bool
}{
{"", false},
{"%", false},
{"http://a.test/", true},
{"http://b.test/", false},
}
for _, tt := range tests {
r, _ := http.NewRequest("GET", "/", nil)
r.Header.Set("Referer", tt.referrer)
if got, want := referrerMatches(hosts, r), tt.valid; got != want {
t.Errorf("referrerMatches(%v, %v) returned %v, want %v", hosts, r, got, want)
}
}
}
2015-05-11 21:36:42 -05:00
func TestValidSignature(t *testing.T) {
key := []byte("c0ffee")
tests := []struct {
url string
options Options
valid bool
}{
{"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ="}, true},
{"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ"}, true},
{"http://test/image", emptyOptions, false},
// url-only signature with options
{"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ", Rotate: 90}, true},
// signature calculated from url plus options
{"http://test/image", Options{Signature: "ZGTzEm32o4iZ7qcChls3EVYaWyrDd9u0etySo0-WkF8=", Rotate: 90}, true},
// invalid base64 encoded signature
{"http://test/image", Options{Signature: "!!"}, false},
2015-05-11 21:36:42 -05:00
}
for _, tt := range tests {
u, err := url.Parse(tt.url)
if err != nil {
t.Errorf("error parsing url %q: %v", tt.url, err)
}
2015-06-14 03:26:40 -05:00
req := &Request{u, tt.options, &http.Request{}}
2015-05-11 21:36:42 -05:00
if got, want := validSignature(key, req), tt.valid; got != want {
t.Errorf("validSignature(%v, %v) returned %v, want %v", key, req, got, want)
2015-05-11 21:36:42 -05:00
}
}
}
func TestShould304(t *testing.T) {
tests := []struct {
req, resp string
is304 bool
}{
{ // etag match
"GET / HTTP/1.1\nIf-None-Match: \"v\"\n\n",
"HTTP/1.1 200 OK\nEtag: \"v\"\n\n",
true,
},
{ // last-modified before
"GET / HTTP/1.1\nIf-Modified-Since: Sun, 02 Jan 2000 00:00:00 GMT\n\n",
"HTTP/1.1 200 OK\nLast-Modified: Sat, 01 Jan 2000 00:00:00 GMT\n\n",
true,
},
{ // last-modified match
"GET / HTTP/1.1\nIf-Modified-Since: Sat, 01 Jan 2000 00:00:00 GMT\n\n",
"HTTP/1.1 200 OK\nLast-Modified: Sat, 01 Jan 2000 00:00:00 GMT\n\n",
true,
},
// mismatches
{
"GET / HTTP/1.1\n\n",
"HTTP/1.1 200 OK\n\n",
false,
},
{
"GET / HTTP/1.1\n\n",
"HTTP/1.1 200 OK\nEtag: \"v\"\n\n",
false,
},
{
"GET / HTTP/1.1\nIf-None-Match: \"v\"\n\n",
"HTTP/1.1 200 OK\n\n",
false,
},
{
"GET / HTTP/1.1\nIf-None-Match: \"a\"\n\n",
"HTTP/1.1 200 OK\nEtag: \"b\"\n\n",
false,
},
{ // last-modified match
"GET / HTTP/1.1\n\n",
"HTTP/1.1 200 OK\nLast-Modified: Sat, 01 Jan 2000 00:00:00 GMT\n\n",
false,
},
{ // last-modified match
"GET / HTTP/1.1\nIf-Modified-Since: Sun, 02 Jan 2000 00:00:00 GMT\n\n",
"HTTP/1.1 200 OK\n\n",
false,
},
{ // last-modified match
"GET / HTTP/1.1\nIf-Modified-Since: Fri, 31 Dec 1999 00:00:00 GMT\n\n",
"HTTP/1.1 200 OK\nLast-Modified: Sat, 01 Jan 2000 00:00:00 GMT\n\n",
false,
},
}
for _, tt := range tests {
buf := bufio.NewReader(strings.NewReader(tt.req))
req, err := http.ReadRequest(buf)
if err != nil {
t.Errorf("http.ReadRequest(%q) returned error: %v", tt.req, err)
}
buf = bufio.NewReader(strings.NewReader(tt.resp))
resp, err := http.ReadResponse(buf, req)
if err != nil {
t.Errorf("http.ReadResponse(%q) returned error: %v", tt.resp, err)
}
if got, want := should304(req, resp), tt.is304; got != want {
t.Errorf("should304(%q, %q) returned: %v, want %v", tt.req, tt.resp, got, want)
}
}
}
2014-12-04 20:30:01 -05:00
// testTransport is an http.RoundTripper that returns certained canned
// responses for particular requests.
type testTransport struct{}
func (t testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
var raw string
switch req.URL.Path {
2018-02-09 16:50:57 -05:00
case "/plain":
2014-12-04 20:30:01 -05:00
raw = "HTTP/1.1 200 OK\n\n"
case "/error":
return nil, errors.New("http protocol error")
case "/nocontent":
raw = "HTTP/1.1 204 No Content\n\n"
2014-12-04 20:30:01 -05:00
case "/etag":
raw = "HTTP/1.1 200 OK\nEtag: \"tag\"\n\n"
case "/png":
m := image.NewNRGBA(image.Rect(0, 0, 1, 1))
img := new(bytes.Buffer)
_ = png.Encode(img, m)
2014-12-04 20:30:01 -05:00
2018-02-09 16:50:57 -05:00
raw = fmt.Sprintf("HTTP/1.1 200 OK\nContent-Length: %d\nContent-Type: image/png\n\n%s", len(img.Bytes()), img.Bytes())
2014-12-04 20:30:01 -05:00
default:
redirectRegexp := regexp.MustCompile(`/redirects-(\d+)`)
if redirectRegexp.MatchString(req.URL.Path) {
redirectsLeft, _ := strconv.ParseUint(redirectRegexp.FindStringSubmatch(req.URL.Path)[1], 10, 8)
if redirectsLeft == 0 {
raw = "HTTP/1.1 200 OK\n\n"
} else {
raw = fmt.Sprintf("HTTP/1.1 302\nLocation: /http://redirect.test/redirects-%d\n\n", redirectsLeft-1)
}
} else {
raw = "HTTP/1.1 404 Not Found\n\n"
}
2014-12-04 20:30:01 -05:00
}
buf := bufio.NewReader(bytes.NewBufferString(raw))
return http.ReadResponse(buf, req)
}
func TestProxy_ServeHTTP(t *testing.T) {
p := &Proxy{
Client: &http.Client{
Transport: testTransport{},
},
AllowHosts: []string{"good.test"},
ContentTypes: []string{"image/*"},
2014-12-04 20:30:01 -05:00
}
tests := []struct {
url string // request URL
code int // expected response status code
}{
{"/favicon.ico", http.StatusOK},
{"//foo", http.StatusBadRequest}, // invalid request URL
{"/http://bad.test/", http.StatusForbidden}, // Disallowed host
2014-12-04 20:30:01 -05:00
{"/http://good.test/error", http.StatusInternalServerError}, // HTTP protocol error
{"/http://good.test/nocontent", http.StatusNoContent}, // non-OK response
2018-02-09 16:50:57 -05:00
{"/100/http://good.test/png", http.StatusOK},
{"/100/http://good.test/plain", http.StatusForbidden}, // non-image response
2019-03-22 03:46:34 -05:00
// health-check URLs
{"/", http.StatusOK},
{"/health-check", http.StatusOK},
2014-12-04 20:30:01 -05:00
}
for _, tt := range tests {
req, _ := http.NewRequest("GET", "http://localhost"+tt.url, nil)
resp := httptest.NewRecorder()
p.ServeHTTP(resp, req)
if got, want := resp.Code, tt.code; got != want {
t.Errorf("ServeHTTP(%v) returned status %d, want %d", req, got, want)
2014-12-04 20:30:01 -05:00
}
}
}
// test that 304 Not Modified responses are returned properly.
func TestProxy_ServeHTTP_is304(t *testing.T) {
p := &Proxy{
Client: &http.Client{
Transport: testTransport{},
},
}
req, _ := http.NewRequest("GET", "http://localhost/http://good.test/etag", nil)
req.Header.Add("If-None-Match", `"tag"`)
resp := httptest.NewRecorder()
p.ServeHTTP(resp, req)
if got, want := resp.Code, http.StatusNotModified; got != want {
t.Errorf("ServeHTTP(%v) returned status %d, want %d", req, got, want)
2014-12-04 20:30:01 -05:00
}
if got, want := resp.Header().Get("Etag"), `"tag"`; got != want {
t.Errorf("ServeHTTP(%v) returned etag header %v, want %v", req, got, want)
2014-12-04 20:30:01 -05:00
}
}
func TestProxy_ServeHTTP_maxRedirects(t *testing.T) {
p := &Proxy{
Client: &http.Client{
Transport: testTransport{},
},
FollowRedirects: true,
}
tests := []struct {
url string
code int
}{
{"/http://redirect.test/redirects-0", http.StatusOK},
{"/http://redirect.test/redirects-2", http.StatusOK},
{"/http://redirect.test/redirects-11", http.StatusInternalServerError}, // too many redirects
}
for _, tt := range tests {
req, _ := http.NewRequest("GET", "http://localhost"+tt.url, nil)
resp := httptest.NewRecorder()
p.ServeHTTP(resp, req)
if got, want := resp.Code, tt.code; got != want {
t.Errorf("ServeHTTP(%v) returned status %d, want %d", req, got, want)
}
}
}
func TestProxy_log(t *testing.T) {
var b strings.Builder
p := &Proxy{
Logger: log.New(&b, "", 0),
}
p.log("Test")
if got, want := b.String(), "Test\n"; got != want {
t.Errorf("log wrote %s, want %s", got, want)
}
b.Reset()
p.logf("Test %v", 123)
if got, want := b.String(), "Test 123\n"; got != want {
t.Errorf("logf wrote %s, want %s", got, want)
}
}
func TestProxy_log_default(t *testing.T) {
var b strings.Builder
defer func(flags int) {
log.SetOutput(os.Stderr)
log.SetFlags(flags)
}(log.Flags())
log.SetOutput(&b)
log.SetFlags(0)
p := &Proxy{}
p.log("Test")
if got, want := b.String(), "Test\n"; got != want {
t.Errorf("log wrote %s, want %s", got, want)
}
b.Reset()
p.logf("Test %v", 123)
if got, want := b.String(), "Test 123\n"; got != want {
t.Errorf("logf wrote %s, want %s", got, want)
}
}
2014-12-04 20:30:01 -05:00
func TestTransformingTransport(t *testing.T) {
client := new(http.Client)
2015-08-12 13:39:38 -05:00
tr := &TransformingTransport{
Transport: testTransport{},
CachingClient: client,
}
2014-12-04 20:30:01 -05:00
client.Transport = tr
tests := []struct {
url string
code int
expectError bool
}{
{"http://good.test/png#1", http.StatusOK, false},
{"http://good.test/error#1", http.StatusInternalServerError, true},
// TODO: test more than just status code... verify that image
// is actually transformed and returned properly and that
// non-image responses are returned as-is
}
for _, tt := range tests {
req, _ := http.NewRequest("GET", tt.url, nil)
resp, err := tr.RoundTrip(req)
if err != nil {
if !tt.expectError {
t.Errorf("RoundTrip(%v) returned unexpected error: %v", tt.url, err)
}
continue
} else if tt.expectError {
t.Errorf("RoundTrip(%v) did not return expected error", tt.url)
}
if got, want := resp.StatusCode, tt.code; got != want {
t.Errorf("RoundTrip(%v) returned status code %d, want %d", tt.url, got, want)
2014-12-04 20:30:01 -05:00
}
}
}
2018-02-09 16:50:57 -05:00
func TestContentTypeMatches(t *testing.T) {
tests := []struct {
patterns []string
contentType string
valid bool
}{
// no patterns
{nil, "", true},
{nil, "text/plain", true},
{[]string{}, "", true},
{[]string{}, "text/plain", true},
// empty pattern
{[]string{""}, "", true},
{[]string{""}, "text/plain", false},
// exact match
{[]string{"text/plain"}, "", false},
{[]string{"text/plain"}, "text", false},
{[]string{"text/plain"}, "text/html", false},
{[]string{"text/plain"}, "text/plain", true},
{[]string{"text/plain"}, "text/plaintext", false},
{[]string{"text/plain"}, "text/plain+foo", false},
// wildcard match
{[]string{"text/*"}, "", false},
{[]string{"text/*"}, "text", false},
{[]string{"text/*"}, "text/html", true},
{[]string{"text/*"}, "text/plain", true},
{[]string{"text/*"}, "image/jpeg", false},
{[]string{"image/svg*"}, "image/svg", true},
{[]string{"image/svg*"}, "image/svg+html", true},
// complete wildcard does not match
{[]string{"*"}, "text/foobar", false},
// multiple patterns
{[]string{"text/*", "image/*"}, "image/jpeg", true},
2018-02-09 16:50:57 -05:00
}
for _, tt := range tests {
got := contentTypeMatches(tt.patterns, tt.contentType)
if want := tt.valid; got != want {
t.Errorf("contentTypeMatches(%q, %q) returned %v, want %v", tt.patterns, tt.contentType, got, want)
2018-02-09 16:50:57 -05:00
}
}
}