]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/jsonrpc/SignalJsonRpcDispatcherHandler.java
Implement socket/tcp for daemon command
[signal-cli] / src / main / java / org / asamk / signal / jsonrpc / SignalJsonRpcDispatcherHandler.java
1 package org.asamk.signal.jsonrpc;
2
3 import com.fasterxml.jackson.core.TreeNode;
4 import com.fasterxml.jackson.core.type.TypeReference;
5 import com.fasterxml.jackson.databind.JsonMappingException;
6 import com.fasterxml.jackson.databind.JsonNode;
7 import com.fasterxml.jackson.databind.ObjectMapper;
8 import com.fasterxml.jackson.databind.node.ContainerNode;
9
10 import org.asamk.signal.JsonReceiveMessageHandler;
11 import org.asamk.signal.JsonWriter;
12 import org.asamk.signal.OutputWriter;
13 import org.asamk.signal.commands.Command;
14 import org.asamk.signal.commands.Commands;
15 import org.asamk.signal.commands.JsonRpcCommand;
16 import org.asamk.signal.commands.SignalCreator;
17 import org.asamk.signal.commands.exceptions.CommandException;
18 import org.asamk.signal.commands.exceptions.IOErrorException;
19 import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException;
20 import org.asamk.signal.commands.exceptions.UserErrorException;
21 import org.asamk.signal.manager.Manager;
22 import org.asamk.signal.util.Util;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
25
26 import java.io.IOException;
27 import java.util.HashMap;
28 import java.util.Map;
29 import java.util.Objects;
30 import java.util.function.Supplier;
31
32 public class SignalJsonRpcDispatcherHandler {
33
34 private final static Logger logger = LoggerFactory.getLogger(SignalJsonRpcDispatcherHandler.class);
35
36 private static final int USER_ERROR = -1;
37 private static final int IO_ERROR = -3;
38 private static final int UNTRUSTED_KEY_ERROR = -4;
39
40 private final ObjectMapper objectMapper;
41 private final JsonRpcSender jsonRpcSender;
42 private final JsonRpcReader jsonRpcReader;
43 private final boolean noReceiveOnStart;
44
45 private SignalCreator c;
46 private final Map<Manager, Manager.ReceiveMessageHandler> receiveHandlers = new HashMap<>();
47
48 private Manager m;
49
50 public SignalJsonRpcDispatcherHandler(
51 final JsonWriter outputWriter, final Supplier<String> lineSupplier, final boolean noReceiveOnStart
52 ) {
53 this.noReceiveOnStart = noReceiveOnStart;
54 this.objectMapper = Util.createJsonObjectMapper();
55 this.jsonRpcSender = new JsonRpcSender(outputWriter);
56 this.jsonRpcReader = new JsonRpcReader(jsonRpcSender, lineSupplier);
57 }
58
59 public void handleConnection(final SignalCreator c) {
60 this.c = c;
61
62 if (!noReceiveOnStart) {
63 c.getAccountNumbers().stream().map(c::getManager).filter(Objects::nonNull).forEach(this::subscribeReceive);
64 }
65
66 handleConnection();
67 }
68
69 public void handleConnection(final Manager m) {
70 this.m = m;
71
72 if (!noReceiveOnStart) {
73 subscribeReceive(m);
74 }
75
76 handleConnection();
77 }
78
79 private void subscribeReceive(final Manager m) {
80 if (receiveHandlers.containsKey(m)) {
81 return;
82 }
83
84 final var receiveMessageHandler = new JsonReceiveMessageHandler(m,
85 s -> jsonRpcSender.sendRequest(JsonRpcRequest.forNotification("receive",
86 objectMapper.valueToTree(s),
87 null)));
88 m.addReceiveHandler(receiveMessageHandler);
89 receiveHandlers.put(m, receiveMessageHandler);
90
91 while (!m.hasCaughtUpWithOldMessages()) {
92 try {
93 synchronized (m) {
94 m.wait();
95 }
96 } catch (InterruptedException ignored) {
97 }
98 }
99 }
100
101 void unsubscribeReceive(final Manager m) {
102 final var receiveMessageHandler = receiveHandlers.remove(m);
103 if (receiveMessageHandler != null) {
104 m.removeReceiveHandler(receiveMessageHandler);
105 }
106 }
107
108 private void handleConnection() {
109 try {
110 jsonRpcReader.readMessages((method, params) -> handleRequest(objectMapper, method, params),
111 response -> logger.debug("Received unexpected response for id {}", response.getId()));
112 } finally {
113 receiveHandlers.forEach(Manager::removeReceiveHandler);
114 receiveHandlers.clear();
115 }
116 }
117
118 private JsonNode handleRequest(
119 final ObjectMapper objectMapper, final String method, ContainerNode<?> params
120 ) throws JsonRpcException {
121 var command = getCommand(method);
122 // TODO implement listAccounts, register, verify, link
123 if (command instanceof JsonRpcCommand<?> jsonRpcCommand) {
124 if (m != null) {
125 return runCommand(objectMapper, params, new CommandRunnerImpl<>(m, jsonRpcCommand));
126 }
127
128 if (params.has("account")) {
129 Manager manager = c.getManager(params.get("account").asText());
130 if (manager != null) {
131 return runCommand(objectMapper, params, new CommandRunnerImpl<>(manager, jsonRpcCommand));
132 }
133 } else {
134 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_PARAMS,
135 "Method requires valid account parameter",
136 null));
137 }
138 }
139
140 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.METHOD_NOT_FOUND,
141 "Method not implemented",
142 null));
143 }
144
145 private Command getCommand(final String method) {
146 if ("subscribeReceive".equals(method)) {
147 return new SubscribeReceiveCommand();
148 }
149 if ("unsubscribeReceive".equals(method)) {
150 return new UnsubscribeReceiveCommand();
151 }
152 return Commands.getCommand(method);
153 }
154
155 private record CommandRunnerImpl<T>(Manager m, JsonRpcCommand<T> command) implements CommandRunner<T> {
156
157 @Override
158 public void handleCommand(final T request, final OutputWriter outputWriter) throws CommandException {
159 command.handleCommand(request, m, outputWriter);
160 }
161
162 @Override
163 public TypeReference<T> getRequestType() {
164 return command.getRequestType();
165 }
166 }
167
168 interface CommandRunner<T> {
169
170 void handleCommand(T request, OutputWriter outputWriter) throws CommandException;
171
172 TypeReference<T> getRequestType();
173 }
174
175 private JsonNode runCommand(
176 final ObjectMapper objectMapper, final ContainerNode<?> params, final CommandRunner<?> command
177 ) throws JsonRpcException {
178 final Object[] result = {null};
179 final JsonWriter commandOutputWriter = s -> {
180 if (result[0] != null) {
181 throw new AssertionError("Command may only write one json result");
182 }
183
184 result[0] = s;
185 };
186
187 try {
188 parseParamsAndRunCommand(objectMapper, params, commandOutputWriter, command);
189 } catch (JsonMappingException e) {
190 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
191 e.getMessage(),
192 null));
193 } catch (UserErrorException e) {
194 throw new JsonRpcException(new JsonRpcResponse.Error(USER_ERROR, e.getMessage(), null));
195 } catch (IOErrorException e) {
196 throw new JsonRpcException(new JsonRpcResponse.Error(IO_ERROR, e.getMessage(), null));
197 } catch (UntrustedKeyErrorException e) {
198 throw new JsonRpcException(new JsonRpcResponse.Error(UNTRUSTED_KEY_ERROR, e.getMessage(), null));
199 } catch (Throwable e) {
200 logger.error("Command execution failed", e);
201 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INTERNAL_ERROR,
202 e.getMessage(),
203 null));
204 }
205
206 Object output = result[0] == null ? Map.of() : result[0];
207 return objectMapper.valueToTree(output);
208 }
209
210 private <T> void parseParamsAndRunCommand(
211 final ObjectMapper objectMapper,
212 final TreeNode params,
213 final OutputWriter outputWriter,
214 final CommandRunner<T> command
215 ) throws CommandException, JsonMappingException {
216 T requestParams = null;
217 final var requestType = command.getRequestType();
218 if (params != null && requestType != null) {
219 try {
220 requestParams = objectMapper.readValue(objectMapper.treeAsTokens(params), requestType);
221 } catch (JsonMappingException e) {
222 throw e;
223 } catch (IOException e) {
224 throw new AssertionError(e);
225 }
226 }
227 command.handleCommand(requestParams, outputWriter);
228 }
229
230 private class SubscribeReceiveCommand implements JsonRpcCommand<Void> {
231
232 @Override
233 public String getName() {
234 return "subscribeReceive";
235 }
236
237 @Override
238 public void handleCommand(
239 final Void request, final Manager m, final OutputWriter outputWriter
240 ) throws CommandException {
241 subscribeReceive(m);
242 }
243 }
244
245 private class UnsubscribeReceiveCommand implements JsonRpcCommand<Void> {
246
247 @Override
248 public String getName() {
249 return "unsubscribeReceive";
250 }
251
252 @Override
253 public void handleCommand(
254 final Void request, final Manager m, final OutputWriter outputWriter
255 ) throws CommandException {
256 unsubscribeReceive(m);
257 }
258 }
259 }