diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index 80a0fea5..ed17430f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -922,8 +922,12 @@ public class ManagerImpl implements Manager { while (!Thread.interrupted()) { SignalServiceEnvelope envelope; final CachedMessage[] cachedMessage = {null}; - account.setLastReceiveTimestamp(System.currentTimeMillis()); + if (account == null) { + logger.debug("Account closed."); + break; + } logger.debug("Checking for new message from server"); + account.setLastReceiveTimestamp(System.currentTimeMillis()); try { var result = signalWebSocket.readOrEmpty(unit.toMillis(timeout), envelope1 -> { final var recipientId = envelope1.hasSourceUuid() @@ -955,7 +959,7 @@ public class ManagerImpl implements Manager { } else { throw e; } - } catch (WebSocketUnavailableException e) { + } catch (IOException e) { logger.debug("Pipe unexpectedly unavailable, connecting"); signalWebSocket.connect(); continue; diff --git a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java index 226de9be..0dbf9297 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.util.KeyHelper; import org.whispersystems.signalservice.api.SignalServiceAccountManager; +import org.whispersystems.signalservice.api.SignalServiceAccountManager.NewDeviceRegistrationReturn; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -90,7 +91,13 @@ public class ProvisioningManager { } public Manager finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExists { - var ret = accountManager.getNewDeviceRegistration(tempIdentityKey); + NewDeviceRegistrationReturn ret; + logger.info("Waiting for addDevice request..."); + try { + ret = accountManager.getNewDeviceRegistration(tempIdentityKey); + } catch (IOException | TimeoutException e) { + throw new TimeoutException(e.getMessage()); + } var number = ret.getNumber(); logger.info("Received link information from {}, linking in progress ...", number); diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 8698428a..f04e195c 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -70,7 +70,7 @@ Exceptions: None listen(number) -> <>:: * number : Phone number -Starting checking the Signal servers on behalf of this number, and export a DBus object path for it. +Start checking the Signal servers on behalf of this number, and export a DBus object path for it. Fails if user is not already registered. Exceptions: Failure @@ -90,6 +90,23 @@ Captcha strings may be obtained from `https://signalcaptchas.org/registration/ge Exceptions: Failure, InvalidNumber, RequiresCaptcha +unlisten(number) -> <>:: +* number : Phone number +* keepData : true or omitted = keep files in data directory; false = delete files + +Stops the current device from listening to DBus. In single-user mode, kills the daemon. + +Exception: Failure + +unregister(number) -> <>:: +unregister(number, keepData) -> <>:: +* number : Phone number +* keepData : true or omitted = keep files in data directory; false = delete files + +Unregisters the current device. In single-user mode, kills the daemon. + +Exception: Failure + verify(number, verificationCode) -> <>:: * number : Phone number * verificationCode : Code received from Signal after successful registration request @@ -173,7 +190,7 @@ isMember(groupId) -> active:: Note that this method does not raise an Exception for a non-existing/unknown group but will simply return 0 (false) sendEndSessionMessage(recipients) -> <>:: -* recipients : Array of phone numbers +* recipients : Array of phone numbers Exceptions: Failure, InvalidNumber, UntrustedIdentity @@ -209,7 +226,7 @@ sendMessage(message, attachments, recipients) -> timestamp:: * message : Text to send (can be UTF8) * attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) * recipient : Phone number of a single recipient -* recipients : Array of phone numbers +* recipients : Array of phone numbers * timestamp : Can be used to identify the corresponding signal reply Depending on the type of the recipient field this sends a message to one or multiple recipients. @@ -285,7 +302,7 @@ groupList : Array of Byte arrays representing the internal group identifiers All groups known are returned, regardless of their active or blocked status. To query that use isMember() and isGroupBlocked() getGroupName(groupId) -> groupName:: -groupName : The display name of the group +groupName : The display name of the group groupId : Byte array representing the internal group identifier Exceptions: None, if the group name is not found an empty string is returned @@ -361,7 +378,7 @@ removeDevice(deviceId) -> <>:: Exception: Failure updateDeviceName(deviceName) -> <>:: -* deviceName : New name +* deviceName : New name Set a new name for this device (main or linked). diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index b8800085..1fb2b743 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -8,6 +8,7 @@ import org.freedesktop.dbus.interfaces.DBusInterface; import org.freedesktop.dbus.interfaces.Properties; import org.freedesktop.dbus.messages.DBusSignal; +import java.io.IOException; import java.util.List; /** @@ -16,7 +17,7 @@ import java.util.List; */ public interface Signal extends DBusInterface { - String getSelfNumber(); + String getSelfNumber(); long sendMessage( String message, List attachments, String recipient diff --git a/src/main/java/org/asamk/SignalControl.java b/src/main/java/org/asamk/SignalControl.java index fe06ecc2..4317d35f 100644 --- a/src/main/java/org/asamk/SignalControl.java +++ b/src/main/java/org/asamk/SignalControl.java @@ -1,5 +1,6 @@ package org.asamk; +import org.asamk.Signal.Error; import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.interfaces.DBusInterface; @@ -24,12 +25,18 @@ public interface SignalControl extends DBusInterface { void verifyWithPin(String number, String verificationCode, String pin) throws Error.Failure, Error.InvalidNumber; + void listen(String number) throws Error.Failure; + + void unlisten(String number) throws Error.Failure; + + void unregister(String number) throws Error.Failure; + void unregister(String number, boolean keepData) throws Error.Failure; + + String link(String newDeviceName) throws Error.Failure; public String version(); - void listen(String number) throws Error.Failure; - List listAccounts(); interface Error { diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index ac48b610..6cf2c013 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -216,7 +216,10 @@ public class DaemonCommand implements MultiLocalCommand { initThread.join(); } catch (InterruptedException ignored) { } - signal.close(); + try { + signal.close(); + } catch (IOException ignored) { + } }); thread.start(); diff --git a/src/main/java/org/asamk/signal/commands/MultiLocalCommand.java b/src/main/java/org/asamk/signal/commands/MultiLocalCommand.java index a573e803..5bb8d6c9 100644 --- a/src/main/java/org/asamk/signal/commands/MultiLocalCommand.java +++ b/src/main/java/org/asamk/signal/commands/MultiLocalCommand.java @@ -10,7 +10,7 @@ import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import java.util.List; public interface MultiLocalCommand extends LocalCommand { - void handleCommand(Namespace ns, List m, SignalCreator c, OutputWriter outputWriter, TrustNewIdentity trustNewIdentity) throws CommandException; + void handleCommand(Namespace ns, List managers, SignalCreator c, OutputWriter outputWriter, TrustNewIdentity trustNewIdentity) throws CommandException; void handleCommand(Namespace ns, Manager m, SignalCreator c, OutputWriter outputWriter, TrustNewIdentity trustNewIdentity) throws CommandException; } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java index 385e456c..ca91bca0 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java @@ -13,11 +13,15 @@ import org.asamk.signal.PlainTextWriter; import org.asamk.signal.commands.DaemonCommand; import org.asamk.signal.commands.SignalCreator; import org.asamk.signal.commands.exceptions.CommandException; +import org.asamk.signal.commands.exceptions.IOErrorException; +import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.PathConfig; import org.asamk.signal.manager.ProvisioningManager; import org.asamk.signal.manager.RegistrationManager; import org.asamk.signal.manager.UserAlreadyExists; import org.asamk.signal.manager.config.ServiceEnvironment; +import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.freedesktop.dbus.DBusPath; @@ -38,7 +42,11 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.net.URI; import java.nio.channels.OverlappingFileLockException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -46,6 +54,7 @@ import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; public class DbusSignalControlImpl implements org.asamk.SignalControl { @@ -163,6 +172,53 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl { } } + @Override + public void unregister(String number) { + unregister(number, true); + } + + @Override + public void unregister(String number, boolean keepData) { + try { + List managers = new ArrayList<>(); + Manager manager = null; + + synchronized (receiveThreads) { + managers = receiveThreads.stream() + .map(Pair::first) + .collect(Collectors.toList()); + } + + if (managers.size() == 0) { + throw new Error.Failure("Unregister error: no manager found."); + } + + for (Manager m : managers) { + try { + m.getSelfNumber(); + } catch (NullPointerException ignore) { + continue; + } + if (m.getSelfNumber().equals(number)) { + manager = m; + break; + } + } + if (manager == null) { + throw new Error.Failure("Unregister error, " + number + " is not listening."); + } + + manager.unregister(); + DBusConnection.DBusBusType busType = DaemonCommand.dBusType; + if (!keepData) { + removeUserData(number); + } + unlisten(number); + } catch (Exception e) { + throw new Error.Failure(e.getClass().getSimpleName() + " Unregister error: " + e.getMessage()); + } + } + @Override public String link(final String newDeviceName) throws Error.Failure { try { @@ -242,6 +298,59 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl { } } + @Override + public void unlisten(String number) { + try { + List managers = new ArrayList<>(); + Manager manager = null; + + synchronized (receiveThreads) { + managers = receiveThreads.stream() + .map(Pair::first) + .collect(Collectors.toList()); + } + + if (managers.size() == 0) { + throw new Error.Failure("Unlisten error: no manager found."); + } + + for (Manager m : managers) { + try { + m.getSelfNumber(); + } catch (NullPointerException ignore) { + continue; + } + if (m.getSelfNumber().equals(number)) { + manager = m; + break; + } + } + if (manager == null) { + throw new Error.Failure("Unlisten error, " + number + " is already not listening."); + } + + String objectPath = DbusConfig.getObjectPath(number); + DBusConnection.DBusBusType busType = DaemonCommand.dBusType; + var conn = DBusConnection.getConnection(busType); + //if single-user mode, just close the manager because we're exiting anyway + //else unexport the object + try { + //this will generate an error if we are in anonymous mode + conn.exportObject(new DbusSignalImpl(manager, conn, objectPath)); + //no error, hence single-user mode + manager.close(); + logger.info("unExported dbus object: " + DbusConfig.getObjectPath()); + } catch (DBusException ignore) { + //anonymous mode + conn.unExportObject(objectPath); + manager.close(); + logger.info("unExported dbus object: " + objectPath); + } + } catch (IOException | DBusException e) { + throw new Error.Failure(e.getClass().getSimpleName() + " Unlisten error: " + e.getMessage()); + } + } + @Override public List listAccounts() { synchronized (receiveThreads) { @@ -252,4 +361,25 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl { .collect(Collectors.toList()); } } + + private void removeUserData(String number) { + File dataPath = PathConfig.createDefault(c.getSettingsPath()).getDataPath(); + number.replaceFirst("_", "+"); + String eraseFileName = dataPath.getAbsolutePath() + File.separator + number; + File eraseFile = new File(eraseFileName); + if (eraseFile.delete()) { + logger.info("erased " + eraseFileName); + } else { + logger.error("erase failed for " + eraseFileName); + } + String erasePath = dataPath.getAbsolutePath() + File.separator + number + ".d/"; + Path rootPath = Paths.get(erasePath); + try (Stream walk = Files.walk(rootPath)) { + walk.sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } catch (IOException e) { + throw new Error.Failure(e.getClass().getSimpleName() + " RemoveUserData failed. " + e.getMessage()); + } + } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index ab9c89b2..d2fbc004 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -1,7 +1,10 @@ package org.asamk.signal.dbus; import org.asamk.Signal; +import org.asamk.Signal.Error; import org.asamk.signal.BaseConfig; +import org.asamk.signal.DbusConfig; +import org.asamk.signal.commands.DaemonCommand; import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; @@ -20,10 +23,13 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.util.ErrorUtils; + import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; @@ -35,16 +41,21 @@ import org.whispersystems.signalservice.internal.contacts.crypto.Unauthenticated import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; +import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -53,6 +64,7 @@ public class DbusSignalImpl implements Signal { private final Manager m; private final DBusConnection connection; private final String objectPath; + private final static Logger logger = LoggerFactory.getLogger(DbusSignalImpl.class); private DBusPath thisDevice; private final List devices = new ArrayList<>(); @@ -67,8 +79,9 @@ public class DbusSignalImpl implements Signal { updateDevices(); } - public void close() { + public void close() throws IOException { unExportDevices(); + m.close(); } @Override