Added socket ipc and docker support

This commit is contained in:
Matteljay 2020-07-07 10:53:11 +02:00
parent 4177deccf1
commit 04a545b395
13 changed files with 757 additions and 4 deletions

View file

@ -40,8 +40,12 @@ public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler
result.putPOJO("envelope", new JsonMessageEnvelope(envelope, content));
}
try {
jsonProcessor.writeValue(System.out, result);
System.out.println();
String output = jsonProcessor.writeValueAsString(result);
if (m.socketTasks != null) {
m.socketTasks.send(output);
} else {
System.out.println(output);
}
} catch (IOException e) {
e.printStackTrace();
}

View file

@ -26,6 +26,7 @@ import net.sourceforge.argparse4j.inf.Subparser;
import net.sourceforge.argparse4j.inf.Subparsers;
import org.asamk.Signal;
import org.asamk.signal.storage.SignalAccount;
import org.asamk.signal.commands.Command;
import org.asamk.signal.commands.Commands;
import org.asamk.signal.commands.DbusCommand;
@ -36,6 +37,7 @@ import org.asamk.signal.dbus.DbusSignalImpl;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.ProvisioningManager;
import org.asamk.signal.manager.ServiceConfig;
import org.asamk.signal.manager.PathConfig;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.SecurityProvider;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
@ -73,7 +75,7 @@ public class Main {
}
private static int handleCommands(Namespace ns) {
final String username = ns.getString("username");
String username = ns.getString("username");
if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) {
try {
@ -103,6 +105,16 @@ public class Main {
dataPath = getDefaultDataPath();
}
if (ns.getBoolean("singleuser")) {
String completeDataPath = PathConfig.createDefault(dataPath).getDataPath();
username = SignalAccount.getSingleUser(completeDataPath);
if (username == null) {
System.exit(1);
}
System.out.println("Assuming username: " + username);
ns.getAttrs().put("username", username);
}
final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration(BaseConfig.USER_AGENT);
if (username == null) {
@ -234,6 +246,9 @@ public class Main {
MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup();
mut.addArgument("-u", "--username")
.help("Specify your phone number, that will be used for verification.");
mut.addArgument("--singleuser")
.help("Can be used if only one user account exists on this machine.")
.action(Arguments.storeTrue());
mut.addArgument("--dbus")
.help("Make request via user dbus.")
.action(Arguments.storeTrue());
@ -267,7 +282,7 @@ public class Main {
System.err.println("You cannot specify a username (phone number) when linking");
System.exit(2);
}
} else if (!ns.getBoolean("dbus") && !ns.getBoolean("dbus_system")) {
} else if (!ns.getBoolean("dbus") && !ns.getBoolean("dbus_system") && !ns.getBoolean("singleuser")) {
if (ns.getString("username") == null) {
parser.printUsage();
System.err.println("You need to specify a username (phone number)");

View file

@ -34,6 +34,7 @@ public class Commands {
addCommand("updateProfile", new UpdateProfileCommand());
addCommand("verify", new VerifyCommand());
addCommand("uploadStickerPack", new UploadStickerPackCommand());
addCommand("socket", new SocketCommand());
}
public static Map<String, Command> getCommands() {

View file

@ -0,0 +1,65 @@
package org.asamk.signal.commands;
import java.util.concurrent.TimeUnit;
import java.io.IOException;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.JsonReceiveMessageHandler;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
import org.asamk.signal.socket.SocketTasks;
public class SocketCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("-a", "--address")
.setDefault("127.0.0.1")
.help("Socket bind address");
subparser.addArgument("-p", "--port")
.type(int.class)
.setDefault(24250)
.help("Socket port to use");
subparser.help("Receive at a socket while being able to send as well");
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
System.out.println("Starting socket thread...");
m.socketTasks = new SocketTasks(ns, m);
try {
Thread thread = new Thread(m.socketTasks);
thread.start();
} catch (Exception e) {
System.err.println(e);
}
try {
// Let the thread settle for a second
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {}
System.out.println("Listening from Signal server...");
final long timeout = 3600 * 1000; // timeout in ms
final boolean returnOnTimeout = false;
final boolean ignoreAttachments = true;
try {
final Manager.ReceiveMessageHandler handler = new JsonReceiveMessageHandler(m);
while(true) {
m.receiveMessages(timeout, TimeUnit.MILLISECONDS, returnOnTimeout, ignoreAttachments, handler);
}
} catch (IOException e) {
System.err.println("Error while receiving messages: " + e.getMessage());
return 3;
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
}
}
}

View file

@ -141,6 +141,8 @@ import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.asamk.signal.socket.SocketTasks;
public class Manager implements Closeable {
private final SleepTimer timer = new UptimeSleepTimer();
@ -153,6 +155,8 @@ public class Manager implements Closeable {
private SignalServiceMessagePipe messagePipe = null;
private SignalServiceMessagePipe unidentifiedMessagePipe = null;
public SocketTasks socketTasks = null;
public Manager(SignalAccount account, PathConfig pathConfig, SignalServiceConfiguration serviceConfiguration, String userAgent) {
this.account = account;
this.pathConfig = pathConfig;
@ -163,6 +167,10 @@ public class Manager implements Closeable {
this.account.setResolver(this::resolveSignalServiceAddress);
}
public SignalAccount getAccount() {
return account;
}
public String getUsername() {
return account.getUsername();
}

View file

@ -0,0 +1,287 @@
package org.asamk.signal.socket;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.io.IOException;
import java.net.Socket;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.core.type.TypeReference;
import org.asamk.signal.storage.SignalAccount;
import org.asamk.signal.storage.contacts.ContactInfo;
import org.asamk.signal.storage.groups.GroupInfo;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.Util;
import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.manager.AttachmentInvalidException;
import org.asamk.signal.manager.NotAGroupMemberException;
import org.whispersystems.util.Base64;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException;
import org.asamk.signal.socket.SocketTasks;
public class Commander {
private Manager m;
private SocketTasks tasks;
private ObjectMapper mapper;
public Commander(Manager manager, SocketTasks socketTasks) {
this.m = manager;
this.tasks = socketTasks;
this.mapper = new ObjectMapper();
}
public void handleCommand(String command, JsonNode arguments) {
switch (command) {
case "getContacts":
this.tasks.send(this.getContacts());
break;
case "updateContacts":
this.updateContacts(arguments);
break;
case "sendMessage":
this.sendMessage(arguments);
break;
case "trust":
this.trust(arguments);
break;
case "endSession":
this.endSession(arguments);
break;
case "getGroups":
this.tasks.send(this.getGroups());
break;
default:
this.tasks.send("Unknown command: '" + command + "'");
}
}
private String getGroups() {
ObjectNode node = this.mapper.createObjectNode();
List<GroupInfo> groups = this.m.getAccount().getGroupStore().getGroups();
for (GroupInfo g: groups) {
ObjectNode group = this.mapper.createObjectNode();
group.put("name", g.name);
group.put("color", g.color);
group.put("messageExpirationTime", g.messageExpirationTime);
group.put("blocked", g.blocked);
group.put("inboxPosition", g.inboxPosition);
group.put("archived", g.archived);
group.put("avatarId", g.getAvatarId());
for (String m: g.getMembersE164()) {
group.putArray("members").add(m);
}
node.set(Base64.encodeBytes(g.groupId), group);
}
String out = null;
try {
out = this.mapper.writeValueAsString(node); //writerWithDefaultPrettyPrinter()
} catch (Exception e) {
System.err.println("Could not send groups");
}
return out;
}
private void sendMessage(JsonNode args) {
Iterator<Map.Entry<String,JsonNode>> entryIt = args.fields();
Map.Entry<String,JsonNode> entry;
String argument;
String message = null;
ArrayList<String> contacts = null;
ArrayList<String> groups = null;
while (entryIt.hasNext()) {
entry = entryIt.next();
argument = entry.getKey();
switch(argument) {
case "contacts":
try {
contacts = this.mapper.readValue(entry.getValue().toString(), new TypeReference<ArrayList<String>>(){});
} catch (IOException e) {
System.err.println("bad list of contacts");
return;
}
break;
case "groups":
try {
groups = this.mapper.readValue(entry.getValue().toString(), new TypeReference<ArrayList<String>>(){});
} catch (IOException e) {
System.err.println("bad list of groups");
return;
}
break;
case "message":
message = entry.getValue().asText();
if (message.trim().length() <= 0) {
System.err.println("cannot send empty message");
return;
}
break;
}
}
if (contacts != null) {
try {
this.m.sendMessage(message, new ArrayList<>(), contacts);
} catch (Exception | EncapsulatedExceptions | InvalidNumberException e) {
System.err.println(e);
}
}
if (groups != null) {
byte[] groupId;
for (String group: groups) {
try {
groupId = Util.decodeGroupId(group);
} catch (GroupIdFormatException e) {
handleGroupIdFormatException(e);
continue;
}
try {
this.m.sendGroupMessage(message, new ArrayList<>(), groupId);
} catch (IOException | EncapsulatedExceptions | GroupNotFoundException | AttachmentInvalidException | NotAGroupMemberException e) {
System.err.println(e);
}
}
}
}
private void endSession(JsonNode args) {
Iterator<Map.Entry<String,JsonNode>> entryIt = args.fields();
Map.Entry<String,JsonNode> entry;
String argument;
ArrayList<String> contacts = null;
while (entryIt.hasNext()) {
entry = entryIt.next();
argument = entry.getKey();
switch(argument) {
case "contacts":
try {
contacts = this.mapper.readValue(entry.getValue().toString(), new TypeReference<ArrayList<String>>(){});
} catch (IOException e) {
System.err.println("bad list of contacts");
return;
}
break;
}
}
if (contacts != null) {
try {
this.m.sendEndSessionMessage(contacts);
System.err.println("WARNING: Ended session with " + String.join(", ", contacts));
} catch (Exception | EncapsulatedExceptions | InvalidNumberException e) {
System.err.println(e);
}
}
}
private void trust(JsonNode args) {
Iterator<Map.Entry<String,JsonNode>> entryIt = args.fields();
Map.Entry<String,JsonNode> entry;
String argument;
ArrayList<String> contacts = null;
while (entryIt.hasNext()) {
entry = entryIt.next();
argument = entry.getKey();
switch(argument) {
case "contacts":
try {
contacts = this.mapper.readValue(entry.getValue().toString(), new TypeReference<ArrayList<String>>(){});
} catch (IOException e) {
System.err.println("bad list of contacts");
return;
}
break;
}
}
if (contacts != null) {
for (String contact: contacts) {
this.m.trustIdentityAllKeys(contact);
}
System.err.println("WARNING: Updated trust for all keys from " + String.join(", ", contacts));
}
}
private String getContacts() {
ObjectNode node = this.mapper.createObjectNode();
List<ContactInfo> contacts = this.m.getContacts();
for (ContactInfo c: contacts) {
ObjectNode group = this.mapper.createObjectNode();
group.put("name", c.name);
group.put("uuid", c.uuid.toString());
group.put("color", c.color);
group.put("messageExpirationTime", c.messageExpirationTime);
group.put("profileKey", c.profileKey);
group.put("blocked", c.blocked);
group.put("inboxPosition", c.inboxPosition);
group.put("archived", c.archived);
node.set(c.number, group);
}
String out = null;
try {
out = this.mapper.writeValueAsString(node); //writerWithDefaultPrettyPrinter()
} catch (Exception e) {
System.err.println("Could not send contacts");
}
return out;
}
private void updateContacts(JsonNode args) {
Iterator<Map.Entry<String,JsonNode>> entryIt = args.fields();
Map.Entry<String,JsonNode> entry;
String number;
while (entryIt.hasNext()) {
entry = entryIt.next();
number = entry.getKey();
ContactInfo contactInfo = this.m.getContact(number);
if (contactInfo == null) {
System.err.println("Could not update " + number);
continue;
} else {
patchContactInfo(contactInfo, entry.getValue());
SignalAccount account = this.m.getAccount();
account.getContactStore().updateContact(contactInfo);
account.save();
System.out.println("Updated account info for " + number);
}
}
}
private void patchContactInfo(ContactInfo contactInfo, JsonNode args) {
Iterator<Map.Entry<String,JsonNode>> entryIt = args.fields();
Map.Entry<String,JsonNode> entry;
String argument;
JsonNode value;
while (entryIt.hasNext()) {
entry = entryIt.next();
argument = entry.getKey();
value = entry.getValue();
switch (argument) {
case "name":
contactInfo.name = value.asText();
break;
case "color":
contactInfo.color = value.asText();
break;
case "messageExpirationTime":
contactInfo.messageExpirationTime = value.asInt();
break;
case "blocked":
contactInfo.blocked = value.asBoolean();
break;
case "inboxPosition":
contactInfo.inboxPosition = value.asInt();
break;
case "archived":
contactInfo.archived = value.asBoolean();
break;
default:
System.err.println("Invalid field '" + argument + "' in " + contactInfo.number);
}
}
}
}

View file

@ -0,0 +1,102 @@
package org.asamk.signal.socket;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.InetAddress;
import java.nio.charset.Charset;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import net.sourceforge.argparse4j.inf.Namespace;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.socket.Commander;
public class SocketTasks implements Runnable {
private String address;
private int port;
private Socket socket;
private OutputStream output = null;
private ObjectMapper mapper;
private Commander commander;
public SocketTasks(Namespace namespace, Manager manager) {
this.address = namespace.getString("address");
this.port = namespace.getInt("port");
this.mapper = new ObjectMapper();
this.commander = new Commander(manager, this);
}
public void send(String message) {
if (message == null) return;
if (this.output == null || this.socket.isClosed()) {
System.err.println("WARNING: a message is ready but the socket isn't connected");
return;
}
message += "\n";
try {
this.output.write(message.getBytes(Charset.forName("UTF-8")));
this.output.flush();
} catch (SocketException e) {
System.err.println("Output socket connection lost!");
this.output = null;
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (true) {
try {
ServerSocket serverSocket = new ServerSocket(this.port, 1, InetAddress.getByName(this.address));
System.out.println("Socket ready, binding to address: " + this.address + ":" + this.port);
this.socket = serverSocket.accept(); // accept() blocks
this.output = socket.getOutputStream();
InputStream input = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input));
System.out.println("Connection established.");
while (this.responseProcessor(bufferedReader.readLine())); // readLine() blocks
bufferedReader.close();
this.output.close();
input.close();
this.socket.close();
serverSocket.close();
} catch (Exception e) { System.err.println(e); }
System.err.println("Socket connection lost.");
try {
// Help relax potential resource hogging and log spam
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {}
}
}
private boolean responseProcessor(String line) {
if (line == null) return false;
JsonNode node = null;
try {
node = this.mapper.readValue(line.trim(), JsonNode.class);
} catch (Exception e) { System.err.println(e); }
if (node == null) return true;
String reply = null;
Iterator<Map.Entry<String,JsonNode>> entryIt = node.fields();
Map.Entry<String,JsonNode> entry;
String command;
while (entryIt.hasNext()) {
entry = entryIt.next();
command = entry.getKey();
this.commander.handleCommand(command, entry.getValue());
}
return true;
}
}

View file

@ -152,6 +152,26 @@ public class SignalAccount implements Closeable {
return !(!f.exists() || f.isDirectory());
}
public static String getSingleUser(String dataPath) {
File folder = new File(dataPath);
File[] listOfFiles = folder.listFiles();
if (listOfFiles == null || listOfFiles.length <= 0) {
System.err.println("No user account found");
return null;
}
String user = null;
for (int i = 0; i < listOfFiles.length; i++) {
if (listOfFiles[i].isFile()) {
if (user != null) {
System.err.println("Too many user accounts found");
return null; // too many users
}
user = listOfFiles[i].getName();
}
}
return user;
}
private void load() throws IOException {
JsonNode rootNode;
synchronized (fileChannel) {