mirror of
https://github.com/willnorris/imageproxy.git
synced 2026-04-24 20:36:24 +02:00
add "valid until" option to limit lifetime of signed requests
Closes #222 Co-authored-by: Will Norris <will@willnorris.com>
This commit is contained in:
parent
b98b3455a1
commit
fe35d19c3e
5 changed files with 58 additions and 5 deletions
|
|
@ -312,6 +312,14 @@ If both a whiltelist and signatureKey are specified, requests can match either.
|
|||
In other words, requests that match one of the allowed hosts don't necessarily
|
||||
need to be signed, though they can be.
|
||||
|
||||
To limit how long a URL is valid (particularly useful for signed URLs),
|
||||
you can specify a "valid until" time using the `vu` option with a Unix timestamp.
|
||||
For example, the following signed URL would only be valid until 2020-01-01:
|
||||
|
||||
```
|
||||
http://localhost:8080/vu1577836800,sjNcVf6LxzKEvR6Owgg3zhEMN7xbWxlpf-eyYbRfFK4A=/https://example.com/image
|
||||
```
|
||||
|
||||
### Default Base URL
|
||||
|
||||
Typically, remote images to be proxied are specified as absolute URLs.
|
||||
|
|
|
|||
20
data.go
20
data.go
|
|
@ -12,6 +12,7 @@ import (
|
|||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
|
|
@ -33,6 +34,7 @@ const (
|
|||
optCropHeight = "ch"
|
||||
optSmartCrop = "sc"
|
||||
optTrim = "trim"
|
||||
optValidUntil = "vu"
|
||||
)
|
||||
|
||||
// URLError reports a malformed URL error.
|
||||
|
|
@ -86,6 +88,9 @@ type Options struct {
|
|||
|
||||
// If true, automatically trim pixels of the same color around the edges
|
||||
Trim bool
|
||||
|
||||
// If non-zero, the URL is valid until this time.
|
||||
ValidUntil time.Time
|
||||
}
|
||||
|
||||
func (o Options) String() string {
|
||||
|
|
@ -132,7 +137,12 @@ func (o Options) String() string {
|
|||
if o.Trim {
|
||||
opts = append(opts, optTrim)
|
||||
}
|
||||
if !o.ValidUntil.IsZero() {
|
||||
opts = append(opts, fmt.Sprintf("%s%d", optValidUntil, o.ValidUntil.Unix()))
|
||||
}
|
||||
|
||||
sort.Strings(opts)
|
||||
|
||||
return strings.Join(opts, ",")
|
||||
}
|
||||
|
||||
|
|
@ -235,6 +245,11 @@ func (o Options) transform() bool {
|
|||
// that have been resized or cropped. The trim option is applied before other
|
||||
// options such as cropping or resizing.
|
||||
//
|
||||
// # Valid Until
|
||||
//
|
||||
// The "vu{unixtime}" option specifies a Unix timestamp at which the request URL is no longer valid.
|
||||
// For example, "vu1800000000" would mean the URL is valid until 2027-01-15T08:00:00Z.
|
||||
//
|
||||
// Examples
|
||||
//
|
||||
// 0x0 - no resizing
|
||||
|
|
@ -289,6 +304,11 @@ func ParseOptions(str string) Options {
|
|||
case strings.HasPrefix(opt, optCropHeight):
|
||||
value := strings.TrimPrefix(opt, optCropHeight)
|
||||
options.CropHeight, _ = strconv.ParseFloat(value, 64)
|
||||
case strings.HasPrefix(opt, optValidUntil):
|
||||
value := strings.TrimPrefix(opt, optValidUntil)
|
||||
if v, _ := strconv.ParseInt(value, 10, 64); v > 0 {
|
||||
options.ValidUntil = time.Unix(v, 0)
|
||||
}
|
||||
case strings.Contains(opt, optSizeDelimiter):
|
||||
size := strings.SplitN(opt, optSizeDelimiter, 2)
|
||||
if w := size[0]; w != "" {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var emptyOptions = Options{}
|
||||
|
|
@ -25,8 +26,8 @@ func TestOptions_String(t *testing.T) {
|
|||
"1x2,fh,fit,fv,q80,r90",
|
||||
},
|
||||
{
|
||||
Options{Width: 0.15, Height: 1.3, Rotate: 45, Quality: 95, Signature: "c0ffee", Format: "png"},
|
||||
"0.15x1.3,png,q95,r45,sc0ffee",
|
||||
Options{Width: 0.15, Height: 1.3, Rotate: 45, Quality: 95, Signature: "c0ffee", Format: "png", ValidUntil: time.Unix(123, 0)},
|
||||
"0.15x1.3,png,q95,r45,sc0ffee,vu123",
|
||||
},
|
||||
{
|
||||
Options{Width: 0.15, Height: 1.3, CropX: 100, CropY: 200},
|
||||
|
|
@ -86,7 +87,7 @@ func TestParseOptions(t *testing.T) {
|
|||
// flags, in different orders
|
||||
{"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"}},
|
||||
{"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"}},
|
||||
{"cx100,cw300,1x2,cy200,ch400,sc,scaleUp", Options{Width: 1, Height: 2, ScaleUp: true, CropX: 100, CropY: 200, CropWidth: 300, CropHeight: 400, SmartCrop: true}},
|
||||
{"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)}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ type Proxy struct {
|
|||
// remote server specifies 'private' or 'no-store' in the cache-control
|
||||
// header.
|
||||
ForceCache bool
|
||||
|
||||
timeNow time.Time // current time, used for testing
|
||||
}
|
||||
|
||||
// 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) {
|
|||
}
|
||||
}
|
||||
resp, err := p.Client.Do(actualReq)
|
||||
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("error fetching remote image: %v", err)
|
||||
p.log(msg)
|
||||
|
|
@ -371,15 +372,29 @@ var (
|
|||
errDeniedHost = errors.New("request contains a denied host")
|
||||
errNotAllowed = errors.New("request does not contain an allowed host or valid signature")
|
||||
errTooManyRedirects = errors.New("too many redirects")
|
||||
errNotValid = errors.New("request is no longer valid")
|
||||
|
||||
msgNotAllowed = "requested URL is not allowed"
|
||||
msgNotAllowedInRedirect = "requested URL in redirect is not allowed"
|
||||
)
|
||||
|
||||
func (p *Proxy) now() time.Time {
|
||||
if !p.timeNow.IsZero() {
|
||||
return p.timeNow
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// allowed determines whether the specified request contains an allowed
|
||||
// referrer, host, and signature. It returns an error if the request is not
|
||||
// allowed.
|
||||
// allowed or not valid any longer.
|
||||
func (p *Proxy) allowed(r *Request) error {
|
||||
if !r.Options.ValidUntil.IsZero() {
|
||||
if !p.now().Before(r.Options.ValidUntil) {
|
||||
return errNotValid
|
||||
}
|
||||
}
|
||||
|
||||
if len(p.Referrers) > 0 && !referrerMatches(p.Referrers, r.Original) {
|
||||
return errReferrer
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,8 +110,11 @@ func TestAllowed(t *testing.T) {
|
|||
return req
|
||||
}
|
||||
|
||||
now := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
tests := []struct {
|
||||
url string
|
||||
now time.Time
|
||||
options Options
|
||||
allowHosts []string
|
||||
denyHosts []string
|
||||
|
|
@ -153,6 +156,11 @@ func TestAllowed(t *testing.T) {
|
|||
{url: "http://test/image", options: Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ="}, denyHosts: []string{"test"}, keys: key, allowed: false},
|
||||
{url: "http://127.0.0.1/image", denyHosts: []string{"127.0.0.0/8"}, allowed: false},
|
||||
{url: "http://127.0.0.1:3000/image", denyHosts: []string{"127.0.0.0/8"}, allowed: false},
|
||||
|
||||
// valid until options
|
||||
{url: "http://test/image", now: now, options: Options{ValidUntil: now.Add(time.Second)}, allowed: true},
|
||||
{url: "http://test/image", now: now, options: Options{ValidUntil: now.Add(-time.Second)}, allowed: false},
|
||||
{url: "http://test/image", now: now, options: Options{ValidUntil: now}, allowed: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -161,6 +169,7 @@ func TestAllowed(t *testing.T) {
|
|||
p.DenyHosts = tt.denyHosts
|
||||
p.SignatureKeys = tt.keys
|
||||
p.Referrers = tt.referrers
|
||||
p.timeNow = tt.now
|
||||
|
||||
u, err := url.Parse(tt.url)
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue