From 68f94751598aafe675f119b344c08bd8cc7313dc Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Fri, 3 Apr 2026 07:31:57 +0200 Subject: [PATCH] Add configurable native Referer header --- .../main/java/com/getcapacitor/CapConfig.java | 13 +++++ .../com/getcapacitor/WebViewLocalServer.java | 9 +-- .../plugin/util/HttpRequestHandler.java | 56 +++++++++++++++---- .../com/getcapacitor/ConfigBuildingTest.java | 2 + .../com/getcapacitor/ConfigReadingTest.java | 1 + .../plugin/util/HttpRequestHandlerTest.java | 44 +++++++++++++++ .../src/test/resources/configs/server.json | 3 +- cli/src/declarations.ts | 15 +++++ .../Capacitor/CAPBridgeViewController.swift | 1 + .../Capacitor/CAPInstanceConfiguration.h | 1 + .../Capacitor/CAPInstanceConfiguration.m | 2 + .../Capacitor/CAPInstanceDescriptor.h | 5 ++ .../Capacitor/CAPInstanceDescriptor.swift | 3 + .../Plugins/HttpRequestHandler.swift | 34 ++++++++++- .../Capacitor/WebViewAssetHandler.swift | 6 ++ .../CapacitorTests/ConfigurationTests.swift | 35 ++++++++++++ .../TestsHostApp/configurations/server.json | 1 + 17 files changed, 207 insertions(+), 24 deletions(-) diff --git a/android/capacitor/src/main/java/com/getcapacitor/CapConfig.java b/android/capacitor/src/main/java/com/getcapacitor/CapConfig.java index b20799ba35..10cc6d68ba 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/CapConfig.java +++ b/android/capacitor/src/main/java/com/getcapacitor/CapConfig.java @@ -35,6 +35,7 @@ public class CapConfig { // Server Config private boolean html5mode = true; private String serverUrl; + private String requestReferer; private String hostname = "localhost"; private String androidScheme = CAPACITOR_HTTPS_SCHEME; private String[] allowNavigation; @@ -158,6 +159,7 @@ private CapConfig(Builder builder) { // Server Config this.html5mode = builder.html5mode; this.serverUrl = builder.serverUrl; + this.requestReferer = builder.requestReferer; this.hostname = builder.hostname; if (this.validateScheme(builder.androidScheme)) { @@ -247,6 +249,7 @@ private void deserializeConfig(@Nullable Context context) { // Server html5mode = JSONUtils.getBoolean(configJSON, "server.html5mode", html5mode); serverUrl = JSONUtils.getString(configJSON, "server.url", null); + requestReferer = JSONUtils.getString(configJSON, "server.referer", null); hostname = JSONUtils.getString(configJSON, "server.hostname", hostname); errorPath = JSONUtils.getString(configJSON, "server.errorPath", null); startPath = JSONUtils.getString(configJSON, "server.appStartPath", null); @@ -342,6 +345,10 @@ public String getErrorPath() { return errorPath; } + public String getRequestReferer() { + return requestReferer; + } + public String getHostname() { return hostname; } @@ -563,6 +570,7 @@ public static class Builder { // Server Config Values private boolean html5mode = true; private String serverUrl; + private String requestReferer; private String errorPath; private String hostname = "localhost"; private String androidScheme = CAPACITOR_HTTPS_SCHEME; @@ -626,6 +634,11 @@ public Builder setServerUrl(String serverUrl) { return this; } + public Builder setRequestReferer(String requestReferer) { + this.requestReferer = requestReferer; + return this; + } + public Builder setErrorPath(String errorPath) { this.errorPath = errorPath; return this; diff --git a/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java b/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java index a39483de91..75a23befa0 100755 --- a/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java +++ b/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java @@ -273,14 +273,7 @@ private WebResourceResponse handleCapacitorHttpRequest(WebResourceRequest reques headers.put(header.getKey(), header.getValue()); } - // a workaround for the following android web view issue: - // https://issues.chromium.org/issues/40450316 - // x-cap-user-agent contains the user agent set in JavaScript - String userAgentValue = headers.getString("x-cap-user-agent"); - if (userAgentValue != null) { - headers.put("User-Agent", userAgentValue); - } - headers.remove("x-cap-user-agent"); + HttpRequestHandler.applyDefaultRequestHeaders(headers, bridge); HttpRequestHandler.HttpURLConnectionBuilder connectionBuilder = new HttpRequestHandler.HttpURLConnectionBuilder() .setUrl(url) diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/util/HttpRequestHandler.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/util/HttpRequestHandler.java index b47d57cf10..70d3afd146 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/util/HttpRequestHandler.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/util/HttpRequestHandler.java @@ -30,6 +30,49 @@ public class HttpRequestHandler { + public static void applyDefaultRequestHeaders(JSObject headers, Bridge bridge) { + if (headers == null || bridge == null) { + return; + } + + // a workaround for the following android web view issue: + // https://issues.chromium.org/issues/40450316 + // x-cap-user-agent contains the user agent set in JavaScript + String userAgentValue = headers.getString("x-cap-user-agent"); + if (userAgentValue != null) { + headers.put("User-Agent", userAgentValue); + } + headers.remove("x-cap-user-agent"); + + if (!headers.has("User-Agent") && !headers.has("user-agent")) { + String overriddenUserAgent = bridge.getConfig().getOverriddenUserAgentString(); + if (overriddenUserAgent != null) { + headers.put("User-Agent", overriddenUserAgent); + } + } + + if (!headers.has("Referer") && !headers.has("referer")) { + String refererValue = bridge.getConfig().getRequestReferer(); + if (isValidHttpReferer(refererValue)) { + headers.put("Referer", refererValue); + } + } + } + + public static boolean isValidHttpReferer(String refererValue) { + if (refererValue == null || refererValue.isBlank()) { + return false; + } + + try { + URL refererUrl = new URL(refererValue); + String protocol = refererUrl.getProtocol(); + return ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) && refererUrl.getHost() != null; + } catch (MalformedURLException ex) { + return false; + } + } + /** * An enum specifying conventional HTTP Response Types * See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType @@ -398,18 +441,7 @@ public static JSObject request(PluginCall call, String httpMethod, Bridge bridge boolean isHttpMutate = method.equals("DELETE") || method.equals("PATCH") || method.equals("POST") || method.equals("PUT"); - // a workaround for the following android web view issue: - // https://issues.chromium.org/issues/40450316 - // x-cap-user-agent contains the user agent set in JavaScript - String userAgentValue = headers.getString("x-cap-user-agent"); - if (userAgentValue != null) { - headers.put("User-Agent", userAgentValue); - } - headers.remove("x-cap-user-agent"); - - if (!headers.has("User-Agent") && !headers.has("user-agent")) { - headers.put("User-Agent", bridge.getConfig().getOverriddenUserAgentString()); - } + applyDefaultRequestHeaders(headers, bridge); URL url = new URL(urlString); HttpURLConnectionBuilder connectionBuilder = new HttpURLConnectionBuilder() diff --git a/android/capacitor/src/test/java/com/getcapacitor/ConfigBuildingTest.java b/android/capacitor/src/test/java/com/getcapacitor/ConfigBuildingTest.java index 1af51039da..c1dd295f1f 100644 --- a/android/capacitor/src/test/java/com/getcapacitor/ConfigBuildingTest.java +++ b/android/capacitor/src/test/java/com/getcapacitor/ConfigBuildingTest.java @@ -54,6 +54,7 @@ public void setup() { .setBackgroundColor("red") .setPluginsConfiguration(pluginConfig) .setServerUrl("http://www.google.com") + .setRequestReferer("https://example.com/app") .setResolveServiceWorkerRequests(false) .create(); } catch (Exception e) { @@ -74,6 +75,7 @@ public void getCoreConfigValues() { assertTrue(config.isWebContentsDebuggingEnabled()); assertEquals("red", config.getBackgroundColor()); assertEquals("http://www.google.com", config.getServerUrl()); + assertEquals("https://example.com/app", config.getRequestReferer()); assertFalse(config.isResolveServiceWorkerRequests()); } diff --git a/android/capacitor/src/test/java/com/getcapacitor/ConfigReadingTest.java b/android/capacitor/src/test/java/com/getcapacitor/ConfigReadingTest.java index 61597b24e4..a3931466af 100644 --- a/android/capacitor/src/test/java/com/getcapacitor/ConfigReadingTest.java +++ b/android/capacitor/src/test/java/com/getcapacitor/ConfigReadingTest.java @@ -108,6 +108,7 @@ public void server() { CapConfig config = CapConfig.loadDefault(context); assertEquals("myhost", config.getHostname()); assertEquals("http://192.168.100.1:2057", config.getServerUrl()); + assertEquals("https://example.com/app", config.getRequestReferer()); assertEquals("override", config.getAndroidScheme()); } catch (IOException e) { fail(); diff --git a/android/capacitor/src/test/java/com/getcapacitor/plugin/util/HttpRequestHandlerTest.java b/android/capacitor/src/test/java/com/getcapacitor/plugin/util/HttpRequestHandlerTest.java index fd8b686ee6..1a785b2095 100644 --- a/android/capacitor/src/test/java/com/getcapacitor/plugin/util/HttpRequestHandlerTest.java +++ b/android/capacitor/src/test/java/com/getcapacitor/plugin/util/HttpRequestHandlerTest.java @@ -2,10 +2,14 @@ import static org.junit.Assert.*; +import android.app.Activity; +import com.getcapacitor.Bridge; +import com.getcapacitor.CapConfig; import com.getcapacitor.JSObject; import com.getcapacitor.plugin.util.HttpRequestHandler.HttpURLConnectionBuilder; import java.net.URL; import org.junit.Test; +import org.mockito.Mockito; public class HttpRequestHandlerTest { @@ -35,4 +39,44 @@ public void testHttpURLConnectionBuilderSetUrlParamsNotEncoded() throws Exceptio .url.toString(); assertEquals(expectedUrl, actualUrl); } + + @Test + public void testApplyDefaultRequestHeadersAddsRefererWhenMissing() { + Bridge bridge = Mockito.mock(Bridge.class); + Activity context = Mockito.mock(Activity.class); + CapConfig config = new CapConfig.Builder(context) + .setRequestReferer("https://example.com/app") + .setWebContentsDebuggingEnabled(false) + .create(); + + Mockito.when(bridge.getConfig()).thenReturn(config); + + JSObject headers = new JSObject(); + HttpRequestHandler.applyDefaultRequestHeaders(headers, bridge); + + assertEquals("https://example.com/app", headers.getString("Referer")); + } + + @Test + public void testApplyDefaultRequestHeadersDoesNotOverrideReferer() { + Bridge bridge = Mockito.mock(Bridge.class); + Activity context = Mockito.mock(Activity.class); + CapConfig config = new CapConfig.Builder(context) + .setRequestReferer("https://example.com/app") + .setWebContentsDebuggingEnabled(false) + .create(); + + Mockito.when(bridge.getConfig()).thenReturn(config); + + JSObject headers = new JSObject(); + headers.put("Referer", "https://request.example/app"); + HttpRequestHandler.applyDefaultRequestHeaders(headers, bridge); + + assertEquals("https://request.example/app", headers.getString("Referer")); + } + + @Test + public void testIsValidHttpRefererRejectsInvalidScheme() { + assertFalse(HttpRequestHandler.isValidHttpReferer("capacitor://localhost")); + } } diff --git a/android/capacitor/src/test/resources/configs/server.json b/android/capacitor/src/test/resources/configs/server.json index 56c821971b..5bea211ede 100644 --- a/android/capacitor/src/test/resources/configs/server.json +++ b/android/capacitor/src/test/resources/configs/server.json @@ -10,6 +10,7 @@ "androidScheme": "override", "allowNavigation": ["capacitorjs.com", "ionic.io", "192.168.0.1"], "hostname": "myhost", + "referer": "https://example.com/app", "url": "http://192.168.100.1:2057" }, "plugins": { @@ -18,4 +19,4 @@ } }, "cordova": {} -} \ No newline at end of file +} diff --git a/cli/src/declarations.ts b/cli/src/declarations.ts index 6c8645406d..b201ce1093 100644 --- a/cli/src/declarations.ts +++ b/cli/src/declarations.ts @@ -617,6 +617,21 @@ export interface CapacitorConfig { */ url?: string; + /** + * Set a default HTTP `Referer` header for Capacitor's native HTTP requests. + * + * This is only applied when a request does not already define its own + * `Referer` header, and the configured value must be a valid `http://` or + * `https://` URL. + * + * This can be useful on platforms that use a custom app scheme, where some + * backends require an HTTP(S) referrer instead of `capacitor://localhost`. + * + * @since 8.4.0 + * @default undefined + */ + referer?: string; + /** * Allow cleartext traffic in the Web View. * diff --git a/ios/Capacitor/Capacitor/CAPBridgeViewController.swift b/ios/Capacitor/Capacitor/CAPBridgeViewController.swift index a7285213d8..08fae51b5b 100644 --- a/ios/Capacitor/Capacitor/CAPBridgeViewController.swift +++ b/ios/Capacitor/Capacitor/CAPBridgeViewController.swift @@ -41,6 +41,7 @@ import Cordova let assetHandler = WebViewAssetHandler(router: router()) assetHandler.setAssetPath(configuration.appLocation.path) assetHandler.setServerUrl(configuration.serverURL) + assetHandler.setRequestConfiguration(configuration) let delegationHandler = WebViewDelegationHandler() prepareWebView(with: configuration, assetHandler: assetHandler, delegationHandler: delegationHandler) view = webView diff --git a/ios/Capacitor/Capacitor/CAPInstanceConfiguration.h b/ios/Capacitor/Capacitor/CAPInstanceConfiguration.h index 171dbb4634..8d6b2106d9 100644 --- a/ios/Capacitor/Capacitor/CAPInstanceConfiguration.h +++ b/ios/Capacitor/Capacitor/CAPInstanceConfiguration.h @@ -14,6 +14,7 @@ NS_SWIFT_NAME(InstanceConfiguration) @property (nonatomic, readonly, nonnull) NSURL *localURL; @property (nonatomic, readonly, nonnull) NSURL *serverURL; @property (nonatomic, readonly, nullable) NSString *errorPath; +@property (nonatomic, readonly, nullable) NSString *requestReferer; @property (nonatomic, readonly, nonnull) NSDictionary *pluginConfigurations; @property (nonatomic, readonly) BOOL loggingEnabled; @property (nonatomic, readonly) BOOL scrollingEnabled; diff --git a/ios/Capacitor/Capacitor/CAPInstanceConfiguration.m b/ios/Capacitor/Capacitor/CAPInstanceConfiguration.m index d6b7785e34..787acafc07 100644 --- a/ios/Capacitor/Capacitor/CAPInstanceConfiguration.m +++ b/ios/Capacitor/Capacitor/CAPInstanceConfiguration.m @@ -17,6 +17,7 @@ - (instancetype)initWithDescriptor:(CAPInstanceDescriptor *)descriptor isDebug:( _overridenUserAgentString = descriptor.overridenUserAgentString; _backgroundColor = descriptor.backgroundColor; _allowedNavigationHostnames = descriptor.allowedNavigationHostnames; + _requestReferer = descriptor.requestReferer; switch (descriptor.loggingBehavior) { case CAPInstanceLoggingBehaviorProduction: _loggingEnabled = true; @@ -65,6 +66,7 @@ - (instancetype)initWithConfiguration:(CAPInstanceConfiguration*)configuration a _localURL = [[configuration localURL] copy]; _serverURL = [[configuration serverURL] copy]; _errorPath = [[configuration errorPath] copy]; + _requestReferer = [[configuration requestReferer] copy]; _pluginConfigurations = [[configuration pluginConfigurations] copy]; _loggingEnabled = configuration.loggingEnabled; _scrollingEnabled = configuration.scrollingEnabled; diff --git a/ios/Capacitor/Capacitor/CAPInstanceDescriptor.h b/ios/Capacitor/Capacitor/CAPInstanceDescriptor.h index f477ef55be..2d5b4e2d5d 100644 --- a/ios/Capacitor/Capacitor/CAPInstanceDescriptor.h +++ b/ios/Capacitor/Capacitor/CAPInstanceDescriptor.h @@ -58,6 +58,11 @@ NS_SWIFT_NAME(InstanceDescriptor) @discussion Defaults to nil. */ @property (nonatomic, copy, nullable) NSString *errorPath; +/** + @brief The default HTTP `Referer` header to use for native requests when one is not explicitly set. + @discussion Defaults to nil. Set by @c server.referer in the configuration file. + */ +@property (nonatomic, copy, nullable) NSString *requestReferer; /** @brief The hostname that will be used for the server URL. @discussion Defaults to @c localhost. Set by @c server.hostname in the configuration file. diff --git a/ios/Capacitor/Capacitor/CAPInstanceDescriptor.swift b/ios/Capacitor/Capacitor/CAPInstanceDescriptor.swift index a7eff5f623..9722beace5 100644 --- a/ios/Capacitor/Capacitor/CAPInstanceDescriptor.swift +++ b/ios/Capacitor/Capacitor/CAPInstanceDescriptor.swift @@ -99,6 +99,9 @@ internal extension InstanceDescriptor { if let errorPathString = (config[keyPath: "server.errorPath"] as? String) { errorPath = errorPathString } + if let referer = config[keyPath: "server.referer"] as? String { + requestReferer = referer + } if let insetBehavior = config[keyPath: "ios.contentInset"] as? String { let availableInsets: [String: UIScrollView.ContentInsetAdjustmentBehavior] = ["automatic": .automatic, "scrollableAxes": .scrollableAxes, diff --git a/ios/Capacitor/Capacitor/Plugins/HttpRequestHandler.swift b/ios/Capacitor/Capacitor/Plugins/HttpRequestHandler.swift index 651c1b76b5..3d978d3fc7 100644 --- a/ios/Capacitor/Capacitor/Plugins/HttpRequestHandler.swift +++ b/ios/Capacitor/Capacitor/Plugins/HttpRequestHandler.swift @@ -49,6 +49,36 @@ private func lowerCaseHeaderDictionary(_ headers: [AnyHashable: Any]) -> [String })) } +private func isValidHttpReferer(_ referer: String?) -> Bool { + guard + let referer, + let url = URL(string: referer), + url.host != nil, + let scheme = url.scheme?.lowercased(), + scheme == "http" || scheme == "https" + else { + return false + } + + return true +} + +func applyCapacitorDefaultRequestHeaders(_ headers: inout [String: Any], _ config: InstanceConfiguration?) { + if let userAgentString = config?.overridenUserAgentString, headers["User-Agent"] == nil, headers["user-agent"] == nil { + headers["User-Agent"] = userAgentString + } + + if headers["Referer"] == nil, headers["referer"] == nil, let referer = config?.requestReferer, isValidHttpReferer(referer) { + headers["Referer"] = referer + } +} + +func applyCapacitorDefaultRequestHeaders(_ request: inout URLRequest, _ config: InstanceConfiguration?) { + if request.value(forHTTPHeaderField: "Referer") == nil, let referer = config?.requestReferer, isValidHttpReferer(referer) { + request.setValue(referer, forHTTPHeaderField: "Referer") + } +} + open class HttpRequestHandler { open class CapacitorHttpRequestBuilder { public var url: URL? @@ -194,9 +224,7 @@ open class HttpRequestHandler { .openConnection() .build() - if let userAgentString = config?.overridenUserAgentString, headers["User-Agent"] == nil, headers["user-agent"] == nil { - headers["User-Agent"] = userAgentString - } + applyCapacitorDefaultRequestHeaders(&headers, config) request.setRequestHeaders(headers) diff --git a/ios/Capacitor/Capacitor/WebViewAssetHandler.swift b/ios/Capacitor/Capacitor/WebViewAssetHandler.swift index acc8effb74..87e58210c2 100644 --- a/ios/Capacitor/Capacitor/WebViewAssetHandler.swift +++ b/ios/Capacitor/Capacitor/WebViewAssetHandler.swift @@ -6,6 +6,7 @@ import UniformTypeIdentifiers open class WebViewAssetHandler: NSObject, WKURLSchemeHandler { private var router: Router private var serverUrl: URL? + private var requestConfiguration: InstanceConfiguration? public init(router: Router) { self.router = router @@ -20,6 +21,10 @@ open class WebViewAssetHandler: NSObject, WKURLSchemeHandler { self.serverUrl = serverUrl } + open func setRequestConfiguration(_ config: InstanceConfiguration?) { + self.requestConfiguration = config + } + private func isUsingLiveReload(_ localUrl: URL) -> Bool { return self.serverUrl != nil && self.serverUrl?.scheme != localUrl.scheme } @@ -138,6 +143,7 @@ open class WebViewAssetHandler: NSObject, WKURLSchemeHandler { !targetUrl.isEmpty { urlRequest.url = URL(string: targetUrl) } + applyCapacitorDefaultRequestHeaders(&urlRequest, requestConfiguration) let urlSession = URLSession.shared let task = urlSession.dataTask(with: urlRequest) { (data, response, error) in diff --git a/ios/Capacitor/CapacitorTests/ConfigurationTests.swift b/ios/Capacitor/CapacitorTests/ConfigurationTests.swift index 778bfd688d..a7e859d37c 100644 --- a/ios/Capacitor/CapacitorTests/ConfigurationTests.swift +++ b/ios/Capacitor/CapacitorTests/ConfigurationTests.swift @@ -102,6 +102,7 @@ class ConfigurationTests: XCTestCase { let descriptor = InstanceDescriptor.init(at: url, configuration: ConfigurationTests.files[.server], cordovaConfiguration: nil) XCTAssertEqual(descriptor.urlScheme, "override") XCTAssertEqual(descriptor.urlHostname, "myhost") + XCTAssertEqual(descriptor.requestReferer, "https://example.com/app") XCTAssertEqual(descriptor.serverURL, "http://192.168.100.1:2057") } @@ -129,6 +130,40 @@ class ConfigurationTests: XCTestCase { let configuration = InstanceConfiguration(with: descriptor, isDebug: true) XCTAssertEqual(configuration.serverURL, URL(string: "http://192.168.100.1:2057")) XCTAssertEqual(configuration.localURL, URL(string: "override://myhost")) + XCTAssertEqual(configuration.requestReferer, "https://example.com/app") + } + + func testApplyDefaultRequestHeadersAddsRefererWhenMissing() throws { + let descriptor = InstanceDescriptor.init() + descriptor.requestReferer = "https://example.com/app" + let configuration = InstanceConfiguration(with: descriptor, isDebug: true) + var headers: [String: Any] = [:] + + applyCapacitorDefaultRequestHeaders(&headers, configuration) + + XCTAssertEqual(headers["Referer"] as? String, "https://example.com/app") + } + + func testApplyDefaultRequestHeadersDoesNotOverrideReferer() throws { + let descriptor = InstanceDescriptor.init() + descriptor.requestReferer = "https://example.com/app" + let configuration = InstanceConfiguration(with: descriptor, isDebug: true) + var headers: [String: Any] = ["Referer": "https://request.example/app"] + + applyCapacitorDefaultRequestHeaders(&headers, configuration) + + XCTAssertEqual(headers["Referer"] as? String, "https://request.example/app") + } + + func testApplyDefaultRequestHeadersIgnoresInvalidReferer() throws { + let descriptor = InstanceDescriptor.init() + descriptor.requestReferer = "capacitor://localhost" + let configuration = InstanceConfiguration(with: descriptor, isDebug: true) + var headers: [String: Any] = [:] + + applyCapacitorDefaultRequestHeaders(&headers, configuration) + + XCTAssertNil(headers["Referer"]) } func testPluginConfig() throws { diff --git a/ios/Capacitor/TestsHostApp/configurations/server.json b/ios/Capacitor/TestsHostApp/configurations/server.json index 9c5b262afc..a590d8b11e 100644 --- a/ios/Capacitor/TestsHostApp/configurations/server.json +++ b/ios/Capacitor/TestsHostApp/configurations/server.json @@ -11,6 +11,7 @@ "iosScheme": "override", "allowNavigation": ["*.capacitorjs.com", "ionic.io", "192.168.0.1", "subdomain.*.ionicframework.com", "*.*.example.com"], "hostname": "myhost", + "referer": "https://example.com/app", "url": "http://192.168.100.1:2057" }, "plugins": {