mirror of
https://github.com/ArcaneChat/android.git
synced 2026-07-03 14:05:24 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bb06bc75b | |||
| 38f6ef7f47 |
@@ -32,6 +32,9 @@ import androidx.core.app.TaskStackBuilder;
|
||||
import androidx.core.content.pm.ShortcutInfoCompat;
|
||||
import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
import androidx.webkit.ScriptHandler;
|
||||
import androidx.webkit.WebViewCompat;
|
||||
import androidx.webkit.WebViewFeature;
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
import chat.delta.rpc.types.WebxdcMessageInfo;
|
||||
@@ -47,6 +50,7 @@ import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@@ -67,8 +71,33 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
private static final String EXTRA_HREF = "href";
|
||||
private static final int REQUEST_CODE_FILE_PICKER = 51426;
|
||||
private static long lastOpenTime = 0;
|
||||
private static final String WEBRTC_BLOCKER_SCRIPT =
|
||||
"(function(){"
|
||||
+ "const message='WebRTC is disabled in webxdc';"
|
||||
+ "const reject=function(){"
|
||||
+ "if(typeof DOMException==='function'){return Promise.reject(new DOMException(message,'NotAllowedError'));}"
|
||||
+ "return Promise.reject(new Error(message));"
|
||||
+ "};"
|
||||
+ "const define=function(scope,name,value){"
|
||||
+ "if(!scope){return;}"
|
||||
+ "try{Object.defineProperty(scope,name,{configurable:false,writable:false,value:value});}catch(e){}"
|
||||
+ "};"
|
||||
+ "const blockCtor=function(name){"
|
||||
+ "const blocked=function(){throw new TypeError(message+': '+name);};"
|
||||
+ "define(globalThis,name,blocked);"
|
||||
+ "define(window,name,blocked);"
|
||||
+ "define(self,name,blocked);"
|
||||
+ "};"
|
||||
+ "['RTCPeerConnection','webkitRTCPeerConnection','RTCDataChannel','RTCRtpSender',"
|
||||
+ "'RTCRtpReceiver','RTCSessionDescription','RTCIceCandidate'].forEach(blockCtor);"
|
||||
+ "if(navigator&&navigator.mediaDevices){"
|
||||
+ "define(navigator.mediaDevices,'getUserMedia',function(){return reject();});"
|
||||
+ "define(navigator.mediaDevices,'getDisplayMedia',function(){return reject();});"
|
||||
+ "}"
|
||||
+ "})();";
|
||||
|
||||
private ValueCallback<Uri[]> filePathCallback;
|
||||
private ScriptHandler webRtcBlockerScriptHandler;
|
||||
private DcContext dcContext;
|
||||
private int accountId;
|
||||
private Rpc rpc;
|
||||
@@ -271,6 +300,11 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
internetAccess); // this does not block network but sets `window.navigator.isOnline` in js
|
||||
// land
|
||||
webView.addJavascriptInterface(new InternalJSApi(), "InternalJSApi");
|
||||
if (!installWebRtcBlockerScript()) {
|
||||
Toast.makeText(this, R.string.webxdc_requires_modern_webview, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
String extraHref = b.getString(EXTRA_HREF, "");
|
||||
if (TextUtils.isEmpty(extraHref)) {
|
||||
@@ -315,6 +349,10 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
lastOpenTime = System.currentTimeMillis();
|
||||
DcHelper.getEventCenter(this.getApplicationContext()).removeObservers(this);
|
||||
leaveRealtimeChannel();
|
||||
if (webRtcBlockerScriptHandler != null) {
|
||||
webRtcBlockerScriptHandler.remove();
|
||||
webRtcBlockerScriptHandler = null;
|
||||
}
|
||||
tts.shutdown();
|
||||
super.onDestroy();
|
||||
}
|
||||
@@ -421,11 +459,6 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
} else if (path.equalsIgnoreCase("/webxdc_bootstrap324567869.html")) {
|
||||
InputStream targetStream = getResources().openRawResource(R.raw.webxdc_wrapper);
|
||||
res = new WebResourceResponse("text/html", "UTF-8", targetStream);
|
||||
} else if (path.equalsIgnoreCase(
|
||||
"/sandboxed_iframe_rtcpeerconnection_check_5965668501706.html")) {
|
||||
InputStream targetStream =
|
||||
getResources().openRawResource(R.raw.sandboxed_iframe_rtcpeerconnection_check);
|
||||
res = new WebResourceResponse("text/html", "UTF-8", targetStream);
|
||||
} else {
|
||||
byte[] blob = this.dcAppMsg.getWebxdcBlob(path);
|
||||
if (blob == null) {
|
||||
@@ -461,8 +494,8 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
res = new WebResourceResponse("text/plain", "UTF-8", targetStream);
|
||||
}
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
if (!internetAccess) {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put(
|
||||
"Content-Security-Policy",
|
||||
"default-src 'self'; "
|
||||
@@ -474,11 +507,31 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
+ "media-src 'self' data: blob: ;"
|
||||
+ "webrtc 'block' ; ");
|
||||
headers.put("X-DNS-Prefetch-Control", "off");
|
||||
res.setResponseHeaders(headers);
|
||||
}
|
||||
headers.put("Permissions-Policy", "camera=(), microphone=(), display-capture=()");
|
||||
res.setResponseHeaders(headers);
|
||||
return res;
|
||||
}
|
||||
|
||||
private boolean installWebRtcBlockerScript() {
|
||||
if (!WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
|
||||
Log.w(TAG, "Document start script not supported, cannot hard-block WebRTC.");
|
||||
return false;
|
||||
}
|
||||
if (webRtcBlockerScriptHandler != null) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
webRtcBlockerScriptHandler =
|
||||
WebViewCompat.addDocumentStartJavaScript(
|
||||
webView, WEBRTC_BLOCKER_SCRIPT, Collections.singleton("*"));
|
||||
return true;
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(TAG, "Failed to add WebRTC blocker document-start script.", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void callJavaScriptFunction(String func) {
|
||||
webView.evaluateJavascript(
|
||||
"document.getElementById('frame').contentWindow." + func + ";", null);
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script>
|
||||
window.addEventListener("message", e => {
|
||||
// Currently this never happens because we don't load other scripts that could `postMessage`
|
||||
// until all checks have passed, but let's play it safe.
|
||||
if (
|
||||
event.origin !== location.origin
|
||||
|| event.source !== parent
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data === "performCheck") {
|
||||
let rtcpcCreationFailed = false;
|
||||
try {
|
||||
new RTCPeerConnection();
|
||||
} catch (e) {
|
||||
rtcpcCreationFailed = true;
|
||||
}
|
||||
|
||||
parent.postMessage({ msgType: "result", rtcpcCreationFailed }, location.origin);
|
||||
}
|
||||
});
|
||||
|
||||
parent.postMessage({ msgType: "ready" }, location.origin);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -29,133 +29,18 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading">
|
||||
<progress id="progress" max="500" value="0"></progress>
|
||||
</div>
|
||||
<div class="iframe-container">
|
||||
<iframe id="frame"></iframe>
|
||||
<iframe id="frame"></iframe>
|
||||
</div>
|
||||
<iframe
|
||||
id="test-isolated-sandbox-context"
|
||||
sandbox="allow-scripts"
|
||||
src="./sandboxed_iframe_rtcpeerconnection_check_5965668501706.html"
|
||||
style="display: none"
|
||||
></iframe>
|
||||
<script>
|
||||
const loadingDiv = document.getElementById("loading");
|
||||
const iframe = document.getElementById("frame");
|
||||
|
||||
let fill500 = async (href) => {
|
||||
const connections = [];
|
||||
const loadingProgress = document.getElementById("progress");
|
||||
const isolatedContextTest = document.getElementById(
|
||||
"test-isolated-sandbox-context"
|
||||
);
|
||||
|
||||
const cert = {
|
||||
certificates: [
|
||||
await RTCPeerConnection.generateCertificate({
|
||||
name: "ECDSA",
|
||||
namedCurve: "P-256",
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
console.log("WEBRTC-WG: allocating");
|
||||
loadingProgress.value = 0;
|
||||
while (connections.length < 500) {
|
||||
try {
|
||||
connections.push(new RTCPeerConnection(cert));
|
||||
if (connections.length%50 == 0) {
|
||||
loadingProgress.value = connections.length;
|
||||
await new Promise((res) => setTimeout(res));
|
||||
}
|
||||
} catch (error) {
|
||||
loadingProgress.value++;
|
||||
new Array(1024*1024).fill(0);
|
||||
await new Promise((res) => setTimeout(res, 500));
|
||||
console.log("WEBRTC-WG: waiting for gc");
|
||||
}
|
||||
}
|
||||
console.log("WEBRTC-WG: done");
|
||||
|
||||
try {
|
||||
connections.push(new RTCPeerConnection());
|
||||
console.log("Error: was able to create more than 500 connections");
|
||||
loadingDiv.innerText =
|
||||
"Error: was not able to block webrtc ERROR_A";
|
||||
} catch (error) {
|
||||
/** @type {Promise<boolean>} */
|
||||
const sandboxedIframeCheckIsGoodPromise = new Promise(resolve => {
|
||||
/** @type {HTMLIFrameElement} */
|
||||
const sandboxedIframe = document.getElementById("test-isolated-sandbox-context");
|
||||
const askIframeToPerformCheck = () => {
|
||||
// Why `"*"`? See the comment below.
|
||||
sandboxedIframe.contentWindow.postMessage("performCheck", "*");
|
||||
};
|
||||
/** @type {(e: MessageEvent) => void} */
|
||||
const messageListener = (e) => {
|
||||
// Checking `event.origin !== location.origin` just in case would be safer,
|
||||
// but `sandbox`ed iframes seem to send messages with `origin` set to `null`,
|
||||
// so we skip the check, relying on the fact that we don't actually load any
|
||||
// untrusted scripts that could `postMessage` before this check is completed.
|
||||
// And we can't just add `sandbox="allow-same-origin` because it could turn off
|
||||
// process-isolation:
|
||||
// https://chromium-review.googlesource.com/c/chromium/src/+/3416475
|
||||
if (event.source !== sandboxedIframe.contentWindow) {
|
||||
return;
|
||||
}
|
||||
switch (event.data.msgType) {
|
||||
case "ready": {
|
||||
askIframeToPerformCheck();
|
||||
break;
|
||||
}
|
||||
case "result": {
|
||||
resolve(event.data.rtcpcCreationFailed);
|
||||
|
||||
sandboxedIframe.remove();
|
||||
window.removeEventListener("message", messageListener);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
window.addEventListener("message", messageListener);
|
||||
askIframeToPerformCheck();
|
||||
});
|
||||
|
||||
if (await sandboxedIframeCheckIsGoodPromise !== true) {
|
||||
console.log(
|
||||
"Error: was able to create more than 500 connections, iframe is probably isolated"
|
||||
);
|
||||
loadingDiv.innerText =
|
||||
"Error: was not able to block webrtc ERROR_C";
|
||||
} else {
|
||||
loadingDiv.innerHTML = "";
|
||||
iframe.style.display = 'block';
|
||||
iframe.src = href;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
len: () => {
|
||||
return connections.length;
|
||||
},
|
||||
});
|
||||
};
|
||||
const iframe = document.getElementById("frame");
|
||||
iframe.style.display = "block";
|
||||
|
||||
const params = new URLSearchParams(document.location.search);
|
||||
const internetAccess = (params.get("i") || "0") === "1";
|
||||
const href = params.get("href");
|
||||
|
||||
if (internetAccess) { // fill500 not needed, just load the frame
|
||||
fill500 = (href) => {
|
||||
loadingDiv.innerHTML = "";
|
||||
iframe.style.display = 'block';
|
||||
iframe.src = href;
|
||||
}
|
||||
if (href) {
|
||||
iframe.src = href;
|
||||
}
|
||||
|
||||
const thisUnchangable = fill500(href)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -540,6 +540,7 @@
|
||||
<string name="send_file_to">Send \"%1$s\" to…</string>
|
||||
<!-- title shown above a list contacts where one should be selected (eg. when a webxdc attempts to send a message to a chat) -->
|
||||
<string name="send_message_to">Send Message to…</string>
|
||||
<string name="webxdc_requires_modern_webview">Please update Android System WebView to open this app securely.</string>
|
||||
|
||||
<!-- punycode warning / labeled links -->
|
||||
<string name="puny_code_warning_header">Suspicious link detected</string>
|
||||
|
||||
Reference in New Issue
Block a user