v0.1.0
A number of updates from @nex3
diff --git a/CHANGELOG.md b/CHANGELOG.md
index faabee1..4009b55 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 0.1.0
+
+* `createProxyHandler` (new deprecated) is replaced with `proxyHandler`.
+
+* Updated to be compatible with RFC 2616 Proxy specification.
+
## 0.0.2
* Updated `README.md` and doc comments on `createProxyHandler`.
diff --git a/LICENSE b/LICENSE
index b490318..5c60afe 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright 2014, the shelf_proxy authors. All rights reserved.
+Copyright 2014, the Dart project authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
diff --git a/README.md b/README.md
index 925154b..0cfa82f 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,19 @@
-[![Build Status](https://drone.io/github.com/kevmoo/shelf_proxy/status.png)](https://drone.io/github.com/kevmoo/shelf_proxy/latest)
+## Proxy for Shelf
-[Shelf][shelf] `handler` to proxy requests to another web server.
+`shelf_proxy` is a [Shelf][] handler that proxies requests to an external
+server. It can be served directly and used as a proxy server, or it can be
+mounted within a larger application to proxy only certain URLs.
-Useful if you want to send a subset of requests to another HTTP endpoint, for
-instance `pub serve`.
+[Shelf]: pub.dartlang.org/packages/shelf
-See `example/example_server.dart` for a usage demonstration.
+```dart
+import 'package:shelf/shelf_io.dart' as shelf_io;
+import 'package:shelf_proxy/shelf_proxy.dart';
-[shelf]: http://pub.dartlang.org/packages/shelf
+void main() {
+ shelf_io.serve(proxyHandler("https://www.dartlang.org"), 'localhost', 8080)
+ .then((server) {
+ print('Proxying at http://${server.address.host}:${server.port}');
+ });
+}
+```
diff --git a/codereview.settings b/codereview.settings
new file mode 100644
index 0000000..620fe13
--- /dev/null
+++ b/codereview.settings
@@ -0,0 +1,3 @@
+CODE_REVIEW_SERVER: https://codereview.chromium.org/
+VIEW_VC: https://github.com/dart-lang/shelf_proxy/commit/
+CC_LIST: reviews@dartlang.org
diff --git a/example/example_server.dart b/example/example_server.dart
index 39be43c..f8dd2cf 100644
--- a/example/example_server.dart
+++ b/example/example_server.dart
@@ -10,7 +10,6 @@
final _encoder = new JsonEncoder.withIndent(' ');
void main() {
-
//
// The api handler responds to requests to '/api' with a JSON document
// containing an incrementing 'count' value.
@@ -33,7 +32,7 @@
//
var cascade = new Cascade()
.add(apiHandler)
- .add(createProxyHandler(Uri.parse('http://localhost:$_PUB_PORT')));
+ .add(proxyHandler(Uri.parse('http://localhost:$_PUB_PORT')));
//
// Creates a pipeline handler which first logs requests and then sends them
diff --git a/lib/shelf_proxy.dart b/lib/shelf_proxy.dart
index 8a9d5b1..fc3cb4d 100644
--- a/lib/shelf_proxy.dart
+++ b/lib/shelf_proxy.dart
@@ -1,75 +1,103 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
library shelf_proxy;
-import 'dart:io';
-
+import 'package:http/http.dart' as http;
+import 'package:path/path.dart' as p;
import 'package:shelf/shelf.dart';
-/// Creates a [Handler] that sends requests to another web server at the
-/// specified [rootUri].
+import 'src/utils.dart';
+
+/// A handler that proxies requests to [url].
///
-/// [rootUri] must be absolue with an http(s) scheme and no query or fragment
-/// components.
+/// To generate the proxy request, this concatenates [url] and [Request.url].
+/// This means that if the handler mounted under `/documentation` and [url] is
+/// `https://www.dartlang.org/docs`, a request to `/documentation/tutorials`
+/// will be proxied to `https://www.dartlang.org/docs/tutorials`.
///
-/// Only requests with method `GET` are allowed. All other methods result in a
-/// `405` - [HttpStatus.METHOD_NOT_ALLOWED] response.
+/// [client] is used internally to make HTTP requests. It defaults to a
+/// `dart:io`-based client.
///
-/// Example:
-///
-/// If [rootUri] is specified as `http://example.com/files`, a request for
-/// `/test/sample.html` would result in a request to
-/// `http://example.com/files/test/sample.html`.
-Handler createProxyHandler(Uri rootUri) {
- if (rootUri.scheme != 'http' && rootUri.scheme != 'https') {
- throw new ArgumentError('rootUri must have a scheme of http or https.');
- }
+/// [proxyName] is used in headers to identify this proxy. It should be a valid
+/// HTTP token or a hostname. It defaults to `shelf_proxy`.
+Handler proxyHandler(url, {http.Client client, String proxyName}) {
+ if (url is String) url = Uri.parse(url);
+ if (client == null) client = new http.Client();
+ if (proxyName == null) proxyName = 'shelf_proxy';
- if (!rootUri.isAbsolute) {
- throw new ArgumentError('rootUri must be absolute.');
- }
+ return (serverRequest) {
+ // TODO(nweiz): Support WebSocket requests.
- if (rootUri.query.isNotEmpty) {
- throw new ArgumentError('rootUri cannot contain a query.');
- }
+ // TODO(nweiz): Handle TRACE requests correctly. See
+ // http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8
+ var requestUrl = url.resolve(serverRequest.url.toString());
+ var clientRequest = new http.StreamedRequest(
+ serverRequest.method, requestUrl);
+ clientRequest.followRedirects = false;
+ clientRequest.headers.addAll(serverRequest.headers);
+ clientRequest.headers['Host'] = url.authority;
- return (Request request) {
- if (request.method != 'GET') {
- return new Response(HttpStatus.METHOD_NOT_ALLOWED);
- }
+ // Add a Via header. See
+ // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
+ _addHeader(clientRequest.headers, 'via',
+ '${serverRequest.protocolVersion} $proxyName');
- // TODO: really need to tear down the client when this is done...
- var client = new HttpClient();
+ store(serverRequest.read(), clientRequest.sink);
+ return client.send(clientRequest).then((clientResponse) {
+ // Add a Via header. See
+ // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
+ _addHeader(clientResponse.headers, 'via', '1.1 $proxyName');
- var url = _getProxyUrl(rootUri, request.url);
+ // Remove the transfer-encoding since the body has already been decoded by
+ // [client].
+ clientResponse.headers.remove('transfer-encoding');
- return client.openUrl(request.method, url).then((ioRequest) {
- return ioRequest.close();
- }).then((ioResponse) {
- var headers = {};
- // dart:io - HttpClientResponse.contentLength is -1 if not defined
- if (ioResponse.contentLength >= 0) {
- headers[HttpHeaders.CONTENT_LENGTH] =
- ioResponse.contentLength.toString();
+ // If the original response was gzipped, it will be decoded by [client]
+ // and we'll have no way of knowing its actual content-length.
+ if (clientResponse.headers['content-encoding'] == 'gzip') {
+ clientResponse.headers.remove('content-encoding');
+ clientResponse.headers.remove('content-length');
+
+ // Add a Warning header. See
+ // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.2
+ _addHeader(clientResponse.headers, 'warning',
+ '214 $proxyName "GZIP decoded"');
}
- return new Response(ioResponse.statusCode, body: ioResponse,
- headers: headers);
+ // Make sure the Location header is pointing to the proxy server rather
+ // than the destination server, if possible.
+ if (clientResponse.isRedirect &&
+ clientResponse.headers.containsKey('location')) {
+ var location = requestUrl.resolve(clientResponse.headers['location'])
+ .toString();
+ if (p.url.isWithin(url.toString(), location)) {
+ clientResponse.headers['location'] =
+ '/' + p.url.relative(location, from: url.toString());
+ } else {
+ clientResponse.headers['location'] = location;
+ }
+ }
+
+ return new Response(clientResponse.statusCode,
+ body: clientResponse.stream,
+ headers: clientResponse.headers);
});
};
}
-Uri _getProxyUrl(Uri proxyRoot, Uri requestUrl) {
- assert(proxyRoot.scheme == 'http' || proxyRoot.scheme == 'https');
- assert(proxyRoot.query == '');
- assert(proxyRoot.isAbsolute);
- assert(!requestUrl.isAbsolute);
+/// Use [proxyHandler] instead.
+@deprecated
+Handler createProxyHandler(Uri rootUri) => proxyHandler(rootUri);
- var updatedPath = proxyRoot.pathSegments.toList()
- ..addAll(requestUrl.pathSegments);
-
- return new Uri(scheme: proxyRoot.scheme,
- userInfo: proxyRoot.userInfo,
- host: proxyRoot.host,
- port: proxyRoot.port,
- pathSegments: updatedPath,
- query: requestUrl.query);
+// TODO(nweiz): use built-in methods for this when http and shelf support them.
+/// Add a header with [name] and [value] to [headers], handling existing headers
+/// gracefully.
+void _addHeader(Map<String, String> headers, String name, String value) {
+ if (headers.containsKey(name)) {
+ headers[name] += ', $value';
+ } else {
+ headers[name] = value;
+ }
}
diff --git a/lib/src/util.dart b/lib/src/util.dart
deleted file mode 100644
index 6085013..0000000
--- a/lib/src/util.dart
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-library shelf_proxy.util;
-
-import 'dart:async';
-
-import 'package:stack_trace/stack_trace.dart';
-
-/// Like [Future.sync], but wraps the Future in [Chain.track] as well.
-Future syncFuture(callback()) => Chain.track(new Future.sync(callback));
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
new file mode 100644
index 0000000..f30545e
--- /dev/null
+++ b/lib/src/utils.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library shelf_proxy.utils;
+
+import 'dart:async';
+
+// TODO(nweiz): remove this when issue 7786 is fixed.
+/// Pipes all data and errors from [stream] into [sink].
+///
+/// When [stream] is done, the returned [Future] is completed and [sink] is
+/// closed if [closeSink] is true.
+///
+/// When an error occurs on [stream], that error is passed to [sink]. If
+/// [cancelOnError] is true, [Future] will be completed successfully and no
+/// more data or errors will be piped from [stream] to [sink]. If
+/// [cancelOnError] and [closeSink] are both true, [sink] will then be
+/// closed.
+Future store(Stream stream, EventSink sink,
+ {bool cancelOnError: true, bool closeSink: true}) {
+ var completer = new Completer();
+ stream.listen(sink.add, onError: (e, stackTrace) {
+ sink.addError(e, stackTrace);
+ if (cancelOnError) {
+ completer.complete();
+ if (closeSink) sink.close();
+ }
+ }, onDone: () {
+ if (closeSink) sink.close();
+ completer.complete();
+ }, cancelOnError: cancelOnError);
+ return completer.future;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index ca9e746..f014699 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,18 +1,12 @@
name: shelf_proxy
-version: 0.0.2
-author: Kevin Moore <github@j832.com>
-description: Shelf handlers to proxy requests to other HTTP servers
-homepage: https://github.com/kevmoo/shelf_proxy
-environment:
- sdk: '>=1.0.0 <2.0.0'
+version: 0.1.0
+author: Dart Team <misc@dartlang.org>
+description: A shelf handler for proxying requests to another server.
+homepage: https://github.com/dart-lang/shelf_proxy
dependencies:
- http_parser: '>=0.0.2+2 <0.1.0'
- mime: '>=0.9.0 <0.10.0'
- shelf: '>=0.5.3 <0.6.0'
+ http: '>=0.9.0 <0.12.0'
+ path: '>=1.0.0 <2.0.0'
+ shelf: '>=0.5.2 <0.6.0'
dev_dependencies:
- browser: any
- hop: '>=0.30.4 <0.32.0'
- hop_unittest: '>=0.1.0 <0.2.0'
- path: '>=1.1.0 <2.0.0'
+ browser: '>=0.10.0 <0.11.0'
scheduled_test: '>=0.11.0 <0.12.0'
- shelf_static: '>=0.2.0 <0.3.0'
diff --git a/test/harness_console.dart b/test/harness_console.dart
deleted file mode 100644
index 8051387..0000000
--- a/test/harness_console.dart
+++ /dev/null
@@ -1,13 +0,0 @@
-library shelf_proxy.harness_console;
-
-import 'package:scheduled_test/scheduled_test.dart';
-
-import 'proxy_test.dart' as proxy;
-import 'static_file_test.dart' as static_file;
-
-void main() {
- groupSep = ' - ';
-
- group('proxy', proxy.main);
- group('static file example', static_file.main);
-}
diff --git a/test/proxy_test.dart b/test/proxy_test.dart
deleted file mode 100644
index 6790e1d..0000000
--- a/test/proxy_test.dart
+++ /dev/null
@@ -1,155 +0,0 @@
-library shelf_proxy.proxy_test;
-
-import 'dart:async';
-import 'dart:io';
-
-import 'package:scheduled_test/scheduled_test.dart';
-import 'package:shelf/shelf.dart';
-import 'package:shelf/shelf_io.dart' as shelf_io;
-import 'package:shelf_proxy/shelf_proxy.dart';
-
-import 'test_util.dart';
-
-void main() {
- group('arguments', () {
- group('root uri must be http or https', () {
- test('http works', () {
- expect(createProxyHandler(Uri.parse('http://example.com')), isNotNull);
- });
- test('http works', () {
- expect(createProxyHandler(Uri.parse('https://example.com')), isNotNull);
- });
- test('ftp does not work', () {
- expect(() => createProxyHandler(Uri.parse('ftp://example.com')),
- throwsArgumentError);
- });
- });
-
- group('root uri must be absolute without query', () {
- test('http works', () {
- expect(createProxyHandler(Uri.parse('http://example.com')), isNotNull);
- });
-
- test('with trailing slash works', () {
- expect(createProxyHandler(Uri.parse('http://example.com/')), isNotNull);
- });
-
- test('with path item', () {
- expect(createProxyHandler(Uri.parse('http://example.com/path')),
- isNotNull);
- });
-
- test('with path item and trailing slash', () {
- expect(createProxyHandler(Uri.parse('http://example.com/path/')),
- isNotNull);
- });
-
- test('with a fragment', () {
- expect(
- () => createProxyHandler(Uri.parse('http://example.com/path#foo')),
- throwsArgumentError);
- });
-
- test('with a query', () {
- expect(
- () => createProxyHandler(Uri.parse('http://example.com/path?a=b')),
- throwsArgumentError);
- });
- });
- });
-
- group('requests', () {
- test('root', () {
- _scheduleServer(_handler);
-
- schedule(() {
- var url = new Uri.http('localhost:$_serverPort', '');
- var handler = createProxyHandler(url);
-
- return makeRequest(handler, '/').then((response) {
- expect(response.statusCode, HttpStatus.OK);
- expect(response.readAsString(), completion('root with slash'));
- });
- });
- });
-
- test('/bar', () {
- _scheduleServer(_handler);
-
- schedule(() {
- var url = new Uri.http('localhost:$_serverPort', '');
- var handler = createProxyHandler(url);
-
- return makeRequest(handler, '/bar').then((response) {
- expect(response.statusCode, HttpStatus.OK);
- expect(response.readAsString(), completion('bar'));
- });
- });
- });
-
- test('/bar/', () {
- _scheduleServer(_handler);
-
- schedule(() {
- var url = new Uri.http('localhost:$_serverPort', '');
- var handler = createProxyHandler(url);
-
- return makeRequest(handler, '/bar/').then((response) {
- expect(response.statusCode, HttpStatus.OK);
- expect(response.readAsString(), completion('bar with slash'));
- });
- });
- });
-
- test('only GET is supported', () {
- _scheduleServer(_handler);
-
- schedule(() {
- var url = new Uri.http('localhost:$_serverPort', '');
- var handler = createProxyHandler(url);
-
- return makeRequest(handler, '/', method: 'PUT').then((response) {
- expect(response.statusCode, HttpStatus.METHOD_NOT_ALLOWED);
- });
- });
- });
- });
-}
-
-Response _handler(Request request) {
- if (request.method != 'GET') {
- return new Response.forbidden("I don't like method ${request.method}.");
- }
-
- String content;
- switch (request.url.path) {
- case '':
- content = 'root';
- break;
- case '/':
- content = 'root with slash';
- break;
- case '/bar':
- content = 'bar';
- break;
- case '/bar/':
- content = 'bar with slash';
- break;
- default:
- return new Response.notFound("I don't like '${request.url.path}'.");
- }
- return new Response.ok(content);
-}
-
-int _serverPort;
-
-Future _scheduleServer(Handler handler) {
- return schedule(() => shelf_io.serve(handler, 'localhost', 0).then((server) {
- currentSchedule.onComplete.schedule(() {
- _serverPort = null;
- return server.close(force: true);
- });
-
- _serverPort = server.port;
- }));
-}
diff --git a/test/shelf_proxy_test.dart b/test/shelf_proxy_test.dart
new file mode 100644
index 0000000..fe76ddc
--- /dev/null
+++ b/test/shelf_proxy_test.dart
@@ -0,0 +1,259 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library shelf_proxy;
+
+import 'dart:async';
+
+import 'package:http/http.dart' as http;
+import 'package:http/testing.dart';
+import 'package:scheduled_test/scheduled_test.dart';
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:shelf/shelf_io.dart' as shelf_io;
+import 'package:shelf_proxy/shelf_proxy.dart';
+
+/// The URI of the server the current proxy server is proxying to.
+Uri targetUri;
+
+/// The URI of the current proxy server.
+Uri proxyUri;
+
+void main() {
+ group("forwarding", () {
+ test("forwards request method", () {
+ createProxy((request) {
+ expect(request.method, equals('DELETE'));
+ return new shelf.Response.ok(':)');
+ });
+
+ schedule(() => http.delete(proxyUri));
+ });
+
+ test("forwards request headers", () {
+ createProxy((request) {
+ expect(request.headers, containsPair('foo', 'bar'));
+ expect(request.headers, containsPair('accept', '*/*'));
+ return new shelf.Response.ok(':)');
+ });
+
+ get(headers: {
+ 'foo': 'bar',
+ 'accept': '*/*'
+ });
+ });
+
+ test("forwards request body", () {
+ createProxy((request) {
+ expect(request.readAsString(), completion(equals('hello, server')));
+ return new shelf.Response.ok(':)');
+ });
+
+ schedule(() => http.post(proxyUri, body: 'hello, server'));
+ });
+
+ test("forwards response status", () {
+ createProxy((request) {
+ return new shelf.Response(567);
+ });
+
+ expect(get().then((response) {
+ expect(response.statusCode, equals(567));
+ }), completes);
+ });
+
+ test("forwards response headers", () {
+ createProxy((request) {
+ return new shelf.Response.ok(':)', headers: {
+ 'foo': 'bar',
+ 'accept': '*/*'
+ });
+ });
+
+ expect(get().then((response) {
+ expect(response.headers, containsPair('foo', 'bar'));
+ expect(response.headers, containsPair('accept', '*/*'));
+ }), completes);
+ });
+
+ test("forwards response body", () {
+ createProxy((request) {
+ return new shelf.Response.ok('hello, client');
+ });
+
+ expect(schedule(() => http.read(proxyUri)),
+ completion(equals('hello, client')));
+ });
+
+ test("adjusts the Host header for the target server", () {
+ createProxy((request) {
+ expect(request.headers, containsPair('host', targetUri.authority));
+ return new shelf.Response.ok(':)');
+ });
+
+ get();
+ });
+ });
+
+ group("via", () {
+ test("adds a Via header to the request", () {
+ createProxy((request) {
+ expect(request.headers, containsPair('via', '1.1 shelf_proxy'));
+ return new shelf.Response.ok(':)');
+ });
+
+ get();
+ });
+
+ test("adds to a request's existing Via header", () {
+ createProxy((request) {
+ expect(request.headers,
+ containsPair('via', '1.0 something, 1.1 shelf_proxy'));
+ return new shelf.Response.ok(':)');
+ });
+
+ get(headers: {'via': '1.0 something'});
+ });
+
+ test("adds a Via header to the response", () {
+ createProxy((request) => new shelf.Response.ok(':)'));
+
+ expect(get().then((response) {
+ expect(response.headers, containsPair('via', '1.1 shelf_proxy'));
+ }), completes);
+ });
+
+ test("adds to a response's existing Via header", () {
+ createProxy((request) {
+ return new shelf.Response.ok(':)', headers: {'via': '1.0 something'});
+ });
+
+ expect(get().then((response) {
+ expect(response.headers,
+ containsPair('via', '1.0 something, 1.1 shelf_proxy'));
+ }), completes);
+ });
+
+ test("adds to a response's existing Via header", () {
+ createProxy((request) {
+ return new shelf.Response.ok(':)', headers: {'via': '1.0 something'});
+ });
+
+ expect(get().then((response) {
+ expect(response.headers,
+ containsPair('via', '1.0 something, 1.1 shelf_proxy'));
+ }), completes);
+ });
+ });
+
+ group("redirects", () {
+ test("doesn't modify a Location for a foreign server", () {
+ createProxy((request) {
+ return new shelf.Response.found('http://dartlang.org');
+ });
+
+ expect(get().then((response) {
+ expect(response.headers,
+ containsPair('location', 'http://dartlang.org'));
+ }), completes);
+ });
+
+ test("relativizes a reachable root-relative Location", () {
+ createProxy((request) {
+ return new shelf.Response.found('/foo/bar');
+ }, targetPath: '/foo');
+
+ expect(get().then((response) {
+ expect(response.headers, containsPair('location', '/bar'));
+ }), completes);
+ });
+
+ test("absolutizes an unreachable root-relative Location", () {
+ createProxy((request) {
+ return new shelf.Response.found('/baz');
+ }, targetPath: '/foo');
+
+ expect(get().then((response) {
+ expect(response.headers,
+ containsPair('location', targetUri.resolve('/baz').toString()));
+ }), completes);
+ });
+ });
+
+ test("removes a transfer-encoding header", () {
+ var handler = mockHandler((request) {
+ return new http.Response('', 200, headers: {
+ 'transfer-encoding': 'chunked'
+ });
+ });
+
+ expect(handler(new shelf.Request('GET', Uri.parse('http://localhost/')))
+ .then((response) {
+ expect(response.headers, isNot(contains("transfer-encoding")));
+ }), completes);
+ });
+
+ test("removes content-length and content-encoding for a gzipped response",
+ () {
+ var handler = mockHandler((request) {
+ return new http.Response('', 200, headers: {
+ 'content-encoding': 'gzip',
+ 'content-length': '1234'
+ });
+ });
+
+ expect(handler(new shelf.Request('GET', Uri.parse('http://localhost/')))
+ .then((response) {
+ expect(response.headers, isNot(contains("content-encoding")));
+ expect(response.headers, isNot(contains("content-length")));
+ expect(response.headers,
+ containsPair('warning', '214 shelf_proxy "GZIP decoded"'));
+ }), completes);
+ });
+}
+
+/// Creates a proxy server proxying to a server running [handler].
+///
+/// [targetPath] is the root-relative path on the target server to proxy to. It
+/// defaults to `/`.
+void createProxy(shelf.Handler handler, {String targetPath}) {
+ handler = wrapAsync(handler, 'target server handler');
+ schedule(() {
+ return shelf_io.serve(handler, 'localhost', 0).then((targetServer) {
+ targetUri = Uri.parse('http://localhost:${targetServer.port}');
+ if (targetPath != null) targetUri = targetUri.resolve(targetPath);
+ var proxyServerHandler = wrapAsync(
+ proxyHandler(targetUri), 'proxy server handler');
+
+ return shelf_io.serve(proxyServerHandler, 'localhost', 0)
+ .then((proxyServer) {
+ proxyUri = Uri.parse('http://localhost:${proxyServer.port}');
+
+ currentSchedule.onComplete.schedule(() {
+ proxyServer.close(force: true);
+ targetServer.close(force: true);
+ }, 'tear down servers');
+ });
+ });
+ }, 'spin up servers');
+}
+
+/// Creates a [shelf.Handler] that's backed by a [MockClient] running
+/// [callback].
+shelf.Handler mockHandler(callback(http.Request request)) {
+ var client = new MockClient((request) {
+ return new Future.sync(() => callback(request));
+ });
+ return proxyHandler('http://dartlang.org', client: client);
+}
+
+/// Schedules a GET request with [headers] to the proxy server.
+Future<http.Response> get({Map<String, String> headers}) {
+ return schedule(() {
+ var uri = proxyUri;
+ var request = new http.Request('GET', uri);
+ if (headers != null) request.headers.addAll(headers);
+ request.followRedirects = false;
+ return request.send().then(http.Response.fromStream);
+ }, 'GET proxy server');
+}
diff --git a/test/static_file_test.dart b/test/static_file_test.dart
deleted file mode 100644
index 411ad58..0000000
--- a/test/static_file_test.dart
+++ /dev/null
@@ -1,46 +0,0 @@
-library shelf_proxy.static_file_test;
-
-import 'dart:async';
-import 'dart:io';
-
-import 'package:scheduled_test/scheduled_test.dart';
-import 'package:shelf/shelf.dart';
-import 'package:shelf/shelf_io.dart' as shelf_io;
-import 'package:shelf_static/shelf_static.dart' as shelf_static;
-import 'package:shelf_proxy/shelf_proxy.dart';
-
-import 'test_util.dart';
-
-void main() {
- test('default document', () {
- _scheduleServer(_handler);
-
- schedule(() {
- var url = new Uri.http('localhost:$_serverPort', '');
- var handler = createProxyHandler(url);
-
- return makeRequest(handler, '/').then((response) {
- expect(response.statusCode, HttpStatus.OK);
- expect(response.readAsString(),
- completion(contains('<title>shelf_static</title>')));
- expect(response.contentLength, 228);
- });
- });
- });
-}
-
-final _handler = shelf_static.createStaticHandler('test/test_files',
- defaultDocument: 'index.html');
-
-int _serverPort;
-
-Future _scheduleServer(Handler handler) {
- return schedule(() => shelf_io.serve(handler, 'localhost', 0).then((server) {
- currentSchedule.onComplete.schedule(() {
- _serverPort = null;
- return server.close(force: true);
- });
-
- _serverPort = server.port;
- }));
-}
diff --git a/test/test_files/dart.png b/test/test_files/dart.png
deleted file mode 100644
index fd6de2a..0000000
--- a/test/test_files/dart.png
+++ /dev/null
Binary files differ
diff --git a/test/test_files/favicon.ico b/test/test_files/favicon.ico
deleted file mode 100644
index e605972..0000000
--- a/test/test_files/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/test/test_files/index.html b/test/test_files/index.html
deleted file mode 100644
index 7bf28d1..0000000
--- a/test/test_files/index.html
+++ /dev/null
@@ -1,12 +0,0 @@
-<!DOCTYPE html>
-
-<html>
- <head>
- <meta charset="utf-8">
- <title>shelf_static</title>
- </head>
- <body>
- <h1>Hello, shelf_static!</h1>
- <img src='dart.png' alt='Dart logo' width='150' height='151'>
- </body>
-</html>
diff --git a/test/test_util.dart b/test/test_util.dart
deleted file mode 100644
index b4275dc..0000000
--- a/test/test_util.dart
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-library shelf_proxy.test_util;
-
-import 'dart:async';
-
-import 'package:path/path.dart' as p;
-import 'package:shelf/shelf.dart';
-import 'package:shelf_proxy/src/util.dart';
-
-final p.Context _ctx = p.url;
-
-/// Makes a simple GET request to [handler] and returns the result.
-Future<Response> makeRequest(Handler handler, String path,
- {String scriptName, Map<String, String> headers, String method}) {
- var rootedHandler = _rootHandler(scriptName, handler);
- return syncFuture(() =>
- rootedHandler(_fromPath(path, headers, method: method)));
-}
-
-Request _fromPath(String path, Map<String, String> headers, {String method}) {
- if (method == null) method = 'GET';
- return new Request(method, Uri.parse('http://localhost' + path),
- headers: headers);
-}
-
-Handler _rootHandler(String scriptName, Handler handler) {
- if (scriptName == null || scriptName.isEmpty) {
- return handler;
- }
-
- if (!scriptName.startsWith('/')) {
- throw new ArgumentError('scriptName must start with "/" or be empty');
- }
-
- return (Request request) {
- if (!_ctx.isWithin(scriptName, request.requestedUri.path)) {
- return new Response.notFound('not found');
- }
- assert(request.scriptName.isEmpty);
-
- var relativeRequest = request.change(scriptName: scriptName);
-
- return handler(relativeRequest);
- };
-}
diff --git a/tool/hop_runner.dart b/tool/hop_runner.dart
deleted file mode 100644
index 262c863..0000000
--- a/tool/hop_runner.dart
+++ /dev/null
@@ -1,30 +0,0 @@
-library hop_runner;
-
-import 'dart:async';
-import 'dart:io';
-import 'package:hop/hop.dart';
-import 'package:hop/hop_tasks.dart';
-import 'package:hop_unittest/hop_unittest.dart';
-
-import '../test/harness_console.dart' as test_console;
-
-void main(List<String> args) {
- addTask('test', createUnitTestTask(test_console.main));
-
- //
- // Analyzer
- //
- addTask('analyze_libs', createAnalyzerTask(_getLibs));
-
- addTask('analyze_test_libs', createAnalyzerTask(
- ['test/harness_console.dart']));
-
- runHop(args);
-}
-
-Future<List<String>> _getLibs() {
- return new Directory('lib').list()
- .where((FileSystemEntity fse) => fse is File)
- .map((File file) => file.path)
- .toList();
-}