Websockets on Tor android

This commit is contained in:
koalasat 2024-11-12 15:55:30 +01:00
parent 02ac9e59dd
commit 016e3ee72d
No known key found for this signature in database
GPG Key ID: 2F7F61C6146AB157
13 changed files with 283 additions and 11 deletions

View File

@ -29,6 +29,7 @@ import {
import { systemClient } from '../../services/System';
import { TorIcon } from '../Icons';
import { apiClient } from '../../services/api';
import { websocketClient } from '../../services/Websocket';
interface SettingsFormProps {
dense?: boolean;
@ -198,7 +199,6 @@ const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
</ListItemIcon>
<ToggleButtonGroup
sx={{ width: '100%' }}
disabled={client === 'mobile'}
exclusive={true}
value={settings.connection}
onChange={(_e, connection) => {
@ -249,6 +249,7 @@ const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
setSettings({ ...settings, useProxy });
systemClient.setItem('settings_use_proxy', String(useProxy));
apiClient.useProxy = useProxy;
websocketClient.useProxy = useProxy;
}}
>
<ToggleButton value={true} color='primary'>

View File

@ -35,7 +35,7 @@ const EncryptedChat: React.FC<Props> = ({
messages,
status,
}: Props): JSX.Element => {
const [turtleMode, setTurtleMode] = useState<boolean>(window.ReactNativeWebView !== undefined);
const [turtleMode, setTurtleMode] = useState<boolean>(false);
return turtleMode ? (
<EncryptedTurtleChat

View File

@ -1,5 +1,6 @@
import i18n from '../i18n/Web';
import { systemClient } from '../services/System';
import { websocketClient } from '../services/Websocket';
import { apiClient } from '../services/api';
import { getHost } from '../utils';
@ -57,6 +58,7 @@ class BaseSettings {
const useProxy = systemClient.getItem('settings_use_proxy');
this.useProxy = client === 'mobile' && useProxy !== 'false';
apiClient.useProxy = this.useProxy;
websocketClient.useProxy = this.useProxy;
}
public frontend: 'basic' | 'pro' = 'basic';

View File

@ -28,6 +28,7 @@ export interface NativeWebViewMessageSystem {
type:
| 'init'
| 'torStatus'
| 'WsMessage'
| 'copyToClipboardString'
| 'setCookie'
| 'deleteCookie'

View File

@ -45,8 +45,8 @@ class NativeRobosats {
if (message.key !== undefined) {
this.cookies[message.key] = String(message.detail);
}
} else if (message.type === 'navigateToPage') {
window.dispatchEvent(new CustomEvent('navigateToPage', { detail: message?.detail }));
} else {
window.dispatchEvent(new CustomEvent(message.type, { detail: message?.detail }));
}
};

View File

@ -0,0 +1,87 @@
import { WebsocketState, type WebsocketClient, type WebsocketConnection } from '..';
import WebsocketWebClient from '../WebsocketWebClient';
class WebsocketConnectionNative implements WebsocketConnection {
constructor(path: string) {
this.path = path;
window.addEventListener('wsMessage', (event) => {
const path: string = event?.detail?.path;
const message: string = event?.detail?.message;
if (path && message && path === this.path) {
this.wsMessagePromises.forEach((fn) => fn({ data: message }));
}
});
}
private readonly path: string;
private readonly wsMessagePromises: ((message: any) => void)[] = [];
private readonly wsClosePromises: (() => void)[] = [];
public send: (message: string) => void = (message: string) => {
window.NativeRobosats?.postMessage({
category: 'ws',
type: 'send',
path: this.path,
message,
});
};
public close: () => void = () => {
window.NativeRobosats?.postMessage({
category: 'ws',
type: 'close',
path: this.path,
}).then((response) => {
if (response.connection) {
this.wsClosePromises.forEach((fn) => fn());
} else {
new Error('Failed to close websocket connection.');
}
});
};
public onMessage: (event: (message: any) => void) => void = (event) => {
this.wsMessagePromises.push(event);
};
public onClose: (event: () => void) => void = (event) => {
this.wsClosePromises.push(event);
};
public onError: (event: (error: any) => void) => void = (_event) => {
// Not implemented
};
public getReadyState: () => number = () => WebsocketState.OPEN;
}
class WebsocketNativeClient 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 await new Promise<WebsocketConnection>((resolve, reject) => {
window.NativeRobosats?.postMessage({
category: 'ws',
type: 'open',
path,
})
.then((response) => {
if (response.connection) {
resolve(new WebsocketConnectionNative(path));
} else {
reject(new Error('Failed to establish a websocket connection.'));
}
})
.catch(() => {
reject(new Error('Failed to establish a websocket connection.'));
});
});
};
}
export default WebsocketNativeClient;

View File

@ -39,6 +39,8 @@ class WebsocketConnectionWeb implements WebsocketConnection {
}
class WebsocketWebClient implements WebsocketClient {
public useProxy = false;
public open: (path: string) => Promise<WebsocketConnection> = async (path) => {
return await new Promise<WebsocketConnection>((resolve, reject) => {
try {

View File

@ -1,3 +1,4 @@
import WebsocketNativeClient from './WebsocketNativeClient';
import WebsocketWebClient from './WebsocketWebClient';
export const WebsocketState = {
@ -17,7 +18,19 @@ export interface WebsocketConnection {
}
export interface WebsocketClient {
useProxy: boolean;
open: (path: string) => Promise<WebsocketConnection>;
}
export const websocketClient: WebsocketClient = new WebsocketWebClient();
function getWebsocketClient(): WebsocketClient {
if (window.navigator.userAgent.includes('robosats')) {
// If userAgent has "RoboSats", we assume the app is running inside of the
// react-native-web view of the RoboSats Android app.
return new WebsocketNativeClient();
} else {
// Otherwise, we assume the app is running in a web browser.
return new WebsocketWebClient();
}
}
export const websocketClient: WebsocketClient = getWebsocketClient();

View File

@ -7,8 +7,6 @@ class ApiNativeClient implements ApiClient {
private readonly webClient: ApiClient = new ApiWebClient();
private readonly assetsPromises = new Map<string, Promise<string | undefined>>();
private readonly getHeaders: (auth?: Auth) => HeadersInit = (auth) => {
let headers = {
'Content-Type': 'application/json',
@ -44,9 +42,9 @@ class ApiNativeClient implements ApiClient {
};
public put: (baseUrl: string, path: string, body: object) => Promise<object | undefined> = async (
baseUrl,
path,
body,
_baseUrl,
_path,
_body,
) => {
return await new Promise<object>((resolve, _reject) => {
resolve({});

View File

@ -41,6 +41,13 @@ const App = () => {
detail: payload.torStatus,
});
});
DeviceEventEmitter.addListener('WsMessage', (payload) => {
injectMessage({
category: 'ws',
type: 'wsMessage',
detail: payload,
});
});
}, []);
useEffect(() => {
@ -123,7 +130,30 @@ const App = () => {
const onMessage = async (event: WebViewMessageEvent) => {
const data = JSON.parse(event.nativeEvent.data);
if (data.category === 'http') {
if (data.category === 'ws') {
TorModule.getTorStatus();
if (data.type === 'open') {
torClient
.wsOpen(data.path)
.then((connection: boolean) => {
injectMessageResolve(data.id, { connection });
})
.catch((e) => onCatch(data.id, e))
.finally(TorModule.getTorStatus);
} else if (data.type === 'send') {
torClient
.wsSend(data.path, data.message)
.catch((e) => onCatch(data.id, e))
.finally(TorModule.getTorStatus);
} else if (data.type === 'close') {
torClient
.wsClose(data.path)
.then((connection: boolean) => {
injectMessageResolve(data.id, { connection });
})
.finally(TorModule.getTorStatus);
}
} else if (data.category === 'http') {
TorModule.getTorStatus();
if (data.type === 'get') {
torClient

View File

@ -19,6 +19,8 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@ -30,10 +32,15 @@ import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
public class TorModule extends ReactContextBaseJavaModule {
private ReactApplicationContext context;
private static final Map<String, WebSocket> webSockets = new HashMap<>();
public TorModule(ReactApplicationContext reactContext) {
context = reactContext;
TorKmp torKmpManager = new TorKmp((Application) context.getApplicationContext());
@ -45,6 +52,81 @@ public class TorModule extends ReactContextBaseJavaModule {
return "TorModule";
}
@ReactMethod
public void sendWsSend(String path, String message, final Promise promise) {
if (webSockets.get(path) != null) {
Objects.requireNonNull(webSockets.get(path)).send(message);
promise.resolve(true);
} else {
promise.resolve(false);
}
}
@ReactMethod
public void sendWsClose(String path, final Promise promise) {
if (webSockets.get(path) != null) {
Objects.requireNonNull(webSockets.get(path)).close(1000, "Closing connection");
promise.resolve(true);
} else {
promise.resolve(false);
}
}
@ReactMethod
public void sendWsOpen(String path, final Promise promise) {
Log.d("Tormodule", "WebSocket opening: " + path);
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS) // Set connection timeout
.readTimeout(30, TimeUnit.SECONDS) // Set read timeout
.proxy(TorKmpManager.INSTANCE.getTorKmpObject().getProxy())
.build();
// Create a request for the WebSocket connection
Request request = new Request.Builder()
.url(path) // Replace with your WebSocket URL
.build();
// Create a WebSocket listener
WebSocketListener listener = new WebSocketListener() {
@Override
public void onOpen(@NonNull WebSocket webSocket, Response response) {
Log.d("Tormodule", "WebSocket opened: " + response.message());
promise.resolve(true);
synchronized (webSockets) {
webSockets.put(path, webSocket); // Store the WebSocket instance with its URL
}
}
@Override
public void onMessage(@NonNull WebSocket webSocket, @NonNull String text) {
Log.d("Tormodule", "WebSocket Message received: " + text);
onWsMessage(path, text);
}
@Override
public void onMessage(@NonNull WebSocket webSocket, ByteString bytes) {
Log.d("Tormodule", "WebSocket Message received: " + bytes.hex());
onWsMessage(path, bytes.hex());
}
@Override
public void onClosing(@NonNull WebSocket webSocket, int code, @NonNull String reason) {
Log.d("Tormodule", "WebSocket closing: " + reason);
synchronized (webSockets) {
webSockets.remove(path); // Remove the WebSocket instance by URL
}
}
@Override
public void onFailure(@NonNull WebSocket webSocket, Throwable t, Response response) {
Log.d("Tormodule", "WebSocket error: " + t.getMessage());
promise.resolve(false);
}
};
client.newWebSocket(request, listener);
}
@ReactMethod
public void sendRequest(String action, String url, String headers, String body, final Promise promise) throws JSONException, UninitializedPropertyAccessException {
OkHttpClient client = new OkHttpClient.Builder()
@ -160,4 +242,21 @@ public class TorModule extends ReactContextBaseJavaModule {
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("TorNewIdentity", payload);
}
private void onWsMessage(String path, String message) {
WritableMap payload = Arguments.createMap();
payload.putString("message", message);
payload.putString("path", path);
context
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("WsMessage", payload);
}
private void onWsError(String path) {
WritableMap payload = Arguments.createMap();
payload.putString("path", path);
context
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("WsError", payload);
}
}

View File

@ -5,6 +5,9 @@ interface TorModuleInterface {
start: () => void;
restart: () => void;
getTorStatus: () => void;
sendWsOpen: (path: string) => Promise<boolean>;
sendWsClose: (path: string) => Promise<boolean>;
sendWsSend: (path: string, message: string) => Promise<boolean>;
sendRequest: (action: string, url: string, headers: string, body: string) => Promise<string>;
}

View File

@ -52,6 +52,42 @@ class TorClient {
}
});
};
public wsOpen: (path: string) => Promise<boolean> = async (path) => {
return await new Promise<boolean>((resolve, reject) => {
try {
TorModule.sendWsOpen(path).then((response) => {
resolve(response);
});
} catch (error) {
reject(error);
}
});
};
public wsClose: (path: string) => Promise<boolean> = async (path) => {
return await new Promise<boolean>((resolve, reject) => {
try {
TorModule.sendWsClose(path).then((response) => {
resolve(response);
});
} catch (error) {
reject(error);
}
});
};
public wsSend: (path: string, message: string) => Promise<boolean> = async (path, message) => {
return await new Promise<boolean>((resolve, reject) => {
try {
TorModule.sendWsSend(path, message).then((response) => {
resolve(response);
});
} catch (error) {
reject(error);
}
});
};
}
export default TorClient;