0
Fork 0
mirror of https://github.com/willnorris/imageproxy.git synced 2024-12-16 21:56:43 -05:00
imageproxy/imageproxy_test.go
Will Norris 0370572130 change how content-type enforcement is handled
If no content types are specified, then accept all responses, regardless
of content type (this is the behavior imageproxy has historically had).
Change default value for the contentTypes flag to be "image/*", so that
the new default when running cmd/imageproxy is that only images will be
proxied.  The old default behavior can be achieved by passing an empty
string for the contentTypes flag:

    imageproxy -contentTypes ""

Do not send the "XCTO: nosniff" header, since all documentation that I
can find still says that it can cause problems when served with images.
If it's effectively a noop when an explicit content-type is specified in
the response, then this shouldn't actually matter for us either way.
But in the absence of certainty, I'd rather err on the side of following
the spec.

Also add documentation for the new functionality.

Fixes #141
2018-09-15 05:36:25 +00:00

462 lines
13 KiB
Go

// Copyright 2013 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package imageproxy
import (
"bufio"
"bytes"
"errors"
"fmt"
"image"
"image/png"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
"testing"
)
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{},
src: http.Header{"A": []string{"a"}},
keys: nil,
want: http.Header{"A": []string{"a"}},
},
{
dst: http.Header{"A": []string{"a"}},
src: http.Header{"B": []string{"b"}},
keys: nil,
want: http.Header{"A": []string{"a"}, "B": []string{"b"}},
},
{
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"}},
},
{
dst: http.Header{"A": []string{"a1"}},
src: http.Header{"A": []string{"a2"}},
keys: nil,
want: http.Header{"A": []string{"a1", "a2"}},
},
}
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) {
whitelist := []string{"good"}
key := []byte("c0ffee")
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
whitelist []string
referrers []string
key []byte
request *http.Request
allowed bool
}{
// no whitelist or signature key
{"http://test/image", emptyOptions, nil, nil, nil, nil, true},
// whitelist
{"http://good/image", emptyOptions, whitelist, nil, nil, nil, true},
{"http://bad/image", emptyOptions, whitelist, nil, nil, nil, false},
// referrer
{"http://test/image", emptyOptions, nil, whitelist, nil, genRequest(map[string]string{"Referer": "http://good/foo"}), true},
{"http://test/image", emptyOptions, nil, whitelist, nil, genRequest(map[string]string{"Referer": "http://bad/foo"}), false},
{"http://test/image", emptyOptions, nil, whitelist, nil, genRequest(map[string]string{"Referer": "MALFORMED!!"}), false},
{"http://test/image", emptyOptions, nil, whitelist, nil, genRequest(map[string]string{}), false},
// signature key
{"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ="}, nil, nil, key, nil, true},
{"http://test/image", Options{Signature: "deadbeef"}, nil, nil, key, nil, false},
{"http://test/image", emptyOptions, nil, nil, key, nil, false},
// whitelist and signature
{"http://good/image", emptyOptions, whitelist, nil, key, nil, true},
{"http://bad/image", Options{Signature: "gWivrPhXBbsYEwpmWAKjbJEiAEgZwbXbltg95O2tgNI="}, nil, nil, key, nil, true},
{"http://bad/image", emptyOptions, whitelist, nil, key, nil, false},
}
for _, tt := range tests {
p := NewProxy(nil, nil)
p.Whitelist = tt.whitelist
p.SignatureKey = tt.key
p.Referrers = tt.referrers
u, err := url.Parse(tt.url)
if err != nil {
t.Errorf("error parsing url %q: %v", tt.url, err)
}
req := &Request{u, tt.options, tt.request}
if got, want := p.allowed(req), tt.allowed; (got == nil) != want {
t.Errorf("allowed(%q) returned %v, want %v.\nTest struct: %#v", req, got, want, tt)
}
}
}
func TestValidHost(t *testing.T) {
whitelist := []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 := validHost(whitelist, u), tt.valid; got != want {
t.Errorf("validHost(%v, %q) returned %v, want %v", whitelist, u, got, want)
}
}
}
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},
}
for _, tt := range tests {
u, err := url.Parse(tt.url)
if err != nil {
t.Errorf("error parsing url %q: %v", tt.url, err)
}
req := &Request{u, tt.options, &http.Request{}}
if got, want := validSignature(key, req), tt.valid; got != want {
t.Errorf("validSignature(%v, %q) returned %v, want %v", key, u, got, want)
}
}
}
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)
}
}
}
// 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 {
case "/plain":
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"
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)
raw = fmt.Sprintf("HTTP/1.1 200 OK\nContent-Length: %d\nContent-Type: image/png\n\n%s", len(img.Bytes()), img.Bytes())
default:
raw = "HTTP/1.1 404 Not Found\n\n"
}
buf := bufio.NewReader(bytes.NewBufferString(raw))
return http.ReadResponse(buf, req)
}
func TestProxy_ServeHTTP(t *testing.T) {
p := &Proxy{
Client: &http.Client{
Transport: testTransport{},
},
Whitelist: []string{"good.test"},
ContentTypes: []string{"image/*"},
}
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
{"/http://good.test/error", http.StatusInternalServerError}, // HTTP protocol error
{"/http://good.test/nocontent", http.StatusNoContent}, // non-OK response
{"/100/http://good.test/png", http.StatusOK},
{"/100/http://good.test/plain", http.StatusForbidden}, // non-image response
}
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)
}
}
}
// 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)
}
if got, want := resp.Header().Get("Etag"), `"tag"`; got != want {
t.Errorf("ServeHTTP(%v) returned etag header %v, want %v", req, got, want)
}
}
func TestTransformingTransport(t *testing.T) {
client := new(http.Client)
tr := &TransformingTransport{
Transport: testTransport{},
CachingClient: client,
}
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)
}
}
}
func TestValidContentType(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},
}
for _, tt := range tests {
got := validContentType(tt.patterns, tt.contentType)
if want := tt.valid; got != want {
t.Errorf("validContentType(%q, %q) returned %v, want %v", tt.patterns, tt.contentType, got, want)
}
}
}