added android proxy controller support

This commit is contained in:
Lorenzo Pichilli 2022-05-02 03:37:02 +02:00
parent 2ab051fca3
commit d3a834c36a
17 changed files with 620 additions and 17 deletions

View File

@ -2,6 +2,7 @@
- Deprecated old classes/properties/methods to make them eventually compatible with other Platforms and WebView engines.
- Added Web support
- Added `ProxyController` for Android
- Added `pauseAllMediaPlayback`, `setAllMediaPlaybackSuspended`, `closeAllMediaPresentations`, `requestMediaPlaybackState`, `isInFullscreen`, `getCameraCaptureState`, `setCameraCaptureState`, `getMicrophoneCaptureState`, `setMicrophoneCaptureState` WebView controller methods
- Added `underPageBackgroundColor`, `isTextInteractionEnabled`, `isSiteSpecificQuirksModeEnabled`, `upgradeKnownHostsToHTTPS` WebView settings
- Added support for `onPermissionRequest` event on iOS 15.0+

View File

@ -12,6 +12,7 @@ import com.pichillilorenzo.flutter_inappwebview.chrome_custom_tabs.ChromeSafariB
import com.pichillilorenzo.flutter_inappwebview.credential_database.CredentialDatabaseHandler;
import com.pichillilorenzo.flutter_inappwebview.in_app_browser.InAppBrowserManager;
import com.pichillilorenzo.flutter_inappwebview.headless_in_app_webview.HeadlessInAppWebViewManager;
import com.pichillilorenzo.flutter_inappwebview.proxy.ProxyManager;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
@ -35,6 +36,7 @@ public class InAppWebViewFlutterPlugin implements FlutterPlugin, ActivityAware {
public MyWebStorage myWebStorage;
public ServiceWorkerManager serviceWorkerManager;
public WebViewFeatureManager webViewFeatureManager;
public ProxyManager proxyManager;
public FlutterWebViewFactory flutterWebViewFactory;
public static ValueCallback<Uri> filePathCallbackLegacy;
public static ValueCallback<Uri[]> filePathCallback;
@ -97,6 +99,7 @@ public class InAppWebViewFlutterPlugin implements FlutterPlugin, ActivityAware {
credentialDatabaseHandler = new CredentialDatabaseHandler(this);
}
webViewFeatureManager = new WebViewFeatureManager(this);
proxyManager = new ProxyManager(this);
}
@Override
@ -141,6 +144,10 @@ public class InAppWebViewFlutterPlugin implements FlutterPlugin, ActivityAware {
webViewFeatureManager.dispose();
webViewFeatureManager = null;
}
if (proxyManager != null) {
proxyManager.dispose();
proxyManager = null;
}
filePathCallbackLegacy = null;
filePathCallback = null;
}

View File

@ -0,0 +1,138 @@
package com.pichillilorenzo.flutter_inappwebview.proxy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.webkit.ProxyConfig;
import androidx.webkit.ProxyController;
import androidx.webkit.WebViewFeature;
import com.pichillilorenzo.flutter_inappwebview.InAppWebViewFlutterPlugin;
import com.pichillilorenzo.flutter_inappwebview.types.ProxyRuleExt;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class ProxyManager implements MethodChannel.MethodCallHandler {
static final String LOG_TAG = "ProxyManager";
public MethodChannel channel;
@Nullable
public static ProxyController proxyController;
@Nullable
public InAppWebViewFlutterPlugin plugin;
public ProxyManager(final InAppWebViewFlutterPlugin plugin) {
this.plugin = plugin;
channel = new MethodChannel(plugin.messenger, "com.pichillilorenzo/flutter_inappwebview_proxycontroller");
channel.setMethodCallHandler(this);
if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {
proxyController = ProxyController.getInstance();
} else {
proxyController = null;
}
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
switch (call.method) {
case "setProxyOverride":
if (proxyController != null) {
HashMap<String, Object> settingsMap = (HashMap<String, Object>) call.argument("settings");
ProxySettings settings = new ProxySettings();
if (settingsMap != null) {
settings.parse(settingsMap);
}
setProxyOverride(settings, result);
} else {
result.success(false);
}
break;
case "clearProxyOverride":
if (proxyController != null ) {
clearProxyOverride(result);
} else {
result.success(false);
}
break;
default:
result.notImplemented();
}
}
private void setProxyOverride(ProxySettings settings, final MethodChannel.Result result) {
if (proxyController != null) {
ProxyConfig.Builder proxyConfigBuilder = new ProxyConfig.Builder();
for (String bypassRule : settings.bypassRules) {
proxyConfigBuilder.addBypassRule(bypassRule);
}
for (String direct : settings.directs) {
proxyConfigBuilder.addDirect(direct);
}
for (ProxyRuleExt proxyRule : settings.proxyRules) {
if (proxyRule.getSchemeFilter() != null) {
proxyConfigBuilder.addProxyRule(proxyRule.getUrl(), proxyRule.getSchemeFilter());
} else {
proxyConfigBuilder.addProxyRule(proxyRule.getUrl());
}
}
if (settings.bypassSimpleHostnames != null && settings.bypassSimpleHostnames) {
proxyConfigBuilder.bypassSimpleHostnames();
}
if (settings.removeImplicitRules != null && settings.removeImplicitRules) {
proxyConfigBuilder.removeImplicitRules();
}
proxyController.setProxyOverride(proxyConfigBuilder.build(), new Executor() {
@Override
public void execute(Runnable command) {
command.run();
}
}, new Runnable() {
@Override
public void run() {
result.success(true);
}
});
}
}
private void clearProxyOverride(final MethodChannel.Result result) {
if (proxyController != null) {
proxyController.clearProxyOverride(new Executor() {
@Override
public void execute(Runnable command) {
command.run();
}
}, new Runnable() {
@Override
public void run() {
result.success(true);
}
});
}
}
public void dispose() {
channel.setMethodCallHandler(null);
if (proxyController != null) {
// Clears the proxy settings
proxyController.clearProxyOverride(new Executor() {
@Override
public void execute(Runnable command) {
}
}, new Runnable() {
@Override
public void run() {
}
});
proxyController = null;
}
plugin = null;
}
}

View File

@ -0,0 +1,88 @@
package com.pichillilorenzo.flutter_inappwebview.proxy;
import androidx.webkit.ProxyConfig;
import com.pichillilorenzo.flutter_inappwebview.ISettings;
import com.pichillilorenzo.flutter_inappwebview.types.ProxyRuleExt;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ProxySettings implements ISettings<ProxyConfig> {
List<String> bypassRules = new ArrayList<>();
List<String> directs = new ArrayList<>();
List<ProxyRuleExt> proxyRules = new ArrayList<>();
Boolean bypassSimpleHostnames = null;
Boolean removeImplicitRules = null;
@Override
public ProxySettings parse(Map<String, Object> settings) {
for (Map.Entry<String, Object> pair : settings.entrySet()) {
String key = pair.getKey();
Object value = pair.getValue();
if (value == null) {
continue;
}
switch (key) {
case "bypassRules":
bypassRules = (List<String>) value;
break;
case "directs":
directs = (List<String>) value;
break;
case "proxyRules":
proxyRules = new ArrayList<>();
List<Map<String, String>> proxyRuleMapList = (List<Map<String, String>>) value;
for (Map<String, String> proxyRuleMap : proxyRuleMapList) {
ProxyRuleExt proxyRuleExt = ProxyRuleExt.fromMap(proxyRuleMap);
if (proxyRuleExt != null) {
proxyRules.add(proxyRuleExt);
}
}
break;
case "bypassSimpleHostnames":
bypassSimpleHostnames = (Boolean) value;
break;
case "removeImplicitRules":
removeImplicitRules = (Boolean) value;
break;
}
}
return this;
}
@Override
public Map<String, Object> toMap() {
List<Map<String, String>> proxyRuleMapList = new ArrayList<>();
for (ProxyRuleExt proxyRuleExt : proxyRules) {
proxyRuleMapList.add(proxyRuleExt.toMap());
}
Map<String, Object> settings = new HashMap<>();
settings.put("bypassRules", bypassRules);
settings.put("directs", directs);
settings.put("proxyRules", proxyRuleMapList);
settings.put("bypassSimpleHostnames", bypassSimpleHostnames);
settings.put("removeImplicitRules", removeImplicitRules);
return settings;
}
@Override
public Map<String, Object> getRealSettings(ProxyConfig proxyConfig) {
Map<String, Object> realSettings = toMap();
List<Map<String, String>> proxyRuleMapList = new ArrayList<>();
List<ProxyConfig.ProxyRule> proxyRules = proxyConfig.getProxyRules();
for (ProxyConfig.ProxyRule proxyRule : proxyRules) {
Map<String, String> proxyRuleMap = new HashMap<>();
proxyRuleMap.put("url", proxyRule.getUrl());
proxyRuleMap.put("schemeFilter", proxyRule.getSchemeFilter());
proxyRuleMapList.add(proxyRuleMap);
}
realSettings.put("bypassRules", proxyConfig.getBypassRules());
realSettings.put("proxyRules", proxyRuleMapList);
return realSettings;
}
}

View File

@ -0,0 +1,81 @@
package com.pichillilorenzo.flutter_inappwebview.types;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
public class ProxyRuleExt {
@Nullable
private String schemeFilter;
@NonNull
private String url;
public ProxyRuleExt(@Nullable String schemeFilter, @NonNull String url) {
this.schemeFilter = schemeFilter;
this.url = url;
}
@Nullable
public static ProxyRuleExt fromMap(@Nullable Map<String, String> map) {
if (map == null) {
return null;
}
String url = (String) map.get("url");
String schemeFilter = (String) map.get("schemeFilter");
return new ProxyRuleExt(schemeFilter, url);
}
public Map<String, String> toMap() {
Map<String, String> proxyRuleMap = new HashMap<>();
proxyRuleMap.put("url", url);
proxyRuleMap.put("schemeFilter", schemeFilter);
return proxyRuleMap;
}
@Nullable
public String getSchemeFilter() {
return schemeFilter;
}
public void setSchemeFilter(@Nullable String schemeFilter) {
this.schemeFilter = schemeFilter;
}
@NonNull
public String getUrl() {
return url;
}
public void setUrl(@NonNull String url) {
this.url = url;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ProxyRuleExt that = (ProxyRuleExt) o;
if (schemeFilter != null ? !schemeFilter.equals(that.schemeFilter) : that.schemeFilter != null)
return false;
return url.equals(that.url);
}
@Override
public int hashCode() {
int result = schemeFilter != null ? schemeFilter.hashCode() : 0;
result = 31 * result + url.hashCode();
return result;
}
@Override
public String toString() {
return "ProxyRuleExt{" +
"schemeFilter='" + schemeFilter + '\'' +
", url='" + url + '\'' +
'}';
}
}

View File

@ -40,6 +40,16 @@ Future main() async {
},
));
}
// var proxyAvailable = await WebViewFeature.isFeatureSupported(
// WebViewFeature.PROXY_OVERRIDE);
// if (proxyAvailable) {
// ProxyController proxyController = ProxyController.instance();
// await proxyController.clearProxyOverride();
// await proxyController.setProxyOverride(settings: ProxySettings(
// proxyRules: [ProxyRule(url: "https://192.168.1.102:4433")],
// ));
// }
}
runApp(MyApp());

View File

@ -1,2 +1,3 @@
export 'service_worker_controller.dart';
export 'webview_feature.dart';
export 'proxy_controller.dart';

View File

@ -0,0 +1,152 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'webview_feature.dart';
import '../in_app_webview/webview.dart';
import '../types/main.dart';
///Manages setting and clearing a process-specific override for the Android system-wide proxy settings that govern network requests made by [WebView].
///
///[WebView] may make network requests in order to fetch content that is not otherwise read from the file system or provided directly by application code.
///In this case by default the system-wide Android network proxy settings are used to redirect requests to appropriate proxy servers.
///
///In the rare case that it is necessary for an application to explicitly specify its proxy configuration,
///this API may be used to explicitly specify the proxy rules that govern WebView initiated network requests.
///
///**Supported Platforms/Implementations**:
///- Android native WebView ([Official API - ProxyController](https://developer.android.com/reference/androidx/webkit/ProxyController))
class ProxyController {
static ProxyController? _instance;
static const MethodChannel _channel = const MethodChannel(
'com.pichillilorenzo/flutter_inappwebview_proxycontroller');
///Gets the [ProxyController] shared instance.
///
///This method should only be called if [WebViewFeature.isFeatureSupported] returns `true` for [WebViewFeature.PROXY_OVERRIDE].
static ProxyController instance() {
return (_instance != null) ? _instance! : _init();
}
static ProxyController _init() {
_channel.setMethodCallHandler(_handleMethod);
_instance = ProxyController();
return _instance!;
}
static Future<dynamic> _handleMethod(MethodCall call) async {
// ProxyController controller = ProxyController.instance();
switch (call.method) {
default:
throw UnimplementedError("Unimplemented ${call.method} method");
}
// return null;
}
///Sets [ProxySettings] which will be used by all [WebView]s in the app.
///URLs that match patterns in the bypass list will not be directed to any proxy.
///Instead, the request will be made directly to the origin specified by the URL.
///Network connections are not guaranteed to immediately use the new proxy setting; wait for the method to return before loading a page.
///
///**Supported Platforms/Implementations**:
///- Android native WebView ([Official API - ProxyController.setProxyOverride](https://developer.android.com/reference/androidx/webkit/ProxyController#setProxyOverride(androidx.webkit.ProxyConfig,%20java.util.concurrent.Executor,%20java.lang.Runnable)))
Future<void> setProxyOverride({required ProxySettings settings}) async {
Map<String, dynamic> args = <String, dynamic>{};
args.putIfAbsent("settings", () => settings.toMap());
return await _channel.invokeMethod('setProxyOverride', args);
}
///Clears the proxy settings.
///Network connections are not guaranteed to immediately use the new proxy setting; wait for the method to return before loading a page.
///
///**Supported Platforms/Implementations**:
///- Android native WebView ([Official API - ProxyController.clearProxyOverride](https://developer.android.com/reference/androidx/webkit/ProxyController#clearProxyOverride(java.util.concurrent.Executor,%20java.lang.Runnable)))
Future<void> clearProxyOverride() async {
Map<String, dynamic> args = <String, dynamic>{};
return await _channel.invokeMethod('clearProxyOverride', args);
}
}
///Class that represents the settings used to configure the [ProxyController].
///
///**Supported Platforms/Implementations**:
///- Android native WebView ([Official API - ProxyConfig](https://developer.android.com/reference/androidx/webkit/ProxyConfig))
class ProxySettings {
///List of bypass rules.
///
///A bypass rule describes URLs that should skip proxy override settings and make a direct connection instead. These can be URLs or IP addresses. Wildcards are accepted.
///For instance, the rule "*example.com" would mean that requests to "http://example.com" and "www.example.com" would not be directed to any proxy,
///instead, would be made directly to the origin specified by the URL.
List<String> bypassRules;
///List of scheme filters.
///
///URLs that match these scheme filters are connected to directly instead of using a proxy server.
List<String> directs;
///List of proxy rules to be used for all URLs. This method can be called multiple times to add multiple rules. Additional rules have decreasing precedence.
///
///Proxy is a string in the format `[scheme://]host[:port]`.
///Scheme is optional, if present must be `HTTP`, `HTTPS` or [SOCKS](https://tools.ietf.org/html/rfc1928) and defaults to `HTTP`.
///Host is one of an IPv6 literal with brackets, an IPv4 literal or one or more labels separated by a period.
///Port number is optional and defaults to `80` for `HTTP`, `443` for `HTTPS` and `1080` for `SOCKS`.
///
///The correct syntax for hosts is defined by [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.2.2).
List<ProxyRule> proxyRules;
///Hostnames without a period in them (and that are not IP literals) will skip proxy settings and be connected to directly instead. Examples: `"abc"`, `"local"`, `"some-domain"`.
///
///Hostnames with a trailing dot are not considered simple by this definition.
bool? bypassSimpleHostnames;
///By default, certain hostnames implicitly bypass the proxy if they are link-local IPs, or localhost addresses.
///For instance hostnames matching any of (non-exhaustive list):
///- localhost
///- *.localhost
///- [::1]
///- 127.0.0.1/8
///- 169.254/16
///- [FE80::]/10
///Set this to `true` to override the default behavior and force localhost and link-local URLs to be sent through the proxy.
bool? removeImplicitRules;
ProxySettings(
{this.bypassRules = const [],
this.directs = const [],
this.proxyRules = const [],
this.bypassSimpleHostnames,
this.removeImplicitRules});
Map<String, dynamic> toMap() {
return {
"bypassRules": bypassRules,
"directs": directs,
"proxyRules": proxyRules.map((e) => e.toMap()).toList(),
"bypassSimpleHostnames": bypassSimpleHostnames,
"removeImplicitRules": removeImplicitRules
};
}
static ProxySettings fromMap(Map<String, dynamic> map) {
var settings = ProxySettings();
settings.bypassRules = map["bypassRules"];
settings.directs = map["directs"];
settings.proxyRules = (map["proxyRules"].cast<Map<String, dynamic>>()
as List<Map<String, dynamic>>)
.map((e) => ProxyRule.fromMap(e)) as List<ProxyRule>;
settings.bypassSimpleHostnames = map["bypassSimpleHostnames"];
settings.removeImplicitRules = map["removeImplicitRules"];
return settings;
}
Map<String, dynamic> toJson() {
return this.toMap();
}
@override
String toString() {
return toMap().toString();
}
ProxySettings copy() {
return ProxySettings.fromMap(this.toMap());
}
}

View File

@ -15,6 +15,8 @@ class ServiceWorkerController {
'com.pichillilorenzo/flutter_inappwebview_serviceworkercontroller');
///Gets the [ServiceWorkerController] shared instance.
///
///This method should only be called if [WebViewFeature.isFeatureSupported] returns `true` for [WebViewFeature.SERVICE_WORKER_BASIC_USAGE].
static ServiceWorkerController instance() {
return (_instance != null) ? _instance! : _init();
}
@ -23,7 +25,8 @@ class ServiceWorkerController {
ServiceWorkerClient? get serviceWorkerClient => _serviceWorkerClient;
///Sets the service worker client.
///Sets the client to capture service worker related callbacks.
///A [ServiceWorkerClient] should be set before any service workers are active, e.g. a safe place is before any WebView instances are created or pages loaded.
///
///**Supported Platforms/Implementations**:
///- Android native WebView ([Official API - ServiceWorkerControllerCompat.setServiceWorkerClient](https://developer.android.com/reference/androidx/webkit/ServiceWorkerControllerCompat#setServiceWorkerClient(androidx.webkit.ServiceWorkerClientCompat)))

View File

@ -1,5 +1,7 @@
import 'dart:async';
import 'package:flutter/services.dart';
import '../in_app_webview/in_app_webview_controller.dart';
import '../in_app_webview/in_app_webview_settings.dart';
///Class that represents an Android-specific utility class for checking which WebView Support Library features are supported on the device.
class WebViewFeature {
@ -70,15 +72,15 @@ class WebViewFeature {
@override
String toString() => _value;
///
///This feature covers [InAppWebViewController.createWebMessageChannel].
static const CREATE_WEB_MESSAGE_CHANNEL =
const WebViewFeature._internal("CREATE_WEB_MESSAGE_CHANNEL");
///
///This feature covers [InAppWebViewSettings.disabledActionModeMenuItems].
static const DISABLED_ACTION_MODE_MENU_ITEMS =
const WebViewFeature._internal("DISABLED_ACTION_MODE_MENU_ITEMS");
///
///This feature covers [InAppWebViewSettings.forceDark].
static const FORCE_DARK = const WebViewFeature._internal("FORCE_DARK");
///

View File

@ -60,11 +60,6 @@ class InAppBrowserClassSettings {
return options;
}
static Map<String, dynamic> instanceToMap(
InAppBrowserClassSettings settings) {
return settings.toMap();
}
Map<String, dynamic> toJson() {
return this.toMap();
}

View File

@ -144,4 +144,6 @@ export 'webview_render_process_action.dart';
export 'window_features.dart';
export 'web_resource_error.dart';
export 'web_resource_error_type.dart';
export 'media_capture_state.dart';
export 'media_capture_state.dart';
export 'proxy_rule.dart';
export 'proxy_scheme_filter.dart';

View File

@ -0,0 +1,36 @@
import 'proxy_scheme_filter.dart';
///Class that holds a scheme filter and a proxy URL.
class ProxyRule {
///Represents the scheme filter.
ProxySchemeFilter? schemeFilter;
///Represents the proxy URL.
String url;
ProxyRule({required this.url, this.schemeFilter});
///Gets a possible [ProxyRule] instance from a [Map] value.
static ProxyRule? fromMap(Map<String, dynamic>? map) {
return map != null
? ProxyRule(
url: map["url"],
schemeFilter: ProxySchemeFilter.fromValue(map["schemeFilter"]))
: null;
}
///Converts instance to a map.
Map<String, dynamic> toMap() {
return {"url": url, "schemeFilter": schemeFilter?.toValue()};
}
///Converts instance to a map.
Map<String, dynamic> toJson() {
return this.toMap();
}
@override
String toString() {
return toMap().toString();
}
}

View File

@ -0,0 +1,49 @@
import '../android/proxy_controller.dart';
///Class that represent scheme filters used by [ProxyController].
class ProxySchemeFilter {
final String _value;
const ProxySchemeFilter._internal(this._value);
///Set of all values of [ProxySchemeFilter].
static final Set<ProxySchemeFilter> values = [
ProxySchemeFilter.MATCH_ALL_SCHEMES,
ProxySchemeFilter.MATCH_HTTP,
ProxySchemeFilter.MATCH_HTTPS,
].toSet();
///Gets a possible [ProxySchemeFilter] instance from a [String] value.
static ProxySchemeFilter? fromValue(String? value) {
if (value != null) {
try {
return ProxySchemeFilter.values
.firstWhere((element) => element.toValue() == value);
} catch (e) {
return null;
}
}
return null;
}
///Gets [String] value.
String toValue() => _value;
@override
String toString() => _value;
///Matches all schemes.
static const MATCH_ALL_SCHEMES = const ProxySchemeFilter._internal("*");
///HTTP scheme.
static const MATCH_HTTP = const ProxySchemeFilter._internal("http");
///HTTPS scheme.
static const MATCH_HTTPS =
const ProxySchemeFilter._internal("https");
bool operator ==(value) => value == _value;
@override
int get hashCode => _value.hashCode;
}

View File

@ -1,12 +1,14 @@
// Example of the server https is taken from here: https://engineering.circle.com/https-authorized-certs-with-node-js-315e548354a2
// Conversion of client1-crt.pem to certificate.pfx: https://stackoverflow.com/a/38408666/4637638
const express = require('express')
const https = require('https')
const cors = require('cors')
const auth = require('basic-auth')
const app = express()
const appHttps = express()
const appAuthBasic = express()
const express = require('express');
const proxy = require('express-http-proxy');
const https = require('https');
const cors = require('cors');
const auth = require('basic-auth');
const app = express();
const appHttps = express();
const appAuthBasic = express();
const appProxy = express();
const fs = require('fs')
const path = require('path')
const bodyParser = require('body-parser');
@ -20,6 +22,7 @@ var options = {
rejectUnauthorized: false
};
appHttps.use('/', proxy('www.google.com'));
appHttps.get('/', (req, res) => {
console.log(JSON.stringify(req.headers))
const cert = req.connection.getPeerCertificate()
@ -197,3 +200,7 @@ app.get("/test-download-file", (req, res) => {
})
app.listen(8082)
appProxy.use(cors());
appProxy.use('/', proxy('www.google.com'));
appProxy.listen(8083);

View File

@ -108,6 +108,11 @@
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -155,6 +160,31 @@
"vary": "~1.1.2"
}
},
"express-http-proxy": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/express-http-proxy/-/express-http-proxy-1.6.3.tgz",
"integrity": "sha512-/l77JHcOUrDUX8V67E287VEUQT0lbm71gdGVoodnlWBziarYKgMcpqT7xvh/HM8Jv52phw8Bd8tY+a7QjOr7Yg==",
"requires": {
"debug": "^3.0.1",
"es6-promise": "^4.1.1",
"raw-body": "^2.3.0"
},
"dependencies": {
"debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}
}
},
"finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",

View File

@ -13,6 +13,7 @@
"body-parser": "^1.19.0",
"cors": "^2.8.5",
"express": "latest",
"express-http-proxy": "^1.6.3",
"https": "latest",
"multiparty": "^4.2.2"
}