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