diff --git a/README.md b/README.md index bc23425..08c2fe5 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,17 @@ full-size codercat image, and one for the resized 500px version. [codercat URL]: http://localhost:8080/500/https://octodex.github.com/images/codercat.jpg +### Referrer Whitelist ### + +You can limit images to only be accessible for certain hosts in the HTTP referrer header. This may be useful to prevent others from hotlinking to images, and using your valuable bandwidth! It can be enabled be running: + + imageproxy -referrers example.com + + +Reload the [codercat URL][], and you should now get an error message. You can +specify multiple hosts as a comma separated list, or prefix a host value with +`*.` to allow all sub-domains as well. + ### Host whitelist ### You can limit the remote hosts that the proxy will fetch images from using the diff --git a/cmd/imageproxy/main.go b/cmd/imageproxy/main.go index 2fea6a7..8ee2add 100644 --- a/cmd/imageproxy/main.go +++ b/cmd/imageproxy/main.go @@ -41,6 +41,7 @@ var ( var addr = flag.String("addr", "localhost:8080", "TCP address to listen on") var whitelist = flag.String("whitelist", "", "comma separated list of allowed remote hosts") +var referrers = flag.String("referrers", "", "comma separated list of allowed referring hosts") var baseURL = flag.String("baseURL", "", "default base URL for relative remote URLs") var cacheDir = flag.String("cacheDir", "", "directory to use for file cache") var cacheSize = flag.Uint64("cacheSize", 100, "maximum size of file cache (in MB)") @@ -71,6 +72,9 @@ func main() { if *whitelist != "" { p.Whitelist = strings.Split(*whitelist, ",") } + if *referrers != "" { + p.Referrers = strings.Split(*referrers, ",") + } if *signatureKey != "" { key := []byte(*signatureKey) if strings.HasPrefix(*signatureKey, "@") { diff --git a/data.go b/data.go index a311505..d912864 100644 --- a/data.go +++ b/data.go @@ -196,8 +196,9 @@ func ParseOptions(str string) Options { // Request is an imageproxy request which includes a remote URL of an image to // proxy, and an optional set of transformations to perform. type Request struct { - URL *url.URL // URL of the image to proxy - Options Options // Image transformation to perform + URL *url.URL // URL of the image to proxy + Options Options // Image transformation to perform + Original *http.Request // The original HTTP request } // String returns the request URL as a string, with r.Options encoded in the @@ -223,7 +224,7 @@ func (r Request) String() string { // http://localhost/http://example.com/image.jpg func NewRequest(r *http.Request, baseURL *url.URL) (*Request, error) { var err error - req := new(Request) + req := &Request{Original: r} path := r.URL.Path[1:] // strip leading slash req.URL, err = url.Parse(path) diff --git a/imageproxy.go b/imageproxy.go index 9ce9817..d7d9902 100644 --- a/imageproxy.go +++ b/imageproxy.go @@ -47,6 +47,11 @@ type Proxy struct { // proxied from. An empty list means all hosts are allowed. Whitelist []string + // Referrers, when given, requires that requests to the image + // proxy come from a referring host. An empty list means all + // hosts are allowed. + Referrers []string + // DefaultBaseURL is the URL that relative remote URLs are resolved in // reference to. If nil, all remote URLs specified in requests must be // absolute. @@ -144,6 +149,11 @@ func copyHeader(w http.ResponseWriter, r *http.Response, header string) { // allowed returns whether the specified request is allowed because it matches // a host in the proxy whitelist or it has a valid signature. func (p *Proxy) allowed(r *Request) bool { + if len(p.Referrers) > 0 && !validReferrer(p.Referrers, r.Original) { + glog.Infof("request not coming from allowed referrer: %v", r) + return false + } + if len(p.Whitelist) == 0 && len(p.SignatureKey) == 0 { return true // no whitelist or signature key, all requests accepted } @@ -179,6 +189,16 @@ func validHost(hosts []string, u *url.URL) bool { return false } +// returns whether the referrer from the request is in the host list. +func validReferrer(hosts []string, r *http.Request) bool { + parsed, err := url.Parse(r.Header.Get("Referer")) + if err != nil { // malformed or blank header, just deny + return false + } + + return validHost(hosts, parsed) +} + // validSignature returns whether the request signature is valid. func validSignature(key []byte, r *Request) bool { sig := r.Options.Signature diff --git a/imageproxy_test.go b/imageproxy_test.go index 7ac5c68..298e41f 100644 --- a/imageproxy_test.go +++ b/imageproxy_test.go @@ -20,43 +20,60 @@ 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, true}, + {"http://test/image", emptyOptions, nil, nil, nil, nil, true}, // whitelist - {"http://good/image", emptyOptions, whitelist, nil, true}, - {"http://bad/image", emptyOptions, whitelist, nil, false}, + {"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, key, true}, - {"http://test/image", Options{Signature: "deadbeef"}, nil, key, false}, - {"http://test/image", emptyOptions, nil, key, false}, + {"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, key, true}, - {"http://bad/image", Options{Signature: "gWivrPhXBbsYEwpmWAKjbJEiAEgZwbXbltg95O2tgNI="}, nil, key, true}, - {"http://bad/image", emptyOptions, whitelist, key, false}, + {"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} + req := &Request{u, tt.options, tt.request} if got, want := p.allowed(req), tt.allowed; got != want { - t.Errorf("allowed(%q) returned %v, want %v", req, got, want) + t.Errorf("allowed(%q) returned %v, want %v.\nTest struct: %#v", req, got, want, tt) } } } @@ -109,7 +126,7 @@ func TestValidSignature(t *testing.T) { if err != nil { t.Errorf("error parsing url %q: %v", tt.url, err) } - req := &Request{u, tt.options} + 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) }