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-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;
|
2016-11-23 22:03:06 +00:00
|
|
|
|
|
|
|
ProxyLayer(this.host, this.port,
|
|
|
|
{this.debug: false,
|
|
|
|
this.mapTo: '/',
|
|
|
|
this.publicPath: '/',
|
2017-01-09 02:11:20 +00:00
|
|
|
this.protocol: 'http',
|
2017-04-24 20:46:52 +00:00
|
|
|
this.recoverFrom404: true,
|
2017-04-22 18:46:00 +00:00
|
|
|
this.streamToIO: false,
|
2016-11-23 22:03:06 +00:00
|
|
|
SecurityContext securityContext}) {
|
|
|
|
_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
|
|
|
|
|
|
|
_printDebug(msg) {
|
|
|
|
if (debug == true) print(msg);
|
|
|
|
}
|
|
|
|
|
|
|
|
void close() => _client.close(force: true);
|
|
|
|
|
|
|
|
void serve(Router router) {
|
|
|
|
_printDebug('Public path prefix: "$_prefix"');
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
if (_prefix.isNotEmpty) {
|
|
|
|
_path = path.replaceAll(new RegExp('^' + _pathify(_prefix)), '');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create mapping
|
2017-01-12 00:12:23 +00:00
|
|
|
_printDebug('Serving path $_path via proxy');
|
2016-11-23 22:03:06 +00:00
|
|
|
final mapping = '$mapTo/$_path'.replaceAll(_straySlashes, '');
|
2017-01-12 00:12:23 +00:00
|
|
|
_printDebug('Mapped path $_path to path $mapping on proxy $host:$port');
|
2016-11-23 22:03:06 +00:00
|
|
|
final rq = await _client.open(req.method, host, port, mapping);
|
2017-01-12 00:12:23 +00:00
|
|
|
_printDebug('Opened client request');
|
2017-01-09 02:11:20 +00:00
|
|
|
|
2017-01-12 00:19:15 +00:00
|
|
|
copyHeaders(req.headers, rq.headers);
|
|
|
|
_printDebug('Copied headers');
|
|
|
|
rq.cookies.addAll(req.cookies ?? []);
|
|
|
|
_printDebug('Added cookies');
|
2017-01-09 02:11:20 +00:00
|
|
|
rq.headers
|
2017-01-12 00:19:15 +00:00
|
|
|
.set('X-Forwarded-For', req.io.connectionInfo.remoteAddress.address);
|
|
|
|
rq.headers
|
|
|
|
..set('X-Forwarded-Port', req.io.connectionInfo.remotePort.toString())
|
2017-01-12 00:12:23 +00:00
|
|
|
..set('X-Forwarded-Host',
|
2017-01-09 02:11:20 +00:00
|
|
|
req.headers.host ?? req.headers.value(HttpHeaders.HOST) ?? 'none')
|
2017-01-12 00:12:23 +00:00
|
|
|
..set('X-Forwarded-Proto', protocol);
|
|
|
|
_printDebug('Added X-Forwarded headers');
|
2017-01-09 02:11:20 +00:00
|
|
|
|
2017-04-02 02:06:27 +00:00
|
|
|
if (app.storeOriginalBuffer == true) {
|
|
|
|
await req.parse();
|
|
|
|
if (req.originalBuffer?.isNotEmpty == true) rq.add(req.originalBuffer);
|
|
|
|
}
|
|
|
|
|
|
|
|
await rq.flush();
|
2016-11-23 22:03:06 +00:00
|
|
|
final HttpClientResponse rs = await rq.close();
|
2017-01-12 00:12:23 +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-04-22 18:46:00 +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-04-22 19:00:38 +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
|
|
|
}
|
|
|
|
}
|