]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/commands/DaemonCommand.java
Implement register and verify commands for json rpc
[signal-cli] / src / main / java / org / asamk / signal / commands / DaemonCommand.java
1 package org.asamk.signal.commands;
2
3 import net.sourceforge.argparse4j.impl.Arguments;
4 import net.sourceforge.argparse4j.inf.Namespace;
5 import net.sourceforge.argparse4j.inf.Subparser;
6
7 import org.asamk.signal.DbusConfig;
8 import org.asamk.signal.JsonReceiveMessageHandler;
9 import org.asamk.signal.JsonWriter;
10 import org.asamk.signal.JsonWriterImpl;
11 import org.asamk.signal.OutputType;
12 import org.asamk.signal.OutputWriter;
13 import org.asamk.signal.PlainTextWriter;
14 import org.asamk.signal.ReceiveMessageHandler;
15 import org.asamk.signal.commands.exceptions.CommandException;
16 import org.asamk.signal.commands.exceptions.IOErrorException;
17 import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
18 import org.asamk.signal.dbus.DbusSignalControlImpl;
19 import org.asamk.signal.dbus.DbusSignalImpl;
20 import org.asamk.signal.jsonrpc.SignalJsonRpcDispatcherHandler;
21 import org.asamk.signal.manager.Manager;
22 import org.asamk.signal.manager.MultiAccountManager;
23 import org.asamk.signal.util.IOUtils;
24 import org.freedesktop.dbus.connections.impl.DBusConnection;
25 import org.freedesktop.dbus.exceptions.DBusException;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
28
29 import java.io.File;
30 import java.io.IOException;
31 import java.net.UnixDomainSocketAddress;
32 import java.nio.channels.Channel;
33 import java.nio.channels.Channels;
34 import java.nio.channels.ServerSocketChannel;
35 import java.nio.channels.SocketChannel;
36 import java.nio.charset.StandardCharsets;
37 import java.util.List;
38 import java.util.Objects;
39 import java.util.function.Consumer;
40 import java.util.stream.Collectors;
41
42 public class DaemonCommand implements MultiLocalCommand, LocalCommand {
43
44 private final static Logger logger = LoggerFactory.getLogger(DaemonCommand.class);
45
46 @Override
47 public String getName() {
48 return "daemon";
49 }
50
51 @Override
52 public void attachToSubparser(final Subparser subparser) {
53 final var defaultSocketPath = new File(new File(IOUtils.getRuntimeDir(), "signal-cli"), "socket");
54 subparser.help("Run in daemon mode and provide an experimental dbus or JSON-RPC interface.");
55 subparser.addArgument("--dbus")
56 .action(Arguments.storeTrue())
57 .help("Expose a DBus interface on the user bus (the default, if no other options are given).");
58 subparser.addArgument("--dbus-system", "--system")
59 .action(Arguments.storeTrue())
60 .help("Expose a DBus interface on the system bus.");
61 subparser.addArgument("--socket")
62 .nargs("?")
63 .type(File.class)
64 .setConst(defaultSocketPath)
65 .help("Expose a JSON-RPC interface on a UNIX socket (default $XDG_RUNTIME_DIR/signal-cli/socket).");
66 subparser.addArgument("--tcp")
67 .nargs("?")
68 .setConst("localhost:7583")
69 .help("Expose a JSON-RPC interface on a TCP socket (default localhost:7583).");
70 subparser.addArgument("--no-receive-stdout")
71 .help("Don’t print received messages to stdout.")
72 .action(Arguments.storeTrue());
73 subparser.addArgument("--receive-mode")
74 .help("Specify when to start receiving messages.")
75 .type(Arguments.enumStringType(ReceiveMode.class))
76 .setDefault(ReceiveMode.ON_START);
77 subparser.addArgument("--ignore-attachments")
78 .help("Don’t download attachments of received messages.")
79 .action(Arguments.storeTrue());
80 }
81
82 @Override
83 public List<OutputType> getSupportedOutputTypes() {
84 return List.of(OutputType.PLAIN_TEXT, OutputType.JSON);
85 }
86
87 @Override
88 public void handleCommand(
89 final Namespace ns, final Manager m, final OutputWriter outputWriter
90 ) throws CommandException {
91 logger.info("Starting daemon in single-account mode for " + m.getSelfNumber());
92 final var noReceiveStdOut = Boolean.TRUE.equals(ns.getBoolean("no-receive-stdout"));
93 final var receiveMode = ns.<ReceiveMode>get("receive-mode");
94 final var ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments"));
95
96 m.setIgnoreAttachments(ignoreAttachments);
97 addDefaultReceiveHandler(m, noReceiveStdOut ? null : outputWriter, receiveMode != ReceiveMode.ON_START);
98
99 final Channel inheritedChannel;
100 try {
101 inheritedChannel = System.inheritedChannel();
102 if (inheritedChannel instanceof ServerSocketChannel serverChannel) {
103 logger.info("Using inherited socket: " + serverChannel.getLocalAddress());
104 runSocketSingleAccount(m, serverChannel, receiveMode == ReceiveMode.MANUAL);
105 }
106 } catch (IOException e) {
107 throw new IOErrorException("Failed to use inherited socket", e);
108 }
109 final var socketFile = ns.<File>get("socket");
110 if (socketFile != null) {
111 final var address = UnixDomainSocketAddress.of(socketFile.toPath());
112 final var serverChannel = IOUtils.bindSocket(address);
113 runSocketSingleAccount(m, serverChannel, receiveMode == ReceiveMode.MANUAL);
114 }
115 final var tcpAddress = ns.getString("tcp");
116 if (tcpAddress != null) {
117 final var address = IOUtils.parseInetSocketAddress(tcpAddress);
118 final var serverChannel = IOUtils.bindSocket(address);
119 runSocketSingleAccount(m, serverChannel, receiveMode == ReceiveMode.MANUAL);
120 }
121 final var isDbusSystem = Boolean.TRUE.equals(ns.getBoolean("dbus-system"));
122 if (isDbusSystem) {
123 runDbusSingleAccount(m, true, receiveMode != ReceiveMode.ON_START);
124 }
125 final var isDbusSession = Boolean.TRUE.equals(ns.getBoolean("dbus"));
126 if (isDbusSession || (
127 !isDbusSystem
128 && socketFile == null
129 && tcpAddress == null
130 && !(inheritedChannel instanceof ServerSocketChannel)
131 )) {
132 runDbusSingleAccount(m, false, receiveMode != ReceiveMode.ON_START);
133 }
134
135 synchronized (this) {
136 try {
137 wait();
138 } catch (InterruptedException ignored) {
139 }
140 }
141 }
142
143 @Override
144 public void handleCommand(
145 final Namespace ns, final MultiAccountManager c, final OutputWriter outputWriter
146 ) throws CommandException {
147 logger.info("Starting daemon in multi-account mode");
148 final var noReceiveStdOut = Boolean.TRUE.equals(ns.getBoolean("no-receive-stdout"));
149 final var receiveMode = ns.<ReceiveMode>get("receive-mode");
150 final var ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments"));
151
152 c.getAccountNumbers().stream().map(c::getManager).filter(Objects::nonNull).forEach(m -> {
153 m.setIgnoreAttachments(ignoreAttachments);
154 addDefaultReceiveHandler(m, noReceiveStdOut ? null : outputWriter, receiveMode != ReceiveMode.ON_START);
155 });
156 c.addOnManagerAddedHandler(m -> {
157 m.setIgnoreAttachments(ignoreAttachments);
158 addDefaultReceiveHandler(m, noReceiveStdOut ? null : outputWriter, receiveMode != ReceiveMode.ON_START);
159 });
160
161 final Channel inheritedChannel;
162 try {
163 inheritedChannel = System.inheritedChannel();
164 if (inheritedChannel instanceof ServerSocketChannel serverChannel) {
165 logger.info("Using inherited socket: " + serverChannel.getLocalAddress());
166 runSocketMultiAccount(c, serverChannel, receiveMode == ReceiveMode.MANUAL);
167 }
168 } catch (IOException e) {
169 throw new IOErrorException("Failed to use inherited socket", e);
170 }
171 final var socketFile = ns.<File>get("socket");
172 if (socketFile != null) {
173 final var address = UnixDomainSocketAddress.of(socketFile.toPath());
174 final var serverChannel = IOUtils.bindSocket(address);
175 runSocketMultiAccount(c, serverChannel, receiveMode == ReceiveMode.MANUAL);
176 }
177 final var tcpAddress = ns.getString("tcp");
178 if (tcpAddress != null) {
179 final var address = IOUtils.parseInetSocketAddress(tcpAddress);
180 final var serverChannel = IOUtils.bindSocket(address);
181 runSocketMultiAccount(c, serverChannel, receiveMode == ReceiveMode.MANUAL);
182 }
183 final var isDbusSystem = Boolean.TRUE.equals(ns.getBoolean("dbus-system"));
184 if (isDbusSystem) {
185 runDbusMultiAccount(c, receiveMode != ReceiveMode.ON_START, true);
186 }
187 final var isDbusSession = Boolean.TRUE.equals(ns.getBoolean("dbus"));
188 if (isDbusSession || (
189 !isDbusSystem
190 && socketFile == null
191 && tcpAddress == null
192 && !(inheritedChannel instanceof ServerSocketChannel)
193 )) {
194 runDbusMultiAccount(c, receiveMode != ReceiveMode.ON_START, false);
195 }
196
197 synchronized (this) {
198 try {
199 wait();
200 } catch (InterruptedException ignored) {
201 }
202 }
203 }
204
205 private void addDefaultReceiveHandler(Manager m, OutputWriter outputWriter, final boolean isWeakListener) {
206 final var handler = outputWriter instanceof JsonWriter o
207 ? new JsonReceiveMessageHandler(m, o)
208 : outputWriter instanceof PlainTextWriter o
209 ? new ReceiveMessageHandler(m, o)
210 : Manager.ReceiveMessageHandler.EMPTY;
211 m.addReceiveHandler(handler, isWeakListener);
212 }
213
214 private void runSocketSingleAccount(
215 final Manager m, final ServerSocketChannel serverChannel, final boolean noReceiveOnStart
216 ) {
217 runSocket(serverChannel, channel -> {
218 final var handler = getSignalJsonRpcDispatcherHandler(channel, noReceiveOnStart);
219 handler.handleConnection(m);
220 });
221 }
222
223 private void runSocketMultiAccount(
224 final MultiAccountManager c, final ServerSocketChannel serverChannel, final boolean noReceiveOnStart
225 ) {
226 runSocket(serverChannel, channel -> {
227 final var handler = getSignalJsonRpcDispatcherHandler(channel, noReceiveOnStart);
228 handler.handleConnection(c);
229 });
230 }
231
232 private void runSocket(final ServerSocketChannel serverChannel, Consumer<SocketChannel> socketHandler) {
233 final var mainThread = Thread.currentThread();
234 new Thread(() -> {
235 while (true) {
236 final SocketChannel channel;
237 final String clientString;
238 try {
239 channel = serverChannel.accept();
240 clientString = channel.getRemoteAddress() + " " + IOUtils.getUnixDomainPrincipal(channel);
241 logger.info("Accepted new client: " + clientString);
242 } catch (IOException e) {
243 logger.error("Failed to accept new socket connection", e);
244 mainThread.notifyAll();
245 break;
246 }
247 new Thread(() -> {
248 try (final var c = channel) {
249 socketHandler.accept(c);
250 logger.info("Connection closed: " + clientString);
251 } catch (IOException e) {
252 logger.warn("Failed to close channel", e);
253 }
254 }).start();
255 }
256 }).start();
257 }
258
259 private SignalJsonRpcDispatcherHandler getSignalJsonRpcDispatcherHandler(
260 final SocketChannel c, final boolean noReceiveOnStart
261 ) {
262 final var lineSupplier = IOUtils.getLineSupplier(Channels.newReader(c, StandardCharsets.UTF_8));
263 final var jsonOutputWriter = new JsonWriterImpl(Channels.newWriter(c, StandardCharsets.UTF_8));
264
265 return new SignalJsonRpcDispatcherHandler(jsonOutputWriter, lineSupplier, noReceiveOnStart);
266 }
267
268 private void runDbusSingleAccount(
269 final Manager m, final boolean isDbusSystem, final boolean noReceiveOnStart
270 ) throws UnexpectedErrorException {
271 runDbus(isDbusSystem, (conn, objectPath) -> {
272 try {
273 exportDbusObject(conn, objectPath, m, noReceiveOnStart).join();
274 } catch (InterruptedException ignored) {
275 }
276 });
277 }
278
279 private void runDbusMultiAccount(
280 final MultiAccountManager c, final boolean noReceiveOnStart, final boolean isDbusSystem
281 ) throws UnexpectedErrorException {
282 runDbus(isDbusSystem, (connection, objectPath) -> {
283 final var signalControl = new DbusSignalControlImpl(c, objectPath);
284 connection.exportObject(signalControl);
285
286 c.addOnManagerAddedHandler(m -> {
287 final var thread = exportMultiAccountManager(connection, m, noReceiveOnStart);
288 if (thread != null) {
289 try {
290 thread.join();
291 } catch (InterruptedException ignored) {
292 }
293 }
294 });
295
296 final var initThreads = c.getAccountNumbers()
297 .stream()
298 .map(c::getManager)
299 .filter(Objects::nonNull)
300 .map(m -> exportMultiAccountManager(connection, m, noReceiveOnStart))
301 .filter(Objects::nonNull)
302 .collect(Collectors.toList());
303
304 for (var t : initThreads) {
305 try {
306 t.join();
307 } catch (InterruptedException ignored) {
308 }
309 }
310 });
311 }
312
313 private void runDbus(
314 final boolean isDbusSystem, DbusRunner dbusRunner
315 ) throws UnexpectedErrorException {
316 DBusConnection.DBusBusType busType;
317 if (isDbusSystem) {
318 busType = DBusConnection.DBusBusType.SYSTEM;
319 } else {
320 busType = DBusConnection.DBusBusType.SESSION;
321 }
322 try {
323 var conn = DBusConnection.getConnection(busType);
324 dbusRunner.run(conn, DbusConfig.getObjectPath());
325
326 conn.requestBusName(DbusConfig.getBusname());
327
328 logger.info("DBus daemon running on {} bus: {}", busType, DbusConfig.getBusname());
329 } catch (DBusException e) {
330 logger.error("Dbus command failed", e);
331 throw new UnexpectedErrorException("Dbus command failed", e);
332 }
333 }
334
335 private Thread exportMultiAccountManager(
336 final DBusConnection conn, final Manager m, final boolean noReceiveOnStart
337 ) {
338 try {
339 final var objectPath = DbusConfig.getObjectPath(m.getSelfNumber());
340 return exportDbusObject(conn, objectPath, m, noReceiveOnStart);
341 } catch (DBusException e) {
342 logger.error("Failed to export object", e);
343 return null;
344 }
345 }
346
347 private Thread exportDbusObject(
348 final DBusConnection conn, final String objectPath, final Manager m, final boolean noReceiveOnStart
349 ) throws DBusException {
350 final var signal = new DbusSignalImpl(m, conn, objectPath, noReceiveOnStart);
351 conn.exportObject(signal);
352 final var initThread = new Thread(signal::initObjects);
353 initThread.start();
354
355 logger.debug("Exported dbus object: " + objectPath);
356
357 return initThread;
358 }
359
360 interface DbusRunner {
361
362 void run(DBusConnection connection, String objectPath) throws DBusException;
363 }
364 }