Skip to content

Commit 0da52d1

Browse files
committed
allow base64 encoding the remote URL
Updates #431 Updates #447
1 parent f2bc671 commit 0da52d1

File tree

3 files changed

+84
-18
lines changed

3 files changed

+84
-18
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,12 @@ The URL of the original image to load is specified as the remainder of the
4949
path, without any encoding. For example,
5050
`http://localhost/200/https://willnorris.com/logo.jpg`.
5151

52-
In order to [optimize caching][], it is recommended that URLs not contain query
53-
strings.
52+
If the URL contains a query string, it is treated as part of the remote URL.
5453

55-
[optimize caching]: http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/
54+
Alternatively, the remote URL may be base64 encoded (URL safe, no padding).
55+
This can be helpful if the URL contains characters or encoding that imageproxy
56+
is not handling properly. For example,
57+
`http://localhost/200/aHR0cHM6Ly93aWxsbm9ycmlzLmNvbS9sb2dvLmpwZw`.
5658

5759
### Examples
5860

data.go

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
package imageproxy
55

66
import (
7+
"encoding/base64"
78
"fmt"
89
"net/http"
910
"net/url"
1011
"regexp"
1112
"sort"
1213
"strconv"
1314
"strings"
15+
"unicode"
1416
)
1517

1618
const (
@@ -325,8 +327,10 @@ func (r Request) String() string {
325327
// NewRequest parses an http.Request into an imageproxy Request. Options and
326328
// the remote image URL are specified in the request path, formatted as:
327329
// /{options}/{remote_url}. Options may be omitted, so a request path may
328-
// simply contain /{remote_url}. The remote URL must be an absolute "http" or
329-
// "https" URL, should not be URL encoded, and may contain a query string.
330+
// simply contain /{remote_url}. The remote URL must either be:
331+
//
332+
// - an absolute "http" or "https" URL, not be URL encoded, with optional query string, or
333+
// - base64 encoded (URL safe, no padding).
330334
//
331335
// Assuming an imageproxy server running on localhost, the following are all
332336
// valid imageproxy requests:
@@ -335,12 +339,14 @@ func (r Request) String() string {
335339
// http://localhost/100x200,r90/http://example.com/image.jpg?foo=bar
336340
// http://localhost//http://example.com/image.jpg
337341
// http://localhost/http://example.com/image.jpg
342+
// http://localhost/100x200/aHR0cDovL2V4YW1wbGUuY29tL2ltYWdlLmpwZw
338343
func NewRequest(r *http.Request, baseURL *url.URL) (*Request, error) {
339344
var err error
340345
req := &Request{Original: r}
346+
var enc bool // whether the remote URL was base64 encoded
341347

342348
path := r.URL.EscapedPath()[1:] // strip leading slash
343-
req.URL, err = parseURL(path)
349+
req.URL, enc, err = parseURL(path, baseURL)
344350
if err != nil || !req.URL.IsAbs() {
345351
// first segment should be options
346352
parts := strings.SplitN(path, "/", 2)
@@ -349,7 +355,7 @@ func NewRequest(r *http.Request, baseURL *url.URL) (*Request, error) {
349355
}
350356

351357
var err error
352-
req.URL, err = parseURL(parts[1])
358+
req.URL, enc, err = parseURL(parts[1], baseURL)
353359
if err != nil {
354360
return nil, URLError{fmt.Sprintf("unable to parse remote URL: %v", err), r.URL}
355361
}
@@ -369,16 +375,38 @@ func NewRequest(r *http.Request, baseURL *url.URL) (*Request, error) {
369375
return nil, URLError{"remote URL must have http or https scheme", r.URL}
370376
}
371377

372-
// query string is always part of the remote URL
373-
req.URL.RawQuery = r.URL.RawQuery
378+
if !enc {
379+
// if the remote URL was not base64-encoded,
380+
// then the query string is part of the remote URL
381+
req.URL.RawQuery = r.URL.RawQuery
382+
}
374383
return req, nil
375384
}
376385

377386
var reCleanedURL = regexp.MustCompile(`^(https?):/+([^/])`)
378387

379388
// parseURL parses s as a URL, handling URLs that have been munged by
380389
// path.Clean or a webserver that collapses multiple slashes.
381-
func parseURL(s string) (*url.URL, error) {
390+
// The returned enc bool indicates whether the remote URL was encoded.
391+
func parseURL(s string, baseURL *url.URL) (_ *url.URL, enc bool, _ error) {
392+
// Try to base64 decode the string. If it is not base64 encoded,
393+
// this will fail quickly on the first invalid character like ":", ".", or "/".
394+
// Accept the decoded string if it looks like an absolute HTTP URL,
395+
// or if we have a baseURL and the decoded string did not contain invalid code points.
396+
// This allows for values like "/path", which do successfully base64 decode,
397+
// but not to valid code points, to be treated as an unencoded string.
398+
if b, err := base64.RawURLEncoding.DecodeString(s); err == nil {
399+
d := string(b)
400+
if strings.HasPrefix(d, "http://") || strings.HasPrefix(d, "https://") {
401+
enc = true
402+
s = d
403+
} else if baseURL != nil && !strings.ContainsRune(d, unicode.ReplacementChar) {
404+
enc = true
405+
s = d
406+
}
407+
}
408+
382409
s = reCleanedURL.ReplaceAllString(s, "$1://$2")
383-
return url.Parse(s)
410+
u, err := url.Parse(s)
411+
return u, enc, err
384412
}

data_test.go

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,27 @@ func TestNewRequest(t *testing.T) {
152152
"http://localhost/http:///example.com/foo",
153153
"http://example.com/foo", emptyOptions, false,
154154
},
155+
// base64 encoded paths
156+
{
157+
"http://localhost/aHR0cDovL2V4YW1wbGUuY29tL2Zvbw",
158+
"http://example.com/foo", emptyOptions, false,
159+
},
160+
{
161+
"http://localhost//aHR0cDovL2V4YW1wbGUuY29tL2Zvbw",
162+
"http://example.com/foo", emptyOptions, false,
163+
},
164+
{
165+
"http://localhost/x/aHR0cDovL2V4YW1wbGUuY29tL2Zvbw",
166+
"http://example.com/foo", emptyOptions, false,
167+
},
168+
{
169+
"http://localhost/x/aHR0cHM6Ly9leGFtcGxlLmNvbS9mb28_YmFy",
170+
"https://example.com/foo?bar", emptyOptions, false,
171+
},
172+
{
173+
"http://localhost/x/aHR0cHM6Ly9leGFtcGxlLmNvbS9mb28_YmFy?baz",
174+
"https://example.com/foo?bar", emptyOptions, false,
175+
},
155176
{ // escaped path
156177
"http://localhost/http://example.com/%2C",
157178
"http://example.com/%2C", emptyOptions, false,
@@ -186,16 +207,31 @@ func TestNewRequest(t *testing.T) {
186207
}
187208

188209
func TestNewRequest_BaseURL(t *testing.T) {
189-
req, _ := http.NewRequest("GET", "/x/path", nil)
190210
base, _ := url.Parse("https://example.com/")
191211

192-
r, err := NewRequest(req, base)
193-
if err != nil {
194-
t.Errorf("NewRequest(%v, %v) returned unexpected error: %v", req, base, err)
212+
tests := []struct {
213+
path string
214+
want string
215+
}{
216+
{
217+
path: "/x/path",
218+
want: "https://example.com/path#0x0",
219+
},
220+
{ // Chinese characters 已然
221+
path: "/x/5bey54S2",
222+
want: "https://example.com/%E5%B7%B2%E7%84%B6#0x0",
223+
},
195224
}
196225

197-
want := "https://example.com/path#0x0"
198-
if got := r.String(); got != want {
199-
t.Errorf("NewRequest(%v, %v) returned %q, want %q", req, base, got, want)
226+
for _, tt := range tests {
227+
req, _ := http.NewRequest("GET", tt.path, nil)
228+
r, err := NewRequest(req, base)
229+
if err != nil {
230+
t.Errorf("NewRequest(%v, %v) returned unexpected error: %v", req, base, err)
231+
}
232+
233+
if got := r.String(); got != tt.want {
234+
t.Errorf("NewRequest(%v, %v) returned %q, want %q", req, base, got, tt.want)
235+
}
200236
}
201237
}

0 commit comments

Comments
 (0)