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

Add ability to restrict http referrer

This commit is contained in:
Connor Peet 2015-06-14 18:26:40 +10:00 committed by Will Norris
parent 8c13d93bde
commit 9213c93c94
5 changed files with 68 additions and 15 deletions

View file

@ -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 [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 ### ### Host whitelist ###
You can limit the remote hosts that the proxy will fetch images from using the You can limit the remote hosts that the proxy will fetch images from using the

View file

@ -41,6 +41,7 @@ var (
var addr = flag.String("addr", "localhost:8080", "TCP address to listen on") 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 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 baseURL = flag.String("baseURL", "", "default base URL for relative remote URLs")
var cacheDir = flag.String("cacheDir", "", "directory to use for file cache") var cacheDir = flag.String("cacheDir", "", "directory to use for file cache")
var cacheSize = flag.Uint64("cacheSize", 100, "maximum size of file cache (in MB)") var cacheSize = flag.Uint64("cacheSize", 100, "maximum size of file cache (in MB)")
@ -71,6 +72,9 @@ func main() {
if *whitelist != "" { if *whitelist != "" {
p.Whitelist = strings.Split(*whitelist, ",") p.Whitelist = strings.Split(*whitelist, ",")
} }
if *referrers != "" {
p.Referrers = strings.Split(*referrers, ",")
}
if *signatureKey != "" { if *signatureKey != "" {
key := []byte(*signatureKey) key := []byte(*signatureKey)
if strings.HasPrefix(*signatureKey, "@") { if strings.HasPrefix(*signatureKey, "@") {

View file

@ -196,8 +196,9 @@ func ParseOptions(str string) Options {
// Request is an imageproxy request which includes a remote URL of an image to // Request is an imageproxy request which includes a remote URL of an image to
// proxy, and an optional set of transformations to perform. // proxy, and an optional set of transformations to perform.
type Request struct { type Request struct {
URL *url.URL // URL of the image to proxy URL *url.URL // URL of the image to proxy
Options Options // Image transformation to perform 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 // 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 // http://localhost/http://example.com/image.jpg
func NewRequest(r *http.Request, baseURL *url.URL) (*Request, error) { func NewRequest(r *http.Request, baseURL *url.URL) (*Request, error) {
var err error var err error
req := new(Request) req := &Request{Original: r}
path := r.URL.Path[1:] // strip leading slash path := r.URL.Path[1:] // strip leading slash
req.URL, err = url.Parse(path) req.URL, err = url.Parse(path)

View file

@ -47,6 +47,11 @@ type Proxy struct {
// proxied from. An empty list means all hosts are allowed. // proxied from. An empty list means all hosts are allowed.
Whitelist []string 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 // DefaultBaseURL is the URL that relative remote URLs are resolved in
// reference to. If nil, all remote URLs specified in requests must be // reference to. If nil, all remote URLs specified in requests must be
// absolute. // 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 // allowed returns whether the specified request is allowed because it matches
// a host in the proxy whitelist or it has a valid signature. // a host in the proxy whitelist or it has a valid signature.
func (p *Proxy) allowed(r *Request) bool { 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 { if len(p.Whitelist) == 0 && len(p.SignatureKey) == 0 {
return true // no whitelist or signature key, all requests accepted return true // no whitelist or signature key, all requests accepted
} }
@ -179,6 +189,16 @@ func validHost(hosts []string, u *url.URL) bool {
return false 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. // validSignature returns whether the request signature is valid.
func validSignature(key []byte, r *Request) bool { func validSignature(key []byte, r *Request) bool {
sig := r.Options.Signature sig := r.Options.Signature

View file

@ -20,43 +20,60 @@ func TestAllowed(t *testing.T) {
whitelist := []string{"good"} whitelist := []string{"good"}
key := []byte("c0ffee") 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 { tests := []struct {
url string url string
options Options options Options
whitelist []string whitelist []string
referrers []string
key []byte key []byte
request *http.Request
allowed bool allowed bool
}{ }{
// no whitelist or signature key // no whitelist or signature key
{"http://test/image", emptyOptions, nil, nil, true}, {"http://test/image", emptyOptions, nil, nil, nil, nil, true},
// whitelist // whitelist
{"http://good/image", emptyOptions, whitelist, nil, true}, {"http://good/image", emptyOptions, whitelist, nil, nil, nil, true},
{"http://bad/image", emptyOptions, whitelist, nil, false}, {"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 // signature key
{"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ="}, nil, key, true}, {"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ="}, nil, nil, key, nil, true},
{"http://test/image", Options{Signature: "deadbeef"}, nil, key, false}, {"http://test/image", Options{Signature: "deadbeef"}, nil, nil, key, nil, false},
{"http://test/image", emptyOptions, nil, key, false}, {"http://test/image", emptyOptions, nil, nil, key, nil, false},
// whitelist and signature // whitelist and signature
{"http://good/image", emptyOptions, whitelist, key, true}, {"http://good/image", emptyOptions, whitelist, nil, key, nil, true},
{"http://bad/image", Options{Signature: "gWivrPhXBbsYEwpmWAKjbJEiAEgZwbXbltg95O2tgNI="}, nil, key, true}, {"http://bad/image", Options{Signature: "gWivrPhXBbsYEwpmWAKjbJEiAEgZwbXbltg95O2tgNI="}, nil, nil, key, nil, true},
{"http://bad/image", emptyOptions, whitelist, key, false}, {"http://bad/image", emptyOptions, whitelist, nil, key, nil, false},
} }
for _, tt := range tests { for _, tt := range tests {
p := NewProxy(nil, nil) p := NewProxy(nil, nil)
p.Whitelist = tt.whitelist p.Whitelist = tt.whitelist
p.SignatureKey = tt.key p.SignatureKey = tt.key
p.Referrers = tt.referrers
u, err := url.Parse(tt.url) u, err := url.Parse(tt.url)
if err != nil { if err != nil {
t.Errorf("error parsing url %q: %v", tt.url, err) 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 { 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 { if err != nil {
t.Errorf("error parsing url %q: %v", tt.url, err) 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 { if got, want := validSignature(key, req), tt.valid; got != want {
t.Errorf("validSignature(%v, %q) returned %v, want %v", key, u, got, want) t.Errorf("validSignature(%v, %q) returned %v, want %v", key, u, got, want)
} }