platform/lib/src/proxy_layer.dart

189 lines
5.9 KiB
Dart
Raw Normal View History

2017-06-20 19:31:35 +00:00
import 'dart:async';
2016-11-23 22:03:06 +00:00
import 'dart:io';
2018-11-02 04:22:32 +00:00
import 'dart:convert';
2016-11-23 22:03:06 +00:00
import 'package:angel_framework/angel_framework.dart';
2018-11-09 00:14:32 +00:00
import 'package:angel_framework/http.dart';
2018-11-02 04:22:32 +00:00
import 'package:http_parser/http_parser.dart';
import 'package:http/http.dart' as http;
2018-11-09 00:14:32 +00:00
import 'package:path/path.dart' as p;
2016-11-23 22:03:06 +00:00
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
2018-11-02 04:22:32 +00:00
final MediaType _fallbackMediaType = MediaType('application', 'octet-stream');
2016-11-23 22:03:06 +00:00
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
2018-11-08 22:13:26 +00:00
final http.BaseClient httpClient;
2017-09-24 18:49:18 +00:00
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;
2018-11-09 00:14:32 +00:00
final Uri baseUrl;
final String publicPath;
2018-11-02 04:22:32 +00:00
/// If `null` then no timout is added for requests
2017-06-20 19:31:35 +00:00
final Duration timeout;
2017-09-24 18:49:18 +00:00
Proxy(
this.httpClient,
2018-11-09 00:14:32 +00:00
this.baseUrl, {
2017-06-20 19:31:35 +00:00
this.publicPath: '/',
this.recoverFromDead: true,
this.recoverFrom404: true,
this.timeout,
}) {
2018-11-09 00:27:50 +00:00
if (!baseUrl.hasScheme || !baseUrl.hasAuthority)
throw new ArgumentError(
'Invalid `baseUrl`. URI must have both a scheme and authority.');
2018-11-08 22:13:26 +00:00
if (this.recoverFromDead == null)
2018-11-09 00:27:50 +00:00
throw new ArgumentError.notNull("recoverFromDead");
2018-11-08 22:13:26 +00:00
if (this.recoverFrom404 == null)
2018-11-09 00:27:50 +00:00
throw new ArgumentError.notNull("recoverFrom404");
2018-11-02 04:22:32 +00:00
_prefix = publicPath?.replaceAll(_straySlashes, '') ?? '';
2016-11-23 22:03:06 +00:00
}
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
2018-11-02 04:22:32 +00:00
if (_prefix.isNotEmpty) {
2018-11-09 00:14:32 +00:00
if (!p.isWithin(_prefix, path) && !p.equals(_prefix, path)) {
return new Future<bool>.value(true);
}
2018-11-02 04:22:32 +00:00
2018-11-09 00:14:32 +00:00
path = p.relative(path, from: _prefix);
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.
2018-11-08 22:13:26 +00:00
Future<bool> servePath(
String path, RequestContext req, ResponseContext res) async {
2017-09-24 18:49:18 +00:00
http.StreamedResponse rs;
2016-11-23 22:03:06 +00:00
2018-11-09 00:14:32 +00:00
var uri = baseUrl.replace(path: p.join(baseUrl.path, path));
2017-04-26 12:20:14 +00:00
2018-11-09 00:14:32 +00:00
try {
if (req is HttpRequestContext &&
WebSocketTransformer.isUpgradeRequest(req.rawRequest)) {
res.detach();
uri = uri.replace(scheme: uri.scheme == 'https' ? 'wss' : 'ws');
try {
var local = await WebSocketTransformer.upgrade(req.rawRequest);
var remote = await WebSocket.connect(uri.toString());
2018-11-09 00:27:50 +00:00
local.pipe(remote);
remote.pipe(local);
2018-11-09 00:14:32 +00:00
return false;
} catch (e, st) {
throw new AngelHttpException(e,
message: 'Could not connect WebSocket', stackTrace: st);
}
}
2017-09-24 18:49:18 +00:00
2018-11-09 00:14:32 +00:00
Future<http.StreamedResponse> accessRemote() async {
2017-09-24 18:49:18 +00:00
var headers = <String, String>{
2018-11-09 00:14:32 +00:00
'host': uri.authority,
'x-forwarded-for': req.remoteAddress.address,
'x-forwarded-port': req.uri.port.toString(),
2018-11-08 22:13:26 +00:00
'x-forwarded-host':
req.headers.host ?? req.headers.value('host') ?? 'none',
2018-11-09 00:14:32 +00:00
'x-forwarded-proto': uri.scheme,
2017-09-24 18:49:18 +00:00
};
req.headers.forEach((name, values) {
headers[name] = values.join(',');
});
2018-11-08 22:13:26 +00:00
headers[HttpHeaders.cookieHeader] =
req.cookies.map<String>((c) => '${c.name}=${c.value}').join('; ');
2017-09-24 18:49:18 +00:00
2018-11-08 22:13:26 +00:00
List<int> body;
2017-09-24 18:49:18 +00:00
2018-12-09 22:39:11 +00:00
if (!req.hasParsedBody) {
body = await req.body
.fold<BytesBuilder>(new BytesBuilder(), (bb, buf) => bb..add(buf))
.then((bb) => bb.takeBytes());
2017-06-20 19:31:35 +00:00
}
2018-11-09 00:14:32 +00:00
var rq = new http.Request(req.method, uri);
2017-09-24 18:49:18 +00:00
rq.headers.addAll(headers);
rq.headers['host'] = rq.url.host;
2018-11-09 00:27:50 +00:00
rq.encoding = new Utf8Codec(allowMalformed: true);
2017-09-24 18:49:18 +00:00
if (body != null) rq.bodyBytes = body;
return 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) {
2018-11-02 04:22:32 +00:00
if (recoverFromDead) return true;
throw new AngelHttpException(
e,
stackTrace: st,
statusCode: 504,
2018-11-08 22:13:26 +00:00
message:
2018-11-09 00:14:32 +00:00
'Connection to remote host "$uri" timed out after ${timeout.inMilliseconds}ms.',
2018-11-02 04:22:32 +00:00
);
} catch (e) {
2018-11-09 00:14:32 +00:00
if (recoverFromDead && e is! AngelHttpException) return true;
2018-11-02 04:22:32 +00:00
rethrow;
}
if (rs.statusCode == 404 && recoverFrom404) return true;
if (rs.contentLength == 0 && recoverFromDead) return true;
MediaType mediaType;
if (rs.headers.containsKey(HttpHeaders.contentTypeHeader)) {
try {
2018-11-09 00:27:50 +00:00
mediaType =
new MediaType.parse(rs.headers[HttpHeaders.contentTypeHeader]);
2018-11-02 04:22:32 +00:00
} on FormatException catch (e, st) {
if (recoverFromDead) return true;
2017-09-24 18:49:18 +00:00
throw new AngelHttpException(
e,
stackTrace: st,
statusCode: 504,
2018-11-09 00:14:32 +00:00
message: 'Host "$uri" returned a malformed content-type',
2017-09-24 18:49:18 +00:00
);
2018-11-02 04:22:32 +00:00
}
} else {
mediaType = _fallbackMediaType;
2017-04-02 02:06:27 +00:00
}
/// if [http.Client] does not provide us with a content length
/// OR [http.Client] is about to decode the response (bytecount returned by [http.Response].stream != known length)
/// then we can not provide a value downstream => set to '-1' for 'unspecified length'
var isContentLengthUnknown = rs.contentLength == null ||
rs.headers[HttpHeaders.contentEncodingHeader]?.isNotEmpty == true ||
rs.headers[HttpHeaders.transferEncodingHeader]?.isNotEmpty == true;
var proxiedHeaders = new Map<String, String>.from(rs.headers)
2018-11-08 22:13:26 +00:00
..remove(
HttpHeaders.contentEncodingHeader) // drop, http.Client has decoded
..remove(
HttpHeaders.transferEncodingHeader) // drop, http.Client has decoded
..[HttpHeaders.contentLengthHeader] =
"${isContentLengthUnknown ? '-1' : rs.contentLength}";
2017-04-24 20:46:52 +00:00
2017-04-02 02:06:27 +00:00
res
2018-11-02 04:22:32 +00:00
..contentType = mediaType
2017-04-02 02:06:27 +00:00
..statusCode = rs.statusCode
2018-11-02 04:22:32 +00:00
..headers.addAll(proxiedHeaders);
2017-04-22 18:46:00 +00:00
await rs.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
}
}