Skip to content

Commit 42f44da

Browse files
committed
image-proxy refactor redirect handling, and fix resource release
1 parent fda20b6 commit 42f44da

File tree

1 file changed

+115
-112
lines changed

1 file changed

+115
-112
lines changed

pkg/image_proxy/lib/image_proxy_service.dart

Lines changed: 115 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -160,89 +160,81 @@ Future<shelf.Response> handler(shelf.Request request) async {
160160
}
161161
final imageUrlBytes = utf8.encode(imageUrl);
162162

163-
if (_constantTimeEquals(hmacSign(secret, imageUrlBytes), signature)) {
164-
final Uri parsedImageUrl;
165-
try {
166-
parsedImageUrl = Uri.parse(imageUrl);
167-
} on FormatException catch (e) {
168-
return shelf.Response.badRequest(
169-
body: 'Malformed proxied url $e',
170-
headers: securityHeaders,
171-
);
172-
}
173-
if (!(parsedImageUrl.isScheme('http') ||
174-
parsedImageUrl.isScheme('https'))) {
175-
return shelf.Response.badRequest(
176-
body: 'Can only proxy http and https urls',
177-
headers: securityHeaders,
178-
);
179-
}
180-
if (!parsedImageUrl.isAbsolute) {
181-
return shelf.Response.badRequest(
182-
body: 'Can only proxy absolute urls',
183-
headers: securityHeaders,
184-
);
185-
}
163+
if (!_constantTimeEquals(hmacSign(secret, imageUrlBytes), signature)) {
164+
return shelf.Response.unauthorized('Bad hmac', headers: securityHeaders);
165+
}
166+
final Uri parsedImageUrl;
167+
try {
168+
parsedImageUrl = Uri.parse(imageUrl);
169+
} on FormatException catch (e) {
170+
return shelf.Response.badRequest(
171+
body: 'Malformed proxied url $e',
172+
headers: securityHeaders,
173+
);
174+
}
175+
if (!(parsedImageUrl.isScheme('http') ||
176+
parsedImageUrl.isScheme('https'))) {
177+
return shelf.Response.badRequest(
178+
body: 'Can only proxy http and https urls',
179+
headers: securityHeaders,
180+
);
181+
}
182+
if (!parsedImageUrl.isAbsolute) {
183+
return shelf.Response.badRequest(
184+
body: 'Can only proxy absolute urls',
185+
headers: securityHeaders,
186+
);
187+
}
186188

187-
int statusCode;
188-
List<int> bytes;
189-
String? contentType;
190-
String? contentEncoding;
189+
Future<
190+
({
191+
int statusCode,
192+
List<int> body,
193+
String? contentType,
194+
String? contentEncoding,
195+
})
196+
>
197+
makeRequest(Uri url, {int redirectCount = 0}) async {
198+
stderr.writeln('Requesting $url');
199+
if (redirectCount > 10) {
200+
throw RedirectException('Too many redirects.');
201+
}
202+
final request = await client.getUrl(url);
203+
final timeout = Timer(timeoutDelay, () {
204+
request.abort(RequestTimeoutException('No response'));
205+
});
206+
HttpClientResponse? response;
191207
try {
192-
(statusCode, bytes, contentType, contentEncoding) = await retry(
193-
maxDelay: timeoutDelay,
194-
maxAttempts: isTesting ? 2 : 8,
195-
() async {
196-
final request = await client.getUrl(parsedImageUrl);
197-
Timer? timeoutTimer;
198-
void scheduleRequestTimeout() {
199-
timeoutTimer?.cancel();
200-
timeoutTimer = Timer(timeoutDelay, () {
201-
request.abort(RequestTimeoutException('No response'));
202-
});
203-
}
204-
205-
request.headers.add(
206-
'user-agent',
207-
'Image proxy for pub.dev. See https://github.com/dart-lang/pub-dev/pkg/image-proxy. If you have any issues, contact [email protected].',
208-
);
209-
request.followRedirects = false;
210-
scheduleRequestTimeout();
211-
var response = await request.close();
212-
var redirectCount = 0;
213-
while (response.isRedirect) {
214-
await response.drain();
215-
redirectCount++;
216-
if (redirectCount > 10) {
217-
throw RedirectException('Too many redirects.');
218-
}
219-
final location = response.headers.value(
220-
HttpHeaders.locationHeader,
221-
);
222-
if (location == null) {
223-
throw RedirectException('No location header in redirect.');
224-
}
225-
final uri = parsedImageUrl.resolve(location);
226-
final request = await client.getUrl(uri);
227-
228-
request.headers.add('user-agent', 'pub-proxy');
229-
// Set the body or headers as desired.
230-
request.followRedirects = false;
231-
scheduleRequestTimeout();
232-
response = await request.close();
233-
}
234-
switch (response.statusCode) {
235-
case final int statusCode && >= 500 && < 600:
236-
throw ServerSideException(statusCode: statusCode);
237-
case final int statusCode && >= 300 && < 400:
238-
throw ServerSideException(statusCode: statusCode);
239-
}
240-
final contentLength = response.contentLength;
241-
if (contentLength != -1 && contentLength > maxImageSize) {
242-
throw TooLargeException();
243-
}
244-
return (
245-
response.statusCode,
208+
request.headers.add(
209+
'user-agent',
210+
'Image proxy for pub.dev. See https://github.com/dart-lang/pub-dev/pkg/image-proxy. If you have any issues, contact [email protected].',
211+
);
212+
request.followRedirects = false;
213+
response = await request.close();
214+
if (response.isRedirect) {
215+
await response.listen((_) => null).cancel();
216+
final location = response.headers.value(HttpHeaders.locationHeader);
217+
if (location == null) {
218+
throw RedirectException('No location header in redirect.');
219+
}
220+
return makeRequest(
221+
parsedImageUrl.resolve(location),
222+
redirectCount: redirectCount + 1,
223+
);
224+
}
225+
switch (response.statusCode) {
226+
case final int statusCode && >= 500 && < 600:
227+
throw ServerSideException(statusCode: statusCode);
228+
case final int statusCode && >= 300 && < 400:
229+
throw ServerSideException(statusCode: statusCode);
230+
}
231+
final contentLength = response.contentLength;
232+
if (contentLength != -1 && contentLength > maxImageSize) {
233+
throw TooLargeException();
234+
}
235+
return (
236+
statusCode: response.statusCode,
237+
body:
246238
await readAllBytes(
247239
response,
248240
contentLength == -1 ? maxImageSize : contentLength,
@@ -252,49 +244,60 @@ Future<shelf.Response> handler(shelf.Request request) async {
252244
throw RequestTimeoutException('No response');
253245
},
254246
),
255-
response.headers.value('content-type'),
256-
response.headers.value('content-encoding'),
257-
);
258-
},
259-
retryIf: (e) =>
260-
e is SocketException ||
261-
e is http.ClientException ||
262-
e is ServerSideException,
263-
);
264-
} on TooLargeException {
265-
return shelf.Response.badRequest(
266-
body: 'Image too large',
267-
headers: securityHeaders,
268-
);
269-
} on RedirectException catch (e) {
270-
return shelf.Response.badRequest(
271-
body: e.message,
272-
headers: securityHeaders,
273-
);
274-
} on RequestTimeoutException catch (e) {
275-
return shelf.Response.badRequest(
276-
body: e.message,
277-
headers: securityHeaders,
278-
);
279-
} on ServerSideException catch (e) {
280-
return shelf.Response.badRequest(
281-
body: 'Failed to retrieve image. Status code ${e.statusCode}',
282-
headers: securityHeaders,
247+
contentType: response.headers.value('content-type'),
248+
contentEncoding: response.headers.value('content-encoding'),
283249
);
250+
} finally {
251+
timeout.cancel();
252+
try {
253+
// Attempt closing resources
254+
request.abort();
255+
await response?.listen((_) => null).cancel();
256+
} catch (_) {}
284257
}
258+
}
259+
260+
try {
261+
final (:statusCode, :body, :contentType, :contentEncoding) = await retry(
262+
maxDelay: timeoutDelay,
263+
maxAttempts: isTesting ? 2 : 8,
264+
() => makeRequest(parsedImageUrl),
265+
retryIf: (e) =>
266+
e is SocketException ||
267+
e is http.ClientException ||
268+
e is ServerSideException,
269+
);
285270

286271
return shelf.Response(
287272
statusCode,
288-
body: bytes,
273+
body: body,
289274
headers: {
290275
'Cache-control': 'max-age=180, public',
291276
'content-type': ?contentType,
292277
'content-encoding': ?contentEncoding,
293278
...securityHeaders,
294279
},
295280
);
296-
} else {
297-
return shelf.Response.unauthorized('Bad hmac', headers: securityHeaders);
281+
} on TooLargeException {
282+
return shelf.Response.badRequest(
283+
body: 'Image too large',
284+
headers: securityHeaders,
285+
);
286+
} on RedirectException catch (e) {
287+
return shelf.Response.badRequest(
288+
body: e.message,
289+
headers: securityHeaders,
290+
);
291+
} on RequestTimeoutException catch (e) {
292+
return shelf.Response.badRequest(
293+
body: e.message,
294+
headers: securityHeaders,
295+
);
296+
} on ServerSideException catch (e) {
297+
return shelf.Response.badRequest(
298+
body: 'Failed to retrieve image. Status code ${e.statusCode}',
299+
headers: securityHeaders,
300+
);
298301
}
299302
} catch (e, st) {
300303
stderr.writeln('Uncaught error: $e $st');

0 commit comments

Comments
 (0)