2017-06-20 19:31:35 +00:00
|
|
|
import 'dart:async';
|
2017-09-24 18:49:18 +00:00
|
|
|
import 'dart:convert';
|
2016-11-23 22:03:06 +00:00
|
|
|
import 'dart:io';
|
|
|
|
import 'package:angel_framework/angel_framework.dart';
|
2017-09-24 18:49:18 +00:00
|
|
|
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;
|
2016-11-23 22:03:06 +00:00
|
|
|
|
|
|
|
final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?');
|
|
|
|
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
|
|
|
|
2017-09-24 18:49:18 +00:00
|
|
|
class Proxy {
|
2016-11-23 22:03:06 +00:00
|
|
|
String _prefix;
|
2017-04-26 12:20:14 +00:00
|
|
|
|
2017-09-24 18:49:18 +00:00
|
|
|
final Angel app;
|
|
|
|
final http.BaseClient httpClient;
|
|
|
|
|
2017-04-26 12:20:14 +00:00
|
|
|
/// If `true` (default), then the plug-in will ignore failures to connect to the proxy, and allow other handlers to run.
|
|
|
|
final bool recoverFromDead;
|
2017-09-24 18:49:18 +00:00
|
|
|
final bool recoverFrom404;
|
2016-11-23 22:03:06 +00:00
|
|
|
final String host, mapTo, publicPath;
|
|
|
|
final int port;
|
2017-01-09 02:11:20 +00:00
|
|
|
final String protocol;
|
2017-06-20 19:31:35 +00:00
|
|
|
final Duration timeout;
|
|
|
|
|
2017-09-24 18:49:18 +00:00
|
|
|
Proxy(
|
|
|
|
this.app,
|
|
|
|
this.httpClient,
|
|
|
|
this.host, {
|
|
|
|
this.port,
|
2017-06-20 19:31:35 +00:00
|
|
|
this.mapTo: '/',
|
|
|
|
this.publicPath: '/',
|
|
|
|
this.protocol: 'http',
|
|
|
|
this.recoverFromDead: true,
|
|
|
|
this.recoverFrom404: true,
|
|
|
|
this.timeout,
|
|
|
|
}) {
|
2016-11-23 22:03:06 +00:00
|
|
|
_prefix = publicPath.replaceAll(_straySlashes, '');
|
|
|
|
}
|
|
|
|
|
2017-09-24 18:49:18 +00:00
|
|
|
void close() => httpClient.close();
|
2016-11-23 22:03:06 +00:00
|
|
|
|
2017-09-24 18:49:18 +00:00
|
|
|
/// Handles an incoming HTTP request.
|
|
|
|
Future<bool> handleRequest(RequestContext req, ResponseContext res) {
|
|
|
|
var path = req.path.replaceAll(_straySlashes, '');
|
2016-11-23 22:03:06 +00:00
|
|
|
|
2017-09-24 18:49:18 +00:00
|
|
|
if (_prefix?.isNotEmpty == true) {
|
|
|
|
if (!path.startsWith(_prefix))
|
|
|
|
return new Future<bool>.value(true);
|
|
|
|
else {
|
|
|
|
path = path.replaceFirst(_prefix, '').replaceAll(_straySlashes, '');
|
|
|
|
}
|
2016-11-23 22:03:06 +00:00
|
|
|
}
|
|
|
|
|
2017-09-24 18:49:18 +00:00
|
|
|
return servePath(path, req, res);
|
2016-11-23 22:03:06 +00:00
|
|
|
}
|
|
|
|
|
2017-09-24 18:49:18 +00:00
|
|
|
/// Proxies a request to the given path on the remote server.
|
|
|
|
Future<bool> servePath(
|
|
|
|
String path, RequestContext req, ResponseContext res) async {
|
|
|
|
http.StreamedResponse rs;
|
2016-11-23 22:03:06 +00:00
|
|
|
|
2017-09-24 18:49:18 +00:00
|
|
|
final mapping = '$mapTo/$path'.replaceAll(_straySlashes, '');
|
2017-04-26 12:20:14 +00:00
|
|
|
|
|
|
|
try {
|
2017-09-24 18:49:18 +00:00
|
|
|
Future<http.StreamedResponse> accessRemote() async {
|
|
|
|
var url = port == null ? host : '$host:$port';
|
|
|
|
url = url.replaceAll(_straySlashes, '');
|
|
|
|
url = '$url/$mapping';
|
|
|
|
|
|
|
|
if (!url.startsWith('http')) url = 'http://$url';
|
|
|
|
url = url.replaceAll(_straySlashes, '');
|
|
|
|
|
|
|
|
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) {
|
2017-06-20 19:31:35 +00:00
|
|
|
await req.parse();
|
2017-09-24 18:49:18 +00:00
|
|
|
if (req.originalBuffer?.isNotEmpty == true) body = req.originalBuffer;
|
2017-06-20 19:31:35 +00:00
|
|
|
}
|
|
|
|
|
2017-09-24 18:49:18 +00:00
|
|
|
var rq = new http.Request(req.method, Uri.parse(url));
|
|
|
|
rq.headers.addAll(headers);
|
|
|
|
rq.headers['host'] = rq.url.host;
|
|
|
|
|
|
|
|
if (body != null) rq.bodyBytes = body;
|
|
|
|
|
|
|
|
return await httpClient.send(rq);
|
2017-04-26 12:20:14 +00:00
|
|
|
}
|
|
|
|
|
2017-06-20 19:31:35 +00:00
|
|
|
var future = accessRemote();
|
|
|
|
if (timeout != null) future = future.timeout(timeout);
|
|
|
|
rs = await future;
|
|
|
|
} on TimeoutException catch (e, st) {
|
|
|
|
if (recoverFromDead != false)
|
|
|
|
return true;
|
|
|
|
else
|
2017-09-24 18:49:18 +00:00
|
|
|
throw new AngelHttpException(
|
|
|
|
e,
|
|
|
|
stackTrace: st,
|
|
|
|
statusCode: 504,
|
|
|
|
message:
|
|
|
|
'Connection to remote host "$host" timed out after ${timeout.inMilliseconds}ms.',
|
|
|
|
);
|
2017-04-26 12:20:14 +00:00
|
|
|
} catch (e) {
|
|
|
|
if (recoverFromDead != false)
|
|
|
|
return true;
|
|
|
|
else
|
|
|
|
rethrow;
|
2017-04-02 02:06:27 +00:00
|
|
|
}
|
|
|
|
|
2017-04-24 20:46:52 +00:00
|
|
|
if (rs.statusCode == 404 && recoverFrom404 != false) return true;
|
|
|
|
|
2017-04-02 02:06:27 +00:00
|
|
|
res
|
|
|
|
..statusCode = rs.statusCode
|
2017-09-24 18:49:18 +00:00
|
|
|
..headers.addAll(rs.headers);
|
2017-04-02 02:06:27 +00:00
|
|
|
|
2017-09-24 18:49:18 +00:00
|
|
|
if (rs.contentLength == 0 && recoverFromDead != false) return true;
|
2017-04-22 18:46:00 +00:00
|
|
|
|
2017-09-24 18:49:18 +00:00
|
|
|
var stream = rs.stream;
|
2017-04-24 20:46:52 +00:00
|
|
|
|
2017-09-24 18:49:18 +00:00
|
|
|
if (rs.headers['content-encoding'] == 'gzip')
|
|
|
|
stream = stream.transform(GZIP.encoder);
|
2017-04-24 20:46:52 +00:00
|
|
|
|
2017-09-24 18:49:18 +00:00
|
|
|
await stream.pipe(res);
|
2017-04-22 18:46:00 +00:00
|
|
|
|
2017-09-24 18:49:18 +00:00
|
|
|
return false;
|
2016-11-23 22:03:06 +00:00
|
|
|
}
|
|
|
|
}
|