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