Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(http): route get requests through custom handler #6818

Merged
merged 26 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
53ae746
fix(http): route get requests through custom handler
ItsChaceD Aug 16, 2023
4ad7dff
Merge branch 'main' into fix/http-request-custom-scheme
jcesarmobile Aug 25, 2023
fb7ecb8
Merge branch 'main' into fix/http-request-custom-scheme
ItsChaceD Sep 6, 2023
ea2d223
Merge branch 'main' into fix/http-request-custom-scheme
ItsChaceD Sep 13, 2023
f77c226
maintain original protocol and create proxy extension function
ItsChaceD Sep 13, 2023
0f94a95
remove duplicate input stream call
ItsChaceD Sep 13, 2023
d0dffb7
Merge branch 'main' into fix/http-request-custom-scheme
ItsChaceD Sep 14, 2023
63ea9df
Merge branch 'main' into fix/http-request-custom-scheme
markemer Oct 4, 2023
1ab637f
Merge branch 'main' into fix/http-request-custom-scheme
ItsChaceD Oct 18, 2023
2f1fb7e
remove duplicate calls on android, move exported functions on web bac…
ItsChaceD Oct 18, 2023
cf9be97
merge conflicts resolved
ItsChaceD Nov 8, 2023
6ca735e
Merge branch 'main' into fix/http-request-custom-scheme
jcesarmobile Nov 13, 2023
639366e
Merge branch 'main' into fix/http-request-custom-scheme
ItsChaceD Dec 12, 2023
fd6a9f0
update proxy url construction to handle cors on ios
ItsChaceD Dec 13, 2023
c278c07
Merge branch 'main' into fix/http-request-custom-scheme
ItsChaceD Dec 20, 2023
a335d83
update android to handle new proxy url construction for cors
ItsChaceD Dec 20, 2023
289c41a
Merge branch 'main' into fix/http-request-custom-scheme
jcesarmobile Jan 11, 2024
595fdbb
Merge branch 'main' into fix/http-request-custom-scheme
ItsChaceD Jan 17, 2024
37f7acc
construct proxy url from server url
ItsChaceD Jan 17, 2024
d62e948
ios live reload replace from localUrl
ItsChaceD Jan 17, 2024
3c54834
Update android/capacitor/src/main/java/com/getcapacitor/WebViewLocalS…
jcesarmobile Jan 18, 2024
1d44250
Merge branch 'main' into fix/http-request-custom-scheme
ItsChaceD Jan 18, 2024
fc41f58
Remove live reload CORS headers on Android
ItsChaceD Jan 22, 2024
64e25b5
Merge branch 'main' into fix/http-request-custom-scheme
ItsChaceD Jan 22, 2024
735c144
run fmt for whitespace
ItsChaceD Jan 22, 2024
666c57a
Merge branch 'main' into fix/http-request-custom-scheme
jcesarmobile Jan 29, 2024
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
40 changes: 36 additions & 4 deletions android/capacitor/src/main/assets/native-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -423,10 +423,25 @@ var nativeBridge = (function (exports) {
if (doPatchHttp) {
// fetch patch
window.fetch = async (resource, options) => {
var _a;
if (!(resource.toString().startsWith('http:') ||
resource.toString().startsWith('https:'))) {
return win.CapacitorWebFetch(resource, options);
}
if (!(options === null || options === void 0 ? void 0 : options.method) ||
options.method.toLocaleUpperCase() === 'GET' ||
options.method.toLocaleUpperCase() === 'HEAD' ||
options.method.toLocaleUpperCase() === 'OPTIONS' ||
options.method.toLocaleUpperCase() === 'TRACE') {
const url = new URL(resource.toString());
if (platform === 'ios') {
url.protocol = (_a = win.WEBVIEW_SERVER_URL) !== null && _a !== void 0 ? _a : '';
}
url.pathname = '/_capacitor_http_interceptor_' + url.pathname;
const modifiedResource = url.toString();
const response = await win.CapacitorWebFetch(modifiedResource, options);
return response;
}
const tag = `CapacitorHttp fetch ${Date.now()} ${resource}`;
console.time(tag);
try {
Expand Down Expand Up @@ -499,7 +514,9 @@ var nativeBridge = (function (exports) {
});
xhr.readyState = 0;
const prototype = win.CapacitorWebXMLHttpRequest.prototype;
const isRelativeURL = (url) => !url || !(url.startsWith('http:') || url.startsWith('https:'));
const isRelativeURL = (url) => !url ||
!(url.startsWith('http:') || url.startsWith('https:')) ||
url.indexOf('/_capacitor_http_interceptor_') > -1;
const isProgressEventAvailable = () => typeof ProgressEvent !== 'undefined' &&
ProgressEvent.prototype instanceof Event;
// XHR patch abort
Expand All @@ -515,10 +532,25 @@ var nativeBridge = (function (exports) {
};
// XHR patch open
prototype.open = function (method, url) {
var _a;
this._method = method.toLocaleUpperCase();
this._url = url;
this._method = method;
if (isRelativeURL(url)) {
return win.CapacitorWebXMLHttpRequest.open.call(this, method, url);
if (!this._method ||
this._method === 'GET' ||
this._method === 'HEAD' ||
this._method === 'OPTIONS' ||
this._method === 'TRACE') {
if (isRelativeURL(url)) {
return win.CapacitorWebXMLHttpRequest.open.call(this, method, url);
}
const modifiedUrl = new URL(this._url);
if (platform === 'ios') {
modifiedUrl.protocol = (_a = win.WEBVIEW_SERVER_URL) !== null && _a !== void 0 ? _a : '';
}
modifiedUrl.pathname =
'/_capacitor_http_interceptor_' + modifiedUrl.pathname;
this._url = modifiedUrl.toString();
return win.CapacitorWebXMLHttpRequest.open.call(this, method, this._url);
}
setTimeout(() => {
this.dispatchEvent(new Event('loadstart'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public class Bridge {
public static final String CAPACITOR_HTTPS_SCHEME = "https";
public static final String CAPACITOR_FILE_START = "/_capacitor_file_";
public static final String CAPACITOR_CONTENT_START = "/_capacitor_content_";
public static final String CAPACITOR_HTTP_INTERCEPTOR_START = "/_capacitor_http_interceptor_";
public static final int DEFAULT_ANDROID_WEBVIEW_VERSION = 60;
public static final int MINIMUM_ANDROID_WEBVIEW_VERSION = 55;
public static final int DEFAULT_HUAWEI_WEBVIEW_VERSION = 10;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@
import android.webkit.CookieManager;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import com.getcapacitor.plugin.util.CapacitorHttpUrlConnection;
import com.getcapacitor.plugin.util.HttpRequestHandler;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
jcesarmobile marked this conversation as resolved.
Show resolved Hide resolved
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -165,6 +169,16 @@ private static Uri parseAndVerifyUrl(String url) {
*/
public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
Uri loadingUrl = request.getUrl();

if (null != loadingUrl.getPath() && loadingUrl.getPath().startsWith(Bridge.CAPACITOR_HTTP_INTERCEPTOR_START)) {
Logger.debug("Handling CapacitorHttp request: " + request.getUrl().toString());
try {
return handleCapacitorHttpRequest(request);
} catch (Exception e) {
Logger.error(e.getLocalizedMessage());
}
}

PathHandler handler;
synchronized (uriMatcher) {
handler = (PathHandler) uriMatcher.match(request.getUrl());
Expand Down Expand Up @@ -199,6 +213,110 @@ private boolean isAllowedUrl(Uri loadingUrl) {
return !(bridge.getServerUrl() == null && !bridge.getAppAllowNavigationMask().matches(loadingUrl.getHost()));
}

private Boolean isDomainExcludedFromSSL(Bridge bridge, URL url) {
ItsChaceD marked this conversation as resolved.
Show resolved Hide resolved
try {
Class<?> sslPinningImpl = Class.forName("io.ionic.sslpinning.SSLPinning");
Method method = sslPinningImpl.getDeclaredMethod("isDomainExcluded", Bridge.class, URL.class);
return (Boolean) method.invoke(sslPinningImpl.newInstance(), bridge, url);
} catch (Exception ignored) {
return false;
}
}

private String getReasonPhraseFromResponseCode(int code) {
return switch (code) {
case 100 -> "Continue";
case 101 -> "Switching Protocols";
case 200 -> "OK";
case 201 -> "Created";
case 202 -> "Accepted";
case 203 -> "Non-Authoritative Information";
case 204 -> "No Content";
case 205 -> "Reset Content";
case 206 -> "Partial Content";
case 300 -> "Multiple Choices";
case 301 -> "Moved Permanently";
case 302 -> "Found";
case 303 -> "See Other";
case 304 -> "Not Modified";
case 400 -> "Bad Request";
case 401 -> "Unauthorized";
case 403 -> "Forbidden";
case 404 -> "Not Found";
case 405 -> "Method Not Allowed";
case 406 -> "Not Acceptable";
case 407 -> "Proxy Authentication Required";
case 408 -> "Request Timeout";
case 409 -> "Conflict";
case 410 -> "Gone";
case 500 -> "Internal Server Error";
case 501 -> "Not Implemented";
case 502 -> "Bad Gateway";
case 503 -> "Service Unavailable";
case 504 -> "Gateway Timeout";
case 505 -> "HTTP Version Not Supported";
default -> "Unknown";
};
}

private WebResourceResponse handleCapacitorHttpRequest(WebResourceRequest request) throws IOException {
String urlString = request.getUrl().toString().replace(Bridge.CAPACITOR_HTTP_INTERCEPTOR_START, "");
URL url = new URL(urlString);
JSObject headers = new JSObject();

for (Map.Entry<String, String> header : request.getRequestHeaders().entrySet()) {
headers.put(header.getKey(), header.getValue());
}

HttpRequestHandler.HttpURLConnectionBuilder connectionBuilder = new HttpRequestHandler.HttpURLConnectionBuilder()
.setUrl(url)
.setMethod(request.getMethod())
.setHeaders(headers)
.openConnection();

CapacitorHttpUrlConnection connection = connectionBuilder.build();

if (null != bridge && !isDomainExcludedFromSSL(bridge, url)) {
connection.setSSLSocketFactory(bridge);
}

connection.connect();

String mimeType = null;
String encoding = null;
Map<String, String> responseHeaders = new LinkedHashMap<>();
for (Map.Entry<String, List<String>> entry : connection.getHeaderFields().entrySet()) {
StringBuilder builder = new StringBuilder();
for (String value : entry.getValue()) {
builder.append(value);
builder.append(", ");
}
builder.setLength(builder.length() - 2);

if ("Content-Type".equalsIgnoreCase(entry.getKey())) {
String[] contentTypeParts = builder.toString().split(";");
mimeType = contentTypeParts[0].trim();
if (contentTypeParts.length > 1) {
String[] encodingParts = contentTypeParts[1].split("=");
if (encodingParts.length > 1) {
encoding = encodingParts[1].trim();
}
}
} else {
responseHeaders.put(entry.getKey(), builder.toString());
}
}

if (null == mimeType) {
mimeType = getMimeType(request.getUrl().getPath(), connection.getInputStream());
ItsChaceD marked this conversation as resolved.
Show resolved Hide resolved
}

int responseCode = connection.getResponseCode();
String reasonPhrase = getReasonPhraseFromResponseCode(responseCode);

return new WebResourceResponse(mimeType, encoding, responseCode, reasonPhrase, responseHeaders, connection.getInputStream());
}

private WebResourceResponse handleLocalRequest(WebResourceRequest request, PathHandler handler) {
String path = request.getUrl().getPath();

Expand Down
55 changes: 51 additions & 4 deletions core/native-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,28 @@ const initBridge = (w: any): void => {
return win.CapacitorWebFetch(resource, options);
}

if (
!options?.method ||
options.method.toLocaleUpperCase() === 'GET' ||
options.method.toLocaleUpperCase() === 'HEAD' ||
options.method.toLocaleUpperCase() === 'OPTIONS' ||
options.method.toLocaleUpperCase() === 'TRACE'
) {
const url = new URL(resource.toString());
ItsChaceD marked this conversation as resolved.
Show resolved Hide resolved
if (platform === 'ios') {
url.protocol = win.WEBVIEW_SERVER_URL ?? '';
}
url.pathname = '/_capacitor_http_interceptor_' + url.pathname;

const modifiedResource = url.toString();
const response = await win.CapacitorWebFetch(
modifiedResource,
options,
);

return response;
}

const tag = `CapacitorHttp fetch ${Date.now()} ${resource}`;
console.time(tag);
try {
Expand Down Expand Up @@ -578,7 +600,9 @@ const initBridge = (w: any): void => {
const prototype = win.CapacitorWebXMLHttpRequest.prototype;

const isRelativeURL = (url: string | undefined) =>
!url || !(url.startsWith('http:') || url.startsWith('https:'));
!url ||
!(url.startsWith('http:') || url.startsWith('https:')) ||
url.indexOf('/_capacitor_http_interceptor_') > -1;
const isProgressEventAvailable = () =>
typeof ProgressEvent !== 'undefined' &&
ProgressEvent.prototype instanceof Event;
Expand All @@ -597,14 +621,37 @@ const initBridge = (w: any): void => {

// XHR patch open
prototype.open = function (method: string, url: string) {
this._method = method.toLocaleUpperCase();
this._url = url;
this._method = method;

if (isRelativeURL(url)) {
if (
!this._method ||
this._method === 'GET' ||
this._method === 'HEAD' ||
this._method === 'OPTIONS' ||
this._method === 'TRACE'
) {
if (isRelativeURL(url)) {
return win.CapacitorWebXMLHttpRequest.open.call(
this,
method,
url,
);
}

const modifiedUrl = new URL(this._url);
if (platform === 'ios') {
modifiedUrl.protocol = win.WEBVIEW_SERVER_URL ?? '';
}
modifiedUrl.pathname =
'/_capacitor_http_interceptor_' + modifiedUrl.pathname;

this._url = modifiedUrl.toString();

return win.CapacitorWebXMLHttpRequest.open.call(
this,
method,
url,
this._url,
);
}

Expand Down
1 change: 1 addition & 0 deletions ios/Capacitor/Capacitor/CapacitorBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ internal class CapacitorBridge: NSObject, CAPBridgeProtocol {
static let tmpVCAppeared = Notification(name: Notification.Name(rawValue: "tmpViewControllerAppeared"))
public static let capacitorSite = "https://capacitorjs.com/"
public static let fileStartIdentifier = "/_capacitor_file_"
public static let httpInterceptorStartIdentifier = "/_capacitor_http_interceptor_"
public static let defaultScheme = "capacitor"

var webViewAssetHandler: WebViewAssetHandler
Expand Down
61 changes: 61 additions & 0 deletions ios/Capacitor/Capacitor/WebViewAssetHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ internal class WebViewAssetHandler: NSObject, WKURLSchemeHandler {
let url = urlSchemeTask.request.url!
let stringToLoad = url.path

if url.path.starts(with: CapacitorBridge.httpInterceptorStartIdentifier) {
handleCapacitorHttpRequest(urlSchemeTask)
return
}

if stringToLoad.starts(with: CapacitorBridge.fileStartIdentifier) {
startPath = stringToLoad.replacingOccurrences(of: CapacitorBridge.fileStartIdentifier, with: "")
} else {
Expand Down Expand Up @@ -121,6 +126,62 @@ internal class WebViewAssetHandler: NSObject, WKURLSchemeHandler {
return false
}

func handleCapacitorHttpRequest(_ urlSchemeTask: WKURLSchemeTask) {
var urlRequest = urlSchemeTask.request
let url = urlRequest.url!
ItsChaceD marked this conversation as resolved.
Show resolved Hide resolved
var targetUrl = url.absoluteString
.replacingOccurrences(of: CapacitorBridge.httpInterceptorStartIdentifier, with: "")

// Only replace first occurrence of the scheme
if let range = targetUrl.range(of: InstanceDescriptorDefaults.scheme) {
targetUrl = targetUrl.replacingCharacters(in: range, with: "https")
ItsChaceD marked this conversation as resolved.
Show resolved Hide resolved
}

urlRequest.url = URL(string: targetUrl)

let urlSession = URLSession.shared
let task = urlSession.dataTask(with: urlRequest) { (data, response, error) in
urlSession.invalidateAndCancel()
ItsChaceD marked this conversation as resolved.
Show resolved Hide resolved
if let error = error {
urlSchemeTask.didFailWithError(error)
return
}

if let response = response as? HTTPURLResponse {
let existingHeaders = response.allHeaderFields
var newHeaders: [AnyHashable: Any] = [:]

// if using live reload, then set CORS headers
if self.serverUrl != nil && self.serverUrl?.scheme != urlRequest.url?.scheme {
ItsChaceD marked this conversation as resolved.
Show resolved Hide resolved
newHeaders = [
"Access-Control-Allow-Origin": self.serverUrl?.absoluteString ?? "",
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS, TRACE"
]
}

if let mergedHeaders = existingHeaders.merging(newHeaders, uniquingKeysWith: { (current, _) in current }) as? [String: String] {

let modifiedResponse = HTTPURLResponse(
url: response.url!,
statusCode: response.statusCode,
httpVersion: nil,
headerFields: mergedHeaders
)!

urlSchemeTask.didReceive(modifiedResponse)

if let data = data {
urlSchemeTask.didReceive(data)
}
}
}
urlSchemeTask.didFinish()
return
}

task.resume()
}

let mimeTypes = [
"aaf": "application/octet-stream",
"aca": "application/octet-stream",
Expand Down
Loading