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 17 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
63 changes: 54 additions & 9 deletions android/capacitor/src/main/assets/native-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,35 @@ var nativeBridge = (function (exports) {
}
return { data: body, type: 'json' };
};
const CAPACITOR_HTTP_INTERCEPTOR = '/_capacitor_http_interceptor_';
const CAPACITOR_HTTPS_INTERCEPTOR = '/_capacitor_https_interceptor_';
// TODO: export as Cap function
const isRelativeOrProxyUrl = (url) => !url ||
!(url.startsWith('http:') || url.startsWith('https:')) ||
url.indexOf(CAPACITOR_HTTP_INTERCEPTOR) > -1 ||
url.indexOf(CAPACITOR_HTTPS_INTERCEPTOR) > -1;
// TODO: export as Cap function
const createProxyUrl = (url, win) => {
var _a, _b, _c;
if (isRelativeOrProxyUrl(url))
return url;
const proxyUrl = new URL(url);
const isHttps = proxyUrl.protocol === 'https:';
const webviewServerUrl = new URL((_a = win.WEBVIEW_SERVER_URL) !== null && _a !== void 0 ? _a : 'capacitor://localhost');
const originalHostname = proxyUrl.hostname;
const originalPathname = proxyUrl.pathname;
if ((_c = (_b = win.webkit) === null || _b === void 0 ? void 0 : _b.messageHandlers) === null || _c === void 0 ? void 0 : _c.bridge) {
proxyUrl.protocol = 'capacitor:';
if (webviewServerUrl.protocol !== 'capacitor:' &&
webviewServerUrl.protocol !== 'http:' &&
webviewServerUrl.protocol !== 'https:') {
proxyUrl.protocol = webviewServerUrl.protocol;
}
}
proxyUrl.hostname = webviewServerUrl.hostname;
proxyUrl.pathname = `/${isHttps ? CAPACITOR_HTTPS_INTERCEPTOR : CAPACITOR_HTTP_INTERCEPTOR}/${originalHostname}${originalPathname}`;
return proxyUrl.toString();
};
const initBridge = (w) => {
const getPlatformId = (win) => {
var _a, _b;
Expand Down Expand Up @@ -472,6 +501,15 @@ var nativeBridge = (function (exports) {
if (request.url.startsWith(`${cap.getServerUrl()}/`)) {
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 modifiedResource = createProxyUrl(resource.toString(), win);
const response = await win.CapacitorWebFetch(modifiedResource, options);
return response;
}
const tag = `CapacitorHttp fetch ${Date.now()} ${resource}`;
console.time(tag);
try {
Expand Down Expand Up @@ -541,12 +579,11 @@ var nativeBridge = (function (exports) {
});
xhr.readyState = 0;
const prototype = win.CapacitorWebXMLHttpRequest.prototype;
const isRelativeURL = (url) => !url || !(url.startsWith('http:') || url.startsWith('https:'));
const isProgressEventAvailable = () => typeof ProgressEvent !== 'undefined' &&
ProgressEvent.prototype instanceof Event;
// XHR patch abort
prototype.abort = function () {
if (isRelativeURL(this._url)) {
if (isRelativeOrProxyUrl(this._url)) {
return win.CapacitorWebXMLHttpRequest.abort.call(this);
}
this.readyState = 0;
Expand All @@ -557,10 +594,18 @@ var nativeBridge = (function (exports) {
};
// XHR patch open
prototype.open = function (method, url) {
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 (isRelativeOrProxyUrl(url)) {
return win.CapacitorWebXMLHttpRequest.open.call(this, method, url);
}
this._url = createProxyUrl(this._url, win);
return win.CapacitorWebXMLHttpRequest.open.call(this, method, this._url);
}
setTimeout(() => {
this.dispatchEvent(new Event('loadstart'));
Expand All @@ -569,14 +614,14 @@ var nativeBridge = (function (exports) {
};
// XHR patch set request header
prototype.setRequestHeader = function (header, value) {
if (isRelativeURL(this._url)) {
if (isRelativeOrProxyUrl(this._url)) {
return win.CapacitorWebXMLHttpRequest.setRequestHeader.call(this, header, value);
}
this._headers[header] = value;
};
// XHR patch send
prototype.send = function (body) {
if (isRelativeURL(this._url)) {
if (isRelativeOrProxyUrl(this._url)) {
return win.CapacitorWebXMLHttpRequest.send.call(this, body);
}
const tag = `CapacitorHttp XMLHttpRequest ${Date.now()} ${this._url}`;
Expand Down Expand Up @@ -695,7 +740,7 @@ var nativeBridge = (function (exports) {
};
// XHR patch getAllResponseHeaders
prototype.getAllResponseHeaders = function () {
if (isRelativeURL(this._url)) {
if (isRelativeOrProxyUrl(this._url)) {
return win.CapacitorWebXMLHttpRequest.getAllResponseHeaders.call(this);
}
let returnString = '';
Expand All @@ -708,7 +753,7 @@ var nativeBridge = (function (exports) {
};
// XHR patch getResponseHeader
prototype.getResponseHeader = function (name) {
if (isRelativeURL(this._url)) {
if (isRelativeOrProxyUrl(this._url)) {
return win.CapacitorWebXMLHttpRequest.getResponseHeader.call(this, name);
}
return this._headers[name];
Expand Down
3 changes: 3 additions & 0 deletions android/capacitor/src/main/java/com/getcapacitor/Bridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ 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 String CAPACITOR_HTTPS_INTERCEPTOR_START = "//_capacitor_https_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 @@ -15,20 +15,26 @@
*/
package com.getcapacitor;

import static com.getcapacitor.plugin.util.HttpRequestHandler.isDomainExcludedFromSSL;

import android.content.Context;
import android.net.Uri;
import android.util.Base64;
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 +171,22 @@ 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) ||
loadingUrl.getPath().startsWith(Bridge.CAPACITOR_HTTPS_INTERCEPTOR_START)
)
) {
Logger.debug("Handling CapacitorHttp request: " + loadingUrl);
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 +221,116 @@ private boolean isAllowedUrl(Uri loadingUrl) {
return !(bridge.getServerUrl() == null && !bridge.getAppAllowNavigationMask().matches(loadingUrl.getHost()));
}

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 {
boolean isHttps =
request.getUrl().getPath() != null && request.getUrl().getPath().startsWith(Bridge.CAPACITOR_HTTPS_INTERCEPTOR_START);

String urlString = request
.getUrl()
.toString()
.replace(bridge.getLocalUrl(), isHttps ? "https:/" : "http:/")
.replace(Bridge.CAPACITOR_HTTP_INTERCEPTOR_START, "")
.replace(Bridge.CAPACITOR_HTTPS_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 (!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());
}
}

// Using Live Reload
if (bridge.getServerUrl() != null) {
responseHeaders.put("Access-Control-Allow-Origin", bridge.getServerUrl());
responseHeaders.put("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS, TRACE");
}
ItsChaceD marked this conversation as resolved.
Show resolved Hide resolved

InputStream inputStream = connection.getInputStream();

if (null == mimeType) {
mimeType = getMimeType(request.getUrl().getPath(), inputStream);
}

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

return new WebResourceResponse(mimeType, encoding, responseCode, reasonPhrase, responseHeaders, inputStream);
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ public static JSObject request(PluginCall call, String httpMethod, Bridge bridge
return response;
}

private static Boolean isDomainExcludedFromSSL(Bridge bridge, URL url) {
public static Boolean isDomainExcludedFromSSL(Bridge bridge, URL url) {
try {
Class<?> sslPinningImpl = Class.forName("io.ionic.sslpinning.SSLPinning");
Method method = sslPinningImpl.getDeclaredMethod("isDomainExcluded", Bridge.class, URL.class);
Expand Down
Loading