From: AsamK Date: Sat, 3 Apr 2021 17:13:12 +0000 (+0200) Subject: Add dbus SignalControl interface to register/verify/link accounts X-Git-Tag: v0.9.0~89 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/8f781c019f5f451c9d6323659bb248be335ad0e5 Add dbus SignalControl interface to register/verify/link accounts --- 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 13038bf1..ef0b404b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -2083,7 +2083,7 @@ public class Manager implements Closeable { var hasCaughtUpWithOldMessages = false; - while (true) { + while (!Thread.interrupted()) { SignalServiceEnvelope envelope; SignalServiceContent content = null; Exception exception = null; diff --git a/src/main/java/org/asamk/SignalControl.java b/src/main/java/org/asamk/SignalControl.java new file mode 100644 index 00000000..911ccb61 --- /dev/null +++ b/src/main/java/org/asamk/SignalControl.java @@ -0,0 +1,56 @@ +package org.asamk; + +import org.freedesktop.dbus.DBusPath; +import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.freedesktop.dbus.interfaces.DBusInterface; + +import java.util.List; + +/** + * DBus interface for the org.asamk.SignalControl interface. + * Including emitted Signals and returned Errors. + */ +public interface SignalControl extends DBusInterface { + + void register( + String number, boolean voiceVerification + ) throws Error.Failure, Error.InvalidNumber, Error.RequiresCaptcha; + + void registerWithCaptcha( + String number, boolean voiceVerification, String captcha + ) throws Error.Failure, Error.InvalidNumber, Error.RequiresCaptcha; + + void verify(String number, String verificationCode) throws Error.Failure, Error.InvalidNumber; + + void verifyWithPin(String number, String verificationCode, String pin) throws Error.Failure, Error.InvalidNumber; + + String link(String newDeviceName) throws Error.Failure; + + public String version(); + + List listAccounts(); + + interface Error { + + class Failure extends DBusExecutionException { + + public Failure(final String message) { + super(message); + } + } + + class InvalidNumber extends DBusExecutionException { + + public InvalidNumber(final String message) { + super(message); + } + } + + class RequiresCaptcha extends DBusExecutionException { + + public RequiresCaptcha(final String message) { + super(message); + } + } + } +} diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index 33338a4c..6cc73655 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -14,6 +14,7 @@ import org.asamk.signal.commands.LocalCommand; import org.asamk.signal.commands.MultiLocalCommand; import org.asamk.signal.commands.ProvisioningCommand; import org.asamk.signal.commands.RegistrationCommand; +import org.asamk.signal.commands.SignalCreator; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; @@ -235,7 +236,17 @@ public class App { } } - command.handleCommand(ns, managers); + command.handleCommand(ns, managers, new SignalCreator() { + @Override + public ProvisioningManager getNewProvisioningManager() { + return ProvisioningManager.init(dataPath, serviceEnvironment, BaseConfig.USER_AGENT); + } + + @Override + public RegistrationManager getNewRegistrationManager(String username) throws IOException { + return RegistrationManager.init(username, dataPath, serviceEnvironment, BaseConfig.USER_AGENT); + } + }); for (var m : managers) { try { diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index f7d8b12e..7988c8ef 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -13,6 +13,7 @@ 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.dbus.DbusSignalControlImpl; import org.asamk.signal.dbus.DbusSignalImpl; import org.asamk.signal.manager.Manager; import org.freedesktop.dbus.connections.impl.DBusConnection; @@ -21,7 +22,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -78,7 +78,9 @@ public class DaemonCommand implements MultiLocalCommand { } @Override - public void handleCommand(final Namespace ns, final List managers) throws CommandException { + public void handleCommand( + final Namespace ns, final List managers, SignalCreator c + ) throws CommandException { boolean ignoreAttachments = ns.getBoolean("ignore-attachments"); DBusConnection.DBusBusType busType; @@ -89,21 +91,24 @@ public class DaemonCommand implements MultiLocalCommand { } try (var conn = DBusConnection.getConnection(busType)) { - var receiveThreads = new ArrayList(); + final var signalControl = new DbusSignalControlImpl(c, m -> { + try { + final var objectPath = DbusConfig.getObjectPath(m.getUsername()); + return run(conn, objectPath, m, ignoreAttachments); + } catch (DBusException e) { + logger.error("Failed to export object", e); + return null; + } + }, DbusConfig.getObjectPath()); + conn.exportObject(signalControl); + for (var m : managers) { - var objectPath = DbusConfig.getObjectPath(m.getUsername()); - var thread = run(conn, objectPath, m, ignoreAttachments); - receiveThreads.add(thread); + signalControl.addManager(m); } conn.requestBusName(DbusConfig.getBusname()); - for (var t : receiveThreads) { - try { - t.join(); - } catch (InterruptedException ignored) { - } - } + signalControl.run(); } catch (DBusException | IOException e) { logger.error("Dbus command failed", e); throw new UnexpectedErrorException("Dbus command failed"); @@ -113,7 +118,9 @@ public class DaemonCommand implements MultiLocalCommand { private Thread run( DBusConnection conn, String objectPath, Manager m, boolean ignoreAttachments ) throws DBusException { - conn.exportObject(objectPath, new DbusSignalImpl(m)); + conn.exportObject(new DbusSignalImpl(m, objectPath)); + + logger.info("Exported dbus object: " + objectPath); final var thread = new Thread(() -> { while (true) { @@ -128,8 +135,6 @@ public class DaemonCommand implements MultiLocalCommand { } }); - logger.info("Exported dbus object: " + objectPath); - thread.start(); return thread; diff --git a/src/main/java/org/asamk/signal/commands/DbusCommand.java b/src/main/java/org/asamk/signal/commands/DbusCommand.java index e4c78c84..1a949c81 100644 --- a/src/main/java/org/asamk/signal/commands/DbusCommand.java +++ b/src/main/java/org/asamk/signal/commands/DbusCommand.java @@ -12,6 +12,6 @@ public interface DbusCommand extends LocalCommand { void handleCommand(Namespace ns, Signal signal) throws CommandException; default void handleCommand(final Namespace ns, final Manager m) throws CommandException { - handleCommand(ns, new DbusSignalImpl(m)); + handleCommand(ns, new DbusSignalImpl(m, null)); } } diff --git a/src/main/java/org/asamk/signal/commands/MultiLocalCommand.java b/src/main/java/org/asamk/signal/commands/MultiLocalCommand.java index 2a8457bd..58416e50 100644 --- a/src/main/java/org/asamk/signal/commands/MultiLocalCommand.java +++ b/src/main/java/org/asamk/signal/commands/MultiLocalCommand.java @@ -9,10 +9,10 @@ import java.util.List; public interface MultiLocalCommand extends LocalCommand { - void handleCommand(Namespace ns, List m) throws CommandException; + void handleCommand(Namespace ns, List m, final SignalCreator c) throws CommandException; @Override default void handleCommand(final Namespace ns, final Manager m) throws CommandException { - handleCommand(ns, List.of(m)); + handleCommand(ns, List.of(m), null); } } diff --git a/src/main/java/org/asamk/signal/commands/SignalCreator.java b/src/main/java/org/asamk/signal/commands/SignalCreator.java new file mode 100644 index 00000000..675d7f2a --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/SignalCreator.java @@ -0,0 +1,13 @@ +package org.asamk.signal.commands; + +import org.asamk.signal.manager.ProvisioningManager; +import org.asamk.signal.manager.RegistrationManager; + +import java.io.IOException; + +public interface SignalCreator { + + ProvisioningManager getNewProvisioningManager(); + + RegistrationManager getNewRegistrationManager(String username) throws IOException; +} diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java new file mode 100644 index 00000000..35f530b0 --- /dev/null +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java @@ -0,0 +1,163 @@ +package org.asamk.signal.dbus; + +import org.asamk.SignalControl; +import org.asamk.signal.BaseConfig; +import org.asamk.signal.DbusConfig; +import org.asamk.signal.commands.SignalCreator; +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.freedesktop.dbus.DBusPath; +import org.whispersystems.libsignal.util.Pair; +import org.whispersystems.signalservice.api.KeyBackupServicePinException; +import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException; +import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class DbusSignalControlImpl implements org.asamk.SignalControl { + + private final SignalCreator c; + private final Function newManagerRunner; + + private final List> receiveThreads = new ArrayList<>(); + private final Object stopTrigger = new Object(); + private final String objectPath; + + public DbusSignalControlImpl( + final SignalCreator c, final Function newManagerRunner, final String objectPath + ) { + this.c = c; + this.newManagerRunner = newManagerRunner; + this.objectPath = objectPath; + } + + public void addManager(Manager m) { + var thread = newManagerRunner.apply(m); + if (thread == null) { + return; + } + synchronized (receiveThreads) { + receiveThreads.add(new Pair<>(m, thread)); + } + } + + public void run() { + synchronized (stopTrigger) { + try { + stopTrigger.wait(); + } catch (InterruptedException ignored) { + } + } + + synchronized (receiveThreads) { + for (var t : receiveThreads) { + t.second().interrupt(); + } + } + while (true) { + final Thread thread; + synchronized (receiveThreads) { + if (receiveThreads.size() == 0) { + break; + } + var pair = receiveThreads.remove(0); + thread = pair.second(); + } + try { + thread.join(); + } catch (InterruptedException ignored) { + } + } + } + + @Override + public boolean isRemote() { + return false; + } + + @Override + public String getObjectPath() { + return objectPath; + } + + @Override + public void register( + final String number, final boolean voiceVerification + ) throws Error.Failure, Error.InvalidNumber { + registerWithCaptcha(number, voiceVerification, null); + } + + @Override + public void registerWithCaptcha( + final String number, final boolean voiceVerification, final String captcha + ) throws Error.Failure, Error.InvalidNumber { + try (final RegistrationManager registrationManager = c.getNewRegistrationManager(number)) { + registrationManager.register(voiceVerification, captcha); + } catch (CaptchaRequiredException e) { + String message = captcha == null ? "Captcha required for verification." : "Invalid captcha given."; + throw new SignalControl.Error.RequiresCaptcha(message); + } catch (IOException e) { + throw new SignalControl.Error.Failure(e.getClass().getSimpleName() + " " + e.getMessage()); + } + } + + @Override + public void verify(final String number, final String verificationCode) throws Error.Failure, Error.InvalidNumber { + verifyWithPin(number, verificationCode, null); + } + + @Override + public void verifyWithPin( + final String number, final String verificationCode, final String pin + ) throws Error.Failure, Error.InvalidNumber { + try (final RegistrationManager registrationManager = c.getNewRegistrationManager(number)) { + final Manager manager = registrationManager.verifyAccount(verificationCode, pin); + addManager(manager); + } catch (IOException | KeyBackupSystemNoDataException | KeyBackupServicePinException e) { + throw new SignalControl.Error.Failure(e.getClass().getSimpleName() + " " + e.getMessage()); + } + } + + @Override + public String link(final String newDeviceName) throws Error.Failure { + try { + final ProvisioningManager provisioningManager = c.getNewProvisioningManager(); + final URI deviceLinkUri = provisioningManager.getDeviceLinkUri(); + new Thread(() -> { + try { + final Manager manager = provisioningManager.finishDeviceLink(newDeviceName); + addManager(manager); + } catch (IOException | TimeoutException | UserAlreadyExists e) { + e.printStackTrace(); + } + }).start(); + return deviceLinkUri.toString(); + } catch (TimeoutException | IOException e) { + throw new SignalControl.Error.Failure(e.getClass().getSimpleName() + " " + e.getMessage()); + } + } + + @Override + public String version() { + return BaseConfig.PROJECT_VERSION; + } + + @Override + public List listAccounts() { + synchronized (receiveThreads) { + return receiveThreads.stream() + .map(Pair::first) + .map(Manager::getUsername) + .map(u -> new DBusPath(DbusConfig.getObjectPath(u))) + .collect(Collectors.toList()); + } + } +} diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 5723b436..b88aa81e 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -34,9 +34,11 @@ import static org.asamk.signal.util.Util.getLegacyIdentifier; public class DbusSignalImpl implements Signal { private final Manager m; + private final String objectPath; - public DbusSignalImpl(final Manager m) { + public DbusSignalImpl(final Manager m, final String objectPath) { this.m = m; + this.objectPath = objectPath; } @Override @@ -46,7 +48,7 @@ public class DbusSignalImpl implements Signal { @Override public String getObjectPath() { - return null; + return objectPath; } @Override