Add 'packages/proxy/' from commit 'daca263062d471ae007730fa8ecf854c09c746f8'

git-subtree-dir: packages/proxy
git-subtree-mainline: ee512d5ccf
git-subtree-split: daca263062
This commit is contained in:
Tobe O 2020-02-15 18:22:25 -05:00
commit dd33154af1
23 changed files with 773 additions and 0 deletions

71
packages/proxy/.gitignore vendored Normal file
View file

@ -0,0 +1,71 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.buildlog
.packages
.project
.pub/
build/
**/packages/
# Files created by dart2js
# (Most Dart developers will use pub build to compile Dart, use/modify these
# rules if you intend to use dart2js directly
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
# differentiate from explicit Javascript files)
*.dart.js
*.part.js
*.js.deps
*.js.map
*.info.json
# Directory created by dartdoc
doc/api/
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)
pubspec.lock
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
# Sensitive or high-churn files:
.idea/dataSources/
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
.dart_tool

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="ECMAScript 6" />
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
<component name="ProjectInspectionProfilesVisibleTreeState">
<entry key="Project Default">
<profile-state>
<expanded-state>
<State>
<id />
</State>
<State>
<id>General</id>
</State>
<State>
<id>XPath</id>
</State>
</expanded-state>
<selected-state>
<State>
<id>AngularJS</id>
</State>
</selected-state>
</profile-state>
</entry>
</component>
</project>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/angel_proxy.iml" filepath="$PROJECT_DIR$/.idea/angel_proxy.iml" />
</modules>
</component>
</project>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
<option name="filePath" value="$PROJECT_DIR$" />
<option name="scope" value="FOLDER" />
<option name="testRunnerOptions" value="-j 4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Basic Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/basic_test.dart" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Pub Serve Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/pub_serve_test.dart" />
<method />
</configuration>
</component>

View 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>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View file

@ -0,0 +1 @@
language: dart

11
packages/proxy/.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Multiple Proxies",
"type": "dart-cli",
"request": "launch",
"program": "${workspaceRoot}/example/multiple.dart"
}
]
}

View file

@ -0,0 +1,26 @@
# 2.2.0
* Use `http.Client` instead of `http.BaseClient`, and make it an
optional parameter.
* Allow `baseUrl` to accept `Uri` or `String`.
* Add `Proxy.pushState`.
# 2.1.2
* Apply lints.
# 2.1.1
* Update for framework@2.0.0-alpha.15
# 2.1.0
- Use `Uri` instead of archaic `host`, `port`, and `mapTo`. Also cleaner + safer + easier.
* Enable WebSocket proxying.
# 2.0.0
- Updates for Angel 2. Big thanks to @denkuy!
- Use `package:path` for better path resolution.
# 1.1.1
- Removed reference to `io`; now works with HTTP/2. Thanks to @daniel-v!

21
packages/proxy/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 The Angel Framework
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

34
packages/proxy/README.md Normal file
View file

@ -0,0 +1,34 @@
# proxy
[![Pub](https://img.shields.io/pub/v/angel_proxy.svg)](https://pub.dartlang.org/packages/angel_proxy)
[![build status](https://travis-ci.org/angel-dart/proxy.svg)](https://travis-ci.org/angel-dart/proxy)
Angel middleware to forward requests to another server (i.e. `webdev serve`).
Also supports WebSockets.
```dart
import 'package:angel_proxy/angel_proxy.dart';
import 'package:http/http.dart' as http;
main() async {
// Forward requests instead of serving statically.
// You can also pass a URI, instead of a string.
var proxy1 = Proxy('http://localhost:3000');
// handle all methods (GET, POST, ...)
app.fallback(proxy.handleRequest);
}
```
You can also restrict the proxy to serving only from a specific root:
```dart
Proxy(baseUrl, publicPath: '/remote');
```
Also, you can map requests to a root path on the remote server:
```dart
Proxy(baseUrl.replace(path: '/path'));
```
Request bodies will be forwarded as well, if they are not empty. This allows things like POST requests to function.
For a request body to be forwarded, the body must not have already been parsed.

View file

@ -0,0 +1,8 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false
linter:
rules:
- unnecessary_const
- unnecessary_new

View file

@ -0,0 +1,63 @@
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_proxy/angel_proxy.dart';
import 'package:logging/logging.dart';
final Duration timeout = Duration(seconds: 5);
main() async {
var app = Angel();
// Forward any /api requests to pub.
// By default, if the host throws a 404, the request will fall through to the next handler.
var pubProxy = Proxy(
'https://pub.dartlang.org',
publicPath: '/pub',
timeout: timeout,
);
app.all("/pub/*", pubProxy.handleRequest);
// Surprise! We can also proxy WebSockets.
//
// Play around with this at http://www.websocket.org/echo.html.
var echoProxy = Proxy(
'http://echo.websocket.org',
publicPath: '/echo',
timeout: timeout,
);
app.get('/echo', echoProxy.handleRequest);
// 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:
app.get('/static/*', (RequestContext req, res) {
return pubProxy.servePath(req.path, req, res);
});
// Anything else should fall through to dartlang.org.
var dartlangProxy = Proxy(
'https://dartlang.org',
timeout: timeout,
recoverFrom404: false,
);
app.all('*', dartlangProxy.handleRequest);
// In case we can't connect to dartlang.org, show an error.
app.fallback(
(req, res) => res.write('Couldn\'t connect to Pub or dartlang.'));
app.logger = Logger('angel')
..onRecord.listen(
(rec) {
print(rec);
if (rec.error != null) print(rec.error);
if (rec.stackTrace != null) print(rec.stackTrace);
},
);
var server =
await AngelHttp(app).startServer(InternetAddress.loopbackIPv4, 8080);
print('Listening at http://${server.address.address}:${server.port}');
print(
'Check this out! http://${server.address.address}:${server.port}/pub/packages/angel_framework');
}

View file

@ -0,0 +1,3 @@
library angel_proxy;
export 'src/proxy_layer.dart';

View file

@ -0,0 +1,218 @@
import 'dart:async';
import 'dart:io';
import 'dart:convert';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:http_parser/http_parser.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as p;
final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)');
final MediaType _fallbackMediaType = MediaType('application', 'octet-stream');
/// A middleware class that forwards requests (reverse proxies) to an upstream server.
///
/// Supports WebSockets, in addition to regular HTTP requests.
class Proxy {
String _prefix;
/// The underlying [Client] to use.
final http.Client httpClient;
/// 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 recoverFrom404;
final Uri baseUrl;
final String publicPath;
/// If `null` then no timout is added for requests
final Duration timeout;
Proxy(
baseUrl, {
http.Client httpClient,
this.publicPath = '/',
this.recoverFromDead = true,
this.recoverFrom404 = true,
this.timeout,
}) : this.baseUrl = baseUrl is Uri ? baseUrl : Uri.parse(baseUrl.toString()),
this.httpClient = httpClient ?? http.Client() {
if (!this.baseUrl.hasScheme || !this.baseUrl.hasAuthority) {
throw ArgumentError(
'Invalid `baseUrl`. URI must have both a scheme and authority.');
}
if (this.recoverFromDead == null) {
throw ArgumentError.notNull("recoverFromDead");
}
if (this.recoverFrom404 == null) {
throw ArgumentError.notNull("recoverFrom404");
}
_prefix = publicPath?.replaceAll(_straySlashes, '') ?? '';
}
void close() => httpClient.close();
/// A handler that serves the file at the given path, unless the user has requested that path.
///
/// You can also limit this functionality to specific values of the `Accept` header, ex. `text/html`.
/// If [accepts] is `null`, OR at least one of the content types in [accepts] is present,
/// the view will be served.
RequestHandler pushState(String path, {Iterable accepts}) {
var vPath = path.replaceAll(_straySlashes, '');
if (_prefix?.isNotEmpty == true) vPath = '$_prefix/$vPath';
return (RequestContext req, ResponseContext res) {
var path = req.path.replaceAll(_straySlashes, '');
if (path == vPath) return Future<bool>.value(true);
if (accepts?.isNotEmpty == true) {
if (!accepts.any((x) => req.accepts(x, strict: true))) {
return Future<bool>.value(true);
}
}
return servePath(vPath, req, res);
};
}
/// Handles an incoming HTTP request.
Future<bool> handleRequest(RequestContext req, ResponseContext res) {
var path = req.path.replaceAll(_straySlashes, '');
if (_prefix.isNotEmpty) {
if (!p.isWithin(_prefix, path) && !p.equals(_prefix, path)) {
return Future<bool>.value(true);
}
path = p.relative(path, from: _prefix);
}
return servePath(path, req, res);
}
/// Proxies a request to the given path on the remote server.
Future<bool> servePath(
String path, RequestContext req, ResponseContext res) async {
http.StreamedResponse rs;
var uri = baseUrl.replace(path: p.join(baseUrl.path, path));
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());
scheduleMicrotask(() => local.pipe(remote));
scheduleMicrotask(() => remote.pipe(local));
return false;
} catch (e, st) {
throw AngelHttpException(e,
message: 'Could not connect WebSocket', stackTrace: st);
}
}
Future<http.StreamedResponse> accessRemote() async {
var headers = <String, String>{
'host': uri.authority,
'x-forwarded-for': req.remoteAddress.address,
'x-forwarded-port': req.uri.port.toString(),
'x-forwarded-host':
req.headers.host ?? req.headers.value('host') ?? 'none',
'x-forwarded-proto': uri.scheme,
};
req.headers.forEach((name, values) {
headers[name] = values.join(',');
});
headers[HttpHeaders.cookieHeader] =
req.cookies.map<String>((c) => '${c.name}=${c.value}').join('; ');
List<int> body;
if (!req.hasParsedBody) {
body = await req.body
.fold<BytesBuilder>(BytesBuilder(), (bb, buf) => bb..add(buf))
.then((bb) => bb.takeBytes());
}
var rq = http.Request(req.method, uri);
rq.headers.addAll(headers);
rq.headers['host'] = rq.url.host;
rq.encoding = Utf8Codec(allowMalformed: true);
if (body != null) rq.bodyBytes = body;
return httpClient.send(rq);
}
var future = accessRemote();
if (timeout != null) future = future.timeout(timeout);
rs = await future;
} on TimeoutException catch (e, st) {
if (recoverFromDead) return true;
throw AngelHttpException(
e,
stackTrace: st,
statusCode: 504,
message:
'Connection to remote host "$uri" timed out after ${timeout.inMilliseconds}ms.',
);
} catch (e) {
if (recoverFromDead && e is! AngelHttpException) return true;
rethrow;
}
if (rs.statusCode == 404 && recoverFrom404) return true;
if (rs.contentLength == 0 && recoverFromDead) return true;
MediaType mediaType;
if (rs.headers.containsKey(HttpHeaders.contentTypeHeader)) {
try {
mediaType = MediaType.parse(rs.headers[HttpHeaders.contentTypeHeader]);
} on FormatException catch (e, st) {
if (recoverFromDead) return true;
throw AngelHttpException(
e,
stackTrace: st,
statusCode: 504,
message: 'Host "$uri" returned a malformed content-type',
);
}
} else {
mediaType = _fallbackMediaType;
}
/// 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 = Map<String, String>.from(rs.headers)
..remove(
HttpHeaders.contentEncodingHeader) // drop, http.Client has decoded
..remove(
HttpHeaders.transferEncodingHeader) // drop, http.Client has decoded
..[HttpHeaders.contentLengthHeader] =
"${isContentLengthUnknown ? '-1' : rs.contentLength}";
res
..contentType = mediaType
..statusCode = rs.statusCode
..headers.addAll(proxiedHeaders);
await rs.stream.pipe(res);
return false;
}
}

View file

@ -0,0 +1,19 @@
name: angel_proxy
description: Angel middleware to forward requests to another server (i.e. pub serve).
version: 2.2.0
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/proxy
environment:
sdk: ">=2.0.0 <3.0.0"
dependencies:
angel_framework: ^2.0.0-alpha
http: ^0.12.0
http_parser: ^3.0.0
path: ^1.0.0
dev_dependencies:
angel_test: ^2.0.0-alpha
logging:
mock_request:
pedantic: ^1.0.0
test: ^1.0.0

View file

@ -0,0 +1,88 @@
import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_proxy/angel_proxy.dart';
import 'package:http/io_client.dart' as http;
import 'package:logging/logging.dart';
import 'package:test/test.dart';
import 'common.dart';
main() {
Angel app;
var client = http.IOClient();
HttpServer server, testServer;
String url;
setUp(() async {
app = Angel();
var appHttp = AngelHttp(app);
testServer = await startTestServer();
var proxy1 = Proxy(
Uri(
scheme: 'http',
host: testServer.address.address,
port: testServer.port),
publicPath: '/proxy',
);
var proxy2 = Proxy(proxy1.baseUrl.replace(path: '/foo'));
print('Proxy 1 on: ${proxy1.baseUrl}');
print('Proxy 2 on: ${proxy2.baseUrl}');
app.all("/proxy/*", proxy1.handleRequest);
app.all("*", proxy2.handleRequest);
app.fallback((req, res) {
print('Intercepting empty from ${req.uri}');
res.write('intercept empty');
});
app.logger = Logger('angel');
Logger.root.onRecord.listen((rec) {
print(rec);
if (rec.error != null) print(rec.error);
if (rec.stackTrace != null) print(rec.stackTrace);
});
await appHttp.startServer();
url = appHttp.uri.toString();
});
tearDown(() async {
await testServer?.close(force: true);
await server?.close(force: true);
app = null;
url = null;
});
test('publicPath', () async {
final response = await client.get('$url/proxy/hello');
print('Response: ${response.body}');
expect(response.body, equals('world'));
});
test('empty', () async {
var response = await client.get('$url/proxy/empty');
print('Response: ${response.body}');
expect(response.body, 'intercept empty');
});
test('mapTo', () async {
final response = await client.get('$url/bar');
print('Response: ${response.body}');
expect(response.body, equals('baz'));
});
test('original buffer', () async {
var response = await client.post('$url/proxy/body',
body: json.encode({'foo': 'bar'}),
headers: {'content-type': 'application/json'});
print('Response: ${response.body}');
expect(response.body, isNotEmpty);
expect(response.body, isNot('intercept empty'));
});
}

View file

@ -0,0 +1,24 @@
import 'dart:async';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:logging/logging.dart';
Future<HttpServer> startTestServer() {
final app = Angel();
app.get('/hello', (req, res) => res.write('world'));
app.get('/foo/bar', (req, res) => res.write('baz'));
app.post('/body', (RequestContext req, res) async {
var body = await req.parseBody().then((_) => req.bodyAsMap);
app.logger.info('Body: $body');
return body;
});
app.logger = Logger('testApp');
var server = AngelHttp(app);
app.dumpTree();
return server.startServer();
}

View file

@ -0,0 +1,94 @@
import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_proxy/angel_proxy.dart';
import 'package:angel_test/angel_test.dart';
import 'package:logging/logging.dart';
import 'package:mock_request/mock_request.dart';
import 'package:test/test.dart';
main() {
Angel app, testApp;
TestClient client;
Proxy layer;
setUp(() async {
testApp = Angel();
testApp.get('/foo', (req, res) async {
res.useBuffer();
res.write('pub serve');
});
testApp.get('/empty', (req, res) => res.close());
testApp.responseFinalizers.add((req, res) async {
print('OUTGOING: ' + String.fromCharCodes(res.buffer.toBytes()));
});
testApp.encoders.addAll({'gzip': gzip.encoder});
var server = await AngelHttp(testApp).startServer();
app = Angel();
app.fallback((req, res) {
res.useBuffer();
return true;
});
app.get('/bar', (req, res) => res.write('normal'));
layer = Proxy(
Uri(scheme: 'http', host: server.address.address, port: server.port),
publicPath: '/proxy',
);
app.fallback(layer.handleRequest);
app.responseFinalizers.add((req, res) async {
print('Normal. Buf: ' +
String.fromCharCodes(res.buffer.toBytes()) +
', headers: ${res.headers}');
});
app.encoders.addAll({'gzip': gzip.encoder});
client = await connectTo(app);
app.logger = testApp.logger = Logger('proxy')
..onRecord.listen((rec) {
print(rec);
if (rec.error != null) print(rec.error);
if (rec.stackTrace != null) print(rec.stackTrace);
});
});
tearDown(() async {
await client.close();
await app.close();
await testApp.close();
app = null;
testApp = null;
});
test('proxied', () async {
var rq = MockHttpRequest('GET', Uri.parse('/proxy/foo'));
await rq.close();
var rqc = await HttpRequestContext.from(rq, app, '/proxy/foo');
var rsc = HttpResponseContext(rq.response, app);
await app.executeHandler(layer.handleRequest, rqc, rsc);
var response = await rq.response
//.transform(gzip.decoder)
.transform(utf8.decoder)
.join();
expect(response, 'pub serve');
});
test('empty', () async {
var response = await client.get('/proxy/empty');
expect(response.body, isEmpty);
});
test('normal', () async {
var response = await client.get('/bar');
expect(response, hasBody('normal'));
});
}