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 05700379..18d708c4 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -132,6 +132,7 @@ public class Manager implements Closeable { private final ProfileHelper profileHelper; private final PinHelper pinHelper; + private final PathConfig pathConfig; private final StorageHelper storageHelper; private final SendHelper sendHelper; private final SyncHelper syncHelper; @@ -152,6 +153,7 @@ public class Manager implements Closeable { ) { this.account = account; this.serviceEnvironmentConfig = serviceEnvironmentConfig; + this.pathConfig = pathConfig; final var credentialsProvider = new DynamicCredentialsProvider(account.getUuid(), account.getUsername(), @@ -243,6 +245,10 @@ public class Manager implements Closeable { jobExecutor); } + public PathConfig getPathConfig() { + return pathConfig; + } + public String getUsername() { return account.getUsername(); } @@ -867,8 +873,12 @@ public class Manager implements Closeable { 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() @@ -900,7 +910,7 @@ public class Manager implements Closeable { } 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 90dc6c66..ac2ddae8 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 d562d064..103e4801 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -114,6 +114,22 @@ sendNoteToSelfMessage(message, attachments) -> timestamp:: Exceptions: Failure, AttachmentInvalid +unlisten() -> <>:: +unlisten(keepData) -> <>:: +* 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() -> <>:: +unregister(keepData) -> <>:: +* 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 + sendMessage(message, attachments, recipient) -> timestamp:: sendMessage(message, attachments, recipients) -> timestamp:: * message : Text to send (can be UTF8) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 821e04d9..25af7957 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -83,6 +83,12 @@ public interface Signal extends DBusInterface { boolean isRegistered(); + void unlisten() throws Error.Failure; + void unlisten(boolean keepData) throws Error.Failure; + + void unregister() throws Error.Failure; + void unregister(boolean keepData) throws Error.Failure; + void updateProfile( String name, String about, String aboutEmoji, String avatarPath, boolean removeAvatar ) throws Error.Failure; diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 4a322b99..3ab386ae 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -13,9 +13,11 @@ import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException; +import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.dbus.DbusSignalControlImpl; import org.asamk.signal.dbus.DbusSignalImpl; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.slf4j.Logger; @@ -28,6 +30,9 @@ import java.util.concurrent.TimeUnit; public class DaemonCommand implements MultiLocalCommand { private final static Logger logger = LoggerFactory.getLogger(DaemonCommand.class); + public static DBusConnection.DBusBusType dBusType; + public static TrustNewIdentity trustNewIdentity; + public static OutputWriter outputWriter; @Override public String getName() { @@ -54,6 +59,20 @@ public class DaemonCommand implements MultiLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { + handleCommand(ns, m, null, outputWriter, null); + } + + @Override + public void handleCommand(final Namespace ns, final List managers, final SignalCreator c, final OutputWriter outputWriter + ) throws CommandException { + handleCommand(ns, managers, c, outputWriter, null); + } + + @Override + public void handleCommand( + final Namespace ns, final Manager m, final SignalCreator c, final OutputWriter outputWriter, final TrustNewIdentity trustNewIdentity + ) throws CommandException { + //single-user mode boolean ignoreAttachments = ns.getBoolean("ignore-attachments"); DBusConnection.DBusBusType busType; @@ -62,6 +81,22 @@ public class DaemonCommand implements MultiLocalCommand { } else { busType = DBusConnection.DBusBusType.SESSION; } + this.dBusType = busType; + this.trustNewIdentity = trustNewIdentity; + this.outputWriter = outputWriter; + if (System.getProperty("os.name").toLowerCase().startsWith("mac ")) { + String dBusVar = System.getenv("DBUS_LAUNCHD_SESSION_BUS_SOCKET"); + + if (dBusVar == null || dBusVar.isBlank()) { + String message = "\n\n" + + "*************************************" + + "\n\nDBUS_LAUNCHD_SESSION_BUS_SOCKET is not set. Issue the command:\n\n" + + "export DBUS_LAUNCHD_SESSION_BUS_SOCKET=$(launchctl getenv DBUS_LAUNCHD_SESSION_BUS_SOCKET)\n" + + "\nand then try again.\n\n" + + "*************************************"; + throw new UserErrorException(message); + } + } try (var conn = DBusConnection.getConnection(busType)) { var objectPath = DbusConfig.getObjectPath(); @@ -73,7 +108,10 @@ public class DaemonCommand implements MultiLocalCommand { t.join(); } catch (InterruptedException ignored) { } - } catch (DBusException | IOException e) { + } catch (DBusException e) { + logger.error("Dbus command failed", e); + throw new UserErrorException("Dbus command failed, daemon already started on this bus."); + } catch (IOException e) { logger.error("Dbus command failed", e); throw new UnexpectedErrorException("Dbus command failed", e); } @@ -81,8 +119,9 @@ public class DaemonCommand implements MultiLocalCommand { @Override public void handleCommand( - final Namespace ns, final List managers, final SignalCreator c, final OutputWriter outputWriter + final Namespace ns, final List managers, final SignalCreator c, final OutputWriter outputWriter, TrustNewIdentity trustNewIdentity ) throws CommandException { + //anonymous mode boolean ignoreAttachments = ns.getBoolean("ignore-attachments"); DBusConnection.DBusBusType busType; @@ -91,6 +130,22 @@ public class DaemonCommand implements MultiLocalCommand { } else { busType = DBusConnection.DBusBusType.SESSION; } + this.dBusType = busType; + this.trustNewIdentity = trustNewIdentity; + this.outputWriter = outputWriter; + if (System.getProperty("os.name").toLowerCase().startsWith("mac ")) { + String dBusVar = System.getenv("DBUS_LAUNCHD_SESSION_BUS_SOCKET"); + + if (dBusVar == null || dBusVar.isBlank()) { + String message = "\n\n" + + "*************************************" + + "\n\nDBUS_LAUNCHD_SESSION_BUS_SOCKET is not set. Issue the command:\n\n" + + "export DBUS_LAUNCHD_SESSION_BUS_SOCKET=$(launchctl getenv DBUS_LAUNCHD_SESSION_BUS_SOCKET)\n\n" + + "and then try again.\n\n" + + "*************************************"; + throw new UserErrorException(message); + } + } try (var conn = DBusConnection.getConnection(busType)) { final var signalControl = new DbusSignalControlImpl(c, m -> { @@ -111,7 +166,10 @@ public class DaemonCommand implements MultiLocalCommand { conn.requestBusName(DbusConfig.getBusname()); signalControl.run(); - } catch (DBusException | IOException e) { + } catch (DBusException e) { + logger.error("Dbus command failed", e); + throw new UserErrorException("Dbus command failed, daemon already started on this bus."); + } catch (IOException e ) { logger.error("Dbus command failed", e); throw new UnexpectedErrorException("Dbus command failed", e); } diff --git a/src/main/java/org/asamk/signal/commands/MultiLocalCommand.java b/src/main/java/org/asamk/signal/commands/MultiLocalCommand.java index 1c01a6ae..4b3a5f54 100644 --- a/src/main/java/org/asamk/signal/commands/MultiLocalCommand.java +++ b/src/main/java/org/asamk/signal/commands/MultiLocalCommand.java @@ -5,13 +5,22 @@ import net.sourceforge.argparse4j.inf.Namespace; import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.manager.Manager; +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 + Namespace ns, List managers, SignalCreator c, OutputWriter outputWriter + ) throws CommandException; + + void handleCommand( + Namespace ns, Manager m, SignalCreator c, OutputWriter outputWriter, TrustNewIdentity trustNewIdentity + ) throws CommandException; + + public void handleCommand( + Namespace ns, List managers, SignalCreator c, OutputWriter outputWriter, TrustNewIdentity trustNewIdentity ) throws CommandException; @Override diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java index 6ec8d964..75b8524f 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java @@ -4,10 +4,13 @@ import org.asamk.SignalControl; import org.asamk.signal.BaseConfig; import org.asamk.signal.DbusConfig; import org.asamk.signal.commands.SignalCreator; +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.ProvisioningManager; import org.asamk.signal.manager.RegistrationManager; import org.asamk.signal.manager.UserAlreadyExists; +import org.asamk.signal.manager.storage.SignalAccount; import org.freedesktop.dbus.DBusPath; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.KeyBackupServicePinException; @@ -140,9 +143,16 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl { try { final Manager manager = provisioningManager.finishDeviceLink(newDeviceName); addManager(manager); - } catch (IOException | TimeoutException | UserAlreadyExists e) { - e.printStackTrace(); - } + } catch (IOException e) { + throw new Error.Failure("Link request error: " + e.getMessage()); + } catch (TimeoutException e) { + throw new Error.Failure("Link request timed out, please try again."); + } catch (UserAlreadyExists e) { + throw new Error.Failure("The user " + + e.getUsername() + + " already exists\nDelete \"" + + e.getFileName() + + "\" before trying again."); } }).start(); return deviceLinkUri.toString(); } catch (TimeoutException | IOException e) { diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 89703387..91c706f0 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -1,10 +1,12 @@ package org.asamk.signal.dbus; - import org.asamk.Signal; 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; +import org.asamk.signal.manager.PathConfig; import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; @@ -18,7 +20,12 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.util.ErrorUtils; import org.asamk.signal.util.Util; + +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.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; @@ -29,13 +36,18 @@ 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.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.concurrent.TimeoutException; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -45,6 +57,7 @@ public class DbusSignalImpl implements Signal { private final Manager m; private final String objectPath; + private final static Logger logger = LoggerFactory.getLogger(DbusSignalImpl.class); public DbusSignalImpl(final Manager m, final String objectPath) { this.m = m; @@ -443,6 +456,54 @@ public class DbusSignalImpl implements Signal { return BaseConfig.PROJECT_VERSION; } + public void unlisten() { + unlisten(true); + } + + @Override + public void unlisten(boolean keepData) { + try { + if (!keepData) { + removeUserData(m.getUsername()); + } + String objectPath = DbusConfig.getObjectPath(m.getUsername()); + 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(m, objectPath)); + //no error, hence single-user mode + m.close(); + logger.info("unExported dbus object: " + DbusConfig.getObjectPath()); + } catch (DBusException ignore) { + //anonymous mode + conn.unExportObject(objectPath); + m.close(); + logger.info("unExported dbus object: " + objectPath); + } + } catch (IOException | DBusException e) { + throw new Error.Failure(e.getClass().getSimpleName() + " Unlisten error: " + e.getMessage()); + } + } + + @Override + public void unregister() { + unregister(true); + } + + @Override + public void unregister(boolean keepData) { + try { + m.unregister(); + DBusConnection.DBusBusType busType = DaemonCommand.dBusType; + unlisten(keepData); + } catch (Exception e) { + throw new Error.Failure(e.getClass().getSimpleName() + "Unregister error: " + e.getMessage()); + } + } + // Create a unique list of Numbers from Identities and Contacts to really get // all numbers the system knows @Override @@ -626,4 +687,26 @@ public class DbusSignalImpl implements Signal { throw new Error.InvalidGroupId("Invalid group id: " + e.getMessage()); } } + + private void removeUserData(String number) { + PathConfig pathConfig = m.getPathConfig(); + File dataPath = pathConfig.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()); + } + } }