From 38bc2b950f7c312ea7a134301850686a564bf516 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Mon, 24 Nov 2025 11:50:53 -0500 Subject: [PATCH] Convert SignalProxyUtil from Java to Kotlin. Resolves #14453 --- .../securesms/util/SignalProxyUtil.java | 181 ------------------ .../securesms/util/SignalProxyUtil.kt | 181 ++++++++++++++++++ 2 files changed, 181 insertions(+), 181 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.java deleted file mode 100644 index ddab060e88..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.java +++ /dev/null @@ -1,181 +0,0 @@ -package org.thoughtcrime.securesms.util; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.WorkerThread; - -import org.conscrypt.ConscryptSignal; -import org.signal.core.util.concurrent.SignalExecutors; -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.push.AccountManagerFactory; -import org.whispersystems.signalservice.api.SignalServiceAccountManager; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState; -import org.whispersystems.signalservice.internal.configuration.SignalProxy; - -import java.io.IOException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.schedulers.Schedulers; - -public final class SignalProxyUtil { - - private static final String TAG = Log.tag(SignalProxyUtil.class); - - private static final String PROXY_LINK_HOST = "signal.tube"; - - private static final Pattern PROXY_LINK_PATTERN = Pattern.compile("^(https|sgnl)://" + PROXY_LINK_HOST + "/#([^:]+).*$"); - private static final Pattern HOST_PATTERN = Pattern.compile("^([^:]+).*$"); - - private SignalProxyUtil() {} - - public static void startListeningToWebsocket() { - if (SignalStore.proxy().isProxyEnabled() && AppDependencies.getAuthWebSocket().getState().firstOrError().blockingGet().isFailure()) { - Log.w(TAG, "Proxy is in a failed state. Restarting."); - AppDependencies.resetNetwork(); - } - - SignalExecutors.UNBOUNDED.execute(AppDependencies::startNetwork); - } - - /** - * Handles all things related to enabling a proxy, including saving it and resetting the relevant - * network connections. - */ - public static void enableProxy(@NonNull SignalProxy proxy) { - SignalStore.proxy().enableProxy(proxy); - ConscryptSignal.setUseEngineSocketByDefault(true); - AppDependencies.resetNetwork(); - startListeningToWebsocket(); - } - - /** - * Handles all things related to disabling a proxy, including saving the change and resetting the - * relevant network connections. - */ - public static void disableProxy() { - SignalStore.proxy().disableProxy(); - ConscryptSignal.setUseEngineSocketByDefault(false); - AppDependencies.resetNetwork(); - startListeningToWebsocket(); - } - - public static void disableAndClearProxy(){ - disableProxy(); - SignalStore.proxy().setProxy(null); - } - - /** - * A blocking call that will wait until the websocket either successfully connects, or fails. - * It is assumed that the app state is already configured how you would like it, e.g. you've - * already configured a proxy if relevant. - * - * @return True if the connection is successful within the specified timeout, otherwise false. - */ - @WorkerThread - public static boolean testWebsocketConnection(long timeout) { - startListeningToWebsocket(); - - if (SignalStore.account().getE164() == null) { - Log.i(TAG, "User is unregistered! Doing simple check."); - return testWebsocketConnectionUnregistered(timeout); - } - - return AppDependencies.getAuthWebSocket() - .getState() - .subscribeOn(Schedulers.trampoline()) - .observeOn(Schedulers.trampoline()) - .timeout(timeout, TimeUnit.MILLISECONDS) - .skipWhile(state -> state != WebSocketConnectionState.CONNECTED && !state.isFailure()) - .firstOrError() - .flatMap(state -> Single.just(state == WebSocketConnectionState.CONNECTED)) - .onErrorReturn(t -> false) - .blockingGet(); - } - - /** - * If this is a valid proxy deep link, this will return the embedded host. If not, it will return - * null. - */ - public static @Nullable String parseHostFromProxyDeepLink(@Nullable String proxyLink) { - if (proxyLink == null) { - return null; - } - - Matcher matcher = PROXY_LINK_PATTERN.matcher(proxyLink); - - if (matcher.matches()) { - return matcher.group(2); - } else { - return null; - } - } - - /** - * Takes in an address that could be in various formats, and converts it to the format we should - * be storing and connecting to. - */ - public static @NonNull String convertUserEnteredAddressToHost(@NonNull String host) { - String parsedHost = SignalProxyUtil.parseHostFromProxyDeepLink(host); - if (parsedHost != null) { - return parsedHost; - } - - Matcher matcher = HOST_PATTERN.matcher(host); - - if (matcher.matches()) { - String result = matcher.group(1); - return result != null ? result : ""; - } else { - return host; - } - } - - public static @NonNull String generateProxyUrl(@NonNull String link) { - String host = link; - String parsed = parseHostFromProxyDeepLink(link); - - if (parsed != null) { - host = parsed; - } - - Matcher matcher = HOST_PATTERN.matcher(host); - - if (matcher.matches()) { - host = matcher.group(1); - } - - return "https://" + PROXY_LINK_HOST + "/#" + host; - } - - private static boolean testWebsocketConnectionUnregistered(long timeout) { - CountDownLatch latch = new CountDownLatch(1); - AtomicBoolean success = new AtomicBoolean(false); - SignalServiceAccountManager accountManager = AccountManagerFactory.getInstance().createUnauthenticated(AppDependencies.getApplication(), "", SignalServiceAddress.DEFAULT_DEVICE_ID, ""); - - SignalExecutors.UNBOUNDED.execute(() -> { - try { - accountManager.checkNetworkConnection(); - success.set(true); - latch.countDown(); - } catch (IOException e) { - latch.countDown(); - } - }); - - try { - latch.await(timeout, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - Log.w(TAG, "Interrupted!", e); - } - - return success.get(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.kt new file mode 100644 index 0000000000..389cde4039 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.kt @@ -0,0 +1,181 @@ +package org.thoughtcrime.securesms.util + +import androidx.annotation.WorkerThread +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.functions.Function +import io.reactivex.rxjava3.functions.Predicate +import io.reactivex.rxjava3.schedulers.Schedulers +import org.conscrypt.ConscryptSignal +import org.signal.core.util.concurrent.SignalExecutors +import org.signal.core.util.logging.Log +import org.signal.core.util.logging.Log.tag +import org.signal.core.util.logging.Log.w +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.push.AccountManagerFactory +import org.whispersystems.signalservice.api.push.SignalServiceAddress +import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState +import org.whispersystems.signalservice.internal.configuration.SignalProxy +import java.io.IOException +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.regex.Pattern + +object SignalProxyUtil { + private val TAG = tag(SignalProxyUtil::class.java) + + private const val PROXY_LINK_HOST = "signal.tube" + + private val PROXY_LINK_PATTERN: Pattern = Pattern.compile("^(https|sgnl)://$PROXY_LINK_HOST/#([^:]+).*$") + private val HOST_PATTERN: Pattern = Pattern.compile("^([^:]+).*$") + + @JvmStatic + fun startListeningToWebsocket() { + if (SignalStore.proxy.isProxyEnabled && AppDependencies.authWebSocket.state.firstOrError().blockingGet().isFailure) { + Log.w(TAG, "Proxy is in a failed state. Restarting.") + AppDependencies.resetNetwork() + } + + SignalExecutors.UNBOUNDED.execute { AppDependencies.startNetwork() } + } + + /** + * Handles all things related to enabling a proxy, including saving it and resetting the relevant + * network connections. + */ + @JvmStatic + fun enableProxy(proxy: SignalProxy) { + SignalStore.proxy.enableProxy(proxy) + ConscryptSignal.setUseEngineSocketByDefault(true) + AppDependencies.resetNetwork() + startListeningToWebsocket() + } + + /** + * Handles all things related to disabling a proxy, including saving the change and resetting the + * relevant network connections. + */ + @JvmStatic + fun disableProxy() { + SignalStore.proxy.disableProxy() + ConscryptSignal.setUseEngineSocketByDefault(false) + AppDependencies.resetNetwork() + startListeningToWebsocket() + } + + @JvmStatic + fun disableAndClearProxy() { + disableProxy() + SignalStore.proxy.proxy = null + } + + /** + * A blocking call that will wait until the websocket either successfully connects, or fails. + * It is assumed that the app state is already configured how you would like it, e.g. you've + * already configured a proxy if relevant. + * + * @return True if the connection is successful within the specified timeout, otherwise false. + */ + @JvmStatic + @WorkerThread + fun testWebsocketConnection(timeout: Long): Boolean { + startListeningToWebsocket() + + if (SignalStore.account.e164 == null) { + Log.i(TAG, "User is unregistered! Doing simple check.") + return testWebsocketConnectionUnregistered(timeout) + } + + return AppDependencies.authWebSocket + .state + .subscribeOn(Schedulers.trampoline()) + .observeOn(Schedulers.trampoline()) + .timeout(timeout, TimeUnit.MILLISECONDS) + .skipWhile(Predicate { state: WebSocketConnectionState -> state != WebSocketConnectionState.CONNECTED && !state.isFailure }) + .firstOrError() + .flatMap(Function { state: WebSocketConnectionState? -> Single.just(state == WebSocketConnectionState.CONNECTED) }) + .onErrorReturn(Function { _: Throwable? -> false }) + .blockingGet() + } + + /** + * If this is a valid proxy deep link, this will return the embedded host. If not, it will return + * null. + */ + @JvmStatic + fun parseHostFromProxyDeepLink(proxyLink: String?): String? { + if (proxyLink == null) { + return null + } + + val matcher = PROXY_LINK_PATTERN.matcher(proxyLink) + + return when { + matcher.matches() -> matcher.group(2) + else -> null + } + } + + /** + * Takes in an address that could be in various formats, and converts it to the format we should + * be storing and connecting to. + */ + @JvmStatic + fun convertUserEnteredAddressToHost(host: String): String { + val parsedHost = parseHostFromProxyDeepLink(host) + if (parsedHost != null) { + return parsedHost + } + + val matcher = HOST_PATTERN.matcher(host) + + return when { + matcher.matches() -> matcher.group(1) ?: "" + else -> host + } + } + + @JvmStatic + fun generateProxyUrl(link: String): String { + var host: String = link + val parsed = parseHostFromProxyDeepLink(link) + + if (parsed != null) { + host = parsed + } + + val matcher = HOST_PATTERN.matcher(host) + + if (matcher.matches()) { + host = matcher.group(1)!! + } + + return "https://$PROXY_LINK_HOST/#$host" + } + + private fun testWebsocketConnectionUnregistered(timeout: Long): Boolean { + val latch = CountDownLatch(1) + val success = AtomicBoolean(false) + val accountManager = AccountManagerFactory.getInstance() + .createUnauthenticated(AppDependencies.application, "", SignalServiceAddress.DEFAULT_DEVICE_ID, "") + + SignalExecutors.UNBOUNDED.execute { + try { + accountManager.checkNetworkConnection() + success.set(true) + latch.countDown() + } catch (_: IOException) { + latch.countDown() + } + } + + try { + latch.await(timeout, TimeUnit.MILLISECONDS) + } catch (e: InterruptedException) { + w(TAG, "Interrupted!", e) + } + + return success.get() + } +}