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