mirror of
https://github.com/willnorris/imageproxy.git
synced 2024-12-30 22:34:18 -05:00
add request signature support to Proxy
This commit is contained in:
parent
9d6f8fdf7d
commit
a9efefc8e7
4 changed files with 105 additions and 23 deletions
9
data.go
9
data.go
|
@ -29,6 +29,7 @@ const (
|
||||||
optFlipHorizontal = "fh"
|
optFlipHorizontal = "fh"
|
||||||
optRotatePrefix = "r"
|
optRotatePrefix = "r"
|
||||||
optQualityPrefix = "q"
|
optQualityPrefix = "q"
|
||||||
|
optSignaturePrefix = "s"
|
||||||
optSizeDelimiter = "x"
|
optSizeDelimiter = "x"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -61,6 +62,9 @@ type Options struct {
|
||||||
|
|
||||||
// Quality of output image
|
// Quality of output image
|
||||||
Quality int
|
Quality int
|
||||||
|
|
||||||
|
// HMAC Signature for signed requests.
|
||||||
|
Signature string
|
||||||
}
|
}
|
||||||
|
|
||||||
var emptyOptions = Options{}
|
var emptyOptions = Options{}
|
||||||
|
@ -83,6 +87,9 @@ func (o Options) String() string {
|
||||||
if o.Quality != 0 {
|
if o.Quality != 0 {
|
||||||
fmt.Fprintf(buf, ",%s%d", string(optQualityPrefix), o.Quality)
|
fmt.Fprintf(buf, ",%s%d", string(optQualityPrefix), o.Quality)
|
||||||
}
|
}
|
||||||
|
if o.Signature != "" {
|
||||||
|
fmt.Fprintf(buf, ",%s%s", string(optSignaturePrefix), o.Signature)
|
||||||
|
}
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,6 +169,8 @@ func ParseOptions(str string) Options {
|
||||||
case strings.HasPrefix(opt, optQualityPrefix):
|
case strings.HasPrefix(opt, optQualityPrefix):
|
||||||
value := strings.TrimPrefix(opt, optQualityPrefix)
|
value := strings.TrimPrefix(opt, optQualityPrefix)
|
||||||
options.Quality, _ = strconv.Atoi(value)
|
options.Quality, _ = strconv.Atoi(value)
|
||||||
|
case strings.HasPrefix(opt, optSignaturePrefix):
|
||||||
|
options.Signature = strings.TrimPrefix(opt, optSignaturePrefix)
|
||||||
case strings.Contains(opt, optSizeDelimiter):
|
case strings.Contains(opt, optSizeDelimiter):
|
||||||
size := strings.SplitN(opt, optSizeDelimiter, 2)
|
size := strings.SplitN(opt, optSizeDelimiter, 2)
|
||||||
if w := size[0]; w != "" {
|
if w := size[0]; w != "" {
|
||||||
|
|
10
data_test.go
10
data_test.go
|
@ -29,12 +29,12 @@ func TestOptions_String(t *testing.T) {
|
||||||
"0x0",
|
"0x0",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Options{1, 2, true, 90, true, true, 80},
|
Options{1, 2, true, 90, true, true, 80, ""},
|
||||||
"1x2,fit,r90,fv,fh,q80",
|
"1x2,fit,r90,fv,fh,q80",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Options{0.15, 1.3, false, 45, false, false, 95},
|
Options{0.15, 1.3, false, 45, false, false, 95, "c0ffee"},
|
||||||
"0.15x1.3,r45,q95",
|
"0.15x1.3,r45,q95,sc0ffee",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,8 +82,8 @@ func TestParseOptions(t *testing.T) {
|
||||||
{"FOO,1,BAR,r90,BAZ", Options{Width: 1, Height: 1, Rotate: 90}},
|
{"FOO,1,BAR,r90,BAZ", Options{Width: 1, Height: 1, Rotate: 90}},
|
||||||
|
|
||||||
// all flags, in different orders
|
// all flags, in different orders
|
||||||
{"q70,1x2,fit,r90,fv,fh", Options{1, 2, true, 90, true, true, 70}},
|
{"q70,1x2,fit,r90,fv,fh,sc0ffee", Options{1, 2, true, 90, true, true, 70, "c0ffee"}},
|
||||||
{"r90,fh,q90,1x2,fv,fit", Options{1, 2, true, 90, true, true, 90}},
|
{"r90,fh,sc0ffee,q90,1x2,fv,fit", Options{1, 2, true, 90, true, true, 90, "c0ffee"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|
|
@ -19,6 +19,9 @@ package imageproxy // import "willnorris.com/go/imageproxy"
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -48,6 +51,9 @@ type Proxy struct {
|
||||||
// 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.
|
||||||
DefaultBaseURL *url.URL
|
DefaultBaseURL *url.URL
|
||||||
|
|
||||||
|
// SignatureKey is the HMAC key used to verify signed requests.
|
||||||
|
SignatureKey []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProxy constructs a new proxy. The provided http RoundTripper will be
|
// NewProxy constructs a new proxy. The provided http RoundTripper will be
|
||||||
|
@ -89,7 +95,7 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !p.allowed(req) {
|
if !p.allowed(req) {
|
||||||
msg := fmt.Sprintf("request does not contain an allowed host")
|
msg := fmt.Sprintf("request does not contain an allowed host or valid signature")
|
||||||
glog.Error(msg)
|
glog.Error(msg)
|
||||||
http.Error(w, msg, http.StatusForbidden)
|
http.Error(w, msg, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
|
@ -136,17 +142,24 @@ 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.
|
// 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.Whitelist) == 0 {
|
if len(p.Whitelist) == 0 && len(p.SignatureKey) == 0 {
|
||||||
return true // no whitelist, all requests accepted
|
return true // no whitelist or signature key, all requests accepted
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(p.Whitelist) > 0 {
|
if len(p.Whitelist) > 0 {
|
||||||
if validHost(p.Whitelist, r.URL) {
|
if validHost(p.Whitelist, r.URL) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
glog.Infof("remote URL is not for an allowed host: %v", r.URL)
|
glog.Infof("request is not for an allowed host: %v", r)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p.SignatureKey) > 0 {
|
||||||
|
if validSignature(p.SignatureKey, r) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
glog.Infof("request contains invalid signature: %v", r)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
@ -166,6 +179,26 @@ func validHost(hosts []string, u *url.URL) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validSignature returns whether the request signature is valid.
|
||||||
|
func validSignature(key []byte, r *Request) bool {
|
||||||
|
sig := r.Options.Signature
|
||||||
|
if m := len(sig) % 4; m != 0 { // add padding if missing
|
||||||
|
sig += strings.Repeat("=", 4-m)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := base64.URLEncoding.DecodeString(sig)
|
||||||
|
if err != nil {
|
||||||
|
glog.Errorf("error base64 decoding signature %q", r.Options.Signature)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
mac := hmac.New(sha256.New, key)
|
||||||
|
mac.Write([]byte(r.URL.String()))
|
||||||
|
want := mac.Sum(nil)
|
||||||
|
|
||||||
|
return hmac.Equal(got, want)
|
||||||
|
}
|
||||||
|
|
||||||
// check304 checks whether we should send a 304 Not Modified in response to
|
// check304 checks whether we should send a 304 Not Modified in response to
|
||||||
// req, based on the response resp. This is determined using the last modified
|
// req, based on the response resp. This is determined using the last modified
|
||||||
// time and the entity tag of resp.
|
// time and the entity tag of resp.
|
||||||
|
|
|
@ -15,31 +15,46 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAllowed(t *testing.T) {
|
func TestAllowed(t *testing.T) {
|
||||||
whitelist := []string{"good.test"}
|
whitelist := []string{"good"}
|
||||||
|
key := []byte("c0ffee")
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
url string
|
url string
|
||||||
|
options Options
|
||||||
whitelist []string
|
whitelist []string
|
||||||
|
key []byte
|
||||||
allowed bool
|
allowed bool
|
||||||
}{
|
}{
|
||||||
{"http://foo/image", nil, true},
|
// no whitelist or signature key
|
||||||
{"http://foo/image", []string{}, true},
|
{"http://test/image", emptyOptions, nil, nil, true},
|
||||||
|
|
||||||
{"http://good.test/image", whitelist, true},
|
// whitelist
|
||||||
{"http://bad.test/image", whitelist, false},
|
{"http://good/image", emptyOptions, whitelist, nil, true},
|
||||||
|
{"http://bad/image", emptyOptions, whitelist, nil, 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},
|
||||||
|
|
||||||
|
// 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},
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
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, emptyOptions}
|
req := &Request{u, tt.options}
|
||||||
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", u, got, want)
|
t.Errorf("allowed(%q) returned %v, want %v", req, got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,6 +89,31 @@ func TestValidHost(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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}
|
||||||
|
if got, want := validSignature(key, req), tt.valid; got != want {
|
||||||
|
t.Errorf("validSignature(%v, %q) returned %v, want %v", key, u, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCheck304(t *testing.T) {
|
func TestCheck304(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
req, resp string
|
req, resp string
|
||||||
|
|
Loading…
Reference in a new issue