]> nmode's Git Repositories - signal-cli/commitdiff
Add dbus SignalControl interface to register/verify/link accounts
authorAsamK <asamk@gmx.de>
Sat, 3 Apr 2021 17:13:12 +0000 (19:13 +0200)
committerAsamK <asamk@gmx.de>
Sun, 8 Aug 2021 16:43:14 +0000 (18:43 +0200)
lib/src/main/java/org/asamk/signal/manager/Manager.java
src/main/java/org/asamk/SignalControl.java [new file with mode: 0644]
src/main/java/org/asamk/signal/App.java
src/main/java/org/asamk/signal/commands/DaemonCommand.java
src/main/java/org/asamk/signal/commands/DbusCommand.java
src/main/java/org/asamk/signal/commands/MultiLocalCommand.java
src/main/java/org/asamk/signal/commands/SignalCreator.java [new file with mode: 0644]
src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java [new file with mode: 0644]
src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java

index 13038bf18790d2691c5d7825837266dd4573388d..ef0b404b98ef38d0af3b0df770fcf3f0828c61ff 100644 (file)
@@ -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 (file)
index 0000000..911ccb6
--- /dev/null
@@ -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<DBusPath> 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);
+            }
+        }
+    }
+}
index 33338a4c29db42b945f550fa117c328d19930746..6cc7365553c044d8fcfc5fcc19567b0556fdac62 100644 (file)
@@ -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 {
index f7d8b12e2e088a4e476c1348fb45e5ea51e81c85..7988c8efcdc44d1faff1a09a91d05e8c51e771d9 100644 (file)
@@ -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<Manager> managers) throws CommandException {
+    public void handleCommand(
+            final Namespace ns, final List<Manager> 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<Thread>();
+            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;
index e4c78c840d9a32481521ea5503075f54db6339e0..1a949c81b3b02d47dafcb3646d58a31b0b0a4ad9 100644 (file)
@@ -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));
     }
 }
index 2a8457bd00978ec2182b13585ddb5014cab6248a..58416e50cb574e9288c365f7c23e04d47342d1d4 100644 (file)
@@ -9,10 +9,10 @@ import java.util.List;
 
 public interface MultiLocalCommand extends LocalCommand {
 
-    void handleCommand(Namespace ns, List<Manager> m) throws CommandException;
+    void handleCommand(Namespace ns, List<Manager> 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 (file)
index 0000000..675d7f2
--- /dev/null
@@ -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 (file)
index 0000000..35f530b
--- /dev/null
@@ -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<Manager, Thread> newManagerRunner;
+
+    private final List<Pair<Manager, Thread>> receiveThreads = new ArrayList<>();
+    private final Object stopTrigger = new Object();
+    private final String objectPath;
+
+    public DbusSignalControlImpl(
+            final SignalCreator c, final Function<Manager, Thread> 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<DBusPath> listAccounts() {
+        synchronized (receiveThreads) {
+            return receiveThreads.stream()
+                    .map(Pair::first)
+                    .map(Manager::getUsername)
+                    .map(u -> new DBusPath(DbusConfig.getObjectPath(u)))
+                    .collect(Collectors.toList());
+        }
+    }
+}
index 5723b4362265047336ec7a489843bbd9d98a97fb..b88aa81e9bedb6087cb5ddd4d059f26a22638d2e 100644 (file)
@@ -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