]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/Main.java
d562c4e1ad11279419a8efc1a40957a740ae383c
[signal-cli] / src / main / java / org / asamk / signal / Main.java
1 /**
2 * Copyright (C) 2015 AsamK
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17 package org.asamk.signal;
18
19 import net.sourceforge.argparse4j.ArgumentParsers;
20 import net.sourceforge.argparse4j.impl.Arguments;
21 import net.sourceforge.argparse4j.inf.*;
22 import org.apache.commons.io.IOUtils;
23 import org.apache.http.util.TextUtils;
24 import org.asamk.Signal;
25 import org.freedesktop.dbus.DBusConnection;
26 import org.freedesktop.dbus.exceptions.DBusException;
27 import org.freedesktop.dbus.exceptions.DBusExecutionException;
28 import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
29 import org.whispersystems.signalservice.api.messages.*;
30 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
31 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
32 import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
33 import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
34 import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
35 import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
36
37 import java.io.File;
38 import java.io.IOException;
39 import java.security.Security;
40 import java.util.ArrayList;
41 import java.util.List;
42
43 public class Main {
44
45 public static final String SIGNAL_BUSNAME = "org.asamk.Signal";
46 public static final String SIGNAL_OBJECTPATH = "/org/asamk/Signal";
47
48 public static void main(String[] args) {
49 // Workaround for BKS truststore
50 Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
51
52 Namespace ns = parseArgs(args);
53 if (ns == null) {
54 System.exit(1);
55 }
56
57 final String username = ns.getString("username");
58 Manager m;
59 Signal ts;
60 DBusConnection dBusConn = null;
61 try {
62 if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) {
63 try {
64 m = null;
65 int busType;
66 if (ns.getBoolean("dbus_system")) {
67 busType = DBusConnection.SYSTEM;
68 } else {
69 busType = DBusConnection.SESSION;
70 }
71 dBusConn = DBusConnection.getConnection(busType);
72 ts = (Signal) dBusConn.getRemoteObject(
73 SIGNAL_BUSNAME, SIGNAL_OBJECTPATH,
74 Signal.class);
75 } catch (DBusException e) {
76 e.printStackTrace();
77 if (dBusConn != null) {
78 dBusConn.disconnect();
79 }
80 System.exit(3);
81 return;
82 }
83 } else {
84 String settingsPath = ns.getString("config");
85 if (TextUtils.isEmpty(settingsPath)) {
86 settingsPath = System.getProperty("user.home") + "/.config/signal";
87 if (!new File(settingsPath).exists()) {
88 String legacySettingsPath = System.getProperty("user.home") + "/.config/textsecure";
89 if (new File(legacySettingsPath).exists()) {
90 settingsPath = legacySettingsPath;
91 }
92 }
93 }
94
95 m = new Manager(username, settingsPath);
96 ts = m;
97 if (m.userExists()) {
98 try {
99 m.load();
100 } catch (Exception e) {
101 System.err.println("Error loading state file \"" + m.getFileName() + "\": " + e.getMessage());
102 System.exit(2);
103 return;
104 }
105 }
106 }
107
108 switch (ns.getString("command")) {
109 case "register":
110 if (dBusConn != null) {
111 System.err.println("register is not yet implementd via dbus");
112 System.exit(1);
113 }
114 if (!m.userHasKeys()) {
115 m.createNewIdentity();
116 }
117 try {
118 m.register(ns.getBoolean("voice"));
119 } catch (IOException e) {
120 System.err.println("Request verify error: " + e.getMessage());
121 System.exit(3);
122 }
123 break;
124 case "verify":
125 if (dBusConn != null) {
126 System.err.println("verify is not yet implementd via dbus");
127 System.exit(1);
128 }
129 if (!m.userHasKeys()) {
130 System.err.println("User has no keys, first call register.");
131 System.exit(1);
132 }
133 if (m.isRegistered()) {
134 System.err.println("User registration is already verified");
135 System.exit(1);
136 }
137 try {
138 m.verifyAccount(ns.getString("verificationCode"));
139 } catch (IOException e) {
140 System.err.println("Verify error: " + e.getMessage());
141 System.exit(3);
142 }
143 break;
144 case "send":
145 if (dBusConn == null && !m.isRegistered()) {
146 System.err.println("User is not registered.");
147 System.exit(1);
148 }
149
150 if (ns.getBoolean("endsession")) {
151 if (ns.getList("recipient") == null) {
152 System.err.println("No recipients given");
153 System.err.println("Aborting sending.");
154 System.exit(1);
155 }
156 try {
157 ts.sendEndSessionMessage(ns.<String>getList("recipient"));
158 } catch (IOException e) {
159 handleIOException(e);
160 } catch (EncapsulatedExceptions e) {
161 handleEncapsulatedExceptions(e);
162 } catch (AssertionError e) {
163 handleAssertionError(e);
164 } catch (DBusExecutionException e) {
165 handleDBusExecutionException(e);
166 }
167 } else {
168 String messageText = ns.getString("message");
169 if (messageText == null) {
170 try {
171 messageText = IOUtils.toString(System.in);
172 } catch (IOException e) {
173 System.err.println("Failed to read message from stdin: " + e.getMessage());
174 System.err.println("Aborting sending.");
175 System.exit(1);
176 }
177 }
178
179 try {
180 List<String> attachments = ns.getList("attachment");
181 if (attachments == null) {
182 attachments = new ArrayList<>();
183 }
184 if (ns.getString("group") != null) {
185 byte[] groupId = decodeGroupId(ns.getString("group"));
186 ts.sendGroupMessage(messageText, attachments, groupId);
187 } else {
188 ts.sendMessage(messageText, attachments, ns.<String>getList("recipient"));
189 }
190 } catch (IOException e) {
191 handleIOException(e);
192 } catch (EncapsulatedExceptions e) {
193 handleEncapsulatedExceptions(e);
194 } catch (AssertionError e) {
195 handleAssertionError(e);
196 } catch (GroupNotFoundException e) {
197 handleGroupNotFoundException(e);
198 } catch (AttachmentInvalidException e) {
199 System.err.println("Failed to add attachment: " + e.getMessage());
200 System.err.println("Aborting sending.");
201 System.exit(1);
202 } catch (DBusExecutionException e) {
203 handleDBusExecutionException(e);
204 }
205 }
206
207 break;
208 case "receive":
209 if (dBusConn != null) {
210 System.err.println("receive is not yet implementd via dbus");
211 System.exit(1);
212 }
213 if (!m.isRegistered()) {
214 System.err.println("User is not registered.");
215 System.exit(1);
216 }
217 int timeout = 5;
218 if (ns.getInt("timeout") != null) {
219 timeout = ns.getInt("timeout");
220 }
221 boolean returnOnTimeout = true;
222 if (timeout < 0) {
223 returnOnTimeout = false;
224 timeout = 3600;
225 }
226 try {
227 m.receiveMessages(timeout, returnOnTimeout, new ReceiveMessageHandler(m));
228 } catch (IOException e) {
229 System.err.println("Error while receiving messages: " + e.getMessage());
230 System.exit(3);
231 } catch (AssertionError e) {
232 handleAssertionError(e);
233 }
234 break;
235 case "quitGroup":
236 if (dBusConn != null) {
237 System.err.println("quitGroup is not yet implementd via dbus");
238 System.exit(1);
239 }
240 if (!m.isRegistered()) {
241 System.err.println("User is not registered.");
242 System.exit(1);
243 }
244
245 try {
246 m.sendQuitGroupMessage(decodeGroupId(ns.getString("group")));
247 } catch (IOException e) {
248 handleIOException(e);
249 } catch (EncapsulatedExceptions e) {
250 handleEncapsulatedExceptions(e);
251 } catch (AssertionError e) {
252 handleAssertionError(e);
253 } catch (GroupNotFoundException e) {
254 handleGroupNotFoundException(e);
255 }
256
257 break;
258 case "updateGroup":
259 if (dBusConn != null) {
260 System.err.println("updateGroup is not yet implementd via dbus");
261 System.exit(1);
262 }
263 if (!m.isRegistered()) {
264 System.err.println("User is not registered.");
265 System.exit(1);
266 }
267
268 try {
269 byte[] groupId = null;
270 if (ns.getString("group") != null) {
271 groupId = decodeGroupId(ns.getString("group"));
272 }
273 byte[] newGroupId = m.sendUpdateGroupMessage(groupId, ns.getString("name"), ns.<String>getList("member"), ns.getString("avatar"));
274 if (groupId == null) {
275 System.out.println("Creating new group \"" + Base64.encodeBytes(newGroupId) + "\" …");
276 }
277 } catch (IOException e) {
278 handleIOException(e);
279 } catch (AttachmentInvalidException e) {
280 System.err.println("Failed to add avatar attachment for group\": " + e.getMessage());
281 System.err.println("Aborting sending.");
282 System.exit(1);
283 } catch (GroupNotFoundException e) {
284 handleGroupNotFoundException(e);
285 } catch (EncapsulatedExceptions e) {
286 handleEncapsulatedExceptions(e);
287 }
288
289 break;
290 case "daemon":
291 if (dBusConn != null) {
292 System.err.println("Stop it.");
293 System.exit(1);
294 }
295 if (!m.isRegistered()) {
296 System.err.println("User is not registered.");
297 System.exit(1);
298 }
299 DBusConnection conn = null;
300 try {
301 try {
302 int busType;
303 if (ns.getBoolean("system")) {
304 busType = DBusConnection.SYSTEM;
305 } else {
306 busType = DBusConnection.SESSION;
307 }
308 conn = DBusConnection.getConnection(busType);
309 conn.exportObject(SIGNAL_OBJECTPATH, m);
310 conn.requestBusName(SIGNAL_BUSNAME);
311 } catch (DBusException e) {
312 e.printStackTrace();
313 System.exit(3);
314 }
315 try {
316 m.receiveMessages(3600, false, new DbusReceiveMessageHandler(m, conn));
317 } catch (IOException e) {
318 System.err.println("Error while receiving messages: " + e.getMessage());
319 System.exit(3);
320 } catch (AssertionError e) {
321 handleAssertionError(e);
322 }
323 } finally {
324 if (conn != null) {
325 conn.disconnect();
326 }
327 }
328
329 break;
330 }
331 System.exit(0);
332 } finally {
333 if (dBusConn != null) {
334 dBusConn.disconnect();
335 }
336 }
337 }
338
339 private static void handleGroupNotFoundException(GroupNotFoundException e) {
340 System.err.println("Failed to send to group: " + e.getMessage());
341 System.err.println("Aborting sending.");
342 System.exit(1);
343 }
344
345 private static void handleDBusExecutionException(DBusExecutionException e) {
346 System.err.println("Cannot connect to dbus: " + e.getMessage());
347 System.err.println("Aborting.");
348 System.exit(1);
349 }
350
351 private static byte[] decodeGroupId(String groupId) {
352 try {
353 return Base64.decode(groupId);
354 } catch (IOException e) {
355 System.err.println("Failed to decode groupId (must be base64) \"" + groupId + "\": " + e.getMessage());
356 System.err.println("Aborting sending.");
357 System.exit(1);
358 return null;
359 }
360 }
361
362 private static Namespace parseArgs(String[] args) {
363 ArgumentParser parser = ArgumentParsers.newArgumentParser("signal-cli")
364 .defaultHelp(true)
365 .description("Commandline interface for Signal.")
366 .version(Manager.PROJECT_NAME + " " + Manager.PROJECT_VERSION);
367
368 parser.addArgument("-v", "--version")
369 .help("Show package version.")
370 .action(Arguments.version());
371 parser.addArgument("--config")
372 .help("Set the path, where to store the config (Default: $HOME/.config/signal-cli).");
373
374 MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup();
375 mut.addArgument("-u", "--username")
376 .help("Specify your phone number, that will be used for verification.");
377 mut.addArgument("--dbus")
378 .help("Make request via user dbus.")
379 .action(Arguments.storeTrue());
380 mut.addArgument("--dbus-system")
381 .help("Make request via system dbus.")
382 .action(Arguments.storeTrue());
383
384 Subparsers subparsers = parser.addSubparsers()
385 .title("subcommands")
386 .dest("command")
387 .description("valid subcommands")
388 .help("additional help");
389
390 Subparser parserRegister = subparsers.addParser("register");
391 parserRegister.addArgument("-v", "--voice")
392 .help("The verification should be done over voice, not sms.")
393 .action(Arguments.storeTrue());
394
395 Subparser parserVerify = subparsers.addParser("verify");
396 parserVerify.addArgument("verificationCode")
397 .help("The verification code you received via sms or voice call.");
398
399 Subparser parserSend = subparsers.addParser("send");
400 parserSend.addArgument("-g", "--group")
401 .help("Specify the recipient group ID.");
402 parserSend.addArgument("recipient")
403 .help("Specify the recipients' phone number.")
404 .nargs("*");
405 parserSend.addArgument("-m", "--message")
406 .help("Specify the message, if missing standard input is used.");
407 parserSend.addArgument("-a", "--attachment")
408 .nargs("*")
409 .help("Add file as attachment");
410 parserSend.addArgument("-e", "--endsession")
411 .help("Clear session state and send end session message.")
412 .action(Arguments.storeTrue());
413
414 Subparser parserLeaveGroup = subparsers.addParser("quitGroup");
415 parserLeaveGroup.addArgument("-g", "--group")
416 .required(true)
417 .help("Specify the recipient group ID.");
418
419 Subparser parserUpdateGroup = subparsers.addParser("updateGroup");
420 parserUpdateGroup.addArgument("-g", "--group")
421 .help("Specify the recipient group ID.");
422 parserUpdateGroup.addArgument("-n", "--name")
423 .help("Specify the new group name.");
424 parserUpdateGroup.addArgument("-a", "--avatar")
425 .help("Specify a new group avatar image file");
426 parserUpdateGroup.addArgument("-m", "--member")
427 .nargs("*")
428 .help("Specify one or more members to add to the group");
429
430 Subparser parserReceive = subparsers.addParser("receive");
431 parserReceive.addArgument("-t", "--timeout")
432 .type(int.class)
433 .help("Number of seconds to wait for new messages (negative values disable timeout)");
434
435 Subparser parserDaemon = subparsers.addParser("daemon");
436 parserDaemon.addArgument("--system")
437 .action(Arguments.storeTrue())
438 .help("Use DBus system bus instead of user bus.");
439
440 try {
441 Namespace ns = parser.parseArgs(args);
442 if (!ns.getBoolean("dbus") && !ns.getBoolean("dbus_system")) {
443 if (ns.getString("username") == null) {
444 parser.printUsage();
445 System.err.println("You need to specify a username (phone number)");
446 System.exit(2);
447 }
448 if (!PhoneNumberFormatter.isValidNumber(ns.getString("username"))) {
449 System.err.println("Invalid username (phone number), make sure you include the country code.");
450 System.exit(2);
451 }
452 }
453 if (ns.getList("recipient") != null && !ns.getList("recipient").isEmpty() && ns.getString("group") != null) {
454 System.err.println("You cannot specify recipients by phone number and groups a the same time");
455 System.exit(2);
456 }
457 return ns;
458 } catch (ArgumentParserException e) {
459 parser.handleError(e);
460 return null;
461 }
462 }
463
464 private static void handleAssertionError(AssertionError e) {
465 System.err.println("Failed to send/receive message (Assertion): " + e.getMessage());
466 e.printStackTrace();
467 System.err.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README");
468 System.exit(1);
469 }
470
471 private static void handleEncapsulatedExceptions(EncapsulatedExceptions e) {
472 System.err.println("Failed to send (some) messages:");
473 for (NetworkFailureException n : e.getNetworkExceptions()) {
474 System.err.println("Network failure for \"" + n.getE164number() + "\": " + n.getMessage());
475 }
476 for (UnregisteredUserException n : e.getUnregisteredUserExceptions()) {
477 System.err.println("Unregistered user \"" + n.getE164Number() + "\": " + n.getMessage());
478 }
479 for (UntrustedIdentityException n : e.getUntrustedIdentityExceptions()) {
480 System.err.println("Untrusted Identity for \"" + n.getE164Number() + "\": " + n.getMessage());
481 }
482 }
483
484 private static void handleIOException(IOException e) {
485 System.err.println("Failed to send message: " + e.getMessage());
486 }
487
488 private static class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
489 final Manager m;
490
491 public ReceiveMessageHandler(Manager m) {
492 this.m = m;
493 }
494
495 @Override
496 public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, GroupInfo group) {
497 SignalServiceAddress source = envelope.getSourceAddress();
498 System.out.println(String.format("Envelope from: %s (device: %d)", source.getNumber(), envelope.getSourceDevice()));
499 if (source.getRelay().isPresent()) {
500 System.out.println("Relayed by: " + source.getRelay().get());
501 }
502 System.out.println("Timestamp: " + envelope.getTimestamp());
503
504 if (envelope.isReceipt()) {
505 System.out.println("Got receipt.");
506 } else if (envelope.isSignalMessage() | envelope.isPreKeySignalMessage()) {
507 if (content == null) {
508 System.out.println("Failed to decrypt message.");
509 } else {
510 if (content.getDataMessage().isPresent()) {
511 SignalServiceDataMessage message = content.getDataMessage().get();
512
513 System.out.println("Message timestamp: " + message.getTimestamp());
514
515 if (message.getBody().isPresent()) {
516 System.out.println("Body: " + message.getBody().get());
517 }
518 if (message.getGroupInfo().isPresent()) {
519 SignalServiceGroup groupInfo = message.getGroupInfo().get();
520 System.out.println("Group info:");
521 System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId()));
522 if (groupInfo.getName().isPresent()) {
523 System.out.println(" Name: " + groupInfo.getName().get());
524 } else if (group != null) {
525 System.out.println(" Name: " + group.name);
526 } else {
527 System.out.println(" Name: <Unknown group>");
528 }
529 System.out.println(" Type: " + groupInfo.getType());
530 if (groupInfo.getMembers().isPresent()) {
531 for (String member : groupInfo.getMembers().get()) {
532 System.out.println(" Member: " + member);
533 }
534 }
535 if (groupInfo.getAvatar().isPresent()) {
536 System.out.println(" Avatar:");
537 printAttachment(groupInfo.getAvatar().get());
538 }
539 }
540 if (message.isEndSession()) {
541 System.out.println("Is end session");
542 }
543
544 if (message.getAttachments().isPresent()) {
545 System.out.println("Attachments: ");
546 for (SignalServiceAttachment attachment : message.getAttachments().get()) {
547 printAttachment(attachment);
548 }
549 }
550 }
551 if (content.getSyncMessage().isPresent()) {
552 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
553 System.out.println("Received sync message");
554 }
555 }
556 } else {
557 System.out.println("Unknown message received.");
558 }
559 System.out.println();
560 }
561
562 private void printAttachment(SignalServiceAttachment attachment) {
563 System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")");
564 if (attachment.isPointer()) {
565 final SignalServiceAttachmentPointer pointer = attachment.asPointer();
566 System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : ""));
567 System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "<unavailable>") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : ""));
568 File file = m.getAttachmentFile(pointer.getId());
569 if (file.exists()) {
570 System.out.println(" Stored plaintext in: " + file);
571 }
572 }
573 }
574 }
575
576 private static class DbusReceiveMessageHandler extends ReceiveMessageHandler {
577 final DBusConnection conn;
578
579 public DbusReceiveMessageHandler(Manager m, DBusConnection conn) {
580 super(m);
581 this.conn = conn;
582 }
583
584 @Override
585 public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, GroupInfo group) {
586 super.handleMessage(envelope, content, group);
587
588 if (!envelope.isReceipt() && content != null && content.getDataMessage().isPresent()) {
589 SignalServiceDataMessage message = content.getDataMessage().get();
590
591 if (!message.isEndSession() &&
592 !(message.getGroupInfo().isPresent() &&
593 message.getGroupInfo().get().getType() != SignalServiceGroup.Type.DELIVER)) {
594 List<String> attachments = new ArrayList<>();
595 if (message.getAttachments().isPresent()) {
596 for (SignalServiceAttachment attachment : message.getAttachments().get()) {
597 if (attachment.isPointer()) {
598 attachments.add(m.getAttachmentFile(attachment.asPointer().getId()).getAbsolutePath());
599 }
600 }
601 }
602
603 try {
604 conn.sendSignal(new Signal.MessageReceived(
605 SIGNAL_OBJECTPATH,
606 message.getTimestamp(),
607 envelope.getSource(),
608 message.getGroupInfo().isPresent() ? message.getGroupInfo().get().getGroupId() : new byte[0],
609 message.getBody().isPresent() ? message.getBody().get() : "",
610 attachments));
611 } catch (DBusException e) {
612 e.printStackTrace();
613 }
614 }
615 }
616 }
617
618 private void printAttachment(SignalServiceAttachment attachment) {
619 System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")");
620 if (attachment.isPointer()) {
621 final SignalServiceAttachmentPointer pointer = attachment.asPointer();
622 System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : ""));
623 System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "<unavailable>") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : ""));
624 File file = m.getAttachmentFile(pointer.getId());
625 if (file.exists()) {
626 System.out.println(" Stored plaintext in: " + file);
627 }
628 }
629 }
630 }
631 }