implement Dbus unregister/unlisten methods

implemented two related Dbus methods:
- unregister
- unlisten

improved error handling for Dbus method:
- link

fixed logic error in Manager.java (line 876) because daemon
could still be running after a particular account is unregistered.

broadened WebSocketUnavailableException (line 913) to catch all
IOExceptions caused by unregistering account while daemon is running.

specified error message handling in ProvisioningManager.java and
DbusSignalControlImpl.java for link subcommand.

helper methods:
- getPathConfig in Manager.java
- private removeUserData in DbusSignalImpl.java
This commit is contained in:
John Freed 2021-09-23 16:35:47 +02:00
parent 982e887c9f
commit 10274cdb3f
8 changed files with 210 additions and 11 deletions

View file

@ -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;

View file

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

View file

@ -114,6 +114,22 @@ sendNoteToSelfMessage(message<s>, attachments<as>) -> timestamp<x>::
Exceptions: Failure, AttachmentInvalid
unlisten() -> <>::
unlisten(keepData<b>) -> <>::
* 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<b>) -> <>::
* 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<s>, attachments<as>, recipient<s>) -> timestamp<x>::
sendMessage(message<s>, attachments<as>, recipients<as>) -> timestamp<x>::
* message : Text to send (can be UTF8)

View file

@ -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;

View file

@ -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<Manager> 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<Manager> managers, final SignalCreator c, final OutputWriter outputWriter
final Namespace ns, final List<Manager> 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);
}

View file

@ -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<Manager> m, SignalCreator c, OutputWriter outputWriter
Namespace ns, List<Manager> 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<Manager> managers, SignalCreator c, OutputWriter outputWriter, TrustNewIdentity trustNewIdentity
) throws CommandException;
@Override

View file

@ -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) {

View file

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