Convert SignalProxyUtil from Java to Kotlin.

Resolves #14453
This commit is contained in:
Jesse Wilson
2025-11-24 11:50:53 -05:00
committed by jeffrey-signal
parent 37f67f9717
commit 38bc2b950f
2 changed files with 181 additions and 181 deletions

View File

@@ -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();
}
}

View File

@@ -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<Boolean>(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()
}
}