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