From: AsamK Date: Sun, 16 Mar 2025 21:06:58 +0000 (+0100) Subject: Update libsignal-service X-Git-Tag: v0.13.14~10 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/f26a0d2891d0702bb6f5ed1e7cf9ddea839dfdb5 Update libsignal-service --- diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d0ac967c..b8d57a12 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" } logback = "ch.qos.logback:logback-classic:1.5.17" -signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_118" +signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_119" sqlite = "org.xerial:sqlite-jdbc:3.49.1.0" hikari = "com.zaxxer:HikariCP:6.2.1" junit-jupiter = "org.junit.jupiter:junit-jupiter:5.12.0" diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index bdb3dbc9..affbaa9d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -1,5 +1,7 @@ package org.asamk.signal.manager; +import com.google.i18n.phonenumbers.PhoneNumberUtil; + import org.asamk.signal.manager.api.AlreadyReceivingException; import org.asamk.signal.manager.api.AttachmentInvalidException; import org.asamk.signal.manager.api.CaptchaRejectedException; @@ -49,7 +51,6 @@ import org.asamk.signal.manager.api.UsernameStatus; import org.asamk.signal.manager.api.VerificationMethodNotAvailableException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import java.io.Closeable; import java.io.File; @@ -65,7 +66,7 @@ import java.util.Set; public interface Manager extends Closeable { static boolean isValidNumber(final String e164Number, final String countryCode) { - return PhoneNumberFormatter.isValidNumber(e164Number, countryCode); + return PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode); } static boolean isSignalClientAvailable() { diff --git a/lib/src/main/java/org/asamk/signal/manager/api/InvalidNumberException.java b/lib/src/main/java/org/asamk/signal/manager/api/InvalidNumberException.java index 8e6a8064..14f37966 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/InvalidNumberException.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/InvalidNumberException.java @@ -2,7 +2,7 @@ package org.asamk.signal.manager.api; public class InvalidNumberException extends Exception { - InvalidNumberException(String message) { + public InvalidNumberException(String message) { super(message); } diff --git a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java index 53da2aad..b1a3ad61 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java @@ -1,8 +1,8 @@ package org.asamk.signal.manager.api; +import org.asamk.signal.manager.util.PhoneNumberFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.UuidUtil; import java.util.UUID; @@ -24,32 +24,28 @@ public sealed interface RecipientIdentifier { sealed interface Single extends RecipientIdentifier { static Single fromString(String identifier, String localNumber) throws InvalidNumberException { - try { - if (UuidUtil.isUuid(identifier)) { - return new Uuid(UUID.fromString(identifier)); - } + if (UuidUtil.isUuid(identifier)) { + return new Uuid(UUID.fromString(identifier)); + } - if (identifier.startsWith("PNI:")) { - final var pni = identifier.substring(4); - if (!UuidUtil.isUuid(pni)) { - throw new InvalidNumberException("Invalid PNI"); - } - return new Pni(UUID.fromString(pni)); + if (identifier.startsWith("PNI:")) { + final var pni = identifier.substring(4); + if (!UuidUtil.isUuid(pni)) { + throw new InvalidNumberException("Invalid PNI"); } + return new Pni(UUID.fromString(pni)); + } - if (identifier.startsWith("u:")) { - return new Username(identifier.substring(2)); - } + if (identifier.startsWith("u:")) { + return new Username(identifier.substring(2)); + } - final var normalizedNumber = PhoneNumberFormatter.formatNumber(identifier, localNumber); - if (!normalizedNumber.equals(identifier)) { - final Logger logger = LoggerFactory.getLogger(RecipientIdentifier.class); - logger.debug("Normalized number {} to {}.", identifier, normalizedNumber); - } - return new Number(normalizedNumber); - } catch (org.whispersystems.signalservice.api.util.InvalidNumberException e) { - throw new InvalidNumberException(e.getMessage(), e); + final var normalizedNumber = PhoneNumberFormatter.formatNumber(identifier, localNumber); + if (!normalizedNumber.equals(identifier)) { + final Logger logger = LoggerFactory.getLogger(RecipientIdentifier.class); + logger.debug("Normalized number {} to {}.", identifier, normalizedNumber); } + return new Number(normalizedNumber); } static Single fromAddress(RecipientAddress address) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java index 0710b50c..7d40f99c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java @@ -32,6 +32,7 @@ import org.whispersystems.signalservice.api.push.ServiceId.PNI; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignedPreKeyEntity; +import org.whispersystems.signalservice.api.push.UsernameLinkComponents; import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; @@ -50,7 +51,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Objects; -import java.util.Optional; +import java.util.UUID; import java.util.concurrent.TimeUnit; import okio.ByteString; @@ -289,12 +290,13 @@ public class AccountHelper { context.getPinHelper(), (sessionId1, verificationCode1, registrationLock) -> { final var registrationApi = dependencies.getRegistrationApi(); + final var accountApi = dependencies.getAccountApi(); try { handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1)); } catch (AlreadyVerifiedException e) { // Already verified so can continue changing number } - return handleResponseException(registrationApi.changeNumber(new ChangePhoneNumberRequest(sessionId1, + return handleResponseException(accountApi.changeNumber(new ChangePhoneNumberRequest(sessionId1, null, newNumber, registrationLock, @@ -378,7 +380,7 @@ public class AccountHelper { candidateHashes.add(Base64.encodeUrlSafeWithoutPadding(candidate.getHash())); } - final var response = dependencies.getAccountManager().reserveUsername(candidateHashes); + final var response = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes)); final var hashIndex = candidateHashes.indexOf(response.getUsernameHash()); if (hashIndex == -1) { logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes."); @@ -388,7 +390,7 @@ public class AccountHelper { logger.debug("[reserveUsername] Successfully reserved username."); final var username = candidates.get(hashIndex); - final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username); + final var linkComponents = confirmUsernameAndCreateNewLink(username); account.setUsername(username.getUsername()); account.setUsernameLink(linkComponents); account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress()); @@ -396,6 +398,40 @@ public class AccountHelper { logger.debug("[confirmUsername] Successfully confirmed username."); } + public UsernameLinkComponents createUsernameLink(Username username) throws IOException { + try { + Username.UsernameLink link = username.generateLink(); + return handleResponseException(dependencies.getAccountApi().createUsernameLink(link)); + } catch (BaseUsernameException e) { + throw new AssertionError(e); + } + } + + private UsernameLinkComponents confirmUsernameAndCreateNewLink(Username username) throws IOException { + try { + Username.UsernameLink link = username.generateLink(); + UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link)); + + return new UsernameLinkComponents(link.getEntropy(), serverId); + } catch (BaseUsernameException e) { + throw new AssertionError(e); + } + } + + private UsernameLinkComponents reclaimUsernameAndLink( + Username username, + UsernameLinkComponents linkComponents + ) throws IOException { + try { + Username.UsernameLink link = username.generateLink(linkComponents.getEntropy()); + UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link)); + + return new UsernameLinkComponents(link.getEntropy(), serverId); + } catch (BaseUsernameException e) { + throw new AssertionError(e); + } + } + public void refreshCurrentUsername() throws IOException, BaseUsernameException { final var localUsername = account.getUsername(); if (localUsername == null) { @@ -438,14 +474,14 @@ public class AccountHelper { final var usernameLink = account.getUsernameLink(); if (usernameLink == null) { - dependencies.getAccountManager() - .reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash()))); + handleResponseException(dependencies.getAccountApi() + .reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash())))); logger.debug("[reserveUsername] Successfully reserved existing username."); - final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username); + final var linkComponents = confirmUsernameAndCreateNewLink(username); account.setUsernameLink(linkComponents); logger.debug("[confirmUsername] Successfully confirmed existing username."); } else { - final var linkComponents = dependencies.getAccountManager().reclaimUsernameAndLink(username, usernameLink); + final var linkComponents = reclaimUsernameAndLink(username, usernameLink); account.setUsernameLink(linkComponents); logger.debug("[confirmUsername] Successfully reclaimed existing username and link."); } @@ -455,7 +491,7 @@ public class AccountHelper { private void tryToSetUsernameLink(Username username) { for (var i = 1; i < 4; i++) { try { - final var linkComponents = dependencies.getAccountManager().createUsernameLink(username); + final var linkComponents = createUsernameLink(username); account.setUsernameLink(linkComponents); break; } catch (IOException e) { @@ -465,9 +501,8 @@ public class AccountHelper { } public void deleteUsername() throws IOException { - dependencies.getAccountManager().deleteUsernameLink(); + handleResponseException(dependencies.getAccountApi().deleteUsername()); account.setUsernameLink(null); - dependencies.getAccountManager().deleteUsername(); account.setUsername(null); logger.debug("[deleteUsername] Successfully deleted the username."); } @@ -479,7 +514,7 @@ public class AccountHelper { } public void updateAccountAttributes() throws IOException { - dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null)); + handleResponseException(dependencies.getAccountApi().setAccountAttributes(account.getAccountAttributes(null))); } public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, org.asamk.signal.manager.api.DeviceLimitExceededException { @@ -510,8 +545,8 @@ public class AccountHelper { } public void removeLinkedDevices(int deviceId) throws IOException { - dependencies.getAccountManager().removeDevice(deviceId); - var devices = dependencies.getAccountManager().getDevices(); + handleResponseException(dependencies.getLinkDeviceApi().removeDevice(deviceId)); + var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices()); account.setMultiDevice(devices.size() > 1); } @@ -519,14 +554,16 @@ public class AccountHelper { var masterKey = account.getOrCreatePinMasterKey(); context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey); - dependencies.getAccountManager().enableRegistrationLock(masterKey); + handleResponseException(dependencies.getAccountApi() + .enableRegistrationLock(masterKey.deriveRegistrationLock())); } public void setRegistrationPin(String pin) throws IOException { var masterKey = account.getOrCreatePinMasterKey(); context.getPinHelper().setRegistrationLockPin(pin, masterKey); - dependencies.getAccountManager().enableRegistrationLock(masterKey); + handleResponseException(dependencies.getAccountApi() + .enableRegistrationLock(masterKey.deriveRegistrationLock())); account.setRegistrationLockPin(pin); updateAccountAttributes(); @@ -535,7 +572,7 @@ public class AccountHelper { public void removeRegistrationPin() throws IOException { // Remove KBS Pin context.getPinHelper().removeRegistrationLockPin(); - dependencies.getAccountManager().disableRegistrationLock(); + handleResponseException(dependencies.getAccountApi().disableRegistrationLock()); account.setRegistrationLockPin(null); } @@ -544,7 +581,7 @@ public class AccountHelper { // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. // If this is the primary device, other users can't send messages to this number anymore. // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore. - dependencies.getAccountManager().setGcmId(Optional.empty()); + handleResponseException(dependencies.getAccountApi().clearFcmToken()); account.setRegistered(false); unregisteredListener.call(); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java index ab75bd16..eb935188 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java @@ -11,10 +11,10 @@ import org.asamk.signal.manager.storage.messageCache.CachedMessage; import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.SignalWebSocket; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId.ACI; +import org.whispersystems.signalservice.api.websocket.SignalWebSocket; import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState; import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException; @@ -28,7 +28,6 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.TimeoutException; -import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.schedulers.Schedulers; public class ReceiveHelper { @@ -94,9 +93,8 @@ public class ReceiveHelper { // Use a Map here because java Set doesn't have a get method ... Map queuedActions = new HashMap<>(); - final var signalWebSocket = dependencies.getSignalWebSocket(); - final var webSocketStateDisposable = Observable.merge(signalWebSocket.getUnidentifiedWebSocketState(), - signalWebSocket.getWebSocketState()) + final var signalWebSocket = dependencies.getAuthenticatedSignalWebSocket(); + final var webSocketStateDisposable = signalWebSocket.getState() .subscribeOn(Schedulers.computation()) .observeOn(Schedulers.computation()) .distinctUntilChanged() @@ -116,7 +114,7 @@ public class ReceiveHelper { } private void receiveMessagesInternal( - final SignalWebSocket signalWebSocket, + final SignalWebSocket.AuthenticatedWebSocket signalWebSocket, Duration timeout, boolean returnOnTimeout, Integer maxMessages, diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java index 125fa0d8..58e7ba59 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java @@ -10,13 +10,13 @@ import org.signal.libsignal.usernames.BaseUsernameException; import org.signal.libsignal.usernames.Username; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.cds.CdsiV2Service; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.PNI; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException; import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException; -import org.whispersystems.signalservice.api.services.CdsiV2Service; import java.io.IOException; import java.util.Collection; @@ -27,6 +27,7 @@ import java.util.Optional; import java.util.Set; import static org.asamk.signal.manager.config.ServiceConfig.MAXIMUM_ONE_OFF_REQUEST_SIZE; +import static org.asamk.signal.manager.util.Utils.handleResponseException; public class RecipientHelper { @@ -108,7 +109,7 @@ public class RecipientHelper { } if (forceRefresh) { try { - final var aci = dependencies.getAccountManager().getAciByUsername(finalUsername); + final var aci = handleResponseException(dependencies.getUsernameApi().getAciByUsername(finalUsername)); return account.getRecipientStore().resolveRecipientTrusted(aci, finalUsername.getUsername()); } catch (IOException e) { throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null, @@ -119,7 +120,7 @@ public class RecipientHelper { } return account.getRecipientStore().resolveRecipientByUsername(finalUsername.getUsername(), () -> { try { - return dependencies.getAccountManager().getAciByUsername(finalUsername); + return handleResponseException(dependencies.getUsernameApi().getAciByUsername(finalUsername)); } catch (Exception e) { return null; } @@ -130,8 +131,8 @@ public class RecipientHelper { try { final var usernameLinkUrl = UsernameLinkUrl.fromUri(username); final var components = usernameLinkUrl.getComponents(); - final var encryptedUsername = dependencies.getAccountManager() - .getEncryptedUsernameFromLinkServerId(components.getServerId()); + final var encryptedUsername = handleResponseException(dependencies.getUsernameApi() + .getEncryptedUsernameFromLinkServerId(components.getServerId())); final var link = new Username.UsernameLink(components.getEntropy(), encryptedUsername); return Username.fromLink(link); @@ -234,13 +235,14 @@ public class RecipientHelper { final CdsiV2Service.Response response; try { - response = dependencies.getAccountManager() - .getRegisteredUsersWithCdsi(token.isEmpty() ? Set.of() : previousNumbers, + response = handleResponseException(dependencies.getCdsApi() + .getRegisteredUsers(token.isEmpty() ? Set.of() : previousNumbers, newNumbers, account.getRecipientStore().getServiceIdToProfileKeyMap(), token, null, dependencies.getLibSignalNetwork(), + false, newToken -> { if (isPartialRefresh) { account.getCdsiStore().updateAfterPartialCdsQuery(newNumbers); @@ -256,7 +258,7 @@ public class RecipientHelper { account.setCdsiToken(newToken); account.setLastRecipientsRefresh(System.currentTimeMillis()); } - }); + })); } catch (CdsiInvalidTokenException | CdsiInvalidArgumentException e) { account.setCdsiToken(null); account.getCdsiStore().clearAll(); diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java index 403b9adf..c5a7552f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java @@ -35,6 +35,7 @@ import org.asamk.signal.manager.api.IdentityVerificationCode; import org.asamk.signal.manager.api.InactiveGroupLinkException; import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.InvalidDeviceLinkException; +import org.asamk.signal.manager.api.InvalidNumberException; import org.asamk.signal.manager.api.InvalidStickerException; import org.asamk.signal.manager.api.InvalidUsernameException; import org.asamk.signal.manager.api.LastGroupAdminException; @@ -87,12 +88,12 @@ import org.asamk.signal.manager.storage.stickers.StickerPack; import org.asamk.signal.manager.util.AttachmentUtils; import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.MimeUtils; +import org.asamk.signal.manager.util.PhoneNumberFormatter; import org.asamk.signal.manager.util.StickerUtils; import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.usernames.BaseUsernameException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServicePreview; @@ -106,8 +107,6 @@ import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhauste import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException; import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; -import org.whispersystems.signalservice.api.util.InvalidNumberException; -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.Util; @@ -132,7 +131,6 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.ReentrantLock; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -142,6 +140,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers; import okio.Utf8; import static org.asamk.signal.manager.config.ServiceConfig.MAX_MESSAGE_SIZE_BYTES; +import static org.asamk.signal.manager.util.Utils.handleResponseException; import static org.signal.core.util.StringExtensionsKt.splitByByteLength; public class ManagerImpl implements Manager { @@ -171,15 +170,7 @@ public class ManagerImpl implements Manager { ) { this.account = account; - final var sessionLock = new SignalSessionLock() { - private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); - - @Override - public Lock acquire() { - LEGACY_LOCK.lock(); - return LEGACY_LOCK::unlock; - } - }; + final var sessionLock = new ReentrantSignalSessionLock(); this.dependencies = new SignalDependencies(serviceEnvironmentConfig, userAgent, account.getCredentialsProvider(), @@ -457,10 +448,10 @@ public class ManagerImpl implements Manager { String challenge, String captcha ) throws IOException, CaptchaRejectedException { - captcha = captcha == null ? null : captcha.replace("signalcaptcha://", ""); + captcha = captcha == null ? "" : captcha.replace("signalcaptcha://", ""); try { - dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha); + handleResponseException(dependencies.getRateLimitChallengeApi().submitCaptchaChallenge(challenge, captcha)); } catch (org.whispersystems.signalservice.internal.push.exceptions.CaptchaRejectedException ignored) { throw new CaptchaRejectedException(); } @@ -468,7 +459,7 @@ public class ManagerImpl implements Manager { @Override public List getLinkedDevices() throws IOException { - var devices = dependencies.getAccountManager().getDevices(); + var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices()); account.setMultiDevice(devices.size() > 1); var identityKey = account.getAciIdentityKeyPair().getPrivateKey(); return devices.stream().map(d -> { @@ -1594,7 +1585,8 @@ public class ManagerImpl implements Manager { context.close(); executor.close(); - dependencies.getSignalWebSocket().disconnect(); + dependencies.getAuthenticatedSignalWebSocket().disconnect(); + dependencies.getUnauthenticatedSignalWebSocket().disconnect(); dependencies.getPushServiceSocket().close(); disposable.dispose(); diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/ProvisioningManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/internal/ProvisioningManagerImpl.java index fcef536a..e5a381e1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/ProvisioningManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/ProvisioningManagerImpl.java @@ -29,12 +29,11 @@ import org.asamk.signal.manager.util.KeyUtils; import org.signal.libsignal.protocol.IdentityKeyPair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; -import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; +import org.whispersystems.signalservice.api.registration.ProvisioningApi; import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.internal.push.ProvisioningSocket; import org.whispersystems.signalservice.internal.push.PushServiceSocket; @@ -58,7 +57,7 @@ public class ProvisioningManagerImpl implements ProvisioningManager { private final Consumer newManagerListener; private final AccountsStore accountsStore; - private final SignalServiceAccountManager accountManager; + private final ProvisioningApi provisioningApi; private final IdentityKeyPair tempIdentityKey; private final String password; @@ -78,7 +77,6 @@ public class ProvisioningManagerImpl implements ProvisioningManager { tempIdentityKey = KeyUtils.generateIdentityKeyPair(); password = KeyUtils.createPassword(); final var clientZkOperations = ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration()); - final var groupsV2Operations = new GroupsV2Operations(clientZkOperations, ServiceConfig.GROUP_MAX_SIZE); final var credentialsProvider = new DynamicCredentialsProvider(null, null, null, @@ -89,21 +87,21 @@ public class ProvisioningManagerImpl implements ProvisioningManager { userAgent, clientZkOperations.getProfileOperations(), ServiceConfig.AUTOMATIC_NETWORK_RETRY); - accountManager = new SignalServiceAccountManager(pushServiceSocket, - new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(), userAgent), - groupsV2Operations); + final var provisioningSocket = new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(), + userAgent); + this.provisioningApi = new ProvisioningApi(pushServiceSocket, provisioningSocket, credentialsProvider); } @Override public URI getDeviceLinkUri() throws TimeoutException, IOException { - var deviceUuid = accountManager.getNewDeviceUuid(); + var deviceUuid = provisioningApi.getNewDeviceUuid(); return new DeviceLinkUrl(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri(); } @Override public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException { - var ret = accountManager.getNewDeviceRegistration(tempIdentityKey); + var ret = provisioningApi.getNewDeviceRegistration(tempIdentityKey); var number = ret.getNumber(); var aci = ret.getAci(); var pni = ret.getPni(); @@ -160,7 +158,7 @@ public class ProvisioningManagerImpl implements ProvisioningManager { final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI)); logger.debug("Finishing new device registration"); - var deviceId = accountManager.finishNewDeviceRegistration(ret.getProvisioningCode(), + var deviceId = provisioningApi.finishNewDeviceRegistration(ret.getProvisioningCode(), account.getAccountAttributes(null), aciPreKeys, pniPreKeys); diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/ReentrantSignalSessionLock.java b/lib/src/main/java/org/asamk/signal/manager/internal/ReentrantSignalSessionLock.java new file mode 100644 index 00000000..78304469 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/internal/ReentrantSignalSessionLock.java @@ -0,0 +1,16 @@ +package org.asamk.signal.manager.internal; + +import org.whispersystems.signalservice.api.SignalSessionLock; + +import java.util.concurrent.locks.ReentrantLock; + +class ReentrantSignalSessionLock implements SignalSessionLock { + + private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); + + @Override + public Lock acquire() { + LEGACY_LOCK.lock(); + return LEGACY_LOCK::unlock; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java index 5faf1371..e103fec3 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java @@ -32,14 +32,11 @@ import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.NumberVerificationUtils; -import org.asamk.signal.manager.util.Utils; import org.signal.libsignal.usernames.BaseUsernameException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.account.PreKeyCollection; -import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; -import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.PNI; @@ -48,13 +45,13 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; import org.whispersystems.signalservice.api.svr.SecureValueRecovery; -import org.whispersystems.signalservice.internal.push.PushServiceSocket; import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; import java.io.IOException; import java.util.function.Consumer; import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType; +import static org.asamk.signal.manager.util.Utils.handleResponseException; public class RegistrationManagerImpl implements RegistrationManager { @@ -199,7 +196,7 @@ public class RegistrationManagerImpl implements RegistrationManager { final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI)); final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI)); final var registrationApi = unauthenticatedAccountManager.getRegistrationApi(); - final var response = Utils.handleResponseException(registrationApi.registerAccount(null, + final var response = handleResponseException(registrationApi.registerAccount(null, recoveryPassword, account.getAccountAttributes(null), aciPreKeys, @@ -221,8 +218,14 @@ public class RegistrationManagerImpl implements RegistrationManager { private boolean attemptReactivateAccount() { try { - final var accountManager = createAuthenticatedSignalServiceAccountManager(); - accountManager.setAccountAttributes(account.getAccountAttributes(null)); + final var dependencies = new SignalDependencies(serviceEnvironmentConfig, + userAgent, + account.getCredentialsProvider(), + account.getSignalServiceDataStore(), + null, + new ReentrantSignalSessionLock()); + handleResponseException(dependencies.getAccountApi() + .setAccountAttributes(account.getAccountAttributes(null))); account.setRegistered(true); logger.info("Reactivated existing account, verify is not necessary."); if (newManagerListener != null) { @@ -241,17 +244,6 @@ public class RegistrationManagerImpl implements RegistrationManager { return false; } - private SignalServiceAccountManager createAuthenticatedSignalServiceAccountManager() { - final var clientZkOperations = ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration()); - final var pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(), - account.getCredentialsProvider(), - userAgent, - clientZkOperations.getProfileOperations(), - ServiceConfig.AUTOMATIC_NETWORK_RETRY); - final var groupsV2Operations = new GroupsV2Operations(clientZkOperations, ServiceConfig.GROUP_MAX_SIZE); - return new SignalServiceAccountManager(pushServiceSocket, null, groupsV2Operations); - } - private VerifyAccountResponse verifyAccountWithCode( final String sessionId, final String verificationCode, @@ -261,11 +253,11 @@ public class RegistrationManagerImpl implements RegistrationManager { ) throws IOException { final var registrationApi = unauthenticatedAccountManager.getRegistrationApi(); try { - Utils.handleResponseException(registrationApi.verifyAccount(sessionId, verificationCode)); + handleResponseException(registrationApi.verifyAccount(sessionId, verificationCode)); } catch (AlreadyVerifiedException e) { // Already verified so can continue registering } - return Utils.handleResponseException(registrationApi.registerAccount(sessionId, + return handleResponseException(registrationApi.registerAccount(sessionId, null, account.getAccountAttributes(registrationLock), aciPreKeys, diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java index 427c1025..972b422f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java @@ -13,7 +13,8 @@ import org.whispersystems.signalservice.api.SignalServiceDataStore; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalSessionLock; -import org.whispersystems.signalservice.api.SignalWebSocket; +import org.whispersystems.signalservice.api.account.AccountApi; +import org.whispersystems.signalservice.api.cds.CdsApi; import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; @@ -21,18 +22,18 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.link.LinkDeviceApi; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi; import org.whispersystems.signalservice.api.registration.RegistrationApi; import org.whispersystems.signalservice.api.services.ProfileService; import org.whispersystems.signalservice.api.storage.StorageServiceApi; import org.whispersystems.signalservice.api.storage.StorageServiceRepository; import org.whispersystems.signalservice.api.svr.SecureValueRecovery; +import org.whispersystems.signalservice.api.username.UsernameApi; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; -import org.whispersystems.signalservice.api.websocket.WebSocketFactory; -import org.whispersystems.signalservice.internal.push.ProvisioningSocket; +import org.whispersystems.signalservice.api.websocket.SignalWebSocket; import org.whispersystems.signalservice.internal.push.PushServiceSocket; import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection; -import org.whispersystems.signalservice.internal.websocket.WebSocketConnection; import java.io.IOException; import java.net.InetSocketAddress; @@ -58,6 +59,10 @@ public class SignalDependencies { private boolean allowStories = true; private SignalServiceAccountManager accountManager; + private AccountApi accountApi; + private RateLimitChallengeApi rateLimitChallengeApi; + private CdsApi cdsApi; + private UsernameApi usernameApi; private GroupsV2Api groupsV2Api; private RegistrationApi registrationApi; private LinkDeviceApi linkDeviceApi; @@ -66,9 +71,9 @@ public class SignalDependencies { private ClientZkOperations clientZkOperations; private PushServiceSocket pushServiceSocket; - private ProvisioningSocket provisioningSocket; private Network libSignalNetwork; - private SignalWebSocket signalWebSocket; + private SignalWebSocket.AuthenticatedWebSocket authenticatedSignalWebSocket; + private SignalWebSocket.UnauthenticatedWebSocket unauthenticatedSignalWebSocket; private SignalServiceMessageReceiver messageReceiver; private SignalServiceMessageSender messageSender; @@ -103,7 +108,12 @@ public class SignalDependencies { this.registrationApi = null; this.secureValueRecovery = null; } - getSignalWebSocket().forceNewWebSockets(); + if (this.authenticatedSignalWebSocket != null) { + this.authenticatedSignalWebSocket.forceNewWebSocket(); + } + if (this.unauthenticatedSignalWebSocket != null) { + this.unauthenticatedSignalWebSocket.forceNewWebSocket(); + } } /** @@ -130,12 +140,6 @@ public class SignalDependencies { ServiceConfig.AUTOMATIC_NETWORK_RETRY)); } - public ProvisioningSocket getProvisioningSocket() { - return getOrCreate(() -> provisioningSocket, - () -> provisioningSocket = new ProvisioningSocket(getServiceEnvironmentConfig().signalServiceConfiguration(), - userAgent)); - } - public Network getLibSignalNetwork() { return getOrCreate(() -> libSignalNetwork, () -> { libSignalNetwork = new Network(serviceEnvironmentConfig.netEnvironment(), userAgent); @@ -169,8 +173,8 @@ public class SignalDependencies { public SignalServiceAccountManager getAccountManager() { return getOrCreate(() -> accountManager, - () -> accountManager = new SignalServiceAccountManager(getPushServiceSocket(), - getProvisioningSocket(), + () -> accountManager = new SignalServiceAccountManager(getAccountApi(), + getPushServiceSocket(), getGroupsV2Operations())); } @@ -186,6 +190,23 @@ public class SignalDependencies { ServiceConfig.GROUP_MAX_SIZE); } + public AccountApi getAccountApi() { + return getOrCreate(() -> accountApi, () -> accountApi = new AccountApi(getAuthenticatedSignalWebSocket())); + } + + public RateLimitChallengeApi getRateLimitChallengeApi() { + return getOrCreate(() -> rateLimitChallengeApi, + () -> rateLimitChallengeApi = new RateLimitChallengeApi(getAuthenticatedSignalWebSocket())); + } + + public CdsApi getCdsApi() { + return getOrCreate(() -> cdsApi, () -> cdsApi = new CdsApi(getAuthenticatedSignalWebSocket())); + } + + public UsernameApi getUsernameApi() { + return getOrCreate(() -> usernameApi, () -> usernameApi = new UsernameApi(getUnauthenticatedSignalWebSocket())); + } + public GroupsV2Api getGroupsV2Api() { return getOrCreate(() -> groupsV2Api, () -> groupsV2Api = getAccountManager().getGroupsV2Api()); } @@ -195,12 +216,14 @@ public class SignalDependencies { } public LinkDeviceApi getLinkDeviceApi() { - return getOrCreate(() -> linkDeviceApi, () -> linkDeviceApi = new LinkDeviceApi(getPushServiceSocket())); + return getOrCreate(() -> linkDeviceApi, + () -> linkDeviceApi = new LinkDeviceApi(getAuthenticatedSignalWebSocket())); } private StorageServiceApi getStorageServiceApi() { return getOrCreate(() -> storageServiceApi, - () -> storageServiceApi = new StorageServiceApi(getPushServiceSocket())); + () -> storageServiceApi = new StorageServiceApi(getAuthenticatedSignalWebSocket(), + getPushServiceSocket())); } public StorageServiceRepository getStorageServiceRepository() { @@ -223,33 +246,35 @@ public class SignalDependencies { return clientZkOperations.getProfileOperations(); } - public SignalWebSocket getSignalWebSocket() { - return getOrCreate(() -> signalWebSocket, () -> { + public SignalWebSocket.AuthenticatedWebSocket getAuthenticatedSignalWebSocket() { + return getOrCreate(() -> authenticatedSignalWebSocket, () -> { final var timer = new UptimeSleepTimer(); final var healthMonitor = new SignalWebSocketHealthMonitor(timer); - final var webSocketFactory = new WebSocketFactory() { - @Override - public WebSocketConnection createWebSocket() { - return new OkHttpWebSocketConnection("normal", - serviceEnvironmentConfig.signalServiceConfiguration(), - Optional.of(credentialsProvider), - userAgent, - healthMonitor, - allowStories); - } - @Override - public WebSocketConnection createUnidentifiedWebSocket() { - return new OkHttpWebSocketConnection("unidentified", - serviceEnvironmentConfig.signalServiceConfiguration(), - Optional.empty(), - userAgent, - healthMonitor, - allowStories); - } - }; - signalWebSocket = new SignalWebSocket(webSocketFactory); - healthMonitor.monitor(signalWebSocket); + authenticatedSignalWebSocket = new SignalWebSocket.AuthenticatedWebSocket(() -> new OkHttpWebSocketConnection( + "normal", + serviceEnvironmentConfig.signalServiceConfiguration(), + Optional.of(credentialsProvider), + userAgent, + healthMonitor, + allowStories)); + healthMonitor.monitor(authenticatedSignalWebSocket); + }); + } + + public SignalWebSocket.UnauthenticatedWebSocket getUnauthenticatedSignalWebSocket() { + return getOrCreate(() -> unauthenticatedSignalWebSocket, () -> { + final var timer = new UptimeSleepTimer(); + final var healthMonitor = new SignalWebSocketHealthMonitor(timer); + + unauthenticatedSignalWebSocket = new SignalWebSocket.UnauthenticatedWebSocket(() -> new OkHttpWebSocketConnection( + "unidentified", + serviceEnvironmentConfig.signalServiceConfiguration(), + Optional.empty(), + userAgent, + healthMonitor, + allowStories)); + healthMonitor.monitor(unauthenticatedSignalWebSocket); }); } @@ -263,7 +288,8 @@ public class SignalDependencies { () -> messageSender = new SignalServiceMessageSender(getPushServiceSocket(), dataStore, sessionLock, - getSignalWebSocket(), + getAuthenticatedSignalWebSocket(), + getUnauthenticatedSignalWebSocket(), Optional.empty(), executor, ServiceConfig.MAX_ENVELOPE_SIZE)); @@ -281,7 +307,8 @@ public class SignalDependencies { return getOrCreate(() -> profileService, () -> profileService = new ProfileService(getClientZkProfileOperations(), getMessageReceiver(), - getSignalWebSocket())); + getAuthenticatedSignalWebSocket(), + getUnauthenticatedSignalWebSocket())); } public SignalServiceCipher getCipher(ServiceIdType serviceIdType) { diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java b/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java index 0fb1585a..b5b855ac 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java @@ -2,195 +2,155 @@ package org.asamk.signal.manager.internal; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.SignalWebSocket; import org.whispersystems.signalservice.api.util.Preconditions; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.websocket.HealthMonitor; +import org.whispersystems.signalservice.api.websocket.SignalWebSocket; import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState; import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection; -import java.util.Arrays; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import io.reactivex.rxjava3.schedulers.Schedulers; +import kotlin.Unit; -/** - * Monitors the health of the identified and unidentified WebSockets. If either one appears to be - * unhealthy, will trigger restarting both. - *

- * The monitor is also responsible for sending heartbeats/keep-alive messages to prevent - * timeouts. - */ final class SignalWebSocketHealthMonitor implements HealthMonitor { private static final Logger logger = LoggerFactory.getLogger(SignalWebSocketHealthMonitor.class); + /** + * This is the amount of time in between sent keep alives. Must be greater than [KEEP_ALIVE_TIMEOUT] + */ private static final long KEEP_ALIVE_SEND_CADENCE = TimeUnit.SECONDS.toMillis(OkHttpWebSocketConnection.KEEPALIVE_FREQUENCY_SECONDS); - private static final long MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE = KEEP_ALIVE_SEND_CADENCE * 3; - - private SignalWebSocket signalWebSocket; - private final SleepTimer sleepTimer; - private volatile KeepAliveSender keepAliveSender; + /** + * This is the amount of time we will wait for a response to the keep alive before we consider the websockets dead. + * It is required that this value be less than [KEEP_ALIVE_SEND_CADENCE] + */ + private static final long KEEP_ALIVE_TIMEOUT = TimeUnit.SECONDS.toMillis(20); - private final HealthState identified = new HealthState(); - private final HealthState unidentified = new HealthState(); + private final Executor executor = Executors.newSingleThreadExecutor(); + private final SleepTimer sleepTimer; + private SignalWebSocket webSocket = null; + private volatile KeepAliveSender keepAliveSender = null; + private boolean needsKeepAlive = false; + private long lastKeepAliveReceived = 0; public SignalWebSocketHealthMonitor(SleepTimer sleepTimer) { this.sleepTimer = sleepTimer; } - public void monitor(SignalWebSocket signalWebSocket) { - Preconditions.checkNotNull(signalWebSocket); - Preconditions.checkArgument(this.signalWebSocket == null, "monitor can only be called once"); - - this.signalWebSocket = signalWebSocket; - - //noinspection ResultOfMethodCallIgnored - signalWebSocket.getWebSocketState() - .subscribeOn(Schedulers.computation()) - .observeOn(Schedulers.computation()) - .distinctUntilChanged() - .subscribe(s -> onStateChange(s, identified)); - - //noinspection ResultOfMethodCallIgnored - signalWebSocket.getUnidentifiedWebSocketState() - .subscribeOn(Schedulers.computation()) - .observeOn(Schedulers.computation()) - .distinctUntilChanged() - .subscribe(s -> onStateChange(s, unidentified)); - } + void monitor(SignalWebSocket webSocket) { + Preconditions.checkNotNull(webSocket); + Preconditions.checkArgument(this.webSocket == null, "monitor can only be called once"); - private synchronized void onStateChange(WebSocketConnectionState connectionState, HealthState healthState) { - switch (connectionState) { - case CONNECTED -> logger.debug("WebSocket is now connected"); - case AUTHENTICATION_FAILED -> logger.debug("WebSocket authentication failed"); - case FAILED -> logger.debug("WebSocket connection failed"); - } + executor.execute(() -> { - healthState.needsKeepAlive = connectionState == WebSocketConnectionState.CONNECTED; + this.webSocket = webSocket; - if (keepAliveSender == null && isKeepAliveNecessary()) { - keepAliveSender = new KeepAliveSender(); - keepAliveSender.start(); - } else if (keepAliveSender != null && !isKeepAliveNecessary()) { - keepAliveSender.shutdown(); - keepAliveSender = null; - } + webSocket.getState() + .subscribeOn(Schedulers.computation()) + .observeOn(Schedulers.computation()) + .distinctUntilChanged() + .subscribe(this::onStateChanged); + + webSocket.setKeepAliveChangedListener(this::updateKeepAliveSenderStatus); + }); + } + + private void onStateChanged(WebSocketConnectionState connectionState) { + executor.execute(() -> { + needsKeepAlive = connectionState == WebSocketConnectionState.CONNECTED; + + updateKeepAliveSenderStatus(); + }); } @Override public void onKeepAliveResponse(long sentTimestamp, boolean isIdentifiedWebSocket) { - if (isIdentifiedWebSocket) { - identified.lastKeepAliveReceived = System.currentTimeMillis(); - } else { - unidentified.lastKeepAliveReceived = System.currentTimeMillis(); - } + final var keepAliveTime = System.currentTimeMillis(); + executor.execute(() -> lastKeepAliveReceived = keepAliveTime); } @Override public void onMessageError(int status, boolean isIdentifiedWebSocket) { - if (status == 409) { - HealthState healthState = (isIdentifiedWebSocket ? identified : unidentified); - if (healthState.mismatchErrorTracker.addSample(System.currentTimeMillis())) { - logger.warn("Received too many mismatch device errors, forcing new websockets."); - signalWebSocket.forceNewWebSockets(); - signalWebSocket.connect(); - } - } } - private boolean isKeepAliveNecessary() { - return identified.needsKeepAlive || unidentified.needsKeepAlive; + private Unit updateKeepAliveSenderStatus() { + if (keepAliveSender == null && sendKeepAlives()) { + keepAliveSender = new KeepAliveSender(); + keepAliveSender.start(); + } else if (keepAliveSender != null && !sendKeepAlives()) { + keepAliveSender.shutdown(); + keepAliveSender = null; + } + return Unit.INSTANCE; } - private static class HealthState { - - private final HttpErrorTracker mismatchErrorTracker = new HttpErrorTracker(5, TimeUnit.MINUTES.toMillis(1)); - - private volatile boolean needsKeepAlive; - private volatile long lastKeepAliveReceived; + private boolean sendKeepAlives() { + return needsKeepAlive && webSocket != null && webSocket.getShouldSendKeepAlives(); } /** - * Sends periodic heartbeats/keep-alives over both WebSockets to prevent connection timeouts. If - * either WebSocket fails 3 times to get a return heartbeat both are forced to be recreated. + * Sends periodic heartbeats/keep-alives over the WebSocket to prevent connection timeouts. If + * the WebSocket fails to get a return heartbeat after [KEEP_ALIVE_TIMEOUT] seconds, it is forced to be recreated. */ - private class KeepAliveSender extends Thread { + private final class KeepAliveSender extends Thread { private volatile boolean shouldKeepRunning = true; + @Override public void run() { - identified.lastKeepAliveReceived = System.currentTimeMillis(); - unidentified.lastKeepAliveReceived = System.currentTimeMillis(); + logger.debug("[KeepAliveSender({})] started", this.threadId()); + lastKeepAliveReceived = System.currentTimeMillis(); - while (shouldKeepRunning && isKeepAliveNecessary()) { + var keepAliveSendTime = System.currentTimeMillis(); + while (shouldKeepRunning && sendKeepAlives()) { try { - sleepTimer.sleep(KEEP_ALIVE_SEND_CADENCE); - - if (shouldKeepRunning && isKeepAliveNecessary()) { - long keepAliveRequiredSinceTime = System.currentTimeMillis() - - MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE; - - if (identified.lastKeepAliveReceived < keepAliveRequiredSinceTime - || unidentified.lastKeepAliveReceived < keepAliveRequiredSinceTime) { - logger.warn("Missed keep alives, identified last: " - + identified.lastKeepAliveReceived - + " unidentified last: " - + unidentified.lastKeepAliveReceived - + " needed by: " - + keepAliveRequiredSinceTime); - signalWebSocket.forceNewWebSockets(); - signalWebSocket.connect(); - } else { - signalWebSocket.sendKeepAlive(); + final var nextKeepAliveSendTime = keepAliveSendTime + KEEP_ALIVE_SEND_CADENCE; + sleepUntil(nextKeepAliveSendTime); + + if (shouldKeepRunning && sendKeepAlives()) { + keepAliveSendTime = System.currentTimeMillis(); + webSocket.sendKeepAlive(); + } + + final var responseRequiredTime = keepAliveSendTime + KEEP_ALIVE_TIMEOUT; + sleepUntil(responseRequiredTime); + + if (shouldKeepRunning && sendKeepAlives()) { + if (lastKeepAliveReceived < keepAliveSendTime) { + logger.debug("Missed keep alive, last: {} needed by: {}", + lastKeepAliveReceived, + responseRequiredTime); + webSocket.forceNewWebSocket(); } } } catch (Throwable e) { - logger.warn("Error occurred in KeepAliveSender, ignoring ...", e); + logger.warn("Keep alive sender failed", e); } } + logger.debug("[KeepAliveSender({})] ended", threadId()); } - public void shutdown() { - shouldKeepRunning = false; - } - } - - private static final class HttpErrorTracker { - - private final long[] timestamps; - private final long errorTimeRange; - - public HttpErrorTracker(int samples, long errorTimeRange) { - this.timestamps = new long[samples]; - this.errorTimeRange = errorTimeRange; - } - - public synchronized boolean addSample(long now) { - long errorsMustBeAfter = now - errorTimeRange; - int count = 1; - int minIndex = 0; - - for (int i = 0; i < timestamps.length; i++) { - if (timestamps[i] < errorsMustBeAfter) { - timestamps[i] = 0; - } else if (timestamps[i] != 0) { - count++; - } - - if (timestamps[i] < timestamps[minIndex]) { - minIndex = i; + void sleepUntil(long timeMillis) { + while (System.currentTimeMillis() < timeMillis) { + final var waitTime = timeMillis - System.currentTimeMillis(); + if (waitTime > 0) { + try { + sleepTimer.sleep(waitTime); + } catch (InterruptedException e) { + logger.warn("WebSocket health monitor interrupted", e); + } } } + } - timestamps[minIndex] = now; - - if (count >= timestamps.length) { - Arrays.fill(timestamps, 0); - return true; - } - return false; + void shutdown() { + shouldKeepRunning = false; } } } + diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/accounts/AccountsStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/accounts/AccountsStore.java index a0fa3012..7be4a401 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/accounts/AccountsStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/accounts/AccountsStore.java @@ -1,6 +1,7 @@ package org.asamk.signal.manager.storage.accounts; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.i18n.phonenumbers.PhoneNumberUtil; import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.ServiceEnvironment; @@ -10,7 +11,6 @@ import org.asamk.signal.manager.util.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.push.ServiceId.ACI; -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -181,7 +181,7 @@ public class AccountsStore { return Arrays.stream(files) .filter(File::isFile) .map(File::getName) - .filter(file -> PhoneNumberFormatter.isValidNumber(file, null)) + .filter(file -> PhoneNumberUtil.getInstance().isPossibleNumber(file, null)) .collect(Collectors.toSet()); } diff --git a/lib/src/main/java/org/asamk/signal/manager/syncStorage/ContactRecordProcessor.java b/lib/src/main/java/org/asamk/signal/manager/syncStorage/ContactRecordProcessor.java index 2863874b..d4841c7a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/syncStorage/ContactRecordProcessor.java +++ b/lib/src/main/java/org/asamk/signal/manager/syncStorage/ContactRecordProcessor.java @@ -195,7 +195,8 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor localNumber.length()) return "+" + number; + + int difference = localNumber.length() - number.length(); + + return "+" + localNumber.substring(0, difference) + number; + } + + public static String formatNumber(String number, String localNumber) throws InvalidNumberException { + if (number == null) { + throw new InvalidNumberException("Null String passed as number."); + } + + if (number.contains("@")) { + throw new InvalidNumberException("Possible attempt to use email address."); + } + + number = number.replaceAll("[^0-9+]", ""); + + if (number.isEmpty()) { + throw new InvalidNumberException("No valid characters found."); + } + + try { + PhoneNumberUtil util = PhoneNumberUtil.getInstance(); + PhoneNumber localNumberObject = util.parse(localNumber, null); + + String localCountryCode = util.getRegionCodeForNumber(localNumberObject); + logger.trace("Got local CC: {}", localCountryCode); + + PhoneNumber numberObject = util.parse(number, localCountryCode); + return util.format(numberObject, PhoneNumberFormat.E164); + } catch (NumberParseException e) { + logger.debug("{}: {}", e.getClass().getSimpleName(), e.getMessage()); + return impreciseFormatNumber(number, localNumber); + } + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java index b90a7873..9d97ed7a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java @@ -162,7 +162,11 @@ public class Utils { throw new IOException(throwableOptional); } } - return response.successOrThrow(); + try { + return response.successOrThrow(); + } catch (Throwable e) { + throw new AssertionError(e); + } } public static ByteString firstNonEmpty(ByteString... strings) {