1.1.0-alpha
This commit is contained in:
parent
487e825f8d
commit
4d7cbb679e
13 changed files with 216 additions and 260 deletions
|
@ -5,10 +5,7 @@
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.pub" />
|
<excludeFolder url="file://$MODULE_DIR$/.pub" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/example/packages" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/packages" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/test/packages" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<configuration default="false" name="All Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
|
<configuration default="false" name="All Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
|
||||||
<option name="filePath" value="$PROJECT_DIR$" />
|
<option name="filePath" value="$PROJECT_DIR$" />
|
||||||
<option name="scope" value="FOLDER" />
|
<option name="scope" value="FOLDER" />
|
||||||
|
<option name="testRunnerOptions" value="-j 4" />
|
||||||
<method />
|
<method />
|
||||||
</configuration>
|
</configuration>
|
||||||
</component>
|
</component>
|
7
.idea/runConfigurations/multiple_dart.xml
Normal file
7
.idea/runConfigurations/multiple_dart.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="multiple.dart" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true" nameIsGenerated="true">
|
||||||
|
<option name="filePath" value="$PROJECT_DIR$/example/multiple.dart" />
|
||||||
|
<option name="workingDirectory" value="$PROJECT_DIR$" />
|
||||||
|
<method />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
30
README.md
30
README.md
|
@ -1,25 +1,33 @@
|
||||||
# angel_proxy
|
# proxy
|
||||||
|
[](https://pub.dartlang.org/packages/angel_proxy)
|
||||||
|
[](https://travis-ci.org/angel-dart/proxy)
|
||||||
|
|
||||||
[](https://pub.dartlang.org/packages/angel_proxy)[](https://travis-ci.org/angel-dart/proxy)
|
Angel middleware to forward requests to another server (i.e. `pub serve`).
|
||||||
|
|
||||||
Angel middleware to forward requests to another server (i.e. pub serve).
|
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
import 'package:angel_proxy/angel_proxy.dart';
|
import 'package:angel_proxy/angel_proxy.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
main() async {
|
main() async {
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
// Forward requests instead of serving statically
|
var client = new http.Client();
|
||||||
await app.configure(new ProxyLayer('localhost', 3000));
|
var proxy = new Proxy(app, client, 'http://localhost:3000');
|
||||||
|
|
||||||
// Or, use one for pub serve.
|
// Forward requests instead of serving statically
|
||||||
//
|
app.use(proxy.handleRequest);
|
||||||
// This automatically deactivates itself if the app is
|
|
||||||
// in production.
|
|
||||||
await app.configure(new PubServeLayer());
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can also restrict the proxy to serving only from a specific root:
|
||||||
|
```dart
|
||||||
|
new Proxy(app, client, '<host>', publicPath: '/remote');
|
||||||
|
```
|
||||||
|
|
||||||
|
Also, you can map requests to a root path on the remote server
|
||||||
|
```dart
|
||||||
|
new Proxy(app, client, '<host>', mapTo: '/path');
|
||||||
|
```
|
||||||
|
|
||||||
If your app's `storeOriginalBuffer` is `true`, then request bodies will be forwarded
|
If your app's `storeOriginalBuffer` is `true`, then request bodies will be forwarded
|
||||||
as well, if they are not empty. This allows things like POST requests to function.
|
as well, if they are not empty. This allows things like POST requests to function.
|
||||||
|
|
|
@ -1,38 +1,55 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:angel_diagnostics/angel_diagnostics.dart';
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:angel_proxy/angel_proxy.dart';
|
import 'package:angel_proxy/angel_proxy.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
final Duration TIMEOUT = new Duration(seconds: 5);
|
final Duration timeout = new Duration(seconds: 5);
|
||||||
|
|
||||||
main() async {
|
main() async {
|
||||||
var app = new Angel();
|
var app = new Angel();
|
||||||
|
var client = new http.Client();
|
||||||
|
|
||||||
// Forward any /api requests to pub.
|
// Forward any /api requests to pub.
|
||||||
// By default, if the host throws a 404, the request will fall through to the next handler.
|
// By default, if the host throws a 404, the request will fall through to the next handler.
|
||||||
var pubProxy = new ProxyLayer('pub.dartlang.org', 80,
|
var pubProxy = new Proxy(
|
||||||
publicPath: '/pub', timeout: TIMEOUT);
|
app,
|
||||||
await app.configure(pubProxy);
|
client,
|
||||||
|
'https://pub.dartlang.org',
|
||||||
|
publicPath: '/pub',
|
||||||
|
timeout: timeout,
|
||||||
|
);
|
||||||
|
app.use(pubProxy.handleRequest);
|
||||||
|
|
||||||
// Pub's HTML assumes that the site's styles, etc. are on the absolute path `/static`.
|
// Pub's HTML assumes that the site's styles, etc. are on the absolute path `/static`.
|
||||||
// This is not the case here. Let's patch that up:
|
// This is not the case here. Let's patch that up:
|
||||||
app.get('/static/*', (RequestContext req, res) {
|
app.get('/static/*', (RequestContext req, res) {
|
||||||
return pubProxy.serveFile(req.path, req, res);
|
return pubProxy.servePath(req.path, req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Anything else should fall through to dartlang.org.
|
// Anything else should fall through to dartlang.org.
|
||||||
await app.configure(new ProxyLayer('dartlang.org', 80, timeout: TIMEOUT));
|
var dartlangProxy = new Proxy(
|
||||||
|
app,
|
||||||
|
client,
|
||||||
|
'https://dartlang.org',
|
||||||
|
timeout: timeout,
|
||||||
|
recoverFrom404: false,
|
||||||
|
);
|
||||||
|
app.use(dartlangProxy.handleRequest);
|
||||||
|
|
||||||
// In case we can't connect to dartlang.org, show an error.
|
// In case we can't connect to dartlang.org, show an error.
|
||||||
app.after.add('Couldn\'t connect to Pub or dartlang.');
|
app.use('Couldn\'t connect to Pub or dartlang.');
|
||||||
|
|
||||||
await app.configure(logRequests());
|
app.logger = new Logger('angel')
|
||||||
|
..onRecord.listen(
|
||||||
app.fatalErrorStream.listen((AngelFatalError e) {
|
(rec) {
|
||||||
print(e.error);
|
print(rec);
|
||||||
print(e.stack);
|
if (rec.error != null) print(rec.error);
|
||||||
});
|
if (rec.stackTrace != null) print(rec.stackTrace);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
var server = await app.startServer(InternetAddress.LOOPBACK_IP_V4, 8080);
|
var server = await app.startServer(InternetAddress.LOOPBACK_IP_V4, 8080);
|
||||||
print('Listening at http://${server.address.address}:${server.port}');
|
print('Listening at http://${server.address.address}:${server.port}');
|
||||||
|
print('Check this out! http://${server.address.address}:${server.port}/pub/packages/angel_framework');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
library angel_proxy;
|
library angel_proxy;
|
||||||
|
|
||||||
export 'src/proxy_layer.dart';
|
export 'src/proxy_layer.dart';
|
||||||
export 'src/pub_serve_layer.dart';
|
|
||||||
|
|
|
@ -1,142 +1,107 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:http/src/base_client.dart' as http;
|
||||||
|
import 'package:http/src/request.dart' as http;
|
||||||
|
import 'package:http/src/response.dart' as http;
|
||||||
|
import 'package:http/src/streamed_response.dart' as http;
|
||||||
|
|
||||||
final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?');
|
final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?');
|
||||||
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
||||||
|
|
||||||
/// Used to mount a route for a [ProxyLayer].
|
class Proxy {
|
||||||
typedef Route ProxyLayerRouteAssigner(Router router, String path, handler);
|
|
||||||
|
|
||||||
String _pathify(String path) {
|
|
||||||
var p = path.replaceAll(_straySlashes, '');
|
|
||||||
|
|
||||||
Map<String, String> replace = {};
|
|
||||||
|
|
||||||
for (Match match in _param.allMatches(p)) {
|
|
||||||
if (match[3] != null) replace[match[0]] = ':${match[1]}';
|
|
||||||
}
|
|
||||||
|
|
||||||
replace.forEach((k, v) {
|
|
||||||
p = p.replaceAll(k, v);
|
|
||||||
});
|
|
||||||
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Copies HTTP headers ;)
|
|
||||||
void copyHeaders(HttpHeaders from, HttpHeaders to) {
|
|
||||||
from.forEach((k, v) {
|
|
||||||
if (k != HttpHeaders.SERVER &&
|
|
||||||
(k != HttpHeaders.CONTENT_ENCODING || !v.contains('gzip')))
|
|
||||||
to.set(k, v);
|
|
||||||
});
|
|
||||||
|
|
||||||
/*to
|
|
||||||
..chunkedTransferEncoding = from.chunkedTransferEncoding
|
|
||||||
..contentLength = from.contentLength
|
|
||||||
..contentType = from.contentType
|
|
||||||
..date = from.date
|
|
||||||
..expires = from.expires
|
|
||||||
..host = from.host
|
|
||||||
..ifModifiedSince = from.ifModifiedSince
|
|
||||||
..persistentConnection = from.persistentConnection
|
|
||||||
..port = from.port;*/
|
|
||||||
}
|
|
||||||
|
|
||||||
class ProxyLayer {
|
|
||||||
Angel app;
|
|
||||||
HttpClient _client;
|
|
||||||
String _prefix;
|
String _prefix;
|
||||||
|
|
||||||
|
final Angel app;
|
||||||
|
final http.BaseClient httpClient;
|
||||||
|
|
||||||
/// If `true` (default), then the plug-in will ignore failures to connect to the proxy, and allow other handlers to run.
|
/// If `true` (default), then the plug-in will ignore failures to connect to the proxy, and allow other handlers to run.
|
||||||
final bool recoverFromDead;
|
final bool recoverFromDead;
|
||||||
final bool debug, recoverFrom404, streamToIO;
|
final bool recoverFrom404;
|
||||||
final String host, mapTo, publicPath;
|
final String host, mapTo, publicPath;
|
||||||
final int port;
|
final int port;
|
||||||
final String protocol;
|
final String protocol;
|
||||||
final Duration timeout;
|
final Duration timeout;
|
||||||
ProxyLayerRouteAssigner routeAssigner;
|
|
||||||
|
|
||||||
ProxyLayer(
|
Proxy(
|
||||||
this.host,
|
this.app,
|
||||||
this.port, {
|
this.httpClient,
|
||||||
this.debug: false,
|
this.host, {
|
||||||
|
this.port,
|
||||||
this.mapTo: '/',
|
this.mapTo: '/',
|
||||||
this.publicPath: '/',
|
this.publicPath: '/',
|
||||||
this.protocol: 'http',
|
this.protocol: 'http',
|
||||||
this.recoverFromDead: true,
|
this.recoverFromDead: true,
|
||||||
this.recoverFrom404: true,
|
this.recoverFrom404: true,
|
||||||
this.streamToIO: false,
|
|
||||||
this.timeout,
|
this.timeout,
|
||||||
this.routeAssigner,
|
|
||||||
SecurityContext securityContext,
|
|
||||||
}) {
|
}) {
|
||||||
_client = new HttpClient(context: securityContext);
|
|
||||||
_prefix = publicPath.replaceAll(_straySlashes, '');
|
_prefix = publicPath.replaceAll(_straySlashes, '');
|
||||||
routeAssigner ??=
|
|
||||||
(Router router, String path, handler) => router.get(path, handler);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
call(Angel app) async => serve(this.app = app);
|
void close() => httpClient.close();
|
||||||
|
|
||||||
void close() => _client.close(force: true);
|
/// Handles an incoming HTTP request.
|
||||||
|
Future<bool> handleRequest(RequestContext req, ResponseContext res) {
|
||||||
void serve(Router router) {
|
|
||||||
// _printDebug('Public path prefix: "$_prefix"');
|
|
||||||
|
|
||||||
handler(RequestContext req, ResponseContext res) async {
|
|
||||||
var path = req.path.replaceAll(_straySlashes, '');
|
var path = req.path.replaceAll(_straySlashes, '');
|
||||||
return serveFile(path, req, res);
|
|
||||||
|
if (_prefix?.isNotEmpty == true) {
|
||||||
|
if (!path.startsWith(_prefix))
|
||||||
|
return new Future<bool>.value(true);
|
||||||
|
else {
|
||||||
|
path = path.replaceFirst(_prefix, '').replaceAll(_straySlashes, '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
routeAssigner(router, '$publicPath/*', handler);
|
return servePath(path, req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
serveFile(String path, RequestContext req, ResponseContext res) async {
|
/// Proxies a request to the given path on the remote server.
|
||||||
var _path = path;
|
Future<bool> servePath(
|
||||||
HttpClientResponse rs;
|
String path, RequestContext req, ResponseContext res) async {
|
||||||
|
http.StreamedResponse rs;
|
||||||
|
|
||||||
if (_prefix.isNotEmpty) {
|
final mapping = '$mapTo/$path'.replaceAll(_straySlashes, '');
|
||||||
_path = path.replaceAll(new RegExp('^' + _pathify(_prefix)), '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create mapping
|
|
||||||
// _printDebug('Serving path $_path via proxy');
|
|
||||||
final mapping = '$mapTo/$_path'.replaceAll(_straySlashes, '');
|
|
||||||
// _printDebug('Mapped path $_path to path $mapping on proxy $host:$port');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Future<HttpClientResponse> accessRemote() async {
|
Future<http.StreamedResponse> accessRemote() async {
|
||||||
var ips = await InternetAddress.lookup(host);
|
var url = port == null ? host : '$host:$port';
|
||||||
if (ips.isEmpty)
|
url = url.replaceAll(_straySlashes, '');
|
||||||
throw new StateError('Could not resolve remote host "$host".');
|
url = '$url/$mapping';
|
||||||
var address = ips.first.address;
|
|
||||||
final rq = await _client.open(req.method, address, port, mapping);
|
|
||||||
// _printDebug('Opened client request at "$address:$port/$mapping"');
|
|
||||||
|
|
||||||
copyHeaders(req.headers, rq.headers);
|
if (!url.startsWith('http')) url = 'http://$url';
|
||||||
rq.headers.set(HttpHeaders.HOST, host);
|
url = url.replaceAll(_straySlashes, '');
|
||||||
// _printDebug('Copied headers');
|
|
||||||
rq.cookies.addAll(req.cookies ?? []);
|
|
||||||
// _printDebug('Added cookies');
|
|
||||||
rq.headers.set(
|
|
||||||
'X-Forwarded-For', req.io.connectionInfo.remoteAddress.address);
|
|
||||||
rq.headers
|
|
||||||
..set('X-Forwarded-Port', req.io.connectionInfo.remotePort.toString())
|
|
||||||
..set('X-Forwarded-Host',
|
|
||||||
req.headers.host ?? req.headers.value(HttpHeaders.HOST) ?? 'none')
|
|
||||||
..set('X-Forwarded-Proto', protocol);
|
|
||||||
// _printDebug('Added X-Forwarded headers');
|
|
||||||
|
|
||||||
if (app.storeOriginalBuffer == true) {
|
var headers = <String, String>{
|
||||||
|
'host': port == null ? host : '$host:$port',
|
||||||
|
'x-forwarded-for': req.io.connectionInfo.remoteAddress.address,
|
||||||
|
'x-forwarded-port': req.io.connectionInfo.remotePort.toString(),
|
||||||
|
'x-forwarded-host':
|
||||||
|
req.headers.host ?? req.headers.value('host') ?? 'none',
|
||||||
|
'x-forwarded-proto': protocol,
|
||||||
|
};
|
||||||
|
|
||||||
|
req.headers.forEach((name, values) {
|
||||||
|
headers[name] = values.join(',');
|
||||||
|
});
|
||||||
|
|
||||||
|
headers['cookie'] =
|
||||||
|
req.cookies.map<String>((c) => '${c.name}=${c.value}').join('; ');
|
||||||
|
|
||||||
|
var body;
|
||||||
|
|
||||||
|
if (req.method != 'GET' && app.storeOriginalBuffer == true) {
|
||||||
await req.parse();
|
await req.parse();
|
||||||
if (req.originalBuffer?.isNotEmpty == true)
|
if (req.originalBuffer?.isNotEmpty == true) body = req.originalBuffer;
|
||||||
rq.add(req.originalBuffer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await rq.flush();
|
var rq = new http.Request(req.method, Uri.parse(url));
|
||||||
return await rq.close();
|
rq.headers.addAll(headers);
|
||||||
|
rq.headers['host'] = rq.url.host;
|
||||||
|
|
||||||
|
if (body != null) rq.bodyBytes = body;
|
||||||
|
|
||||||
|
return await httpClient.send(rq);
|
||||||
}
|
}
|
||||||
|
|
||||||
var future = accessRemote();
|
var future = accessRemote();
|
||||||
|
@ -146,11 +111,13 @@ class ProxyLayer {
|
||||||
if (recoverFromDead != false)
|
if (recoverFromDead != false)
|
||||||
return true;
|
return true;
|
||||||
else
|
else
|
||||||
throw new AngelHttpException(e,
|
throw new AngelHttpException(
|
||||||
|
e,
|
||||||
stackTrace: st,
|
stackTrace: st,
|
||||||
statusCode: HttpStatus.GATEWAY_TIMEOUT,
|
statusCode: 504,
|
||||||
message:
|
message:
|
||||||
'Connection to remote host "$host" timed out after ${timeout.inMilliseconds}ms.');
|
'Connection to remote host "$host" timed out after ${timeout.inMilliseconds}ms.',
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (recoverFromDead != false)
|
if (recoverFromDead != false)
|
||||||
return true;
|
return true;
|
||||||
|
@ -158,44 +125,21 @@ class ProxyLayer {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
|
|
||||||
// _printDebug(
|
|
||||||
// 'Proxy responded to $mapping with status code ${rs.statusCode}');
|
|
||||||
|
|
||||||
if (rs.statusCode == 404 && recoverFrom404 != false) return true;
|
if (rs.statusCode == 404 && recoverFrom404 != false) return true;
|
||||||
|
|
||||||
res
|
res
|
||||||
..statusCode = rs.statusCode
|
..statusCode = rs.statusCode
|
||||||
..contentType = rs.headers.contentType;
|
..headers.addAll(rs.headers);
|
||||||
|
|
||||||
// _printDebug('Proxy response headers:\n${rs.headers}');
|
if (rs.contentLength == 0 && recoverFromDead != false) return true;
|
||||||
|
|
||||||
if (streamToIO == true) {
|
var stream = rs.stream;
|
||||||
res
|
|
||||||
..willCloseItself = true
|
|
||||||
..end();
|
|
||||||
|
|
||||||
copyHeaders(rs.headers, res.io.headers);
|
if (rs.headers['content-encoding'] == 'gzip')
|
||||||
res.io.statusCode = rs.statusCode;
|
stream = stream.transform(GZIP.encoder);
|
||||||
|
|
||||||
if (rs.headers.contentType != null)
|
await stream.pipe(res);
|
||||||
res.io.headers.contentType = rs.headers.contentType;
|
|
||||||
|
|
||||||
// _printDebug('Outgoing content length: ${res.io.contentLength}');
|
return false;
|
||||||
|
|
||||||
if (rs.headers[HttpHeaders.CONTENT_ENCODING]?.contains('gzip') == true) {
|
|
||||||
res.io.headers.set(HttpHeaders.CONTENT_ENCODING, 'gzip');
|
|
||||||
await rs.transform(GZIP.encoder).pipe(res.io);
|
|
||||||
} else
|
|
||||||
await rs.pipe(res.io);
|
|
||||||
} else {
|
|
||||||
rs.headers.forEach((k, v) {
|
|
||||||
if (k != HttpHeaders.CONTENT_ENCODING || !v.contains('gzip'))
|
|
||||||
res.headers[k] = v.join(',');
|
|
||||||
});
|
|
||||||
|
|
||||||
await rs.forEach(res.buffer.add);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.buffer.isEmpty;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
import 'package:angel_route/angel_route.dart';
|
|
||||||
import 'proxy_layer.dart';
|
|
||||||
|
|
||||||
class PubServeLayer extends ProxyLayer {
|
|
||||||
PubServeLayer(
|
|
||||||
{bool debug: false,
|
|
||||||
bool recoverFromDead: true,
|
|
||||||
bool recoverFrom404: true,
|
|
||||||
bool streamToIO: true,
|
|
||||||
String host: 'localhost',
|
|
||||||
String mapTo: '/',
|
|
||||||
int port: 8080,
|
|
||||||
String protocol: 'http',
|
|
||||||
String publicPath: '/',
|
|
||||||
ProxyLayerRouteAssigner routeAssigner,
|
|
||||||
Duration timeout})
|
|
||||||
: super(host, port,
|
|
||||||
debug: debug,
|
|
||||||
mapTo: mapTo,
|
|
||||||
protocol: protocol,
|
|
||||||
publicPath: publicPath,
|
|
||||||
recoverFromDead: recoverFromDead != false,
|
|
||||||
recoverFrom404: recoverFrom404 != false,
|
|
||||||
streamToIO: streamToIO != false,
|
|
||||||
routeAssigner: routeAssigner,
|
|
||||||
timeout: timeout);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void serve(Router router) {
|
|
||||||
if (app?.isProduction == true) {
|
|
||||||
// Auto-deactivate in production ;)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
super.serve(router);
|
|
||||||
}
|
|
||||||
}
|
|
10
pubspec.yaml
10
pubspec.yaml
|
@ -1,16 +1,14 @@
|
||||||
name: angel_proxy
|
name: angel_proxy
|
||||||
description: Angel middleware to forward requests to another server (i.e. pub serve).
|
description: Angel middleware to forward requests to another server (i.e. pub serve).
|
||||||
version: 1.0.9
|
version: 1.1.0-alpha
|
||||||
author: Tobe O <thosakwe@gmail.com>
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
homepage: https://github.com/angel-dart/proxy
|
homepage: https://github.com/angel-dart/proxy
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=1.19.0"
|
sdk: ">=1.19.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
angel_framework: ^1.0.0-dev
|
angel_framework: ^1.1.0-alpha
|
||||||
dev_dependencies:
|
|
||||||
angel_compress: ^1.0.0
|
|
||||||
angel_diagnostics: ^1.0.0
|
|
||||||
angel_test: ^1.0.0
|
|
||||||
http: ^0.11.3
|
http: ^0.11.3
|
||||||
|
dev_dependencies:
|
||||||
|
angel_test: ^1.1.0-alpha
|
||||||
test: ^0.12.15
|
test: ^0.12.15
|
||||||
|
|
|
@ -2,8 +2,8 @@ import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:angel_proxy/angel_proxy.dart';
|
import 'package:angel_proxy/angel_proxy.dart';
|
||||||
import 'package:angel_test/angel_test.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'common.dart';
|
import 'common.dart';
|
||||||
|
|
||||||
|
@ -15,18 +15,44 @@ main() {
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
app = new Angel()..storeOriginalBuffer = true;
|
app = new Angel()..storeOriginalBuffer = true;
|
||||||
|
var httpClient = new http.Client();
|
||||||
|
|
||||||
testServer = await testApp().startServer();
|
testServer = await testApp().startServer();
|
||||||
|
|
||||||
await app.configure(new ProxyLayer(
|
var proxy1 = new Proxy(
|
||||||
testServer.address.address, testServer.port,
|
app,
|
||||||
|
httpClient,
|
||||||
|
testServer.address.address,
|
||||||
|
port: testServer.port,
|
||||||
publicPath: '/proxy',
|
publicPath: '/proxy',
|
||||||
routeAssigner: (router, path, handler) => router.all(path, handler)));
|
);
|
||||||
await app.configure(new ProxyLayer(
|
var proxy2 = new Proxy(
|
||||||
testServer.address.address, testServer.port,
|
app,
|
||||||
mapTo: '/foo'));
|
httpClient,
|
||||||
|
testServer.address.address,
|
||||||
|
port: testServer.port,
|
||||||
|
mapTo: '/foo',
|
||||||
|
);
|
||||||
|
|
||||||
app.after.add((req, res) async => res.write('intercept empty'));
|
app.use(proxy1.handleRequest);
|
||||||
|
app.use(proxy2.handleRequest);
|
||||||
|
|
||||||
|
app.use((req, res) {
|
||||||
|
print('Intercepting empty from ${req.uri}');
|
||||||
|
res.write('intercept empty');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.shutdownHooks.add((_) async {
|
||||||
|
httpClient.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.logger = new Logger('angel');
|
||||||
|
|
||||||
|
Logger.root.onRecord.listen((rec) {
|
||||||
|
print(rec);
|
||||||
|
if (rec.error != null) print(rec.error);
|
||||||
|
if (rec.stackTrace != null) print(rec.stackTrace);
|
||||||
|
});
|
||||||
|
|
||||||
server = await app.startServer();
|
server = await app.startServer();
|
||||||
url = 'http://${server.address.address}:${server.port}';
|
url = 'http://${server.address.address}:${server.port}';
|
||||||
|
@ -48,11 +74,6 @@ main() {
|
||||||
test('empty', () async {
|
test('empty', () async {
|
||||||
var response = await client.get('$url/proxy/empty');
|
var response = await client.get('$url/proxy/empty');
|
||||||
print('Response: ${response.body}');
|
print('Response: ${response.body}');
|
||||||
|
|
||||||
// Shouldn't say it is gzipped...
|
|
||||||
expect(response, isNot(hasHeader('content-encoding')));
|
|
||||||
|
|
||||||
// Should have gzipped body
|
|
||||||
expect(response.body, 'intercept empty');
|
expect(response.body, 'intercept empty');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -64,10 +85,10 @@ main() {
|
||||||
|
|
||||||
test('original buffer', () async {
|
test('original buffer', () async {
|
||||||
var response = await client.post('$url/proxy/body',
|
var response = await client.post('$url/proxy/body',
|
||||||
body: {'foo': 'bar'},
|
body: JSON.encode({'foo': 'bar'}),
|
||||||
headers: {'content-type': 'application/x-www-form-urlencoded'});
|
headers: {'content-type': 'application/json'});
|
||||||
print('Response: ${response.body}');
|
print('Response: ${response.body}');
|
||||||
expect(response.body, isNotEmpty);
|
expect(response.body, isNotEmpty);
|
||||||
expect(JSON.decode(response.body), {'foo': 'bar'});
|
expect(response.body, isNot('intercept empty'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
Angel testApp() {
|
Angel testApp() {
|
||||||
final app = new Angel();
|
final app = new Angel()..lazyParseBodies = true;
|
||||||
|
|
||||||
app.get('/hello', 'world');
|
app.get('/hello', 'world');
|
||||||
app.get('/foo/bar', 'baz');
|
app.get('/foo/bar', 'baz');
|
||||||
app.post('/body', (req, res) => req.lazyBody());
|
app.post('/body', (RequestContext req, res) async {
|
||||||
|
var body = await req.lazyBody();
|
||||||
app.fatalErrorStream.listen((e) {
|
print('Body: $body');
|
||||||
print('FATAL IN TEST APP: ${e.error}');
|
return body;
|
||||||
print(e.stack);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.logger = new Logger('testApp');
|
||||||
|
|
||||||
return app..dumpTree();
|
return app..dumpTree();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:angel_compress/angel_compress.dart';
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:angel_proxy/angel_proxy.dart';
|
import 'package:angel_proxy/angel_proxy.dart';
|
||||||
import 'package:angel_test/angel_test.dart';
|
import 'package:angel_test/angel_test.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:mock_request/mock_request.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
|
@ -12,27 +13,40 @@ main() {
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
testApp = new Angel();
|
testApp = new Angel();
|
||||||
testApp.get('/foo', (req, res) => res.write('pub serve'));
|
testApp.get('/foo', (req, res) async {
|
||||||
|
res.write('pub serve');
|
||||||
|
});
|
||||||
testApp.get('/empty', (req, res) => res.end());
|
testApp.get('/empty', (req, res) => res.end());
|
||||||
testApp.responseFinalizers.add(gzip());
|
|
||||||
|
testApp.responseFinalizers.add((req, ResponseContext res) async {
|
||||||
|
print('OUTGOING: ' + new String.fromCharCodes(res.buffer.toBytes()));
|
||||||
|
});
|
||||||
|
|
||||||
|
testApp.injectEncoders({'gzip': GZIP.encoder});
|
||||||
|
|
||||||
var server = await testApp.startServer();
|
var server = await testApp.startServer();
|
||||||
|
|
||||||
app = new Angel();
|
app = new Angel();
|
||||||
app.get('/bar', (req, res) => res.write('normal'));
|
app.get('/bar', (req, res) => res.write('normal'));
|
||||||
var layer = new PubServeLayer(
|
|
||||||
debug: true,
|
var httpClient = new http.Client();
|
||||||
|
|
||||||
|
var layer = new Proxy(
|
||||||
|
app,
|
||||||
|
httpClient,
|
||||||
|
server.address.address,
|
||||||
|
port: server.port,
|
||||||
publicPath: '/proxy',
|
publicPath: '/proxy',
|
||||||
host: server.address.address,
|
);
|
||||||
port: server.port);
|
app.use(layer.handleRequest);
|
||||||
print('streamToIO: ${layer.streamToIO}');
|
|
||||||
await app.configure(layer);
|
|
||||||
|
|
||||||
app.responseFinalizers.add((req, ResponseContext res) async {
|
app.responseFinalizers.add((req, ResponseContext res) async {
|
||||||
print('Normal. Buf: ' +
|
print('Normal. Buf: ' +
|
||||||
new String.fromCharCodes(res.buffer.toBytes()) +
|
new String.fromCharCodes(res.buffer.toBytes()) +
|
||||||
', headers: ${res.headers}');
|
', headers: ${res.headers}');
|
||||||
});
|
});
|
||||||
app.responseFinalizers.add(gzip());
|
|
||||||
|
app.injectEncoders({'gzip': GZIP.encoder});
|
||||||
|
|
||||||
client = await connectTo(app);
|
client = await connectTo(app);
|
||||||
});
|
});
|
||||||
|
@ -46,34 +60,19 @@ main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('proxied', () async {
|
test('proxied', () async {
|
||||||
var response = await client.get('/proxy/foo');
|
var rq = new MockHttpRequest('GET', Uri.parse('/proxy/foo'))..close();
|
||||||
|
await app.handleRequest(rq);
|
||||||
// Should say it is gzipped...
|
var response = await rq.response.transform(UTF8.decoder).join();
|
||||||
expect(response, hasHeader('content-encoding', 'gzip'));
|
expect(response, 'pub serve');
|
||||||
|
|
||||||
// Should have gzipped body
|
|
||||||
//
|
|
||||||
// We have to decode it, because `mock_request` does not auto-decode.
|
|
||||||
expect(response, hasBody('pub serve'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('empty', () async {
|
test('empty', () async {
|
||||||
var response = await client.get('/proxy/empty');
|
var response = await client.get('/proxy/empty');
|
||||||
|
|
||||||
// Should say it is gzipped...
|
|
||||||
expect(response, hasHeader('content-encoding', 'gzip'));
|
|
||||||
|
|
||||||
// Should have gzipped body
|
|
||||||
expect(response.body, isEmpty);
|
expect(response.body, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('normal', () async {
|
test('normal', () async {
|
||||||
var response = await client.get('/bar');
|
var response = await client.get('/bar');
|
||||||
|
|
||||||
// Should say it is gzipped...
|
|
||||||
expect(response, hasHeader('content-encoding', 'gzip'));
|
|
||||||
|
|
||||||
// Should have normal body
|
|
||||||
expect(response, hasBody('normal'));
|
expect(response, hasBody('normal'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue