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