Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions android/capacitor/src/main/java/com/getcapacitor/CapConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -342,6 +345,10 @@ public String getErrorPath() {
return errorPath;
}

public String getRequestReferer() {
return requestReferer;
}

public String getHostname() {
return hostname;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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"));
}
}
3 changes: 2 additions & 1 deletion android/capacitor/src/test/resources/configs/server.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -18,4 +19,4 @@
}
},
"cordova": {}
}
}
15 changes: 15 additions & 0 deletions cli/src/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
1 change: 1 addition & 0 deletions ios/Capacitor/Capacitor/CAPBridgeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions ios/Capacitor/Capacitor/CAPInstanceConfiguration.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions ios/Capacitor/Capacitor/CAPInstanceConfiguration.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions ios/Capacitor/Capacitor/CAPInstanceDescriptor.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions ios/Capacitor/Capacitor/CAPInstanceDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
34 changes: 31 additions & 3 deletions ios/Capacitor/Capacitor/Plugins/HttpRequestHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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)

Expand Down
6 changes: 6 additions & 0 deletions ios/Capacitor/Capacitor/WebViewAssetHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
Loading