Skip to content

Commit fe35d19

Browse files
slinsowillnorris
andcommitted
add "valid until" option to limit lifetime of signed requests
Closes #222 Co-authored-by: Will Norris <[email protected]>
1 parent b98b345 commit fe35d19

File tree

5 files changed

+58
-5
lines changed

5 files changed

+58
-5
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,14 @@ If both a whiltelist and signatureKey are specified, requests can match either.
312312
In other words, requests that match one of the allowed hosts don't necessarily
313313
need to be signed, though they can be.
314314

315+
To limit how long a URL is valid (particularly useful for signed URLs),
316+
you can specify a "valid until" time using the `vu` option with a Unix timestamp.
317+
For example, the following signed URL would only be valid until 2020-01-01:
318+
319+
```
320+
http://localhost:8080/vu1577836800,sjNcVf6LxzKEvR6Owgg3zhEMN7xbWxlpf-eyYbRfFK4A=/https://example.com/image
321+
```
322+
315323
### Default Base URL
316324

317325
Typically, remote images to be proxied are specified as absolute URLs.

data.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"sort"
1313
"strconv"
1414
"strings"
15+
"time"
1516
"unicode"
1617
)
1718

@@ -33,6 +34,7 @@ const (
3334
optCropHeight = "ch"
3435
optSmartCrop = "sc"
3536
optTrim = "trim"
37+
optValidUntil = "vu"
3638
)
3739

3840
// URLError reports a malformed URL error.
@@ -86,6 +88,9 @@ type Options struct {
8688

8789
// If true, automatically trim pixels of the same color around the edges
8890
Trim bool
91+
92+
// If non-zero, the URL is valid until this time.
93+
ValidUntil time.Time
8994
}
9095

9196
func (o Options) String() string {
@@ -132,7 +137,12 @@ func (o Options) String() string {
132137
if o.Trim {
133138
opts = append(opts, optTrim)
134139
}
140+
if !o.ValidUntil.IsZero() {
141+
opts = append(opts, fmt.Sprintf("%s%d", optValidUntil, o.ValidUntil.Unix()))
142+
}
143+
135144
sort.Strings(opts)
145+
136146
return strings.Join(opts, ",")
137147
}
138148

@@ -235,6 +245,11 @@ func (o Options) transform() bool {
235245
// that have been resized or cropped. The trim option is applied before other
236246
// options such as cropping or resizing.
237247
//
248+
// # Valid Until
249+
//
250+
// The "vu{unixtime}" option specifies a Unix timestamp at which the request URL is no longer valid.
251+
// For example, "vu1800000000" would mean the URL is valid until 2027-01-15T08:00:00Z.
252+
//
238253
// Examples
239254
//
240255
// 0x0 - no resizing
@@ -289,6 +304,11 @@ func ParseOptions(str string) Options {
289304
case strings.HasPrefix(opt, optCropHeight):
290305
value := strings.TrimPrefix(opt, optCropHeight)
291306
options.CropHeight, _ = strconv.ParseFloat(value, 64)
307+
case strings.HasPrefix(opt, optValidUntil):
308+
value := strings.TrimPrefix(opt, optValidUntil)
309+
if v, _ := strconv.ParseInt(value, 10, 64); v > 0 {
310+
options.ValidUntil = time.Unix(v, 0)
311+
}
292312
case strings.Contains(opt, optSizeDelimiter):
293313
size := strings.SplitN(opt, optSizeDelimiter, 2)
294314
if w := size[0]; w != "" {

data_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/http"
88
"net/url"
99
"testing"
10+
"time"
1011
)
1112

1213
var emptyOptions = Options{}
@@ -25,8 +26,8 @@ func TestOptions_String(t *testing.T) {
2526
"1x2,fh,fit,fv,q80,r90",
2627
},
2728
{
28-
Options{Width: 0.15, Height: 1.3, Rotate: 45, Quality: 95, Signature: "c0ffee", Format: "png"},
29-
"0.15x1.3,png,q95,r45,sc0ffee",
29+
Options{Width: 0.15, Height: 1.3, Rotate: 45, Quality: 95, Signature: "c0ffee", Format: "png", ValidUntil: time.Unix(123, 0)},
30+
"0.15x1.3,png,q95,r45,sc0ffee,vu123",
3031
},
3132
{
3233
Options{Width: 0.15, Height: 1.3, CropX: 100, CropY: 200},
@@ -86,7 +87,7 @@ func TestParseOptions(t *testing.T) {
8687
// flags, in different orders
8788
{"q70,1x2,fit,r90,fv,fh,sc0ffee,png", Options{Width: 1, Height: 2, Fit: true, Rotate: 90, FlipVertical: true, FlipHorizontal: true, Quality: 70, Signature: "c0ffee", Format: "png"}},
8889
{"r90,fh,sc0ffee,png,q90,1x2,fv,fit", Options{Width: 1, Height: 2, Fit: true, Rotate: 90, FlipVertical: true, FlipHorizontal: true, Quality: 90, Signature: "c0ffee", Format: "png"}},
89-
{"cx100,cw300,1x2,cy200,ch400,sc,scaleUp", Options{Width: 1, Height: 2, ScaleUp: true, CropX: 100, CropY: 200, CropWidth: 300, CropHeight: 400, SmartCrop: true}},
90+
{"cx100,cw300,1x2,cy200,ch400,sc,scaleUp,vu1234567890", Options{Width: 1, Height: 2, ScaleUp: true, CropX: 100, CropY: 200, CropWidth: 300, CropHeight: 400, SmartCrop: true, ValidUntil: time.Unix(1234567890, 0)}},
9091
}
9192

9293
for _, tt := range tests {

imageproxy.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ type Proxy struct {
101101
// remote server specifies 'private' or 'no-store' in the cache-control
102102
// header.
103103
ForceCache bool
104+
105+
timeNow time.Time // current time, used for testing
104106
}
105107

106108
// NewProxy constructs a new proxy. The provided http RoundTripper will be
@@ -278,7 +280,6 @@ func (p *Proxy) serveImage(w http.ResponseWriter, r *http.Request) {
278280
}
279281
}
280282
resp, err := p.Client.Do(actualReq)
281-
282283
if err != nil {
283284
msg := fmt.Sprintf("error fetching remote image: %v", err)
284285
p.log(msg)
@@ -371,15 +372,29 @@ var (
371372
errDeniedHost = errors.New("request contains a denied host")
372373
errNotAllowed = errors.New("request does not contain an allowed host or valid signature")
373374
errTooManyRedirects = errors.New("too many redirects")
375+
errNotValid = errors.New("request is no longer valid")
374376

375377
msgNotAllowed = "requested URL is not allowed"
376378
msgNotAllowedInRedirect = "requested URL in redirect is not allowed"
377379
)
378380

381+
func (p *Proxy) now() time.Time {
382+
if !p.timeNow.IsZero() {
383+
return p.timeNow
384+
}
385+
return time.Now()
386+
}
387+
379388
// allowed determines whether the specified request contains an allowed
380389
// referrer, host, and signature. It returns an error if the request is not
381-
// allowed.
390+
// allowed or not valid any longer.
382391
func (p *Proxy) allowed(r *Request) error {
392+
if !r.Options.ValidUntil.IsZero() {
393+
if !p.now().Before(r.Options.ValidUntil) {
394+
return errNotValid
395+
}
396+
}
397+
383398
if len(p.Referrers) > 0 && !referrerMatches(p.Referrers, r.Original) {
384399
return errReferrer
385400
}

imageproxy_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,11 @@ func TestAllowed(t *testing.T) {
110110
return req
111111
}
112112

113+
now := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
114+
113115
tests := []struct {
114116
url string
117+
now time.Time
115118
options Options
116119
allowHosts []string
117120
denyHosts []string
@@ -153,6 +156,11 @@ func TestAllowed(t *testing.T) {
153156
{url: "http://test/image", options: Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ="}, denyHosts: []string{"test"}, keys: key, allowed: false},
154157
{url: "http://127.0.0.1/image", denyHosts: []string{"127.0.0.0/8"}, allowed: false},
155158
{url: "http://127.0.0.1:3000/image", denyHosts: []string{"127.0.0.0/8"}, allowed: false},
159+
160+
// valid until options
161+
{url: "http://test/image", now: now, options: Options{ValidUntil: now.Add(time.Second)}, allowed: true},
162+
{url: "http://test/image", now: now, options: Options{ValidUntil: now.Add(-time.Second)}, allowed: false},
163+
{url: "http://test/image", now: now, options: Options{ValidUntil: now}, allowed: false},
156164
}
157165

158166
for _, tt := range tests {
@@ -161,6 +169,7 @@ func TestAllowed(t *testing.T) {
161169
p.DenyHosts = tt.denyHosts
162170
p.SignatureKeys = tt.keys
163171
p.Referrers = tt.referrers
172+
p.timeNow = tt.now
164173

165174
u, err := url.Parse(tt.url)
166175
if err != nil {

0 commit comments

Comments
 (0)