mirror of
https://github.com/willnorris/imageproxy.git
synced 2025-03-11 02:19:14 -05:00
allow request signatures to cover options
URL-only signatures are still accepted, though no longer recommended. Fixes #145
This commit is contained in:
parent
ae2a31cc01
commit
38d3bcc7fe
3 changed files with 93 additions and 12 deletions
|
@ -1,6 +1,71 @@
|
||||||
# How to generate signed requests
|
# How to generate signed requests
|
||||||
|
|
||||||
## Go
|
Signing requests allows an imageproxy instance to proxy images from arbitrary
|
||||||
|
remote hosts, but without opening the service up for potential abuse. When
|
||||||
|
appropriately configured, the imageproxy instance will only serve requests that
|
||||||
|
are for allowed hosts, or which have a valid signature.
|
||||||
|
|
||||||
|
Signatures can be calculated in two ways:
|
||||||
|
|
||||||
|
1. they can be calculated solely on the remote image URL, in which case any
|
||||||
|
transformations of the image can be requested without changes to the
|
||||||
|
signature value. This used to be the only way to sign requests, but is no
|
||||||
|
longer recommended since it still leaves the imageproxy instance open to
|
||||||
|
potential abuse.
|
||||||
|
|
||||||
|
2. they can be calculated based on the combination of the remote image URL and
|
||||||
|
the requested transformation options.
|
||||||
|
|
||||||
|
In both cases, the signature is calculated using HMAC-SHA256 and a secret key
|
||||||
|
which is provided to imageproxy on startup. The message to be signed is the
|
||||||
|
remote URL, with the transformation options optionally set as the URL fragment,
|
||||||
|
[as documented below](#Signing-options). The signature is url-safe base64
|
||||||
|
encoded, and [provided as an option][s-option] in the imageproxy request.
|
||||||
|
|
||||||
|
imageproxy will accept signatures for URLs with or without options
|
||||||
|
transparently. It's up to the publisher of the signed URLs to decide which
|
||||||
|
method they use to generate the URL.
|
||||||
|
|
||||||
|
[s-option]: https://godoc.org/willnorris.com/go/imageproxy#hdr-Signature
|
||||||
|
|
||||||
|
## Signing options
|
||||||
|
|
||||||
|
Transformation options for a proxied URL are [specified as a comma separated
|
||||||
|
string][ParseOptions] of individual options, which can be supplied in any
|
||||||
|
order. When calculating a signature, options should be put in their canonical
|
||||||
|
form, sorted in lexigraphical order (omitting the signature option itself), and
|
||||||
|
appended to the remote URL as the URL fragment.
|
||||||
|
|
||||||
|
Currently, only [size option][] has a canonical form, which is
|
||||||
|
`{width}x{height}` with the number `0` used when no value is specified. For
|
||||||
|
example, a request that does not request any size option would still have a
|
||||||
|
canonical size value of `0x0`, indicating that no size transformation is being
|
||||||
|
performed. If only a height of 500px is requested, the canonical form would be
|
||||||
|
`0x500`.
|
||||||
|
|
||||||
|
For example, requesting the remote URL of `http://example.com/image.jpg`,
|
||||||
|
resized to 100 pixels square, rotated 90 degrees, and converted to 75% quality
|
||||||
|
might produce an imageproxy URL similar to:
|
||||||
|
|
||||||
|
http://localhost:8080/100,r90,q75/http://example.com/image.jpg
|
||||||
|
|
||||||
|
When calculating a signature for this request including transformation options,
|
||||||
|
the signed value would be:
|
||||||
|
|
||||||
|
http://example.com/image.jpg#100x100,q75,r90
|
||||||
|
|
||||||
|
The `100` size option was put in its canonical form of `100x100`, and the
|
||||||
|
options are sorted, moving `q75` before `r90`.
|
||||||
|
|
||||||
|
[ParseOptions]: https://godoc.org/willnorris.com/go/imageproxy#ParseOptions
|
||||||
|
[size option]: https://godoc.org/willnorris.com/go/imageproxy#hdr-Size_and_Cropping
|
||||||
|
|
||||||
|
## Language Examples
|
||||||
|
|
||||||
|
Here are examples of calculating signatures in a variety of languages. These
|
||||||
|
demonstrate the HMAC-SHA256 bits, but not the option canonicalization.
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
main.go:
|
main.go:
|
||||||
```go
|
```go
|
||||||
|
@ -27,14 +92,14 @@ $ go run main.go "test" "https://www.google.fr/images/srpr/logo11w.png"
|
||||||
result: RYifAJRfbhsitJeOrDNxWURCCkPsVR4ihCPXNv-ePbA=
|
result: RYifAJRfbhsitJeOrDNxWURCCkPsVR4ihCPXNv-ePbA=
|
||||||
```
|
```
|
||||||
|
|
||||||
## OpenSSL
|
### OpenSSL
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ echo -n "https://www.google.fr/images/srpr/logo11w.png" | openssl dgst -sha256 -hmac "test" -binary|base64| tr '/+' '_-'
|
$ echo -n "https://www.google.fr/images/srpr/logo11w.png" | openssl dgst -sha256 -hmac "test" -binary|base64| tr '/+' '_-'
|
||||||
RYifAJRfbhsitJeOrDNxWURCCkPsVR4ihCPXNv-ePbA=
|
RYifAJRfbhsitJeOrDNxWURCCkPsVR4ihCPXNv-ePbA=
|
||||||
```
|
```
|
||||||
|
|
||||||
## Java
|
### Java
|
||||||
|
|
||||||
```java
|
```java
|
||||||
import org.apache.commons.codec.binary.Base64;
|
import org.apache.commons.codec.binary.Base64;
|
||||||
|
@ -63,7 +128,7 @@ $ java -cp commons-codec-1.10.jar:. EncodeUrl test https://www.google.fr/images/
|
||||||
RYifAJRfbhsitJeOrDNxWURCCkPsVR4ihCPXNv-ePbA
|
RYifAJRfbhsitJeOrDNxWURCCkPsVR4ihCPXNv-ePbA
|
||||||
```
|
```
|
||||||
|
|
||||||
## Ruby
|
### Ruby
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
require 'openssl'
|
require 'openssl'
|
||||||
|
@ -79,7 +144,7 @@ puts Base64.urlsafe_encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'),
|
||||||
RYifAJRfbhsitJeOrDNxWURCCkPsVR4ihCPXNv-ePbA=
|
RYifAJRfbhsitJeOrDNxWURCCkPsVR4ihCPXNv-ePbA=
|
||||||
```
|
```
|
||||||
|
|
||||||
## Python
|
### Python
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import hmac
|
import hmac
|
||||||
|
@ -91,7 +156,7 @@ data = 'https://octodex.github.com/images/codercat.jpg'
|
||||||
print base64.urlsafe_b64encode(hmac.new(key, msg=data, digestmod=hashlib.sha256).digest())
|
print base64.urlsafe_b64encode(hmac.new(key, msg=data, digestmod=hashlib.sha256).digest())
|
||||||
```
|
```
|
||||||
|
|
||||||
## JavaScript
|
### JavaScript
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
@ -102,7 +167,7 @@ let data = 'https://octodex.github.com/images/codercat.jpg';
|
||||||
console.log(URLSafeBase64.encode(crypto.createHmac('sha256', key).update(data).digest()));
|
console.log(URLSafeBase64.encode(crypto.createHmac('sha256', key).update(data).digest()));
|
||||||
```
|
```
|
||||||
|
|
||||||
## PHP
|
### PHP
|
||||||
|
|
||||||
````php
|
````php
|
||||||
<?php
|
<?php
|
||||||
|
@ -114,4 +179,4 @@ echo strtr(base64_encode(hash_hmac('sha256', $data, $key, 1)), '/+' , '_-');
|
||||||
````shell
|
````shell
|
||||||
$ php ex.php
|
$ php ex.php
|
||||||
RYifAJRfbhsitJeOrDNxWURCCkPsVR4ihCPXNv-ePbA=
|
RYifAJRfbhsitJeOrDNxWURCCkPsVR4ihCPXNv-ePbA=
|
||||||
````
|
````
|
||||||
|
|
|
@ -146,15 +146,15 @@ func (p *Proxy) serveImage(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// assign static settings from proxy to req.Options
|
|
||||||
req.Options.ScaleUp = p.ScaleUp
|
|
||||||
|
|
||||||
if err := p.allowed(req); err != nil {
|
if err := p.allowed(req); err != nil {
|
||||||
log.Printf("%s: %v", err, req)
|
log.Printf("%s: %v", err, req)
|
||||||
http.Error(w, msgNotAllowed, http.StatusForbidden)
|
http.Error(w, msgNotAllowed, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// assign static settings from proxy to req.Options
|
||||||
|
req.Options.ScaleUp = p.ScaleUp
|
||||||
|
|
||||||
actualReq, _ := http.NewRequest("GET", req.String(), nil)
|
actualReq, _ := http.NewRequest("GET", req.String(), nil)
|
||||||
if p.UserAgent != "" {
|
if p.UserAgent != "" {
|
||||||
actualReq.Header.Set("User-Agent", p.UserAgent)
|
actualReq.Header.Set("User-Agent", p.UserAgent)
|
||||||
|
@ -322,10 +322,22 @@ func validSignature(key []byte, r *Request) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check signature with URL only
|
||||||
mac := hmac.New(sha256.New, key)
|
mac := hmac.New(sha256.New, key)
|
||||||
mac.Write([]byte(r.URL.String()))
|
mac.Write([]byte(r.URL.String()))
|
||||||
want := mac.Sum(nil)
|
want := mac.Sum(nil)
|
||||||
|
if hmac.Equal(got, want) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// check signature with URL and options
|
||||||
|
u, opt := *r.URL, r.Options // make copies
|
||||||
|
opt.Signature = ""
|
||||||
|
u.Fragment = opt.String()
|
||||||
|
|
||||||
|
mac = hmac.New(sha256.New, key)
|
||||||
|
mac.Write([]byte(u.String()))
|
||||||
|
want = mac.Sum(nil)
|
||||||
return hmac.Equal(got, want)
|
return hmac.Equal(got, want)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -228,6 +228,10 @@ func TestValidSignature(t *testing.T) {
|
||||||
{"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ="}, true},
|
{"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ="}, true},
|
||||||
{"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ"}, true},
|
{"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ"}, true},
|
||||||
{"http://test/image", emptyOptions, false},
|
{"http://test/image", emptyOptions, false},
|
||||||
|
// url-only signature with options
|
||||||
|
{"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ", Rotate: 90}, true},
|
||||||
|
// signature calculated from url plus options
|
||||||
|
{"http://test/image", Options{Signature: "ZGTzEm32o4iZ7qcChls3EVYaWyrDd9u0etySo0-WkF8=", Rotate: 90}, true},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@ -237,7 +241,7 @@ func TestValidSignature(t *testing.T) {
|
||||||
}
|
}
|
||||||
req := &Request{u, tt.options, &http.Request{}}
|
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, %v) returned %v, want %v", key, req, got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue