Skip to content

Commit 2254a1f

Browse files
beattyml1willnorris
andcommitted
add support for URL encoding remote URL
Updates #250 Updates #290 Fixes #447 Co-authored-by: Will Norris <[email protected]>
1 parent d04e37f commit 2254a1f

File tree

3 files changed

+82
-14
lines changed

3 files changed

+82
-14
lines changed

README.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,23 @@ See the full list of available options at
4646
### Remote URL
4747

4848
The URL of the original image to load is specified as the remainder of the
49-
path, without any encoding. For example,
50-
`http://localhost/200/https://willnorris.com/logo.jpg`.
51-
52-
If the URL contains a query string, it is treated as part of the remote URL.
53-
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`.
49+
path. It may be included in plain text without any encoding,
50+
percent-encoded (aka URL encoded), or base64 encoded (URL safe, no padding).
51+
52+
When no encoding is used, any URL query string is treated as part of the remote URL.
53+
For example, given the proxy URL of `http://localhost/x/http://example.com/?id=1`,
54+
the remote URL is `http://example.com/?id=1`.
55+
56+
When percent-encoding is used, the full URL must be encoded.
57+
Any query string on the proxy URL is NOT included as part of the remote URL.
58+
Percent-encoded URLs must be absolute URLs;
59+
they cannot be relative URLs used with a default base URL.
60+
For example, `http://localhost/x/http%3A%2F%2Fexample.com%2F%3Fid%3D1`.
61+
62+
When base64 encoding is used, the full URL must be encoded.
63+
Any query string on the proxy URL is NOT included as part of the remote URL.
64+
Base64 encoded URLs may be relative URLs used with a default base URL.
65+
For example, `http://localhost/x/aHR0cDovL2V4YW1wbGUuY29tLz9pZD0x`.
5866

5967
### Examples
6068

data.go

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -327,10 +327,23 @@ func (r Request) String() string {
327327
// NewRequest parses an http.Request into an imageproxy Request. Options and
328328
// the remote image URL are specified in the request path, formatted as:
329329
// /{options}/{remote_url}. Options may be omitted, so a request path may
330-
// simply contain /{remote_url}. The remote URL must either be:
330+
// simply contain /{remote_url}.
331331
//
332-
// - an absolute "http" or "https" URL, not be URL encoded, with optional query string, or
333-
// - base64 encoded (URL safe, no padding).
332+
// The remote URL may be included in plain text without any encoding,
333+
// percent-encoded (aka URL encoded), or base64 encoded (URL safe, no padding).
334+
//
335+
// When no encoding is used, any URL query string is treated as part of the remote URL.
336+
// For example, given the proxy URL of `http://localhost/x/http://example.com/?id=1`,
337+
// the remote URL is `http://example.com/?id=1`.
338+
//
339+
// When percent-encoding is used, the full URL must be encoded.
340+
// Any query string on the proxy URL is NOT included as part of the remote URL.
341+
// Percent-encoded URLs must be absolute URLs;
342+
// they cannot be relative URLs used with a default base URL.
343+
//
344+
// When base64 encoding is used, the full URL must be encoded.
345+
// Any query string on the proxy URL is NOT included as part of the remote URL.
346+
// Base64 encoded URLs may be relative URLs used with a default base URL.
334347
//
335348
// Assuming an imageproxy server running on localhost, the following are all
336349
// valid imageproxy requests:
@@ -339,11 +352,12 @@ func (r Request) String() string {
339352
// http://localhost/100x200,r90/http://example.com/image.jpg?foo=bar
340353
// http://localhost//http://example.com/image.jpg
341354
// http://localhost/http://example.com/image.jpg
355+
// http://localhost/x/http%3A%2F%2Fexample.com%2Fimage.jpg
342356
// http://localhost/100x200/aHR0cDovL2V4YW1wbGUuY29tL2ltYWdlLmpwZw
343357
func NewRequest(r *http.Request, baseURL *url.URL) (*Request, error) {
344358
var err error
345359
req := &Request{Original: r}
346-
var enc bool // whether the remote URL was base64 encoded
360+
var enc bool // whether the remote URL was base64 or URL encoded
347361

348362
path := r.URL.EscapedPath()[1:] // strip leading slash
349363
req.URL, enc, err = parseURL(path, baseURL)
@@ -376,14 +390,15 @@ func NewRequest(r *http.Request, baseURL *url.URL) (*Request, error) {
376390
}
377391

378392
if !enc {
379-
// if the remote URL was not base64-encoded,
393+
// if the remote URL was not base64 or URL encoded,
380394
// then the query string is part of the remote URL
381395
req.URL.RawQuery = r.URL.RawQuery
382396
}
383397
return req, nil
384398
}
385399

386400
var reCleanedURL = regexp.MustCompile(`^(https?):/+([^/])`)
401+
var reIsEncodedURL = regexp.MustCompile(`^(?i)https?%3A%2F`)
387402

388403
// parseURL parses s as a URL, handling URLs that have been munged by
389404
// path.Clean or a webserver that collapses multiple slashes.
@@ -406,6 +421,14 @@ func parseURL(s string, baseURL *url.URL) (_ *url.URL, enc bool, _ error) {
406421
}
407422
}
408423

424+
// If the string looks like a URL encoded absolute HTTP(S) URL, decode it.
425+
if reIsEncodedURL.MatchString(s) {
426+
if u, err := url.PathUnescape(s); err == nil {
427+
enc = true
428+
s = u
429+
}
430+
}
431+
409432
s = reCleanedURL.ReplaceAllString(s, "$1://$2")
410433
u, err := url.Parse(s)
411434
return u, enc, err

data_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,43 @@ func TestNewRequest(t *testing.T) {
177177
"http://localhost/http://example.com/%2C",
178178
"http://example.com/%2C", emptyOptions, false,
179179
},
180+
// percent encoded cases
181+
{
182+
"http://localhost/1x2/http%3A%2F%2Fexample.com%2Ffoo",
183+
"http://example.com/foo", Options{Width: 1, Height: 2}, false,
184+
},
185+
{
186+
"http://localhost/1x2/http%3A%2F%2Fexample.com%2Fhttp%2Fstuff",
187+
"http://example.com/http/stuff", Options{Width: 1, Height: 2}, false,
188+
},
189+
{
190+
"http://localhost/http%3A%2F%2Fexample.com%2Ffoo",
191+
"http://example.com/foo", emptyOptions, false,
192+
},
193+
{
194+
"http://localhost/HTTP%3a%2f%2fexample.com%2Ffoo",
195+
"http://example.com/foo", emptyOptions, false,
196+
},
197+
{
198+
"http://localhost/http%3A%2Fexample.com%2Ffoo",
199+
"http://example.com/foo", emptyOptions, false,
200+
},
201+
{
202+
"http://localhost/http%3A%2F%2F%2Fexample.com%2Ffoo",
203+
"http://example.com/foo", emptyOptions, false,
204+
},
205+
{
206+
"http://localhost//http%3A%2F%2Fexample.com%2Ffoo",
207+
"http://example.com/foo", emptyOptions, false,
208+
},
209+
{
210+
"http://localhost/http%3A%2F%2Fexample.com%2Ffoo%3Ftest%3D1%26test%3D2",
211+
"http://example.com/foo?test=1&test=2", emptyOptions, false,
212+
},
213+
{
214+
"http://localhost/1x2/http%3A%2F%2Fexample.com%2Ffoo%3Ftest%3D1%26test%3D2",
215+
"http://example.com/foo?test=1&test=2", Options{Width: 1, Height: 2}, false,
216+
},
180217
}
181218

182219
for _, tt := range tests {

0 commit comments

Comments
 (0)