Internet helper: Support forwarding POST requests

This commit is contained in:
Arjan Schrijver
2025-12-29 23:10:09 +01:00
parent f4653e58f8
commit 002c0f7e53
6 changed files with 155 additions and 7 deletions
@@ -52,6 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
import nodomain.freeyourgadget.gadgetbridge.webview.GBChromeClient;
import nodomain.freeyourgadget.gadgetbridge.webview.GBWebClient;
import nodomain.freeyourgadget.gadgetbridge.webview.RequestInterceptorInterface;
public class ExternalPebbleJSActivity extends AbstractGBActivity {
@@ -115,7 +116,8 @@ public class ExternalPebbleJSActivity extends AbstractGBActivity {
setContentView(R.layout.activity_legacy_external_pebble_js);
myWebView = findViewById(R.id.configureWebview);
myWebView.clearCache(true);
myWebView.setWebViewClient(new GBWebClient(GBWebClient.REQUEST_TYPE_PEBBLE_APP_CONFIG));
GBWebClient gbWebClient = new GBWebClient(GBWebClient.REQUEST_TYPE_PEBBLE_APP_CONFIG);
myWebView.setWebViewClient(gbWebClient);
myWebView.setWebChromeClient(new GBChromeClient());
WebSettings webSettings = myWebView.getSettings();
//noinspection SetJavaScriptEnabled
@@ -132,6 +134,7 @@ public class ExternalPebbleJSActivity extends AbstractGBActivity {
JSInterface gbJSInterface = new JSInterface(device, uuid);
myWebView.addJavascriptInterface(gbJSInterface, "GBjs");
myWebView.addJavascriptInterface(new ActivityJSInterface(), "GBActivity");
myWebView.addJavascriptInterface(new RequestInterceptorInterface(gbWebClient), "GBReqInt");
myWebView.loadUrl("file:///android_asset/app_config/configure.html?rand=" + Math.random() * 500);
}
@@ -38,6 +38,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.InternetUtils
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils
import nodomain.freeyourgadget.gadgetbridge.webview.GBChromeClient
import nodomain.freeyourgadget.gadgetbridge.webview.GBWebClient
import nodomain.freeyourgadget.gadgetbridge.webview.RequestInterceptorInterface
import org.json.JSONObject
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@@ -135,7 +136,7 @@ class RebbleAppStoreActivity : AbstractGBActivity() {
settings.loadWithOverviewMode = true
settings.useWideViewPort = true
webView!!.webViewClient = object : GBWebClient(REQUEST_TYPE_PEBBLE_APP_STORE) {
val gbWebClient = object : GBWebClient(REQUEST_TYPE_PEBBLE_APP_STORE) {
override fun shouldOverrideUrlLoading(
wv: WebView,
request: WebResourceRequest
@@ -170,6 +171,7 @@ class RebbleAppStoreActivity : AbstractGBActivity() {
view.loadUrl("about:blank")
}
}
webView!!.webViewClient = gbWebClient
webView!!.webChromeClient = object : GBChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
@@ -177,6 +179,7 @@ class RebbleAppStoreActivity : AbstractGBActivity() {
}
}
webView!!.addJavascriptInterface(RequestInterceptorInterface(gbWebClient), "GBReqInt")
webView!!.loadUrl(url)
}
}
@@ -54,6 +54,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.banglejs.BangleJSDev
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.webview.GBChromeClient;
import nodomain.freeyourgadget.gadgetbridge.webview.GBWebClient;
import nodomain.freeyourgadget.gadgetbridge.webview.RequestInterceptorInterface;
public class AppsManagementActivity extends AbstractGBActivity {
private static final Logger LOG = LoggerFactory.getLogger(AppsManagementActivity.class);
@@ -218,7 +219,7 @@ public class AppsManagementActivity extends AbstractGBActivity {
webView.addJavascriptInterface(new WebViewInterface(this), "Android");
webView.setWebContentsDebuggingEnabled(true); // FIXME
webView.setWebViewClient(new GBWebClient(GBWebClient.REQUEST_TYPE_BANGLE_APP_LOADER){
GBWebClient gbWebClient = new GBWebClient(GBWebClient.REQUEST_TYPE_BANGLE_APP_LOADER){
@Override
public void onPageFinished(WebView view, String weburl){
//webView.loadUrl("javascript:showToast('WebView in Espruino')");
@@ -229,7 +230,10 @@ public class AppsManagementActivity extends AbstractGBActivity {
Toast.makeText(AppsManagementActivity.this, "Error:" + description, Toast.LENGTH_SHORT).show();
view.loadUrl("about:blank");
}
});
};
webView.setWebViewClient(gbWebClient);
webView.addJavascriptInterface(new RequestInterceptorInterface(gbWebClient), "GBReqInt");
Prefs devicePrefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(mGBDevice.getAddress()));
String url = devicePrefs.getString(PREF_BANGLEJS_WEBVIEW_URL, "").trim();
@@ -33,6 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
import nodomain.freeyourgadget.gadgetbridge.util.GB
import nodomain.freeyourgadget.gadgetbridge.webview.GBChromeClient
import nodomain.freeyourgadget.gadgetbridge.webview.GBWebClient
import nodomain.freeyourgadget.gadgetbridge.webview.RequestInterceptorInterface
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.UUID
@@ -102,6 +103,7 @@ class PebbleJsService : Service() {
LOG.info("WEBVIEW starting for device ${device.address}")
WebView.setWebContentsDebuggingEnabled(true)
val uiContext = applicationContext.createConfigurationContext(resources.configuration)
val gbWebClient = GBWebClient(GBWebClient.REQUEST_TYPE_PEBBLE_BACKGROUND_JS)
val wv = WebView(uiContext).apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
@@ -111,10 +113,11 @@ class PebbleJsService : Service() {
settings.allowFileAccessFromFileURLs = true
settings.allowUniversalAccessFromFileURLs = true
visibility = View.GONE
webViewClient = GBWebClient(GBWebClient.REQUEST_TYPE_PEBBLE_BACKGROUND_JS)
webViewClient = gbWebClient
webChromeClient = GBChromeClient()
addJavascriptInterface(RequestInterceptorInterface(gbWebClient), "GBReqInt")
setWillNotDraw(true)
}
wv.setWillNotDraw(true)
wv.clearCache(true)
wv.resumeTimers()
webviews[device.address] = wv
@@ -18,6 +18,7 @@
package nodomain.freeyourgadget.gadgetbridge.webview;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.RemoteException;
import android.webkit.WebResourceRequest;
@@ -39,7 +40,10 @@ import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@@ -65,11 +69,54 @@ public class GBWebClient extends WebViewClient {
"rawgit.com", //for trekvolle
};
private final Map<String, List<Entry>> postData = new HashMap<>();
private record Entry(long timestamp, String body) {}
public GBWebClient(int type) {
super();
requestType = type;
}
/**
* Stores HTTP POST body strings for later retrieval in mimicReply()
*/
public synchronized void storePostBody(String url, String body) {
long now = System.currentTimeMillis();
// Cleanup expired entries (more than a minute old)
Iterator<Map.Entry<String, List<Entry>>> mapIt = postData.entrySet().iterator();
while (mapIt.hasNext()) {
Map.Entry<String, List<Entry>> mapEntry = mapIt.next();
List<Entry> list = mapEntry.getValue();
list.removeIf(e -> now - e.timestamp > 60*1000);
if (list.isEmpty()) {
mapIt.remove();
}
}
// Add new entry
postData
.computeIfAbsent(url, k -> new ArrayList<>())
.add(new Entry(now, body));
}
/**
* Returns the earliest known HTTP POST body for a certain URL
*/
private synchronized String getFirstPostForUrl(String url) {
List<Entry> list = postData.get(url);
if (list == null || list.isEmpty()) {
return null;
}
Entry first = list.remove(0);
if (list.isEmpty()) {
postData.remove(url);
}
return first.body;
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
Uri parsedUri = request.getUrl();
@@ -135,7 +182,13 @@ public class GBWebClient extends WebViewClient {
if (!forceLocal && !directInternetAccess && InternetHelperSingleton.INSTANCE.ensureInternetHelperBound()) {
LOG.debug("WEBVIEW forwarding request to the internet helper");
try {
WebResourceResponse wrr = InternetHelperSingleton.INSTANCE.send(requestedUri, HttpRequest.Method.valueOf(method), requestHeaders, null, "application/octet-stream", false);
HttpRequest.Method requestMethod = HttpRequest.Method.valueOf(method);
String body = null;
if (requestMethod == HttpRequest.Method.POST) {
body = getFirstPostForUrl(requestedUri.toString());
LOG.debug("WEBVIEW POSTing with body: {}", body);
}
WebResourceResponse wrr = InternetHelperSingleton.INSTANCE.send(requestedUri, requestMethod, requestHeaders, body, "application/octet-stream", false);
if (wrr != null && wrr.getStatusCode() < 400)
return wrr;
else
@@ -192,6 +245,57 @@ public class GBWebClient extends WebViewClient {
return shouldOverrideUrlLoading(view, request.getUrl().toString());
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
injectPostInterceptor(view);
}
@Override
public void onPageFinished(WebView view, String url) {
injectPostInterceptor(view);
}
private void injectPostInterceptor(WebView view) {
// Inject JavaScript for capturing POST body data, which we need when forwarding
// requests to the internet helper
view.evaluateJavascript(
"""
(function () {
if (window.__p) return; window.__p = 1;
const sb = b => b instanceof FormData
? [...b].map(e => e.map(encodeURIComponent).join('=')).join('&')
: b ? b.toString() : null;
const send = (u, b) => GBReqInt.onPostBody(new URL(u, location.href).toString(), sb(b));
const xo = XMLHttpRequest.prototype.open;
const xs = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (m, u) {
this._m = m; this._u = u; return xo.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function (b) {
if (this._m === 'POST') send(this._u, b);
return xs.apply(this, arguments);
};
const f = window.fetch;
window.fetch = function (i, o = {}) {
if ((o.method || 'GET') === 'POST')
send(typeof i === 'string' ? i : i.url, o.body);
return f.apply(this, arguments);
};
document.addEventListener('submit', e => {
const f = e.target;
if (f.method && f.method.toUpperCase() === 'POST')
send(f.action || location.href, new FormData(f));
}, true);
})();
""",
null
);
}
private WebResourceResponse mimicRawGitResponse(String path) {
if("/aHcVolle/TrekVolle/master/online.html".equals(path)) { //TrekVolle online check
Map<String, String> headers = new HashMap<>();
@@ -0,0 +1,31 @@
/* Copyright (C) 2025 Arjan Schrijver
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.webview
import android.webkit.JavascriptInterface
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class RequestInterceptorInterface(val gbWebClient: GBWebClient) {
val LOG: Logger = LoggerFactory.getLogger(RequestInterceptorInterface::class.java)
@JavascriptInterface
fun onPostBody(url: String, body: String) {
LOG.debug("POST_INTERCEPTED FOR {}, BODY: {}", url, body)
gbWebClient.storePostBody(url, body)
}
}