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