Add 'packages/proxy/' from commit 'daca263062d471ae007730fa8ecf854c09c746f8'
git-subtree-dir: packages/proxy git-subtree-mainline:ee512d5ccf
git-subtree-split:daca263062
This commit is contained in:
commit
dd33154af1
23 changed files with 773 additions and 0 deletions
71
packages/proxy/.gitignore
vendored
Normal file
71
packages/proxy/.gitignore
vendored
Normal 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
|
16
packages/proxy/.idea/angel_proxy.iml
Normal file
16
packages/proxy/.idea/angel_proxy.iml
Normal 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>
|
7
packages/proxy/.idea/jsLibraryMappings.xml
Normal file
7
packages/proxy/.idea/jsLibraryMappings.xml
Normal 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>
|
28
packages/proxy/.idea/misc.xml
Normal file
28
packages/proxy/.idea/misc.xml
Normal 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>
|
8
packages/proxy/.idea/modules.xml
Normal file
8
packages/proxy/.idea/modules.xml
Normal 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>
|
8
packages/proxy/.idea/runConfigurations/All_Tests.xml
Normal file
8
packages/proxy/.idea/runConfigurations/All_Tests.xml
Normal 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>
|
6
packages/proxy/.idea/runConfigurations/Basic_Tests.xml
Normal file
6
packages/proxy/.idea/runConfigurations/Basic_Tests.xml
Normal 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>
|
|
@ -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>
|
7
packages/proxy/.idea/runConfigurations/multiple_dart.xml
Normal file
7
packages/proxy/.idea/runConfigurations/multiple_dart.xml
Normal 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>
|
6
packages/proxy/.idea/vcs.xml
Normal file
6
packages/proxy/.idea/vcs.xml
Normal 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>
|
1
packages/proxy/.travis.yml
Normal file
1
packages/proxy/.travis.yml
Normal file
|
@ -0,0 +1 @@
|
|||
language: dart
|
11
packages/proxy/.vscode/launch.json
vendored
Normal file
11
packages/proxy/.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Multiple Proxies",
|
||||
"type": "dart-cli",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}/example/multiple.dart"
|
||||
}
|
||||
]
|
||||
}
|
26
packages/proxy/CHANGELOG.md
Normal file
26
packages/proxy/CHANGELOG.md
Normal 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
21
packages/proxy/LICENSE
Normal 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
34
packages/proxy/README.md
Normal 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.
|
8
packages/proxy/analysis_options.yaml
Normal file
8
packages/proxy/analysis_options.yaml
Normal file
|
@ -0,0 +1,8 @@
|
|||
include: package:pedantic/analysis_options.yaml
|
||||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
||||
linter:
|
||||
rules:
|
||||
- unnecessary_const
|
||||
- unnecessary_new
|
63
packages/proxy/example/main.dart
Normal file
63
packages/proxy/example/main.dart
Normal 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');
|
||||
}
|
3
packages/proxy/lib/angel_proxy.dart
Normal file
3
packages/proxy/lib/angel_proxy.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
library angel_proxy;
|
||||
|
||||
export 'src/proxy_layer.dart';
|
218
packages/proxy/lib/src/proxy_layer.dart
Normal file
218
packages/proxy/lib/src/proxy_layer.dart
Normal 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;
|
||||
}
|
||||
}
|
19
packages/proxy/pubspec.yaml
Normal file
19
packages/proxy/pubspec.yaml
Normal 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
|
||||
|
88
packages/proxy/test/basic_test.dart
Normal file
88
packages/proxy/test/basic_test.dart
Normal 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'));
|
||||
});
|
||||
}
|
24
packages/proxy/test/common.dart
Normal file
24
packages/proxy/test/common.dart
Normal 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();
|
||||
}
|
94
packages/proxy/test/pub_serve_test.dart
Normal file
94
packages/proxy/test/pub_serve_test.dart
Normal 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'));
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue