Skip to content

Commit a9efefc

Browse files
committed
add request signature support to Proxy
1 parent 9d6f8fd commit a9efefc

File tree

4 files changed

+106
-24
lines changed

4 files changed

+106
-24
lines changed

data.go

+15-6
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@ import (
2424
)
2525

2626
const (
27-
optFit = "fit"
28-
optFlipVertical = "fv"
29-
optFlipHorizontal = "fh"
30-
optRotatePrefix = "r"
31-
optQualityPrefix = "q"
32-
optSizeDelimiter = "x"
27+
optFit = "fit"
28+
optFlipVertical = "fv"
29+
optFlipHorizontal = "fh"
30+
optRotatePrefix = "r"
31+
optQualityPrefix = "q"
32+
optSignaturePrefix = "s"
33+
optSizeDelimiter = "x"
3334
)
3435

3536
// URLError reports a malformed URL error.
@@ -61,6 +62,9 @@ type Options struct {
6162

6263
// Quality of output image
6364
Quality int
65+
66+
// HMAC Signature for signed requests.
67+
Signature string
6468
}
6569

6670
var emptyOptions = Options{}
@@ -83,6 +87,9 @@ func (o Options) String() string {
8387
if o.Quality != 0 {
8488
fmt.Fprintf(buf, ",%s%d", string(optQualityPrefix), o.Quality)
8589
}
90+
if o.Signature != "" {
91+
fmt.Fprintf(buf, ",%s%s", string(optSignaturePrefix), o.Signature)
92+
}
8693
return buf.String()
8794
}
8895

@@ -162,6 +169,8 @@ func ParseOptions(str string) Options {
162169
case strings.HasPrefix(opt, optQualityPrefix):
163170
value := strings.TrimPrefix(opt, optQualityPrefix)
164171
options.Quality, _ = strconv.Atoi(value)
172+
case strings.HasPrefix(opt, optSignaturePrefix):
173+
options.Signature = strings.TrimPrefix(opt, optSignaturePrefix)
165174
case strings.Contains(opt, optSizeDelimiter):
166175
size := strings.SplitN(opt, optSizeDelimiter, 2)
167176
if w := size[0]; w != "" {

data_test.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ func TestOptions_String(t *testing.T) {
2929
"0x0",
3030
},
3131
{
32-
Options{1, 2, true, 90, true, true, 80},
32+
Options{1, 2, true, 90, true, true, 80, ""},
3333
"1x2,fit,r90,fv,fh,q80",
3434
},
3535
{
36-
Options{0.15, 1.3, false, 45, false, false, 95},
37-
"0.15x1.3,r45,q95",
36+
Options{0.15, 1.3, false, 45, false, false, 95, "c0ffee"},
37+
"0.15x1.3,r45,q95,sc0ffee",
3838
},
3939
}
4040

@@ -82,8 +82,8 @@ func TestParseOptions(t *testing.T) {
8282
{"FOO,1,BAR,r90,BAZ", Options{Width: 1, Height: 1, Rotate: 90}},
8383

8484
// all flags, in different orders
85-
{"q70,1x2,fit,r90,fv,fh", Options{1, 2, true, 90, true, true, 70}},
86-
{"r90,fh,q90,1x2,fv,fit", Options{1, 2, true, 90, true, true, 90}},
85+
{"q70,1x2,fit,r90,fv,fh,sc0ffee", Options{1, 2, true, 90, true, true, 70, "c0ffee"}},
86+
{"r90,fh,sc0ffee,q90,1x2,fv,fit", Options{1, 2, true, 90, true, true, 90, "c0ffee"}},
8787
}
8888

8989
for _, tt := range tests {

imageproxy.go

+38-5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ package imageproxy // import "willnorris.com/go/imageproxy"
1919
import (
2020
"bufio"
2121
"bytes"
22+
"crypto/hmac"
23+
"crypto/sha256"
24+
"encoding/base64"
2225
"fmt"
2326
"io"
2427
"io/ioutil"
@@ -48,6 +51,9 @@ type Proxy struct {
4851
// reference to. If nil, all remote URLs specified in requests must be
4952
// absolute.
5053
DefaultBaseURL *url.URL
54+
55+
// SignatureKey is the HMAC key used to verify signed requests.
56+
SignatureKey []byte
5157
}
5258

5359
// 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) {
8995
}
9096

9197
if !p.allowed(req) {
92-
msg := fmt.Sprintf("request does not contain an allowed host")
98+
msg := fmt.Sprintf("request does not contain an allowed host or valid signature")
9399
glog.Error(msg)
94100
http.Error(w, msg, http.StatusForbidden)
95101
return
@@ -136,17 +142,24 @@ func copyHeader(w http.ResponseWriter, r *http.Response, header string) {
136142
}
137143

138144
// allowed returns whether the specified request is allowed because it matches
139-
// a host in the proxy whitelist.
145+
// a host in the proxy whitelist or it has a valid signature.
140146
func (p *Proxy) allowed(r *Request) bool {
141-
if len(p.Whitelist) == 0 {
142-
return true // no whitelist, all requests accepted
147+
if len(p.Whitelist) == 0 && len(p.SignatureKey) == 0 {
148+
return true // no whitelist or signature key, all requests accepted
143149
}
144150

145151
if len(p.Whitelist) > 0 {
146152
if validHost(p.Whitelist, r.URL) {
147153
return true
148154
}
149-
glog.Infof("remote URL is not for an allowed host: %v", r.URL)
155+
glog.Infof("request is not for an allowed host: %v", r)
156+
}
157+
158+
if len(p.SignatureKey) > 0 {
159+
if validSignature(p.SignatureKey, r) {
160+
return true
161+
}
162+
glog.Infof("request contains invalid signature: %v", r)
150163
}
151164

152165
return false
@@ -166,6 +179,26 @@ func validHost(hosts []string, u *url.URL) bool {
166179
return false
167180
}
168181

182+
// validSignature returns whether the request signature is valid.
183+
func validSignature(key []byte, r *Request) bool {
184+
sig := r.Options.Signature
185+
if m := len(sig) % 4; m != 0 { // add padding if missing
186+
sig += strings.Repeat("=", 4-m)
187+
}
188+
189+
got, err := base64.URLEncoding.DecodeString(sig)
190+
if err != nil {
191+
glog.Errorf("error base64 decoding signature %q", r.Options.Signature)
192+
return false
193+
}
194+
195+
mac := hmac.New(sha256.New, key)
196+
mac.Write([]byte(r.URL.String()))
197+
want := mac.Sum(nil)
198+
199+
return hmac.Equal(got, want)
200+
}
201+
169202
// check304 checks whether we should send a 304 Not Modified in response to
170203
// req, based on the response resp. This is determined using the last modified
171204
// time and the entity tag of resp.

imageproxy_test.go

+48-8
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,46 @@ import (
1515
)
1616

1717
func TestAllowed(t *testing.T) {
18-
whitelist := []string{"good.test"}
18+
whitelist := []string{"good"}
19+
key := []byte("c0ffee")
1920

2021
tests := []struct {
2122
url string
23+
options Options
2224
whitelist []string
25+
key []byte
2326
allowed bool
2427
}{
25-
{"http://foo/image", nil, true},
26-
{"http://foo/image", []string{}, true},
27-
28-
{"http://good.test/image", whitelist, true},
29-
{"http://bad.test/image", whitelist, false},
28+
// no whitelist or signature key
29+
{"http://test/image", emptyOptions, nil, nil, true},
30+
31+
// whitelist
32+
{"http://good/image", emptyOptions, whitelist, nil, true},
33+
{"http://bad/image", emptyOptions, whitelist, nil, false},
34+
35+
// signature key
36+
{"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ="}, nil, key, true},
37+
{"http://test/image", Options{Signature: "deadbeef"}, nil, key, false},
38+
{"http://test/image", emptyOptions, nil, key, false},
39+
40+
// whitelist and signature
41+
{"http://good/image", emptyOptions, whitelist, key, true},
42+
{"http://bad/image", Options{Signature: "gWivrPhXBbsYEwpmWAKjbJEiAEgZwbXbltg95O2tgNI="}, nil, key, true},
43+
{"http://bad/image", emptyOptions, whitelist, key, false},
3044
}
3145

3246
for _, tt := range tests {
3347
p := NewProxy(nil, nil)
3448
p.Whitelist = tt.whitelist
49+
p.SignatureKey = tt.key
3550

3651
u, err := url.Parse(tt.url)
3752
if err != nil {
3853
t.Errorf("error parsing url %q: %v", tt.url, err)
3954
}
40-
req := &Request{u, emptyOptions}
55+
req := &Request{u, tt.options}
4156
if got, want := p.allowed(req), tt.allowed; got != want {
42-
t.Errorf("allowed(%q) returned %v, want %v", u, got, want)
57+
t.Errorf("allowed(%q) returned %v, want %v", req, got, want)
4358
}
4459
}
4560
}
@@ -74,6 +89,31 @@ func TestValidHost(t *testing.T) {
7489
}
7590
}
7691

92+
func TestValidSignature(t *testing.T) {
93+
key := []byte("c0ffee")
94+
95+
tests := []struct {
96+
url string
97+
options Options
98+
valid bool
99+
}{
100+
{"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ="}, true},
101+
{"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ"}, true},
102+
{"http://test/image", emptyOptions, false},
103+
}
104+
105+
for _, tt := range tests {
106+
u, err := url.Parse(tt.url)
107+
if err != nil {
108+
t.Errorf("error parsing url %q: %v", tt.url, err)
109+
}
110+
req := &Request{u, tt.options}
111+
if got, want := validSignature(key, req), tt.valid; got != want {
112+
t.Errorf("validSignature(%v, %q) returned %v, want %v", key, u, got, want)
113+
}
114+
}
115+
}
116+
77117
func TestCheck304(t *testing.T) {
78118
tests := []struct {
79119
req, resp string

0 commit comments

Comments
 (0)