mirror of
https://github.com/RoboSats/robosats.git
synced 2025-07-17 08:13:19 +00:00
Federation Android App and new Tor engine
This commit is contained in:
@ -44,7 +44,7 @@ const RobotPage = (): JSX.Element => {
|
|||||||
const token = urlToken ?? garage.currentSlot;
|
const token = urlToken ?? garage.currentSlot;
|
||||||
if (token !== undefined && token !== null && page === 'robot') {
|
if (token !== undefined && token !== null && page === 'robot') {
|
||||||
setInputToken(token);
|
setInputToken(token);
|
||||||
if (window.NativeRobosats === undefined || torStatus === '"Done"') {
|
if (window.NativeRobosats === undefined || torStatus === 'ON') {
|
||||||
getGenerateRobot(token);
|
getGenerateRobot(token);
|
||||||
setView('profile');
|
setView('profile');
|
||||||
}
|
}
|
||||||
@ -83,7 +83,7 @@ const RobotPage = (): JSX.Element => {
|
|||||||
garage.deleteSlot();
|
garage.deleteSlot();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!(window.NativeRobosats === undefined) && !(torStatus === 'DONE' || torStatus === '"Done"')) {
|
if (!(window.NativeRobosats === undefined) && !(torStatus === 'ON')) {
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
elevation={12}
|
elevation={12}
|
||||||
|
@ -26,9 +26,10 @@ class RoboGenerator {
|
|||||||
const numCores = 8;
|
const numCores = 8;
|
||||||
|
|
||||||
for (let i = 0; i < numCores; i++) {
|
for (let i = 0; i < numCores; i++) {
|
||||||
const worker = new Worker(new URL('./robohash.worker.ts', import.meta.url));
|
// FIXME
|
||||||
worker.onmessage = this.assignTasksToWorkers.bind(this);
|
// const worker = new Worker(new URL('./robohash.worker.ts', import.meta.url));
|
||||||
this.workers.push({ worker, busy: false });
|
// worker.onmessage = this.assignTasksToWorkers.bind(this);
|
||||||
|
// this.workers.push({ worker, busy: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,6 +82,8 @@ class RoboGenerator {
|
|||||||
hash,
|
hash,
|
||||||
size,
|
size,
|
||||||
) => {
|
) => {
|
||||||
|
// FIXME
|
||||||
|
return '';
|
||||||
const cacheKey = `${size}px;${hash}`;
|
const cacheKey = `${size}px;${hash}`;
|
||||||
if (this.assetsCache[cacheKey]) {
|
if (this.assetsCache[cacheKey]) {
|
||||||
return this.assetsCache[cacheKey];
|
return this.assetsCache[cacheKey];
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import { async_generate_robohash } from 'robo-identities-wasm';
|
// FIXME
|
||||||
|
// import { async_generate_robohash } from 'robo-identities-wasm';
|
||||||
|
|
||||||
// Listen for messages from the main thread
|
// // Listen for messages from the main thread
|
||||||
self.addEventListener('message', (event) => {
|
// self.addEventListener('message', (event) => {
|
||||||
void (async () => {
|
// void (async () => {
|
||||||
const { hash, size, cacheKey } = event.data;
|
// const { hash, size, cacheKey } = event.data;
|
||||||
|
|
||||||
// Generate the image using async_image_base
|
// // Generate the image using async_image_base
|
||||||
const t0 = performance.now();
|
// const t0 = performance.now();
|
||||||
const avatarB64: string = await async_generate_robohash(hash, size === 'small' ? 80 : 256);
|
// const avatarB64: string = await async_generate_robohash(hash, size === 'small' ? 80 : 256);
|
||||||
const imageUrl = `data:image/png;base64,${avatarB64}`;
|
// const imageUrl = `data:image/png;base64,${avatarB64}`;
|
||||||
const t1 = performance.now();
|
// const t1 = performance.now();
|
||||||
console.log(`Avatar generated in: ${t1 - t0} ms`);
|
// console.log(`Avatar generated in: ${t1 - t0} ms`);
|
||||||
// Send the result back to the main thread
|
// // Send the result back to the main thread
|
||||||
self.postMessage({ cacheKey, imageUrl });
|
// self.postMessage({ cacheKey, imageUrl });
|
||||||
})();
|
// })();
|
||||||
});
|
// });
|
||||||
|
@ -95,7 +95,6 @@ const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) =
|
|||||||
(signedInvoice) => {
|
(signedInvoice) => {
|
||||||
console.log('Signed message:', signedInvoice);
|
console.log('Signed message:', signedInvoice);
|
||||||
void coordinator.fetchReward(signedInvoice, garage, slot?.token).then((data) => {
|
void coordinator.fetchReward(signedInvoice, garage, slot?.token).then((data) => {
|
||||||
console.log(data);
|
|
||||||
setBadInvoice(data.bad_invoice ?? '');
|
setBadInvoice(data.bad_invoice ?? '');
|
||||||
setShowRewardsSpinner(false);
|
setShowRewardsSpinner(false);
|
||||||
setWithdrawn(data.successful_withdrawal);
|
setWithdrawn(data.successful_withdrawal);
|
||||||
|
@ -62,7 +62,7 @@ const TorConnectionBadge = (): JSX.Element => {
|
|||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (torStatus === 'NOTINIT') {
|
if (torStatus === 'OFF' || torStatus === 'STOPPING') {
|
||||||
return (
|
return (
|
||||||
<TorIndicator
|
<TorIndicator
|
||||||
color='primary'
|
color='primary'
|
||||||
@ -80,7 +80,7 @@ const TorConnectionBadge = (): JSX.Element => {
|
|||||||
title={t('Connecting to TOR network')}
|
title={t('Connecting to TOR network')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (torStatus === '"Done"' || torStatus === 'DONE') {
|
} else if (torStatus === 'ON') {
|
||||||
return <TorIndicator color='success' progress={false} title={t('Connected to TOR network')} />;
|
return <TorIndicator color='success' progress={false} title={t('Connected to TOR network')} />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
|
@ -62,7 +62,7 @@ const TorConnectionBadge = (): JSX.Element => {
|
|||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (torStatus === 'NOTINIT') {
|
if (torStatus === 'OFF' || torStatus === 'STOPING') {
|
||||||
return (
|
return (
|
||||||
<TorIndicator
|
<TorIndicator
|
||||||
color='primary'
|
color='primary'
|
||||||
@ -80,7 +80,7 @@ const TorConnectionBadge = (): JSX.Element => {
|
|||||||
title={t('Connecting to TOR network')}
|
title={t('Connecting to TOR network')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (torStatus === '"Done"' || torStatus === 'DONE') {
|
} else if (torStatus === 'ON') {
|
||||||
return <TorIndicator color='success' progress={false} title={t('Connected to TOR network')} />;
|
return <TorIndicator color='success' progress={false} title={t('Connected to TOR network')} />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
|
@ -34,7 +34,7 @@ export interface SlideDirection {
|
|||||||
out: 'left' | 'right' | undefined;
|
out: 'left' | 'right' | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TorStatus = 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE';
|
export type TorStatus = 'ON' | 'STARTING' | 'STOPPING' | 'OFF';
|
||||||
|
|
||||||
export const isNativeRoboSats = !(window.NativeRobosats === undefined);
|
export const isNativeRoboSats = !(window.NativeRobosats === undefined);
|
||||||
|
|
||||||
@ -139,7 +139,7 @@ export interface UseAppStoreType {
|
|||||||
|
|
||||||
export const initialAppContext: UseAppStoreType = {
|
export const initialAppContext: UseAppStoreType = {
|
||||||
theme: undefined,
|
theme: undefined,
|
||||||
torStatus: 'NOTINIT',
|
torStatus: 'STARTING',
|
||||||
settings: new Settings(),
|
settings: new Settings(),
|
||||||
setSettings: () => {},
|
setSettings: () => {},
|
||||||
page: entryPage,
|
page: entryPage,
|
||||||
@ -209,7 +209,7 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E
|
|||||||
() => {
|
() => {
|
||||||
setTorStatus(event?.detail);
|
setTorStatus(event?.detail);
|
||||||
},
|
},
|
||||||
event?.detail === '"Done"' ? 5000 : 0,
|
event?.detail === 'ON' ? 5000 : 0,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -15,6 +15,7 @@ import { federationLottery } from '../utils';
|
|||||||
|
|
||||||
import { AppContext, type UseAppStoreType } from './AppContext';
|
import { AppContext, type UseAppStoreType } from './AppContext';
|
||||||
import { GarageContext, type UseGarageStoreType } from './GarageContext';
|
import { GarageContext, type UseGarageStoreType } from './GarageContext';
|
||||||
|
import NativeRobosats from '../services/Native';
|
||||||
|
|
||||||
// Refresh delays (ms) according to Order status
|
// Refresh delays (ms) according to Order status
|
||||||
const defaultDelay = 5000;
|
const defaultDelay = 5000;
|
||||||
@ -105,15 +106,17 @@ export const FederationContextProvider = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// On bitcoin network change we reset book, limits and federation info and fetch everything again
|
// On bitcoin network change we reset book, limits and federation info and fetch everything again
|
||||||
const newFed = initialFederationContext.federation;
|
if (window.NativeRobosats === undefined || torStatus === 'ON') {
|
||||||
newFed.registerHook('onFederationUpdate', () => {
|
const newFed = initialFederationContext.federation;
|
||||||
setFederationUpdatedAt(new Date().toISOString());
|
newFed.registerHook('onFederationUpdate', () => {
|
||||||
});
|
setFederationUpdatedAt(new Date().toISOString());
|
||||||
newFed.registerHook('onCoordinatorUpdate', () => {
|
});
|
||||||
setCoordinatorUpdatedAt(new Date().toISOString());
|
newFed.registerHook('onCoordinatorUpdate', () => {
|
||||||
});
|
setCoordinatorUpdatedAt(new Date().toISOString());
|
||||||
void newFed.start(origin, settings, hostUrl);
|
});
|
||||||
setFederation(newFed);
|
void newFed.start(origin, settings, hostUrl);
|
||||||
|
setFederation(newFed);
|
||||||
|
}
|
||||||
}, [settings.network, torStatus]);
|
}, [settings.network, torStatus]);
|
||||||
|
|
||||||
const onOrderReceived = (order: Order): void => {
|
const onOrderReceived = (order: Order): void => {
|
||||||
|
@ -205,6 +205,7 @@ export class Coordinator {
|
|||||||
apiClient
|
apiClient
|
||||||
.get(this.url, `${this.basePath}/api/book/`)
|
.get(this.url, `${this.basePath}/api/book/`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
console.log('BOOK', data);
|
||||||
if (!data?.not_found) {
|
if (!data?.not_found) {
|
||||||
this.book = (data as PublicOrder[]).map((order) => {
|
this.book = (data as PublicOrder[]).map((order) => {
|
||||||
order.coordinatorShortAlias = this.shortAlias;
|
order.coordinatorShortAlias = this.shortAlias;
|
||||||
@ -370,7 +371,6 @@ export class Coordinator {
|
|||||||
return await apiClient
|
return await apiClient
|
||||||
.get(this.url, `${this.basePath}/api/order/?order_id=${orderId}`, authHeaders)
|
.get(this.url, `${this.basePath}/api/order/?order_id=${orderId}`, authHeaders)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log('data', data);
|
|
||||||
const order: Order = {
|
const order: Order = {
|
||||||
...defaultOrder,
|
...defaultOrder,
|
||||||
...data,
|
...data,
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import { sha256 } from 'js-sha256';
|
import { sha256 } from 'js-sha256';
|
||||||
import { Robot, type Order } from '.';
|
import { Robot, type Order } from '.';
|
||||||
import { robohash } from '../components/RobotAvatar/RobohashGenerator';
|
import { robohash } from '../components/RobotAvatar/RobohashGenerator';
|
||||||
import { generate_roboname } from 'robo-identities-wasm';
|
// import { generate_roboname } from 'robo-identities-wasm';
|
||||||
|
|
||||||
class Slot {
|
class Slot {
|
||||||
constructor(token: string, shortAliases: string[], robotAttributes: Record<any, any>) {
|
constructor(token: string, shortAliases: string[], robotAttributes: Record<any, any>) {
|
||||||
this.token = token;
|
this.token = token;
|
||||||
|
|
||||||
this.hashId = sha256(sha256(this.token));
|
this.hashId = sha256(sha256(this.token));
|
||||||
this.nickname = generate_roboname(this.hashId);
|
// FIXME
|
||||||
|
// this.nickname = generate_roboname(this.hashId);
|
||||||
|
this.nickname = 'Robot';
|
||||||
// trigger RoboHash avatar generation in webworker and store in RoboHash class cache.
|
// trigger RoboHash avatar generation in webworker and store in RoboHash class cache.
|
||||||
void robohash.generate(this.hashId, 'small');
|
void robohash.generate(this.hashId, 'small');
|
||||||
void robohash.generate(this.hashId, 'large');
|
void robohash.generate(this.hashId, 'large');
|
||||||
|
@ -30,6 +30,7 @@ class ApiNativeClient implements ApiClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly parseResponse = (response: Record<string, any>): object => {
|
private readonly parseResponse = (response: Record<string, any>): object => {
|
||||||
|
console.log('response', response);
|
||||||
if (response.headers['set-cookie'] != null) {
|
if (response.headers['set-cookie'] != null) {
|
||||||
response.headers['set-cookie'].forEach((cookie: string) => {
|
response.headers['set-cookie'].forEach((cookie: string) => {
|
||||||
const keySplit: string[] = cookie.split('=');
|
const keySplit: string[] = cookie.split('=');
|
||||||
|
@ -1,23 +1,45 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { WebView, WebViewMessageEvent } from 'react-native-webview';
|
import { WebView, WebViewMessageEvent } from 'react-native-webview';
|
||||||
import { SafeAreaView, Text, Platform, Appearance } from 'react-native';
|
import { SafeAreaView, Text, Platform, Appearance, DeviceEventEmitter } from 'react-native';
|
||||||
import TorClient from './services/Tor';
|
import TorClient from './services/Tor';
|
||||||
import Clipboard from '@react-native-clipboard/clipboard';
|
import Clipboard from '@react-native-clipboard/clipboard';
|
||||||
import NetInfo from '@react-native-community/netinfo';
|
|
||||||
import EncryptedStorage from 'react-native-encrypted-storage';
|
import EncryptedStorage from 'react-native-encrypted-storage';
|
||||||
import { name as app_name, version as app_version } from './package.json';
|
import { name as app_name, version as app_version } from './package.json';
|
||||||
|
import TorModule from './lib/native/TorModule';
|
||||||
|
|
||||||
const backgroundColors = {
|
const backgroundColors = {
|
||||||
light: 'white',
|
light: 'white',
|
||||||
dark: 'black',
|
dark: 'black',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TorStatus = 'ON' | 'STARTING' | 'STOPPING' | 'OFF';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const colorScheme = Appearance.getColorScheme() ?? 'light';
|
const colorScheme = Appearance.getColorScheme() ?? 'light';
|
||||||
const torClient = new TorClient();
|
const torClient = new TorClient();
|
||||||
const webViewRef = useRef<WebView>();
|
const webViewRef = useRef<WebView>();
|
||||||
const uri = (Platform.OS === 'android' ? 'file:///android_asset/' : '') + 'Web.bundle/index.html';
|
const uri = (Platform.OS === 'android' ? 'file:///android_asset/' : '') + 'Web.bundle/index.html';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
TorModule.start();
|
||||||
|
DeviceEventEmitter.addListener('TorStatus', (payload) => {
|
||||||
|
console.log(payload.torStatus);
|
||||||
|
if (payload.torStatus === 'OFF') TorModule.restart();
|
||||||
|
injectMessage({
|
||||||
|
category: 'system',
|
||||||
|
type: 'torStatus',
|
||||||
|
detail: payload.torStatus,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
TorModule.getTorStatus();
|
||||||
|
}, 2000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const injectMessageResolve = (id: string, data?: object) => {
|
const injectMessageResolve = (id: string, data?: object) => {
|
||||||
const json = JSON.stringify(data || {});
|
const json = JSON.stringify(data || {});
|
||||||
webViewRef.current?.injectJavaScript(
|
webViewRef.current?.injectJavaScript(
|
||||||
@ -72,7 +94,7 @@ const App = () => {
|
|||||||
const onMessage = async (event: WebViewMessageEvent) => {
|
const onMessage = async (event: WebViewMessageEvent) => {
|
||||||
const data = JSON.parse(event.nativeEvent.data);
|
const data = JSON.parse(event.nativeEvent.data);
|
||||||
if (data.category === 'http') {
|
if (data.category === 'http') {
|
||||||
sendTorStatus();
|
TorModule.getTorStatus();
|
||||||
if (data.type === 'get') {
|
if (data.type === 'get') {
|
||||||
torClient
|
torClient
|
||||||
.get(data.baseUrl, data.path, data.headers)
|
.get(data.baseUrl, data.path, data.headers)
|
||||||
@ -80,7 +102,7 @@ const App = () => {
|
|||||||
injectMessageResolve(data.id, response);
|
injectMessageResolve(data.id, response);
|
||||||
})
|
})
|
||||||
.catch((e) => onCatch(data.id, e))
|
.catch((e) => onCatch(data.id, e))
|
||||||
.finally(sendTorStatus);
|
.finally(TorModule.getTorStatus);
|
||||||
} else if (data.type === 'post') {
|
} else if (data.type === 'post') {
|
||||||
torClient
|
torClient
|
||||||
.post(data.baseUrl, data.path, data.body, data.headers)
|
.post(data.baseUrl, data.path, data.body, data.headers)
|
||||||
@ -88,7 +110,7 @@ const App = () => {
|
|||||||
injectMessageResolve(data.id, response);
|
injectMessageResolve(data.id, response);
|
||||||
})
|
})
|
||||||
.catch((e) => onCatch(data.id, e))
|
.catch((e) => onCatch(data.id, e))
|
||||||
.finally(sendTorStatus);
|
.finally(TorModule.getTorStatus);
|
||||||
} else if (data.type === 'delete') {
|
} else if (data.type === 'delete') {
|
||||||
torClient
|
torClient
|
||||||
.delete(data.baseUrl, data.path, data.headers)
|
.delete(data.baseUrl, data.path, data.headers)
|
||||||
@ -96,7 +118,7 @@ const App = () => {
|
|||||||
injectMessageResolve(data.id, response);
|
injectMessageResolve(data.id, response);
|
||||||
})
|
})
|
||||||
.catch((e) => onCatch(data.id, e))
|
.catch((e) => onCatch(data.id, e))
|
||||||
.finally(sendTorStatus);
|
.finally(TorModule.getTorStatus);
|
||||||
} else if (data.type === 'xhr') {
|
} else if (data.type === 'xhr') {
|
||||||
torClient
|
torClient
|
||||||
.request(data.baseUrl, data.path)
|
.request(data.baseUrl, data.path)
|
||||||
@ -104,7 +126,7 @@ const App = () => {
|
|||||||
injectMessageResolve(data.id, response);
|
injectMessageResolve(data.id, response);
|
||||||
})
|
})
|
||||||
.catch((e) => onCatch(data.id, e))
|
.catch((e) => onCatch(data.id, e))
|
||||||
.finally(sendTorStatus);
|
.finally(TorModule.getTorStatus);
|
||||||
}
|
}
|
||||||
} else if (data.category === 'system') {
|
} else if (data.category === 'system') {
|
||||||
if (data.type === 'init') {
|
if (data.type === 'init') {
|
||||||
@ -132,23 +154,6 @@ const App = () => {
|
|||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendTorStatus = async (event?: any) => {
|
|
||||||
NetInfo.fetch().then(async (state) => {
|
|
||||||
let daemonStatus = 'ERROR';
|
|
||||||
if (state.isInternetReachable) {
|
|
||||||
try {
|
|
||||||
daemonStatus = await torClient.daemon.getDaemonStatus();
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
injectMessage({
|
|
||||||
category: 'system',
|
|
||||||
type: 'torStatus',
|
|
||||||
detail: daemonStatus,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: backgroundColors[colorScheme] }}>
|
<SafeAreaView style={{ flex: 1, backgroundColor: backgroundColors[colorScheme] }}>
|
||||||
<WebView
|
<WebView
|
||||||
|
@ -271,6 +271,7 @@ android {
|
|||||||
packagingOptions {
|
packagingOptions {
|
||||||
// Make sure libjsc.so does not packed in APK
|
// Make sure libjsc.so does not packed in APK
|
||||||
exclude "**/libjsc.so"
|
exclude "**/libjsc.so"
|
||||||
|
jniLibs.useLegacyPackaging = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,7 +283,9 @@ dependencies {
|
|||||||
|
|
||||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||||
|
|
||||||
implementation files("../../node_modules/react-native-tor/android/libs/sifir_android.aar")
|
implementation "io.matthewnelson.kotlin-components:kmp-tor:4.8.6-0-1.4.4"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"
|
||||||
|
|
||||||
if (enableHermes) {
|
if (enableHermes) {
|
||||||
//noinspection GradleDynamicVersion
|
//noinspection GradleDynamicVersion
|
||||||
@ -326,3 +329,5 @@ def isNewArchitectureEnabled() {
|
|||||||
// - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true`
|
// - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true`
|
||||||
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
|
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
android:extractNativeLibs="true"
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
@ -1,17 +1,14 @@
|
|||||||
package com.robosats;
|
package com.robosats;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.content.Context;
|
|
||||||
import com.facebook.react.PackageList;
|
import com.facebook.react.PackageList;
|
||||||
import com.facebook.react.ReactApplication;
|
import com.facebook.react.ReactApplication;
|
||||||
import com.facebook.react.ReactInstanceManager;
|
|
||||||
import com.facebook.react.ReactNativeHost;
|
import com.facebook.react.ReactNativeHost;
|
||||||
import com.facebook.react.ReactPackage;
|
import com.facebook.react.ReactPackage;
|
||||||
import com.facebook.react.config.ReactFeatureFlags;
|
import com.facebook.react.config.ReactFeatureFlags;
|
||||||
import com.facebook.soloader.SoLoader;
|
import com.facebook.soloader.SoLoader;
|
||||||
import android.webkit.WebView;
|
import android.webkit.WebView;
|
||||||
import com.robosats.newarchitecture.MainApplicationReactNativeHost;
|
import com.robosats.newarchitecture.MainApplicationReactNativeHost;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class MainApplication extends Application implements ReactApplication {
|
public class MainApplication extends Application implements ReactApplication {
|
||||||
@ -29,6 +26,8 @@ public class MainApplication extends Application implements ReactApplication {
|
|||||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||||
// packages.add(new MyReactNativePackage());
|
// packages.add(new MyReactNativePackage());
|
||||||
|
packages.add(new RobosatsPackage());
|
||||||
|
|
||||||
return packages;
|
return packages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
package com.robosats;
|
||||||
|
|
||||||
|
import com.facebook.react.ReactPackage;
|
||||||
|
import com.facebook.react.bridge.NativeModule;
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
import com.facebook.react.uimanager.ViewManager;
|
||||||
|
import com.robosats.modules.TorModule;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class RobosatsPackage implements ReactPackage {
|
||||||
|
@Override
|
||||||
|
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<NativeModule> createNativeModules(
|
||||||
|
ReactApplicationContext reactContext) {
|
||||||
|
List<NativeModule> modules = new ArrayList<>();
|
||||||
|
|
||||||
|
modules.add(new TorModule(reactContext));
|
||||||
|
|
||||||
|
return modules;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,164 @@
|
|||||||
|
package com.robosats.modules;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.Arguments;
|
||||||
|
import com.facebook.react.bridge.Promise;
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||||
|
import com.facebook.react.bridge.ReactMethod;
|
||||||
|
import com.facebook.react.bridge.WritableMap;
|
||||||
|
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||||
|
import com.robosats.tor.TorKmpManager;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import okhttp3.Call;
|
||||||
|
import okhttp3.Callback;
|
||||||
|
import okhttp3.MediaType;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
|
||||||
|
public class TorModule extends ReactContextBaseJavaModule {
|
||||||
|
private TorKmpManager torKmpManager;
|
||||||
|
private ReactApplicationContext context;
|
||||||
|
public TorModule(ReactApplicationContext reactContext) {
|
||||||
|
context = reactContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "TorModule";
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void sendRequest(String action, String url, String headers, String body, final Promise promise) throws JSONException {
|
||||||
|
Log.d("RobosatsUrl", url);
|
||||||
|
OkHttpClient client = new OkHttpClient.Builder()
|
||||||
|
.connectTimeout(60, TimeUnit.SECONDS) // Set connection timeout
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS) // Set read timeout
|
||||||
|
.proxy(torKmpManager.getProxy()).build();
|
||||||
|
|
||||||
|
Request.Builder requestBuilder = new Request.Builder().url(url);
|
||||||
|
|
||||||
|
JSONObject headersObject = new JSONObject(headers);
|
||||||
|
headersObject.keys().forEachRemaining(key -> {
|
||||||
|
String value = headersObject.optString(key);
|
||||||
|
requestBuilder.addHeader(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Objects.equals(action, "DELETE")) {
|
||||||
|
requestBuilder.delete();
|
||||||
|
} else if (Objects.equals(action, "POST")) {
|
||||||
|
RequestBody requestBody = RequestBody.create(body, MediaType.get("application/json; charset=utf-8"));
|
||||||
|
requestBuilder.post(requestBody);
|
||||||
|
} else {
|
||||||
|
requestBuilder.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Request request = requestBuilder.build();
|
||||||
|
client.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
||||||
|
Log.d("RobosatsError", e.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call call, Response response) throws IOException {
|
||||||
|
Log.d("RobosatsCode", String.valueOf(response.code()));
|
||||||
|
String body = response.body() != null ? response.body().string() : "{}";
|
||||||
|
JSONObject headersJson = new JSONObject();
|
||||||
|
response.headers().names().forEach(name -> {
|
||||||
|
try {
|
||||||
|
headersJson.put(name, response.header(name));
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (response.code() != 200) {
|
||||||
|
Log.d("RobosatsError", "Request error code: " + response.code());
|
||||||
|
} else if (response.isSuccessful()) {
|
||||||
|
promise.resolve("{\"json\":" + body + ", \"headers\": " + headersJson +"}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void getTorStatus() {
|
||||||
|
String torState = torKmpManager.getTorState().getState().name();
|
||||||
|
WritableMap payload = Arguments.createMap();
|
||||||
|
payload.putString("torStatus", torState);
|
||||||
|
context
|
||||||
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||||
|
.emit("TorStatus", payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void isConnected() {
|
||||||
|
String isConnected = String.valueOf(torKmpManager.isConnected());
|
||||||
|
WritableMap payload = Arguments.createMap();
|
||||||
|
payload.putString("isConnected", isConnected);
|
||||||
|
context
|
||||||
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||||
|
.emit("TorIsConnected", payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void isStarting() {
|
||||||
|
String isStarting = String.valueOf(torKmpManager.isStarting());
|
||||||
|
WritableMap payload = Arguments.createMap();
|
||||||
|
payload.putString("isStarting", isStarting);
|
||||||
|
context
|
||||||
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||||
|
.emit("TorIsStarting", payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void stop() {
|
||||||
|
torKmpManager.getTorOperationManager().stopQuietly();
|
||||||
|
WritableMap payload = Arguments.createMap();
|
||||||
|
context
|
||||||
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||||
|
.emit("TorStop", payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void start() {
|
||||||
|
torKmpManager = new TorKmpManager(context.getCurrentActivity().getApplication());
|
||||||
|
torKmpManager.getTorOperationManager().startQuietly();
|
||||||
|
WritableMap payload = Arguments.createMap();
|
||||||
|
context
|
||||||
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||||
|
.emit("TorStart", payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void restart() {
|
||||||
|
torKmpManager = new TorKmpManager(context.getCurrentActivity().getApplication());
|
||||||
|
torKmpManager.getTorOperationManager().restartQuietly();
|
||||||
|
WritableMap payload = Arguments.createMap();
|
||||||
|
context
|
||||||
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||||
|
.emit("TorRestart", payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void newIdentity() {
|
||||||
|
torKmpManager.newIdentity(context.getCurrentActivity().getApplication());
|
||||||
|
WritableMap payload = Arguments.createMap();
|
||||||
|
context
|
||||||
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||||
|
.emit("TorNewIdentity", payload);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package com.robosats.tor
|
||||||
|
|
||||||
|
enum class EnumTorState {
|
||||||
|
STARTING,
|
||||||
|
ON,
|
||||||
|
STOPPING,
|
||||||
|
OFF
|
||||||
|
}
|
@ -0,0 +1,389 @@
|
|||||||
|
package com.robosats.tor
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import io.matthewnelson.kmp.tor.KmpTorLoaderAndroid
|
||||||
|
import io.matthewnelson.kmp.tor.TorConfigProviderAndroid
|
||||||
|
import io.matthewnelson.kmp.tor.common.address.*
|
||||||
|
import io.matthewnelson.kmp.tor.controller.common.config.TorConfig
|
||||||
|
import io.matthewnelson.kmp.tor.controller.common.config.TorConfig.Option.*
|
||||||
|
import io.matthewnelson.kmp.tor.controller.common.config.TorConfig.Setting.*
|
||||||
|
import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlInfoGet
|
||||||
|
import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlSignal
|
||||||
|
import io.matthewnelson.kmp.tor.controller.common.events.TorEvent
|
||||||
|
import io.matthewnelson.kmp.tor.manager.TorManager
|
||||||
|
import io.matthewnelson.kmp.tor.manager.TorServiceConfig
|
||||||
|
import io.matthewnelson.kmp.tor.manager.common.TorControlManager
|
||||||
|
import io.matthewnelson.kmp.tor.manager.common.TorOperationManager
|
||||||
|
import io.matthewnelson.kmp.tor.manager.common.event.TorManagerEvent
|
||||||
|
import io.matthewnelson.kmp.tor.manager.common.state.isOff
|
||||||
|
import io.matthewnelson.kmp.tor.manager.common.state.isOn
|
||||||
|
import io.matthewnelson.kmp.tor.manager.common.state.isStarting
|
||||||
|
import io.matthewnelson.kmp.tor.manager.common.state.isStopping
|
||||||
|
import io.matthewnelson.kmp.tor.manager.R
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Proxy
|
||||||
|
|
||||||
|
class TorKmpManager(application : Application) {
|
||||||
|
|
||||||
|
private val TAG = "TorListener"
|
||||||
|
|
||||||
|
private val providerAndroid by lazy {
|
||||||
|
object : TorConfigProviderAndroid(context = application) {
|
||||||
|
override fun provide(): TorConfig {
|
||||||
|
return TorConfig.Builder {
|
||||||
|
// Set multiple ports for all of the things
|
||||||
|
val dns = Ports.Dns()
|
||||||
|
put(dns.set(AorDorPort.Value(PortProxy(9252))))
|
||||||
|
put(dns.set(AorDorPort.Value(PortProxy(9253))))
|
||||||
|
|
||||||
|
val socks = Ports.Socks()
|
||||||
|
put(socks.set(AorDorPort.Value(PortProxy(9254))))
|
||||||
|
put(socks.set(AorDorPort.Value(PortProxy(9255))))
|
||||||
|
|
||||||
|
val http = Ports.HttpTunnel()
|
||||||
|
put(http.set(AorDorPort.Value(PortProxy(9258))))
|
||||||
|
put(http.set(AorDorPort.Value(PortProxy(9259))))
|
||||||
|
|
||||||
|
val trans = Ports.Trans()
|
||||||
|
put(trans.set(AorDorPort.Value(PortProxy(9262))))
|
||||||
|
put(trans.set(AorDorPort.Value(PortProxy(9263))))
|
||||||
|
|
||||||
|
// If a port (9263) is already taken (by ^^^^ trans port above)
|
||||||
|
// this will take its place and "overwrite" the trans port entry
|
||||||
|
// because port 9263 is taken.
|
||||||
|
put(socks.set(AorDorPort.Value(PortProxy(9263))))
|
||||||
|
|
||||||
|
// Set Flags
|
||||||
|
socks.setFlags(setOf(
|
||||||
|
Ports.Socks.Flag.OnionTrafficOnly
|
||||||
|
)).setIsolationFlags(setOf(
|
||||||
|
Ports.IsolationFlag.IsolateClientAddr,
|
||||||
|
)).set(AorDorPort.Value(PortProxy(9264)))
|
||||||
|
put(socks)
|
||||||
|
|
||||||
|
// reset our socks object to defaults
|
||||||
|
socks.setDefault()
|
||||||
|
|
||||||
|
// Not necessary, as if ControlPort is missing it will be
|
||||||
|
// automatically added for you; but for demonstration purposes...
|
||||||
|
// put(Ports.Control().set(AorDorPort.Auto))
|
||||||
|
|
||||||
|
// Use a UnixSocket instead of TCP for the ControlPort.
|
||||||
|
//
|
||||||
|
// A unix domain socket will always be preferred on Android
|
||||||
|
// if neither Ports.Control or UnixSockets.Control are provided.
|
||||||
|
put(UnixSockets.Control().set(FileSystemFile(
|
||||||
|
workDir.builder {
|
||||||
|
|
||||||
|
// Put the file in the "data" directory
|
||||||
|
// so that we avoid any directory permission
|
||||||
|
// issues.
|
||||||
|
//
|
||||||
|
// Note that DataDirectory is automatically added
|
||||||
|
// for you if it is not present in your provided
|
||||||
|
// config. If you set a custom Path for it, you
|
||||||
|
// should use it here.
|
||||||
|
addSegment(DataDirectory.DEFAULT_NAME)
|
||||||
|
|
||||||
|
addSegment(UnixSockets.Control.DEFAULT_NAME)
|
||||||
|
}
|
||||||
|
)))
|
||||||
|
|
||||||
|
// Use a UnixSocket instead of TCP for the SocksPort.
|
||||||
|
put(UnixSockets.Socks().set(FileSystemFile(
|
||||||
|
workDir.builder {
|
||||||
|
|
||||||
|
// Put the file in the "data" directory
|
||||||
|
// so that we avoid any directory permission
|
||||||
|
// issues.
|
||||||
|
//
|
||||||
|
// Note that DataDirectory is automatically added
|
||||||
|
// for you if it is not present in your provided
|
||||||
|
// config. If you set a custom Path for it, you
|
||||||
|
// should use it here.
|
||||||
|
addSegment(DataDirectory.DEFAULT_NAME)
|
||||||
|
|
||||||
|
addSegment(UnixSockets.Socks.DEFAULT_NAME)
|
||||||
|
}
|
||||||
|
)))
|
||||||
|
|
||||||
|
// For Android, disabling & reducing connection padding is
|
||||||
|
// advisable to minimize mobile data usage.
|
||||||
|
put(ConnectionPadding().set(AorTorF.False))
|
||||||
|
put(ConnectionPaddingReduced().set(TorF.True))
|
||||||
|
|
||||||
|
// Tor default is 24h. Reducing to 10 min helps mitigate
|
||||||
|
// unnecessary mobile data usage.
|
||||||
|
put(DormantClientTimeout().set(Time.Minutes(10)))
|
||||||
|
|
||||||
|
// Tor defaults this setting to false which would mean if
|
||||||
|
// Tor goes dormant, the next time it is started it will still
|
||||||
|
// be in the dormant state and will not bootstrap until being
|
||||||
|
// set to "active". This ensures that if it is a fresh start,
|
||||||
|
// dormancy will be cancelled automatically.
|
||||||
|
put(DormantCanceledByStartup().set(TorF.True))
|
||||||
|
|
||||||
|
// If planning to use v3 Client Authentication in a persistent
|
||||||
|
// manner (where private keys are saved to disk via the "Persist"
|
||||||
|
// flag), this is needed to be set.
|
||||||
|
put(ClientOnionAuthDir().set(FileSystemDir(
|
||||||
|
workDir.builder { addSegment(ClientOnionAuthDir.DEFAULT_NAME) }
|
||||||
|
)))
|
||||||
|
|
||||||
|
val hsPath = workDir.builder {
|
||||||
|
addSegment(HiddenService.DEFAULT_PARENT_DIR_NAME)
|
||||||
|
addSegment("test_service")
|
||||||
|
}
|
||||||
|
// Add Hidden services
|
||||||
|
put(HiddenService()
|
||||||
|
.setPorts(ports = setOf(
|
||||||
|
// Use a unix domain socket to communicate via IPC instead of over TCP
|
||||||
|
HiddenService.UnixSocket(virtualPort = Port(80), targetUnixSocket = hsPath.builder {
|
||||||
|
addSegment(HiddenService.UnixSocket.DEFAULT_UNIX_SOCKET_NAME)
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
.setMaxStreams(maxStreams = HiddenService.MaxStreams(value = 2))
|
||||||
|
.setMaxStreamsCloseCircuit(value = TorF.True)
|
||||||
|
.set(FileSystemDir(path = hsPath))
|
||||||
|
)
|
||||||
|
|
||||||
|
put(HiddenService()
|
||||||
|
.setPorts(ports = setOf(
|
||||||
|
HiddenService.Ports(virtualPort = Port(80), targetPort = Port(1030)), // http
|
||||||
|
HiddenService.Ports(virtualPort = Port(443), targetPort = Port(1030)) // https
|
||||||
|
))
|
||||||
|
.set(FileSystemDir(path =
|
||||||
|
workDir.builder {
|
||||||
|
addSegment(HiddenService.DEFAULT_PARENT_DIR_NAME)
|
||||||
|
addSegment("test_service_2")
|
||||||
|
}
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val loaderAndroid by lazy {
|
||||||
|
KmpTorLoaderAndroid(provider = providerAndroid)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val manager: TorManager by lazy {
|
||||||
|
TorManager.newInstance(application = application, loader = loaderAndroid, requiredEvents = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// only expose necessary interfaces
|
||||||
|
val torOperationManager: TorOperationManager get() = manager
|
||||||
|
val torControlManager: TorControlManager get() = manager
|
||||||
|
|
||||||
|
private val listener = TorListener()
|
||||||
|
|
||||||
|
val events: LiveData<String> get() = listener.eventLines
|
||||||
|
|
||||||
|
private val appScope by lazy {
|
||||||
|
CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
|
||||||
|
}
|
||||||
|
|
||||||
|
val torStateLiveData: MutableLiveData<TorState> = MutableLiveData()
|
||||||
|
get() = field
|
||||||
|
var torState: TorState = TorState()
|
||||||
|
get() = field
|
||||||
|
|
||||||
|
var proxy: Proxy? = null
|
||||||
|
get() = field
|
||||||
|
|
||||||
|
init {
|
||||||
|
manager.debug(true)
|
||||||
|
manager.addListener(listener)
|
||||||
|
listener.addLine(TorServiceConfig.getMetaData(application).toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isConnected(): Boolean {
|
||||||
|
return manager.state.isOn() && manager.state.bootstrap >= 100
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isStarting(): Boolean {
|
||||||
|
return manager.state.isStarting() ||
|
||||||
|
(manager.state.isOn() && manager.state.bootstrap < 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun newIdentity(appContext: Application) {
|
||||||
|
appScope.launch {
|
||||||
|
val result = manager.signal(TorControlSignal.Signal.NewNym)
|
||||||
|
result.onSuccess {
|
||||||
|
if (it !is String) {
|
||||||
|
listener.addLine(TorControlSignal.NEW_NYM_SUCCESS)
|
||||||
|
Toast.makeText(appContext, TorControlSignal.NEW_NYM_SUCCESS, Toast.LENGTH_SHORT).show()
|
||||||
|
return@onSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
val post: String? = when {
|
||||||
|
it.startsWith(TorControlSignal.NEW_NYM_RATE_LIMITED) -> {
|
||||||
|
// Rate limiting NEWNYM request: delaying by 8 second(s)
|
||||||
|
val seconds: Int? = it.drop(TorControlSignal.NEW_NYM_RATE_LIMITED.length)
|
||||||
|
.substringBefore(' ')
|
||||||
|
.toIntOrNull()
|
||||||
|
|
||||||
|
if (seconds == null) {
|
||||||
|
it
|
||||||
|
} else {
|
||||||
|
appContext.getString(
|
||||||
|
R.string.kmp_tor_newnym_rate_limited,
|
||||||
|
seconds
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it == TorControlSignal.NEW_NYM_SUCCESS -> {
|
||||||
|
appContext.getString(R.string.kmp_tor_newnym_success)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (post != null) {
|
||||||
|
listener.addLine(post)
|
||||||
|
Toast.makeText(appContext, post, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.onFailure {
|
||||||
|
val msg = "Tor identity change failed"
|
||||||
|
listener.addLine(msg)
|
||||||
|
Toast.makeText(appContext, msg, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class TorListener: TorManagerEvent.Listener() {
|
||||||
|
private val _eventLines: MutableLiveData<String> = MutableLiveData("")
|
||||||
|
val eventLines: LiveData<String> = _eventLines
|
||||||
|
private val events: MutableList<String> = ArrayList(50)
|
||||||
|
fun addLine(line: String) {
|
||||||
|
synchronized(this) {
|
||||||
|
if (events.size > 49) {
|
||||||
|
events.removeAt(0)
|
||||||
|
}
|
||||||
|
events.add(line)
|
||||||
|
//Log.i(TAG, line)
|
||||||
|
//_eventLines.value = events.joinToString("\n")
|
||||||
|
_eventLines.postValue(events.joinToString("\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEvent(event: TorManagerEvent) {
|
||||||
|
|
||||||
|
if (event is TorManagerEvent.State) {
|
||||||
|
val stateEvent: TorManagerEvent.State = event
|
||||||
|
val state = stateEvent.torState
|
||||||
|
torState.progressIndicator = state.bootstrap
|
||||||
|
val liveTorState = TorState()
|
||||||
|
liveTorState.progressIndicator = state.bootstrap
|
||||||
|
|
||||||
|
if (state.isOn()) {
|
||||||
|
if (state.bootstrap >= 100) {
|
||||||
|
torState.state = EnumTorState.ON
|
||||||
|
liveTorState.state = EnumTorState.ON
|
||||||
|
} else {
|
||||||
|
torState.state = EnumTorState.STARTING
|
||||||
|
liveTorState.state = EnumTorState.STARTING
|
||||||
|
}
|
||||||
|
} else if (state.isStarting()) {
|
||||||
|
torState.state = EnumTorState.STARTING
|
||||||
|
liveTorState.state = EnumTorState.STARTING
|
||||||
|
} else if (state.isOff()) {
|
||||||
|
torState.state = EnumTorState.OFF
|
||||||
|
liveTorState.state = EnumTorState.OFF
|
||||||
|
} else if (state.isStopping()) {
|
||||||
|
torState.state = EnumTorState.STOPPING
|
||||||
|
liveTorState.state = EnumTorState.STOPPING
|
||||||
|
}
|
||||||
|
torStateLiveData.postValue(liveTorState)
|
||||||
|
}
|
||||||
|
addLine(event.toString())
|
||||||
|
super.onEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEvent(event: TorEvent.Type.SingleLineEvent, output: String) {
|
||||||
|
addLine("$event - $output")
|
||||||
|
|
||||||
|
super.onEvent(event, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEvent(event: TorEvent.Type.MultiLineEvent, output: List<String>) {
|
||||||
|
addLine("multi-line event: $event. See Logs.")
|
||||||
|
|
||||||
|
// these events are many many many lines and should be moved
|
||||||
|
// off the main thread if ever needed to be dealt with.
|
||||||
|
val enabled = false
|
||||||
|
if (enabled) {
|
||||||
|
appScope.launch(Dispatchers.IO) {
|
||||||
|
Log.d(TAG, "-------------- multi-line event START: $event --------------")
|
||||||
|
for (line in output) {
|
||||||
|
Log.d(TAG, line)
|
||||||
|
}
|
||||||
|
Log.d(TAG, "--------------- multi-line event END: $event ---------------")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onEvent(event, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun managerEventError(t: Throwable) {
|
||||||
|
t.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun managerEventAddressInfo(info: TorManagerEvent.AddressInfo) {
|
||||||
|
if (info.isNull) {
|
||||||
|
// Tear down HttpClient
|
||||||
|
} else {
|
||||||
|
info.socksInfoToProxyAddressOrNull()?.firstOrNull()?.let { proxyAddress ->
|
||||||
|
@Suppress("UNUSED_VARIABLE")
|
||||||
|
val socket = InetSocketAddress(proxyAddress.address.value, proxyAddress.port.value)
|
||||||
|
proxy = Proxy(Proxy.Type.SOCKS, socket)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun managerEventStartUpCompleteForTorInstance() {
|
||||||
|
// Do one-time things after we're bootstrapped
|
||||||
|
|
||||||
|
appScope.launch {
|
||||||
|
torControlManager.onionAddNew(
|
||||||
|
type = OnionAddress.PrivateKey.Type.ED25519_V3,
|
||||||
|
hsPorts = setOf(HiddenService.Ports(virtualPort = Port(443))),
|
||||||
|
flags = null,
|
||||||
|
maxStreams = null,
|
||||||
|
).onSuccess { hsEntry ->
|
||||||
|
addLine(
|
||||||
|
"New HiddenService: " +
|
||||||
|
"\n - Address: https://${hsEntry.address.canonicalHostname()}" +
|
||||||
|
"\n - PrivateKey: ${hsEntry.privateKey}"
|
||||||
|
)
|
||||||
|
|
||||||
|
torControlManager.onionDel(hsEntry.address).onSuccess {
|
||||||
|
addLine("Aaaaaaaaand it's gone...")
|
||||||
|
}.onFailure { t ->
|
||||||
|
t.printStackTrace()
|
||||||
|
}
|
||||||
|
}.onFailure { t ->
|
||||||
|
t.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(20_000L)
|
||||||
|
|
||||||
|
torControlManager.infoGet(TorControlInfoGet.KeyWord.Uptime()).onSuccess { uptime ->
|
||||||
|
addLine("Uptime - $uptime")
|
||||||
|
}.onFailure { t ->
|
||||||
|
t.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package com.robosats.tor
|
||||||
|
|
||||||
|
class TorState {
|
||||||
|
var state : EnumTorState = EnumTorState.OFF
|
||||||
|
get() = field
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
var progressIndicator : Int = 0
|
||||||
|
get() = field
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
}
|
@ -9,7 +9,6 @@ buildscript {
|
|||||||
compileSdkVersion = 33
|
compileSdkVersion = 33
|
||||||
targetSdkVersion = 33
|
targetSdkVersion = 33
|
||||||
kotlin_version = "1.8.21"
|
kotlin_version = "1.8.21"
|
||||||
kotlinVersion = "1.8.21" //for react-native-tor
|
|
||||||
|
|
||||||
if (System.properties['os.arch'] == "aarch64") {
|
if (System.properties['os.arch'] == "aarch64") {
|
||||||
// For M1 Users we need to use the NDK 24 which added support for aarch64
|
// For M1 Users we need to use the NDK 24 which added support for aarch64
|
||||||
|
1
mobile/package-lock.json
generated
1
mobile/package-lock.json
generated
@ -13,7 +13,6 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-native": "^0.71.8",
|
"react-native": "^0.71.8",
|
||||||
"react-native-encrypted-storage": "^4.0.3",
|
"react-native-encrypted-storage": "^4.0.3",
|
||||||
"react-native-tor": "^0.1.8",
|
|
||||||
"react-native-webview": "^13.3.0"
|
"react-native-webview": "^13.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-native": "^0.71.8",
|
"react-native": "^0.71.8",
|
||||||
"react-native-encrypted-storage": "^4.0.3",
|
"react-native-encrypted-storage": "^4.0.3",
|
||||||
"react-native-tor": "^0.1.8",
|
|
||||||
"react-native-webview": "^13.3.0"
|
"react-native-webview": "^13.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -1,29 +1,14 @@
|
|||||||
import Tor from 'react-native-tor';
|
// import Tor from 'react-native-tor';
|
||||||
|
|
||||||
|
import TorModule from '../../lib/native/TorModule';
|
||||||
|
|
||||||
class TorClient {
|
class TorClient {
|
||||||
daemon: ReturnType<typeof Tor>;
|
daemon: object;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.daemon = Tor({
|
this.daemon = {};
|
||||||
stopDaemonOnBackground: false,
|
|
||||||
numberConcurrentRequests: 0,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly connectDaemon: () => void = async () => {
|
|
||||||
try {
|
|
||||||
this.daemon.startIfNotStarted();
|
|
||||||
} catch {
|
|
||||||
console.log('TOR already started');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public reset: () => void = async () => {
|
|
||||||
console.log('Reset TOR');
|
|
||||||
await this.daemon.stopIfRunning();
|
|
||||||
await this.daemon.startIfNotStarted();
|
|
||||||
};
|
|
||||||
|
|
||||||
public get: (baseUrl: string, path: string, headers: object) => Promise<object> = async (
|
public get: (baseUrl: string, path: string, headers: object) => Promise<object> = async (
|
||||||
baseUrl,
|
baseUrl,
|
||||||
path,
|
path,
|
||||||
@ -31,9 +16,13 @@ class TorClient {
|
|||||||
) => {
|
) => {
|
||||||
return await new Promise<object>(async (resolve, reject) => {
|
return await new Promise<object>(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const response = await this.daemon.get(`${baseUrl}${path}`, headers);
|
const response = await TorModule.sendRequest(
|
||||||
|
'GET',
|
||||||
resolve(response);
|
`${baseUrl}${path}`,
|
||||||
|
JSON.stringify(headers),
|
||||||
|
'{}',
|
||||||
|
);
|
||||||
|
resolve(JSON.parse(response));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
@ -47,9 +36,13 @@ class TorClient {
|
|||||||
) => {
|
) => {
|
||||||
return await new Promise<object>(async (resolve, reject) => {
|
return await new Promise<object>(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const response = await this.daemon.delete(`${baseUrl}${path}`, '', headers);
|
const response = await TorModule.sendRequest(
|
||||||
|
'DELETE',
|
||||||
resolve(response);
|
`${baseUrl}${path}`,
|
||||||
|
JSON.stringify(headers),
|
||||||
|
'{}',
|
||||||
|
);
|
||||||
|
resolve(JSON.parse(response));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
@ -62,13 +55,12 @@ class TorClient {
|
|||||||
) => {
|
) => {
|
||||||
return await new Promise<object>(async (resolve, reject) => {
|
return await new Promise<object>(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const response = await this.daemon
|
// const response = await this.daemon
|
||||||
.request(`${baseUrl}${path}`, 'GET', '', {}, true)
|
// .request(`${baseUrl}${path}`, 'GET', '', {}, true)
|
||||||
.then((resp) => {
|
// .then((resp) => {
|
||||||
resolve(resp);
|
// resolve(resp);
|
||||||
});
|
// });
|
||||||
|
// resolve(response);
|
||||||
resolve(response);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
@ -80,9 +72,13 @@ class TorClient {
|
|||||||
return await new Promise<object>(async (resolve, reject) => {
|
return await new Promise<object>(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const json = JSON.stringify(body);
|
const json = JSON.stringify(body);
|
||||||
const response = await this.daemon.post(`${baseUrl}${path}`, json, headers);
|
const response = await TorModule.sendRequest(
|
||||||
|
'POST',
|
||||||
resolve(response);
|
`${baseUrl}${path}`,
|
||||||
|
JSON.stringify(headers),
|
||||||
|
json,
|
||||||
|
);
|
||||||
|
resolve(JSON.parse(response));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user