]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/jsonrpc/SignalJsonRpcDispatcherHandler.java
Implement register and verify commands for json rpc
[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 import com.fasterxml.jackson.databind.node.ObjectNode;
10
11 import org.asamk.signal.JsonReceiveMessageHandler;
12 import org.asamk.signal.JsonWriter;
13 import org.asamk.signal.commands.Command;
14 import org.asamk.signal.commands.Commands;
15 import org.asamk.signal.commands.JsonRpcMultiCommand;
16 import org.asamk.signal.commands.JsonRpcRegistrationCommand;
17 import org.asamk.signal.commands.JsonRpcSingleCommand;
18 import org.asamk.signal.commands.exceptions.CommandException;
19 import org.asamk.signal.commands.exceptions.IOErrorException;
20 import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException;
21 import org.asamk.signal.commands.exceptions.UserErrorException;
22 import org.asamk.signal.manager.Manager;
23 import org.asamk.signal.manager.MultiAccountManager;
24 import org.asamk.signal.manager.RegistrationManager;
25 import org.asamk.signal.util.Util;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
28
29 import java.io.IOException;
30 import java.util.HashMap;
31 import java.util.Map;
32 import java.util.Objects;
33 import java.util.function.Supplier;
34
35 public class SignalJsonRpcDispatcherHandler {
36
37 private final static Logger logger = LoggerFactory.getLogger(SignalJsonRpcDispatcherHandler.class);
38
39 private static final int USER_ERROR = -1;
40 private static final int IO_ERROR = -3;
41 private static final int UNTRUSTED_KEY_ERROR = -4;
42
43 private final ObjectMapper objectMapper;
44 private final JsonRpcSender jsonRpcSender;
45 private final JsonRpcReader jsonRpcReader;
46 private final boolean noReceiveOnStart;
47
48 private MultiAccountManager c;
49 private final Map<Manager, Manager.ReceiveMessageHandler> receiveHandlers = new HashMap<>();
50
51 private Manager m;
52
53 public SignalJsonRpcDispatcherHandler(
54 final JsonWriter jsonWriter, final Supplier<String> lineSupplier, final boolean noReceiveOnStart
55 ) {
56 this.noReceiveOnStart = noReceiveOnStart;
57 this.objectMapper = Util.createJsonObjectMapper();
58 this.jsonRpcSender = new JsonRpcSender(jsonWriter);
59 this.jsonRpcReader = new JsonRpcReader(jsonRpcSender, lineSupplier);
60 }
61
62 public void handleConnection(final MultiAccountManager c) {
63 this.c = c;
64
65 if (!noReceiveOnStart) {
66 c.getAccountNumbers().stream().map(c::getManager).filter(Objects::nonNull).forEach(this::subscribeReceive);
67 }
68
69 handleConnection();
70 }
71
72 public void handleConnection(final Manager m) {
73 this.m = m;
74
75 if (!noReceiveOnStart) {
76 subscribeReceive(m);
77 }
78
79 handleConnection();
80 }
81
82 private void subscribeReceive(final Manager m) {
83 if (receiveHandlers.containsKey(m)) {
84 return;
85 }
86
87 final var receiveMessageHandler = new JsonReceiveMessageHandler(m,
88 s -> jsonRpcSender.sendRequest(JsonRpcRequest.forNotification("receive",
89 objectMapper.valueToTree(s),
90 null)));
91 m.addReceiveHandler(receiveMessageHandler);
92 receiveHandlers.put(m, receiveMessageHandler);
93
94 while (!m.hasCaughtUpWithOldMessages()) {
95 try {
96 synchronized (m) {
97 m.wait();
98 }
99 } catch (InterruptedException ignored) {
100 }
101 }
102 }
103
104 void unsubscribeReceive(final Manager m) {
105 final var receiveMessageHandler = receiveHandlers.remove(m);
106 if (receiveMessageHandler != null) {
107 m.removeReceiveHandler(receiveMessageHandler);
108 }
109 }
110
111 private void handleConnection() {
112 try {
113 jsonRpcReader.readMessages((method, params) -> handleRequest(objectMapper, method, params),
114 response -> logger.debug("Received unexpected response for id {}", response.getId()));
115 } finally {
116 receiveHandlers.forEach(Manager::removeReceiveHandler);
117 receiveHandlers.clear();
118 }
119 }
120
121 private JsonNode handleRequest(
122 final ObjectMapper objectMapper, final String method, ContainerNode<?> params
123 ) throws JsonRpcException {
124 var command = getCommand(method);
125 // TODO implement link
126 if (c != null) {
127 if (command instanceof JsonRpcMultiCommand<?> jsonRpcCommand) {
128 return runCommand(objectMapper, params, new MultiCommandRunnerImpl<>(c, jsonRpcCommand));
129 }
130 if (command instanceof JsonRpcRegistrationCommand<?> jsonRpcCommand) {
131 try (var manager = getRegistrationManagerFromParams(params)) {
132 if (manager != null) {
133 return runCommand(objectMapper,
134 params,
135 new RegistrationCommandRunnerImpl<>(manager, c, jsonRpcCommand));
136 } else {
137 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_PARAMS,
138 "Method requires valid account parameter",
139 null));
140 }
141 } catch (IOException e) {
142 logger.warn("Failed to close registration manager", e);
143 }
144 }
145 }
146 if (command instanceof JsonRpcSingleCommand<?> jsonRpcCommand) {
147 if (m != null) {
148 return runCommand(objectMapper, params, new CommandRunnerImpl<>(m, jsonRpcCommand));
149 }
150
151 final var manager = getManagerFromParams(params);
152 if (manager != null) {
153 return runCommand(objectMapper, params, new CommandRunnerImpl<>(manager, jsonRpcCommand));
154 } else {
155 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_PARAMS,
156 "Method requires valid account parameter",
157 null));
158 }
159 }
160
161 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.METHOD_NOT_FOUND,
162 "Method not implemented",
163 null));
164 }
165
166 private Manager getManagerFromParams(final ContainerNode<?> params) {
167 if (params != null && params.has("account")) {
168 final var manager = c.getManager(params.get("account").asText());
169 ((ObjectNode) params).remove("account");
170 return manager;
171 }
172 return null;
173 }
174
175 private RegistrationManager getRegistrationManagerFromParams(final ContainerNode<?> params) {
176 if (params != null && params.has("account")) {
177 try {
178 final var registrationManager = c.getNewRegistrationManager(params.get("account").asText());
179 ((ObjectNode) params).remove("account");
180 return registrationManager;
181 } catch (IOException | IllegalStateException e) {
182 logger.warn("Failed to load registration manager", e);
183 return null;
184 }
185 }
186 return null;
187 }
188
189 private Command getCommand(final String method) {
190 if ("subscribeReceive".equals(method)) {
191 return new SubscribeReceiveCommand();
192 }
193 if ("unsubscribeReceive".equals(method)) {
194 return new UnsubscribeReceiveCommand();
195 }
196 return Commands.getCommand(method);
197 }
198
199 private record CommandRunnerImpl<T>(Manager m, JsonRpcSingleCommand<T> command) implements CommandRunner<T> {
200
201 @Override
202 public void handleCommand(final T request, final JsonWriter jsonWriter) throws CommandException {
203 command.handleCommand(request, m, jsonWriter);
204 }
205
206 @Override
207 public TypeReference<T> getRequestType() {
208 return command.getRequestType();
209 }
210 }
211
212 private record RegistrationCommandRunnerImpl<T>(
213 RegistrationManager m, MultiAccountManager c, JsonRpcRegistrationCommand<T> command
214 ) implements CommandRunner<T> {
215
216 @Override
217 public void handleCommand(final T request, final JsonWriter jsonWriter) throws CommandException {
218 command.handleCommand(request, m, jsonWriter);
219 }
220
221 @Override
222 public TypeReference<T> getRequestType() {
223 return command.getRequestType();
224 }
225 }
226
227 private record MultiCommandRunnerImpl<T>(
228 MultiAccountManager c, JsonRpcMultiCommand<T> command
229 ) implements CommandRunner<T> {
230
231 @Override
232 public void handleCommand(final T request, final JsonWriter jsonWriter) throws CommandException {
233 command.handleCommand(request, c, jsonWriter);
234 }
235
236 @Override
237 public TypeReference<T> getRequestType() {
238 return command.getRequestType();
239 }
240 }
241
242 interface CommandRunner<T> {
243
244 void handleCommand(T request, JsonWriter jsonWriter) throws CommandException;
245
246 TypeReference<T> getRequestType();
247 }
248
249 private JsonNode runCommand(
250 final ObjectMapper objectMapper, final ContainerNode<?> params, final CommandRunner<?> command
251 ) throws JsonRpcException {
252 final Object[] result = {null};
253 final JsonWriter commandJsonWriter = s -> {
254 if (result[0] != null) {
255 throw new AssertionError("Command may only write one json result");
256 }
257
258 result[0] = s;
259 };
260
261 try {
262 parseParamsAndRunCommand(objectMapper, params, commandJsonWriter, command);
263 } catch (JsonMappingException e) {
264 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
265 e.getMessage(),
266 null));
267 } catch (UserErrorException e) {
268 throw new JsonRpcException(new JsonRpcResponse.Error(USER_ERROR, e.getMessage(), null));
269 } catch (IOErrorException e) {
270 throw new JsonRpcException(new JsonRpcResponse.Error(IO_ERROR, e.getMessage(), null));
271 } catch (UntrustedKeyErrorException e) {
272 throw new JsonRpcException(new JsonRpcResponse.Error(UNTRUSTED_KEY_ERROR, e.getMessage(), null));
273 } catch (Throwable e) {
274 logger.error("Command execution failed", e);
275 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INTERNAL_ERROR,
276 e.getMessage(),
277 null));
278 }
279
280 Object output = result[0] == null ? Map.of() : result[0];
281 return objectMapper.valueToTree(output);
282 }
283
284 private <T> void parseParamsAndRunCommand(
285 final ObjectMapper objectMapper,
286 final TreeNode params,
287 final JsonWriter jsonWriter,
288 final CommandRunner<T> command
289 ) throws CommandException, JsonMappingException {
290 T requestParams = null;
291 final var requestType = command.getRequestType();
292 if (params != null && requestType != null) {
293 try {
294 requestParams = objectMapper.readValue(objectMapper.treeAsTokens(params), requestType);
295 } catch (JsonMappingException e) {
296 throw e;
297 } catch (IOException e) {
298 throw new AssertionError(e);
299 }
300 }
301 command.handleCommand(requestParams, jsonWriter);
302 }
303
304 private class SubscribeReceiveCommand implements JsonRpcSingleCommand<Void> {
305
306 @Override
307 public String getName() {
308 return "subscribeReceive";
309 }
310
311 @Override
312 public void handleCommand(
313 final Void request, final Manager m, final JsonWriter jsonWriter
314 ) throws CommandException {
315 subscribeReceive(m);
316 }
317 }
318
319 private class UnsubscribeReceiveCommand implements JsonRpcSingleCommand<Void> {
320
321 @Override
322 public String getName() {
323 return "unsubscribeReceive";
324 }
325
326 @Override
327 public void handleCommand(
328 final Void request, final Manager m, final JsonWriter jsonWriter
329 ) throws CommandException {
330 unsubscribeReceive(m);
331 }
332 }
333 }