platform/lib/src/proxy_layer.dart

195 lines
5.7 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';
import 'package:angel_framework/angel_framework.dart';
final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?');
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
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;
}
2017-01-09 02:11:20 +00:00
/// Copies HTTP headers ;)
void copyHeaders(HttpHeaders from, HttpHeaders to) {
2017-01-25 22:43:46 +00:00
from.forEach((k, v) {
2017-04-24 20:46:52 +00:00
if (k != HttpHeaders.SERVER &&
(k != HttpHeaders.CONTENT_ENCODING || !v.contains('gzip')))
to.set(k, v);
2017-01-25 22:43:46 +00:00
});
2017-01-12 00:19:15 +00:00
/*to
2017-01-09 02:11:20 +00:00
..chunkedTransferEncoding = from.chunkedTransferEncoding
..contentLength = from.contentLength
..contentType = from.contentType
..date = from.date
..expires = from.expires
..host = from.host
..ifModifiedSince = from.ifModifiedSince
..persistentConnection = from.persistentConnection
2017-01-12 00:19:15 +00:00
..port = from.port;*/
2017-01-09 02:11:20 +00:00
}
2016-11-23 22:03:06 +00:00
class ProxyLayer {
2017-04-02 02:06:27 +00:00
Angel app;
2016-11-23 22:03:06 +00:00
HttpClient _client;
String _prefix;
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-04-24 20:46:52 +00:00
final bool debug, recoverFrom404, streamToIO;
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;
ProxyLayer(
this.host,
this.port, {
this.debug: false,
this.mapTo: '/',
this.publicPath: '/',
this.protocol: 'http',
this.recoverFromDead: true,
this.recoverFrom404: true,
this.streamToIO: false,
this.timeout,
SecurityContext securityContext,
}) {
2016-11-23 22:03:06 +00:00
_client = new HttpClient(context: securityContext);
_prefix = publicPath.replaceAll(_straySlashes, '');
}
2017-04-02 02:06:27 +00:00
call(Angel app) async => serve(this.app = app);
2016-11-23 22:03:06 +00:00
void close() => _client.close(force: true);
void serve(Router router) {
2017-06-20 19:31:35 +00:00
// _printDebug('Public path prefix: "$_prefix"');
2016-11-23 22:03:06 +00:00
handler(RequestContext req, ResponseContext res) async {
var path = req.path.replaceAll(_straySlashes, '');
return serveFile(path, req, res);
}
2017-04-02 02:06:27 +00:00
router.all('$publicPath/*', handler);
2016-11-23 22:03:06 +00:00
}
serveFile(String path, RequestContext req, ResponseContext res) async {
var _path = path;
2017-04-26 12:20:14 +00:00
HttpClientResponse rs;
2016-11-23 22:03:06 +00:00
if (_prefix.isNotEmpty) {
_path = path.replaceAll(new RegExp('^' + _pathify(_prefix)), '');
}
// Create mapping
2017-06-20 19:31:35 +00:00
// _printDebug('Serving path $_path via proxy');
2016-11-23 22:03:06 +00:00
final mapping = '$mapTo/$_path'.replaceAll(_straySlashes, '');
2017-06-20 19:31:35 +00:00
// _printDebug('Mapped path $_path to path $mapping on proxy $host:$port');
2017-04-26 12:20:14 +00:00
try {
2017-06-20 19:31:35 +00:00
Future<HttpClientResponse> accessRemote() async {
var ips = await InternetAddress.lookup(host);
if (ips.isEmpty)
throw new StateError('Could not resolve remote host "$host".');
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);
rq.headers.set(HttpHeaders.HOST, host);
// _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) {
await req.parse();
if (req.originalBuffer?.isNotEmpty == true)
rq.add(req.originalBuffer);
}
await rq.flush();
return await rq.close();
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
throw new AngelHttpException(e,
stackTrace: st,
statusCode: HttpStatus.GATEWAY_TIMEOUT,
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-06-20 19:31:35 +00:00
// _printDebug(
// 'Proxy responded to $mapping with status code ${rs.statusCode}');
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
..contentType = rs.headers.contentType;
2017-06-20 19:31:35 +00:00
// _printDebug('Proxy response headers:\n${rs.headers}');
2017-04-02 02:06:27 +00:00
2017-04-22 18:46:00 +00:00
if (streamToIO == true) {
res
..willCloseItself = true
..end();
copyHeaders(rs.headers, res.io.headers);
2017-04-24 20:46:52 +00:00
res.io.statusCode = rs.statusCode;
if (rs.headers.contentType != null)
res.io.headers.contentType = rs.headers.contentType;
2017-06-20 19:31:35 +00:00
// _printDebug('Outgoing content length: ${res.io.contentLength}');
2017-04-22 18:46:00 +00:00
2017-04-22 19:00:38 +00:00
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);
2017-04-22 18:46:00 +00:00
} 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;
2016-11-23 22:03:06 +00:00
}
}