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