1 package org.asamk.signal;
2 
3 import net.sourceforge.argparse4j.ArgumentParsers;
4 import net.sourceforge.argparse4j.impl.Arguments;
5 import net.sourceforge.argparse4j.inf.ArgumentParser;
6 import net.sourceforge.argparse4j.inf.Namespace;
7 
8 import org.asamk.Signal;
9 import org.asamk.signal.commands.Command;
10 import org.asamk.signal.commands.Commands;
11 import org.asamk.signal.commands.DbusCommand;
12 import org.asamk.signal.commands.ExtendedDbusCommand;
13 import org.asamk.signal.commands.LocalCommand;
14 import org.asamk.signal.commands.MultiLocalCommand;
15 import org.asamk.signal.commands.ProvisioningCommand;
16 import org.asamk.signal.commands.RegistrationCommand;
17 import org.asamk.signal.commands.SignalCreator;
18 import org.asamk.signal.commands.exceptions.CommandException;
19 import org.asamk.signal.commands.exceptions.IOErrorException;
20 import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
21 import org.asamk.signal.commands.exceptions.UserErrorException;
22 import org.asamk.signal.manager.Manager;
23 import org.asamk.signal.manager.NotRegisteredException;
24 import org.asamk.signal.manager.ProvisioningManager;
25 import org.asamk.signal.manager.RegistrationManager;
26 import org.asamk.signal.manager.config.ServiceConfig;
27 import org.asamk.signal.manager.config.ServiceEnvironment;
28 import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
29 import org.asamk.signal.util.IOUtils;
30 import org.freedesktop.dbus.connections.impl.DBusConnection;
31 import org.freedesktop.dbus.exceptions.DBusException;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34 import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
35 
36 import java.io.File;
37 import java.io.IOException;
38 import java.util.ArrayList;
39 import java.util.List;
40 
41 import static net.sourceforge.argparse4j.DefaultSettings.VERSION_0_9_0_DEFAULT_SETTINGS;
42 
43 public class App {
44 
45     private final static Logger logger = LoggerFactory.getLogger(App.class);
46 
47     private final Namespace ns;
48 
buildArgumentParser()49     static ArgumentParser buildArgumentParser() {
50         var parser = ArgumentParsers.newFor("signal-cli", VERSION_0_9_0_DEFAULT_SETTINGS)
51                 .includeArgumentNamesAsKeysInResult(true)
52                 .build()
53                 .defaultHelp(true)
54                 .description("Commandline interface for Signal.")
55                 .version(BaseConfig.PROJECT_NAME + " " + BaseConfig.PROJECT_VERSION);
56 
57         parser.addArgument("-v", "--version").help("Show package version.").action(Arguments.version());
58         parser.addArgument("--verbose")
59                 .help("Raise log level and include lib signal logs.")
60                 .action(Arguments.storeTrue());
61         parser.addArgument("--config")
62                 .help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).");
63 
64         parser.addArgument("-u", "--username").help("Specify your phone number, that will be your identifier.");
65 
66         var mut = parser.addMutuallyExclusiveGroup();
67         mut.addArgument("--dbus").help("Make request via user dbus.").action(Arguments.storeTrue());
68         mut.addArgument("--dbus-system").help("Make request via system dbus.").action(Arguments.storeTrue());
69 
70         parser.addArgument("-o", "--output")
71                 .help("Choose to output in plain text or JSON")
72                 .type(Arguments.enumStringType(OutputType.class));
73 
74         parser.addArgument("--service-environment")
75                 .help("Choose the server environment to use.")
76                 .type(Arguments.enumStringType(ServiceEnvironmentCli.class))
77                 .setDefault(ServiceEnvironmentCli.LIVE);
78 
79         parser.addArgument("--trust-new-identities")
80                 .help("Choose when to trust new identities.")
81                 .type(Arguments.enumStringType(TrustNewIdentityCli.class))
82                 .setDefault(TrustNewIdentityCli.ON_FIRST_USE);
83 
84         var subparsers = parser.addSubparsers().title("subcommands").dest("command");
85 
86         Commands.getCommandSubparserAttachers().forEach((key, value) -> {
87             var subparser = subparsers.addParser(key);
88             value.attachToSubparser(subparser);
89         });
90 
91         return parser;
92     }
93 
App(final Namespace ns)94     public App(final Namespace ns) {
95         this.ns = ns;
96     }
97 
init()98     public void init() throws CommandException {
99         var commandKey = ns.getString("command");
100         var command = Commands.getCommand(commandKey);
101         if (command == null) {
102             throw new UserErrorException("Command not implemented!");
103         }
104 
105         var outputTypeInput = ns.<OutputType>get("output");
106         var outputType = outputTypeInput == null
107                 ? command.getSupportedOutputTypes().stream().findFirst().orElse(null)
108                 : outputTypeInput;
109         var outputWriter = outputType == null
110                 ? null
111                 : outputType == OutputType.JSON ? new JsonWriterImpl(System.out) : new PlainTextWriterImpl(System.out);
112 
113         if (outputWriter != null && !command.getSupportedOutputTypes().contains(outputType)) {
114             throw new UserErrorException("Command doesn't support output type " + outputType);
115         }
116 
117         var username = ns.getString("username");
118 
119         final var useDbus = ns.getBoolean("dbus");
120         final var useDbusSystem = ns.getBoolean("dbus-system");
121         if (useDbus || useDbusSystem) {
122             // If username is null, it will connect to the default object path
123             initDbusClient(command, username, useDbusSystem, outputWriter);
124             return;
125         }
126 
127         final File dataPath;
128         var config = ns.getString("config");
129         if (config != null) {
130             dataPath = new File(config);
131         } else {
132             dataPath = getDefaultDataPath();
133         }
134 
135         if (!ServiceConfig.getCapabilities().isGv2()) {
136             logger.warn("WARNING: Support for new group V2 is disabled,"
137                     + " because the required native library dependency is missing: libzkgroup");
138         }
139 
140         if (!ServiceConfig.isSignalClientAvailable()) {
141             throw new UserErrorException("Missing required native library dependency: libsignal-client");
142         }
143 
144         final var serviceEnvironmentCli = ns.<ServiceEnvironmentCli>get("service-environment");
145         final var serviceEnvironment = serviceEnvironmentCli == ServiceEnvironmentCli.LIVE
146                 ? ServiceEnvironment.LIVE
147                 : ServiceEnvironment.SANDBOX;
148 
149         final var trustNewIdentityCli = ns.<TrustNewIdentityCli>get("trust-new-identities");
150         final var trustNewIdentity = trustNewIdentityCli == TrustNewIdentityCli.ON_FIRST_USE
151                 ? TrustNewIdentity.ON_FIRST_USE
152                 : trustNewIdentityCli == TrustNewIdentityCli.ALWAYS ? TrustNewIdentity.ALWAYS : TrustNewIdentity.NEVER;
153 
154         if (command instanceof ProvisioningCommand) {
155             if (username != null) {
156                 throw new UserErrorException("You cannot specify a username (phone number) when linking");
157             }
158 
159             handleProvisioningCommand((ProvisioningCommand) command, dataPath, serviceEnvironment, outputWriter);
160             return;
161         }
162 
163         if (username == null) {
164             var usernames = Manager.getAllLocalUsernames(dataPath);
165 
166             if (command instanceof MultiLocalCommand) {
167                 handleMultiLocalCommand((MultiLocalCommand) command,
168                         dataPath,
169                         serviceEnvironment,
170                         usernames,
171                         outputWriter,
172                         trustNewIdentity);
173                 return;
174             }
175 
176             if (usernames.size() == 0) {
177                 throw new UserErrorException("No local users found, you first need to register or link an account");
178             } else if (usernames.size() > 1) {
179                 throw new UserErrorException(
180                         "Multiple users found, you need to specify a username (phone number) with -u");
181             }
182 
183             username = usernames.get(0);
184         } else if (!PhoneNumberFormatter.isValidNumber(username, null)) {
185             throw new UserErrorException("Invalid username (phone number), make sure you include the country code.");
186         }
187 
188         if (command instanceof RegistrationCommand) {
189             handleRegistrationCommand((RegistrationCommand) command, username, dataPath, serviceEnvironment);
190             return;
191         }
192 
193         if (!(command instanceof LocalCommand)) {
194             throw new UserErrorException("Command only works via dbus");
195         }
196 
197         handleLocalCommand((LocalCommand) command,
198                 username,
199                 dataPath,
200                 serviceEnvironment,
201                 outputWriter,
202                 trustNewIdentity);
203     }
204 
handleProvisioningCommand( final ProvisioningCommand command, final File dataPath, final ServiceEnvironment serviceEnvironment, final OutputWriter outputWriter )205     private void handleProvisioningCommand(
206             final ProvisioningCommand command,
207             final File dataPath,
208             final ServiceEnvironment serviceEnvironment,
209             final OutputWriter outputWriter
210     ) throws CommandException {
211         var pm = ProvisioningManager.init(dataPath, serviceEnvironment, BaseConfig.USER_AGENT);
212         command.handleCommand(ns, pm, outputWriter);
213     }
214 
handleRegistrationCommand( final RegistrationCommand command, final String username, final File dataPath, final ServiceEnvironment serviceEnvironment )215     private void handleRegistrationCommand(
216             final RegistrationCommand command,
217             final String username,
218             final File dataPath,
219             final ServiceEnvironment serviceEnvironment
220     ) throws CommandException {
221         final RegistrationManager manager;
222         try {
223             manager = RegistrationManager.init(username, dataPath, serviceEnvironment, BaseConfig.USER_AGENT);
224         } catch (Throwable e) {
225             throw new UnexpectedErrorException("Error loading or creating state file: "
226                     + e.getMessage()
227                     + " ("
228                     + e.getClass().getSimpleName()
229                     + ")", e);
230         }
231         try (var m = manager) {
232             command.handleCommand(ns, m);
233         } catch (IOException e) {
234             logger.warn("Cleanup failed", e);
235         }
236     }
237 
handleLocalCommand( final LocalCommand command, final String username, final File dataPath, final ServiceEnvironment serviceEnvironment, final OutputWriter outputWriter, final TrustNewIdentity trustNewIdentity )238     private void handleLocalCommand(
239             final LocalCommand command,
240             final String username,
241             final File dataPath,
242             final ServiceEnvironment serviceEnvironment,
243             final OutputWriter outputWriter,
244             final TrustNewIdentity trustNewIdentity
245     ) throws CommandException {
246         try (var m = loadManager(username, dataPath, serviceEnvironment, trustNewIdentity)) {
247             command.handleCommand(ns, m, outputWriter);
248         } catch (IOException e) {
249             logger.warn("Cleanup failed", e);
250         }
251     }
252 
handleMultiLocalCommand( final MultiLocalCommand command, final File dataPath, final ServiceEnvironment serviceEnvironment, final List<String> usernames, final OutputWriter outputWriter, final TrustNewIdentity trustNewIdentity )253     private void handleMultiLocalCommand(
254             final MultiLocalCommand command,
255             final File dataPath,
256             final ServiceEnvironment serviceEnvironment,
257             final List<String> usernames,
258             final OutputWriter outputWriter,
259             final TrustNewIdentity trustNewIdentity
260     ) throws CommandException {
261         final var managers = new ArrayList<Manager>();
262         for (String u : usernames) {
263             try {
264                 managers.add(loadManager(u, dataPath, serviceEnvironment, trustNewIdentity));
265             } catch (CommandException e) {
266                 logger.warn("Ignoring {}: {}", u, e.getMessage());
267             }
268         }
269 
270         command.handleCommand(ns, managers, new SignalCreator() {
271             @Override
272             public ProvisioningManager getNewProvisioningManager() {
273                 return ProvisioningManager.init(dataPath, serviceEnvironment, BaseConfig.USER_AGENT);
274             }
275 
276             @Override
277             public RegistrationManager getNewRegistrationManager(String username) throws IOException {
278                 return RegistrationManager.init(username, dataPath, serviceEnvironment, BaseConfig.USER_AGENT);
279             }
280         }, outputWriter);
281 
282         for (var m : managers) {
283             try {
284                 m.close();
285             } catch (IOException e) {
286                 logger.warn("Cleanup failed", e);
287             }
288         }
289     }
290 
loadManager( final String username, final File dataPath, final ServiceEnvironment serviceEnvironment, final TrustNewIdentity trustNewIdentity )291     private Manager loadManager(
292             final String username,
293             final File dataPath,
294             final ServiceEnvironment serviceEnvironment,
295             final TrustNewIdentity trustNewIdentity
296     ) throws CommandException {
297         Manager manager;
298         try {
299             manager = Manager.init(username, dataPath, serviceEnvironment, BaseConfig.USER_AGENT, trustNewIdentity);
300         } catch (NotRegisteredException e) {
301             throw new UserErrorException("User " + username + " is not registered.");
302         } catch (Throwable e) {
303             throw new UnexpectedErrorException("Error loading state file for user "
304                     + username
305                     + ": "
306                     + e.getMessage()
307                     + " ("
308                     + e.getClass().getSimpleName()
309                     + ")", e);
310         }
311 
312         try {
313             manager.checkAccountState();
314         } catch (IOException e) {
315             throw new IOErrorException("Error while checking account " + username + ": " + e.getMessage(), e);
316         }
317 
318         return manager;
319     }
320 
initDbusClient( final Command command, final String username, final boolean systemBus, final OutputWriter outputWriter )321     private void initDbusClient(
322             final Command command, final String username, final boolean systemBus, final OutputWriter outputWriter
323     ) throws CommandException {
324         try {
325             DBusConnection.DBusBusType busType;
326             if (systemBus) {
327                 busType = DBusConnection.DBusBusType.SYSTEM;
328             } else {
329                 busType = DBusConnection.DBusBusType.SESSION;
330             }
331             try (var dBusConn = DBusConnection.getConnection(busType)) {
332                 var ts = dBusConn.getRemoteObject(DbusConfig.getBusname(),
333                         DbusConfig.getObjectPath(username),
334                         Signal.class);
335 
336                 handleCommand(command, ts, dBusConn, outputWriter);
337             }
338         } catch (DBusException | IOException e) {
339             logger.error("Dbus client failed", e);
340             throw new UnexpectedErrorException("Dbus client failed", e);
341         }
342     }
343 
handleCommand( Command command, Signal ts, DBusConnection dBusConn, OutputWriter outputWriter )344     private void handleCommand(
345             Command command, Signal ts, DBusConnection dBusConn, OutputWriter outputWriter
346     ) throws CommandException {
347         if (command instanceof ExtendedDbusCommand) {
348             ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn, outputWriter);
349         } else if (command instanceof DbusCommand) {
350             ((DbusCommand) command).handleCommand(ns, ts, outputWriter);
351         } else {
352             throw new UserErrorException("Command is not yet implemented via dbus");
353         }
354     }
355 
356     /**
357      * @return the default data directory to be used by signal-cli.
358      */
getDefaultDataPath()359     private static File getDefaultDataPath() {
360         return new File(IOUtils.getDataHomeDir(), "signal-cli");
361     }
362 }
363