mirror of
https://github.com/RoboSats/robosats.git
synced 2025-09-13 00:56:22 +00:00
websockets
This commit is contained in:
@ -11,9 +11,15 @@ interface AndroidAppRobosats {
|
|||||||
generateRobohash: (uuid: string, initialString: string) => void;
|
generateRobohash: (uuid: string, initialString: string) => void;
|
||||||
copyToClipboard: (value: string) => void;
|
copyToClipboard: (value: string) => void;
|
||||||
getTorStatus: (uuid: string) => void;
|
getTorStatus: (uuid: string) => void;
|
||||||
|
openWS: (uuid: string, path: string) => void;
|
||||||
|
sendWsMessage: (uuid: string, path: string, message: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AndroidRobosats {
|
class AndroidRobosats {
|
||||||
|
private WSConnections: Record<string, (message?: string) => void> = {};
|
||||||
|
private WSError: Record<string, () => void> = {};
|
||||||
|
private WSClose: Record<string, () => void> = {};
|
||||||
|
|
||||||
private promises: Record<
|
private promises: Record<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
@ -51,6 +57,35 @@ class AndroidRobosats {
|
|||||||
console.warn(`No promise found for UUID: ${uuid}`);
|
console.warn(`No promise found for UUID: ${uuid}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public registerWSConnection: (
|
||||||
|
path: string,
|
||||||
|
onMesage: (message?: string) => void,
|
||||||
|
onClose: () => void,
|
||||||
|
onError: () => void,
|
||||||
|
) => void = (path, onMesage, onClose, onError) => {
|
||||||
|
this.WSConnections[path] = onMesage;
|
||||||
|
this.WSError[path] = onError;
|
||||||
|
this.WSClose[path] = onClose;
|
||||||
|
};
|
||||||
|
|
||||||
|
public removeWSConnection: (path: string) => void = (path) => {
|
||||||
|
delete this.WSConnections[path];
|
||||||
|
delete this.WSError[path];
|
||||||
|
delete this.WSClose[path];
|
||||||
|
};
|
||||||
|
|
||||||
|
public onWSMessage: (path: string, message: string) => void = (path, message) => {
|
||||||
|
this.WSConnections[path](message);
|
||||||
|
};
|
||||||
|
|
||||||
|
public onWsError: (path: string) => void = (path) => {
|
||||||
|
this.WSError[path]();
|
||||||
|
};
|
||||||
|
|
||||||
|
public onWsClose: (path: string) => void = (path) => {
|
||||||
|
this.WSClose[path]();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AndroidRobosats;
|
export default AndroidRobosats;
|
||||||
|
|||||||
@ -0,0 +1,73 @@
|
|||||||
|
import { WebsocketState, type WebsocketClient, type WebsocketConnection } from '..';
|
||||||
|
import WebsocketWebClient from '../WebsocketWebClient';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
class WebsocketConnectionAndroid implements WebsocketConnection {
|
||||||
|
constructor(path: string) {
|
||||||
|
this.path = path;
|
||||||
|
window.AndroidRobosats?.registerWSConnection(
|
||||||
|
path,
|
||||||
|
(message) => {
|
||||||
|
this.wsMessagePromises.forEach((f) => f({ data: message }));
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.wsClosePromises.forEach((f) => f());
|
||||||
|
},
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly path: string;
|
||||||
|
|
||||||
|
private readonly wsMessagePromises: Array<(message: object) => void> = [];
|
||||||
|
private readonly wsClosePromises: Array<() => void> = [];
|
||||||
|
|
||||||
|
public send: (message: string) => void = (message: string) => {
|
||||||
|
const uuid: string = uuidv4();
|
||||||
|
window.AndroidAppRobosats?.sendWsMessage(uuid, this.path, message);
|
||||||
|
};
|
||||||
|
|
||||||
|
public close: () => void = () => {
|
||||||
|
window.AndroidRobosats?.removeWSConnection(this.path);
|
||||||
|
};
|
||||||
|
|
||||||
|
public onMessage: (event: (message: object) => void) => void = (event) => {
|
||||||
|
this.wsMessagePromises.push(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
public onClose: (event: () => void) => void = (event) => {
|
||||||
|
this.wsClosePromises.push(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
public onError: (event: (error: object) => void) => void = (_event) => {
|
||||||
|
// Not implemented
|
||||||
|
};
|
||||||
|
|
||||||
|
public getReadyState: () => number = () => WebsocketState.OPEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebsocketAndroidClient implements WebsocketClient {
|
||||||
|
public useProxy = true;
|
||||||
|
|
||||||
|
private readonly webClient: WebsocketWebClient = new WebsocketWebClient();
|
||||||
|
|
||||||
|
public open: (path: string) => Promise<WebsocketConnection> = async (path) => {
|
||||||
|
if (!this.useProxy) return await this.webClient.open(path);
|
||||||
|
|
||||||
|
return new Promise<WebsocketConnectionAndroid>((resolve, reject) => {
|
||||||
|
const uuid: string = uuidv4();
|
||||||
|
window.AndroidAppRobosats?.openWS(uuid, path);
|
||||||
|
window.AndroidRobosats?.storePromise(
|
||||||
|
uuid,
|
||||||
|
() => {
|
||||||
|
resolve(new WebsocketConnectionAndroid(path));
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
reject(new Error('Failed to establish a websocket connection.'));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WebsocketAndroidClient;
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import WebsocketAndroidClient from './WebsocketAndroidClient';
|
||||||
import WebsocketNativeClient from './WebsocketNativeClient';
|
import WebsocketNativeClient from './WebsocketNativeClient';
|
||||||
import WebsocketWebClient from './WebsocketWebClient';
|
import WebsocketWebClient from './WebsocketWebClient';
|
||||||
|
|
||||||
@ -23,7 +24,11 @@ export interface WebsocketClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getWebsocketClient(): WebsocketClient {
|
function getWebsocketClient(): WebsocketClient {
|
||||||
if (window.navigator.userAgent.includes('robosats')) {
|
if (window.navigator.userAgent.includes('AndroidRobosats')) {
|
||||||
|
// If userAgent has "AndroidRobosats", we assume the app is running inside of the
|
||||||
|
// WebView of the Kotlin RoboSats Android app.
|
||||||
|
return new WebsocketAndroidClient();
|
||||||
|
} else if (window.navigator.userAgent.includes('robosats')) {
|
||||||
// If userAgent has "RoboSats", we assume the app is running inside of the
|
// If userAgent has "RoboSats", we assume the app is running inside of the
|
||||||
// react-native-web view of the RoboSats Android app.
|
// react-native-web view of the RoboSats Android app.
|
||||||
return new WebsocketNativeClient();
|
return new WebsocketNativeClient();
|
||||||
|
|||||||
@ -70,6 +70,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
|
implementation(libs.okhttp)
|
||||||
implementation(libs.kmp.tor)
|
implementation(libs.kmp.tor)
|
||||||
// Add the KMP Tor binary dependency (contains the native .so files)
|
// Add the KMP Tor binary dependency (contains the native .so files)
|
||||||
implementation(libs.kmp.tor.binary)
|
implementation(libs.kmp.tor.binary)
|
||||||
|
|||||||
@ -203,7 +203,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val proxyPort = System.getProperty("http.proxyPort")?.toIntOrNull()
|
val proxyPort = System.getProperty("http.proxyPort")?.toIntOrNull()
|
||||||
?: throw SecurityException("Missing or invalid proxy port in system properties")
|
?: throw SecurityException("Missing or invalid proxy port in system properties")
|
||||||
|
|
||||||
Log.d("TorProxy", "Using proxy settings: $proxyHost:$proxyPort")
|
Log.d("WebViewProxy", "Using proxy settings: $proxyHost:$proxyPort")
|
||||||
|
|
||||||
// Success - now configure WebViewClient and load URL on UI thread
|
// Success - now configure WebViewClient and load URL on UI thread
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
@ -243,7 +243,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val urlString = request.url.toString()
|
val urlString = request.url.toString()
|
||||||
Log.d("TorProxy", "Intercepting request: $urlString")
|
Log.d("WebViewProxy", "Intercepting request: $urlString")
|
||||||
|
|
||||||
// Block all external requests that aren't to .onion domains or local files
|
// Block all external requests that aren't to .onion domains or local files
|
||||||
if (!isAllowedRequest(urlString)) {
|
if (!isAllowedRequest(urlString)) {
|
||||||
@ -274,7 +274,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (isOnionDomain) {
|
if (isOnionDomain) {
|
||||||
Log.d("TorProxy", "Handling .onion domain with SOCKS proxy: $urlString")
|
Log.d("WebViewProxy", "Handling .onion domain with SOCKS proxy: $urlString")
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's a local file, return it directly
|
// If it's a local file, return it directly
|
||||||
@ -345,7 +345,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val mimeType = connection.contentType ?: "text/plain"
|
val mimeType = connection.contentType ?: "text/plain"
|
||||||
val encoding = connection.contentEncoding ?: "UTF-8"
|
val encoding = connection.contentEncoding ?: "UTF-8"
|
||||||
|
|
||||||
Log.d("TorProxy", "Successfully proxied request to $url (HTTP ${connection.responseCode})")
|
Log.d("WebViewProxy", "Successfully proxied request to $url (HTTP ${connection.responseCode})")
|
||||||
|
|
||||||
// Get the correct input stream based on response code
|
// Get the correct input stream based on response code
|
||||||
val inputStream = if (responseCode >= 400) {
|
val inputStream = if (responseCode >= 400) {
|
||||||
@ -409,7 +409,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
} else {
|
} else {
|
||||||
// For non-HTTP connections (rare)
|
// For non-HTTP connections (rare)
|
||||||
val inputStream = connection.getInputStream()
|
val inputStream = connection.getInputStream()
|
||||||
Log.d("TorProxy", "Successfully established non-HTTP connection to $url")
|
Log.d("WebViewProxy", "Successfully established non-HTTP connection to $url")
|
||||||
return WebResourceResponse(
|
return WebResourceResponse(
|
||||||
"application/octet-stream",
|
"application/octet-stream",
|
||||||
"UTF-8",
|
"UTF-8",
|
||||||
@ -417,7 +417,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("TorProxy", "Error proxying request: $urlString - ${e.message}", e)
|
Log.e("WebViewProxy", "Error proxying request: $urlString - ${e.message}", e)
|
||||||
|
|
||||||
// For security, block the request rather than falling back to system handling
|
// For security, block the request rather than falling back to system handling
|
||||||
return WebResourceResponse("text/plain", "UTF-8", null)
|
return WebResourceResponse("text/plain", "UTF-8", null)
|
||||||
|
|||||||
@ -1,15 +1,23 @@
|
|||||||
package com.robosats
|
package com.robosats
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.webkit.JavascriptInterface
|
import android.webkit.JavascriptInterface
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.robosats.tor.TorKmpManager.getTorKmpObject
|
import com.robosats.tor.TorKmpManager.getTorKmpObject
|
||||||
import android.annotation.SuppressLint
|
import okhttp3.OkHttpClient
|
||||||
import android.text.TextUtils
|
import okhttp3.OkHttpClient.Builder
|
||||||
import java.util.UUID
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.WebSocket
|
||||||
|
import okhttp3.WebSocketListener
|
||||||
|
import okio.ByteString
|
||||||
|
import java.util.Objects
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
import okhttp3.Request.Builder as RequestBuilder
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a secure bridge between JavaScript and native Android code.
|
* Provides a secure bridge between JavaScript and native Android code.
|
||||||
@ -20,6 +28,7 @@ import java.util.regex.Pattern
|
|||||||
class WebAppInterface(private val context: Context, private val webView: WebView) {
|
class WebAppInterface(private val context: Context, private val webView: WebView) {
|
||||||
private val TAG = "WebAppInterface"
|
private val TAG = "WebAppInterface"
|
||||||
private val roboIdentities = RoboIdentities()
|
private val roboIdentities = RoboIdentities()
|
||||||
|
private val webSockets: MutableMap<String?, WebSocket?> = HashMap<String?, WebSocket?>()
|
||||||
|
|
||||||
// Security patterns for input validation
|
// Security patterns for input validation
|
||||||
private val UUID_PATTERN = Pattern.compile("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", Pattern.CASE_INSENSITIVE)
|
private val UUID_PATTERN = Pattern.compile("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", Pattern.CASE_INSENSITIVE)
|
||||||
@ -40,61 +49,6 @@ class WebAppInterface(private val context: Context, private val webView: WebView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates that a string contains only safe characters and is within length limits
|
|
||||||
*/
|
|
||||||
private fun isValidInput(input: String?, maxLength: Int = MAX_INPUT_LENGTH): Boolean {
|
|
||||||
if (input == null || input.isEmpty() || input.length > maxLength) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return SAFE_STRING_PATTERN.matcher(input).matches()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates that a string is a valid UUID
|
|
||||||
*/
|
|
||||||
private fun isValidUuid(uuid: String?): Boolean {
|
|
||||||
if (uuid == null || uuid.isEmpty()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return UUID_PATTERN.matcher(uuid).matches()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Safely evaluates JavaScript, escaping any potentially dangerous characters
|
|
||||||
*/
|
|
||||||
private fun safeEvaluateJavascript(script: String) {
|
|
||||||
// Remove any null bytes which could be used to trick the JS interpreter
|
|
||||||
val sanitizedScript = script.replace("\u0000", "")
|
|
||||||
|
|
||||||
webView.post {
|
|
||||||
try {
|
|
||||||
webView.evaluateJavascript(sanitizedScript, null)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Error evaluating JavaScript: $e")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Safely encodes a string for use in JavaScript
|
|
||||||
*/
|
|
||||||
private fun encodeForJavaScript(input: String): String {
|
|
||||||
return input.replace("\\", "\\\\")
|
|
||||||
.replace("'", "\\'")
|
|
||||||
.replace("\"", "\\\"")
|
|
||||||
.replace("\n", "\\n")
|
|
||||||
.replace("\r", "\\r")
|
|
||||||
.replace("<", "\\u003C")
|
|
||||||
.replace(">", "\\u003E")
|
|
||||||
.replace("&", "\\u0026")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a robot name from the given message
|
|
||||||
* @param uuid A unique identifier for the JavaScript Promise
|
|
||||||
* @param message The input message to generate a robot name from
|
|
||||||
*/
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun generateRoboname(uuid: String, message: String) {
|
fun generateRoboname(uuid: String, message: String) {
|
||||||
// Validate inputs before processing
|
// Validate inputs before processing
|
||||||
@ -119,37 +73,6 @@ class WebAppInterface(private val context: Context, private val webView: WebView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to safely resolve a JavaScript Promise
|
|
||||||
*/
|
|
||||||
private fun resolvePromise(uuid: String, result: String) {
|
|
||||||
if (!isValidUuid(uuid)) {
|
|
||||||
Log.e(TAG, "Invalid UUID for promise resolution: $uuid")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val encodedResult = encodeForJavaScript(result)
|
|
||||||
safeEvaluateJavascript("javascript:window.AndroidRobosats.onResolvePromise('$uuid', '$encodedResult')")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to safely reject a JavaScript Promise
|
|
||||||
*/
|
|
||||||
private fun rejectPromise(uuid: String, errorMessage: String) {
|
|
||||||
if (!isValidUuid(uuid)) {
|
|
||||||
Log.e(TAG, "Invalid UUID for promise rejection: $uuid")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val encodedError = encodeForJavaScript(errorMessage)
|
|
||||||
safeEvaluateJavascript("javascript:window.AndroidRobosats.onRejectPromise('$uuid', '$encodedError')")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a robot hash from the given message
|
|
||||||
* @param uuid A unique identifier for the JavaScript Promise
|
|
||||||
* @param message The input message to generate a robot hash from
|
|
||||||
*/
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun generateRobohash(uuid: String, message: String) {
|
fun generateRobohash(uuid: String, message: String) {
|
||||||
// Validate inputs before processing
|
// Validate inputs before processing
|
||||||
@ -174,10 +97,6 @@ class WebAppInterface(private val context: Context, private val webView: WebView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy text to the clipboard
|
|
||||||
* @param message The text to copy to the clipboard
|
|
||||||
*/
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun copyToClipboard(message: String) {
|
fun copyToClipboard(message: String) {
|
||||||
// Validate input
|
// Validate input
|
||||||
@ -211,10 +130,6 @@ class WebAppInterface(private val context: Context, private val webView: WebView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current Tor connection status
|
|
||||||
* @param uuid A unique identifier for the JavaScript Promise
|
|
||||||
*/
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun getTorStatus(uuid: String) {
|
fun getTorStatus(uuid: String) {
|
||||||
// Validate UUID
|
// Validate UUID
|
||||||
@ -234,4 +149,165 @@ class WebAppInterface(private val context: Context, private val webView: WebView
|
|||||||
rejectPromise(uuid, "Error retrieving Tor status")
|
rejectPromise(uuid, "Error retrieving Tor status")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun openWS(uuid: String, path: String) {
|
||||||
|
// Validate UUID
|
||||||
|
if (!isValidUuid(uuid)) {
|
||||||
|
Log.e(TAG, "Invalid UUID for getTorStatus: $uuid")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "WebSocket opening: " + path)
|
||||||
|
val client: OkHttpClient = Builder()
|
||||||
|
.connectTimeout(60, TimeUnit.SECONDS) // Set connection timeout
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS) // Set read timeout
|
||||||
|
.proxy(getTorKmpObject().proxy)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
// Create a request for the WebSocket connection
|
||||||
|
val request: Request = RequestBuilder()
|
||||||
|
.url(path) // Replace with your WebSocket URL
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
// Create a WebSocket listener
|
||||||
|
val listener: WebSocketListener = object : WebSocketListener() {
|
||||||
|
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||||
|
Log.d(TAG, "WebSocket opened: " + response.message)
|
||||||
|
resolvePromise(uuid, "true")
|
||||||
|
synchronized(webSockets) {
|
||||||
|
webSockets.put(
|
||||||
|
path,
|
||||||
|
webSocket
|
||||||
|
) // Store the WebSocket instance with its URL
|
||||||
|
resolvePromise(uuid, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||||
|
onWsMessage(path, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
|
||||||
|
onWsMessage(path, bytes.hex())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
||||||
|
Log.d(TAG, "WebSocket closing: " + reason)
|
||||||
|
onWsClose(path)
|
||||||
|
synchronized(webSockets) {
|
||||||
|
webSockets.remove(path) // Remove the WebSocket instance by URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(
|
||||||
|
webSocket: WebSocket,
|
||||||
|
t: Throwable,
|
||||||
|
response: Response?
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "WebSocket error: " + t.message)
|
||||||
|
onWsError(path)
|
||||||
|
rejectPromise(uuid, "false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.newWebSocket(request, listener)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error connecting to WebSocket", e)
|
||||||
|
rejectPromise(uuid, "Error connecting Tor WebSocket")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun sendWsMessage(uuid: String, path: String, message: String) {
|
||||||
|
// Validate UUID
|
||||||
|
if (!isValidUuid(uuid)) {
|
||||||
|
Log.e(TAG, "Invalid UUID for getTorStatus: $uuid")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val websocket = webSockets.get(path)
|
||||||
|
if (websocket != null) {
|
||||||
|
websocket.send(message)
|
||||||
|
resolvePromise(uuid, "true")
|
||||||
|
} else {
|
||||||
|
rejectPromise(uuid, "Error sending WebSocket message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onWsMessage(path: String?, message: String?) {
|
||||||
|
safeEvaluateJavascript("javascript:window.AndroidRobosats.onWSMessage('$path', '$message')")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onWsError(path: String?) {
|
||||||
|
Log.d(TAG, "WebSocket error: $path")
|
||||||
|
safeEvaluateJavascript("javascript:window.AndroidRobosats.onWsError('$path')")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onWsClose(path: String?) {
|
||||||
|
Log.d(TAG, "WebSocket close: $path")
|
||||||
|
safeEvaluateJavascript("javascript:window.AndroidRobosats.onWsClose('$path')")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolvePromise(uuid: String, result: String) {
|
||||||
|
if (!isValidUuid(uuid)) {
|
||||||
|
Log.e(TAG, "Invalid UUID for promise resolution: $uuid")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val encodedResult = encodeForJavaScript(result)
|
||||||
|
safeEvaluateJavascript("javascript:window.AndroidRobosats.onResolvePromise('$uuid', '$encodedResult')")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rejectPromise(uuid: String, errorMessage: String) {
|
||||||
|
if (!isValidUuid(uuid)) {
|
||||||
|
Log.e(TAG, "Invalid UUID for promise rejection: $uuid")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val encodedError = encodeForJavaScript(errorMessage)
|
||||||
|
safeEvaluateJavascript("javascript:window.AndroidRobosats.onRejectPromise('$uuid', '$encodedError')")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValidInput(input: String?, maxLength: Int = MAX_INPUT_LENGTH): Boolean {
|
||||||
|
if (input == null || input.isEmpty() || input.length > maxLength) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return SAFE_STRING_PATTERN.matcher(input).matches()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValidUuid(uuid: String?): Boolean {
|
||||||
|
if (uuid == null || uuid.isEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return UUID_PATTERN.matcher(uuid).matches()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun safeEvaluateJavascript(script: String) {
|
||||||
|
// Remove any null bytes which could be used to trick the JS interpreter
|
||||||
|
val sanitizedScript = script.replace("\u0000", "")
|
||||||
|
|
||||||
|
webView.post {
|
||||||
|
try {
|
||||||
|
webView.evaluateJavascript(sanitizedScript, null)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error evaluating JavaScript: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun encodeForJavaScript(input: String): String {
|
||||||
|
return input.replace("\\", "\\\\")
|
||||||
|
.replace("'", "\\'")
|
||||||
|
.replace("\"", "\\\"")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\r", "\\r")
|
||||||
|
.replace("<", "\\u003C")
|
||||||
|
.replace(">", "\\u003E")
|
||||||
|
.replace("&", "\\u0026")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ activity = "1.10.1"
|
|||||||
constraintlayout = "2.2.1"
|
constraintlayout = "2.2.1"
|
||||||
kmpTor= "4.8.10-0-1.4.5"
|
kmpTor= "4.8.10-0-1.4.5"
|
||||||
kmpTorBinary= "4.8.10-0"
|
kmpTorBinary= "4.8.10-0"
|
||||||
|
okhttp = "5.0.0-alpha.14"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@ -23,6 +24,7 @@ androidx-activity = { group = "androidx.activity", name = "activity", version.re
|
|||||||
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||||
kmp-tor = { group = "io.matthewnelson.kotlin-components", name = "kmp-tor", version.ref = "kmpTor" }
|
kmp-tor = { group = "io.matthewnelson.kotlin-components", name = "kmp-tor", version.ref = "kmpTor" }
|
||||||
kmp-tor-binary = { group = "io.matthewnelson.kotlin-components", name = "kmp-tor-binary-extract-android", version.ref = "kmpTorBinary" }
|
kmp-tor-binary = { group = "io.matthewnelson.kotlin-components", name = "kmp-tor-binary-extract-android", version.ref = "kmpTorBinary" }
|
||||||
|
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Reference in New Issue
Block a user