mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-13 10:56:24 +00:00
Websockets on Tor android
This commit is contained in:
parent
02ac9e59dd
commit
016e3ee72d
@ -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'>
|
||||
|
@ -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
|
||||
|
@ -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';
|
||||
|
1
frontend/src/services/Native/index.d.ts
vendored
1
frontend/src/services/Native/index.d.ts
vendored
@ -28,6 +28,7 @@ export interface NativeWebViewMessageSystem {
|
||||
type:
|
||||
| 'init'
|
||||
| 'torStatus'
|
||||
| 'WsMessage'
|
||||
| 'copyToClipboardString'
|
||||
| 'setCookie'
|
||||
| 'deleteCookie'
|
||||
|
@ -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 }));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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;
|
@ -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 {
|
||||
|
@ -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();
|
||||
|
@ -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({});
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user