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