Skip to content

Commit

Permalink
fix(http): route get requests through custom handler (#6818)
Browse files Browse the repository at this point in the history
Co-authored-by: jcesarmobile <[email protected]>
Co-authored-by: Mark Anderson <[email protected]>
  • Loading branch information
3 people committed Feb 28, 2024
1 parent 9b71399 commit fd64dfc
Show file tree
Hide file tree
Showing 8 changed files with 367 additions and 32 deletions.
54 changes: 45 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,26 @@ 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;
if (isRelativeOrProxyUrl(url))
return url;
let proxyUrl = new URL(url);
const isHttps = proxyUrl.protocol === 'https:';
const originalHostname = proxyUrl.hostname;
const originalPathname = proxyUrl.pathname;
proxyUrl = new URL((_b = (_a = win.Capacitor) === null || _a === void 0 ? void 0 : _a.getServerUrl()) !== null && _b !== void 0 ? _b : '');
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 @@ -476,6 +496,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 @@ -546,12 +575,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 @@ -562,10 +590,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 @@ -574,14 +610,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 @@ -710,7 +746,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 @@ -723,7 +759,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);
}
for (const key in this._headers) {
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,12 +15,16 @@
*/
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.net.HttpURLConnection;
Expand All @@ -29,6 +33,7 @@
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 +170,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 +220,110 @@ 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());
}
}

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

0 comments on commit fd64dfc

Please sign in to comment.