1 package org
.asamk
.signal
.jsonrpc
;
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
;
11 import org
.asamk
.signal
.commands
.Command
;
12 import org
.asamk
.signal
.commands
.JsonRpcMultiCommand
;
13 import org
.asamk
.signal
.commands
.JsonRpcRegistrationCommand
;
14 import org
.asamk
.signal
.commands
.JsonRpcSingleCommand
;
15 import org
.asamk
.signal
.commands
.exceptions
.CommandException
;
16 import org
.asamk
.signal
.commands
.exceptions
.IOErrorException
;
17 import org
.asamk
.signal
.commands
.exceptions
.RateLimitErrorException
;
18 import org
.asamk
.signal
.commands
.exceptions
.UnexpectedErrorException
;
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
.manager
.MultiAccountManager
;
23 import org
.asamk
.signal
.manager
.RegistrationManager
;
24 import org
.asamk
.signal
.output
.JsonWriter
;
25 import org
.slf4j
.Logger
;
26 import org
.slf4j
.LoggerFactory
;
28 import java
.io
.IOException
;
29 import java
.nio
.channels
.OverlappingFileLockException
;
31 import java
.util
.function
.Function
;
33 public class SignalJsonRpcCommandHandler
{
35 private static final Logger logger
= LoggerFactory
.getLogger(SignalJsonRpcDispatcherHandler
.class);
37 private static final int USER_ERROR
= -1;
38 private static final int IO_ERROR
= -3;
39 private static final int UNTRUSTED_KEY_ERROR
= -4;
40 private static final int RATELIMIT_ERROR
= -5;
42 private final Manager m
;
43 private final MultiAccountManager c
;
44 private final Function
<String
, Command
> commandProvider
;
46 public SignalJsonRpcCommandHandler(final Manager m
, final Function
<String
, Command
> commandProvider
) {
49 this.commandProvider
= commandProvider
;
52 public SignalJsonRpcCommandHandler(final MultiAccountManager c
, final Function
<String
, Command
> commandProvider
) {
55 this.commandProvider
= commandProvider
;
58 public JsonNode
handleRequest(
59 final ObjectMapper objectMapper
,
61 ContainerNode
<?
> params
62 ) throws JsonRpcException
{
63 var command
= getCommand(method
);
65 if (command
instanceof JsonRpcSingleCommand
<?
> jsonRpcCommand
) {
66 final var manager
= getManagerFromParams(params
);
67 if (manager
!= null) {
68 return runCommand(objectMapper
, params
, new CommandRunnerImpl
<>(manager
, jsonRpcCommand
));
71 if (command
instanceof JsonRpcMultiCommand
<?
> jsonRpcCommand
) {
72 return runCommand(objectMapper
, params
, new MultiCommandRunnerImpl
<>(c
, jsonRpcCommand
));
74 if (command
instanceof JsonRpcRegistrationCommand
<?
> jsonRpcCommand
) {
75 try (var manager
= getRegistrationManagerFromParams(params
)) {
76 if (manager
!= null) {
77 return runCommand(objectMapper
,
79 new RegistrationCommandRunnerImpl
<>(manager
, c
, jsonRpcCommand
));
81 throw new JsonRpcException(new JsonRpcResponse
.Error(JsonRpcResponse
.Error
.INVALID_PARAMS
,
82 "Method requires valid account parameter",
85 } catch (IOException e
) {
86 logger
.warn("Failed to close registration manager", e
);
90 if (command
instanceof JsonRpcSingleCommand
<?
> jsonRpcCommand
) {
92 return runCommand(objectMapper
, params
, new CommandRunnerImpl
<>(m
, jsonRpcCommand
));
95 var manager
= getManagerFromParams(params
);
96 if (manager
== null) {
97 final var managers
= c
.getManagers();
98 if (managers
.size() == 1) {
99 manager
= managers
.getFirst();
102 if (manager
!= null) {
103 return runCommand(objectMapper
, params
, new CommandRunnerImpl
<>(manager
, jsonRpcCommand
));
105 throw new JsonRpcException(new JsonRpcResponse
.Error(JsonRpcResponse
.Error
.INVALID_PARAMS
,
106 "Method requires valid account parameter",
111 throw new JsonRpcException(new JsonRpcResponse
.Error(JsonRpcResponse
.Error
.METHOD_NOT_FOUND
,
112 "Method not implemented",
116 private Manager
getManagerFromParams(final ContainerNode
<?
> params
) throws JsonRpcException
{
117 if (params
!= null && params
.hasNonNull("account")) {
118 final var manager
= c
.getManager(params
.get("account").asText());
119 ((ObjectNode
) params
).remove("account");
120 if (manager
== null) {
121 throw new JsonRpcException(new JsonRpcResponse
.Error(JsonRpcResponse
.Error
.INVALID_PARAMS
,
122 "Specified account does not exist",
130 private RegistrationManager
getRegistrationManagerFromParams(final ContainerNode
<?
> params
) {
131 if (params
!= null && params
.has("account")) {
133 final var registrationManager
= c
.getNewRegistrationManager(params
.get("account").asText());
134 ((ObjectNode
) params
).remove("account");
135 return registrationManager
;
136 } catch (OverlappingFileLockException e
) {
137 logger
.warn("Account is already in use");
139 } catch (IOException
| IllegalStateException e
) {
140 logger
.warn("Failed to load registration manager", e
);
147 private Command
getCommand(final String method
) {
148 return commandProvider
.apply(method
);
151 private record CommandRunnerImpl
<T
>(Manager m
, JsonRpcSingleCommand
<T
> command
) implements CommandRunner
<T
> {
154 public void handleCommand(final T request
, final JsonWriter jsonWriter
) throws CommandException
{
155 command
.handleCommand(request
, m
, jsonWriter
);
159 public TypeReference
<T
> getRequestType() {
160 return command
.getRequestType();
164 private record RegistrationCommandRunnerImpl
<T
>(
165 RegistrationManager m
, MultiAccountManager c
, JsonRpcRegistrationCommand
<T
> command
166 ) implements CommandRunner
<T
> {
169 public void handleCommand(final T request
, final JsonWriter jsonWriter
) throws CommandException
{
170 command
.handleCommand(request
, m
, jsonWriter
);
174 public TypeReference
<T
> getRequestType() {
175 return command
.getRequestType();
179 private record MultiCommandRunnerImpl
<T
>(
180 MultiAccountManager c
, JsonRpcMultiCommand
<T
> command
181 ) implements CommandRunner
<T
> {
184 public void handleCommand(final T request
, final JsonWriter jsonWriter
) throws CommandException
{
185 command
.handleCommand(request
, c
, jsonWriter
);
189 public TypeReference
<T
> getRequestType() {
190 return command
.getRequestType();
194 interface CommandRunner
<T
> {
196 void handleCommand(T request
, JsonWriter jsonWriter
) throws CommandException
;
198 TypeReference
<T
> getRequestType();
201 private JsonNode
runCommand(
202 final ObjectMapper objectMapper
,
203 final ContainerNode
<?
> params
,
204 final CommandRunner
<?
> command
205 ) throws JsonRpcException
{
206 final Object
[] result
= {null};
207 final JsonWriter commandJsonWriter
= s
-> {
208 if (result
[0] != null) {
209 throw new AssertionError("Command may only write one json result");
216 parseParamsAndRunCommand(objectMapper
, params
, commandJsonWriter
, command
);
217 } catch (JsonMappingException e
) {
218 throw new JsonRpcException(new JsonRpcResponse
.Error(JsonRpcResponse
.Error
.INVALID_REQUEST
,
221 } catch (CommandException ce
) {
223 case UserErrorException e
-> throw new JsonRpcException(new JsonRpcResponse
.Error(USER_ERROR
,
225 getErrorDataNode(objectMapper
, result
)));
226 case IOErrorException e
-> throw new JsonRpcException(new JsonRpcResponse
.Error(IO_ERROR
,
228 getErrorDataNode(objectMapper
, result
)));
229 case UntrustedKeyErrorException e
-> throw new JsonRpcException(new JsonRpcResponse
.Error(
232 getErrorDataNode(objectMapper
, result
)));
233 case RateLimitErrorException e
-> throw new JsonRpcException(new JsonRpcResponse
.Error(RATELIMIT_ERROR
,
235 getErrorDataNode(objectMapper
, result
)));
236 case UnexpectedErrorException e
-> {
237 logger
.error("Command execution failed with unexpected error", e
);
238 throw new JsonRpcException(new JsonRpcResponse
.Error(JsonRpcResponse
.Error
.INTERNAL_ERROR
,
239 e
.getMessage() + " (" + e
.getClass().getSimpleName() + ")",
240 getErrorDataNode(objectMapper
, result
)));
243 } catch (Throwable e
) {
244 logger
.error("Command execution failed", e
);
245 throw new JsonRpcException(new JsonRpcResponse
.Error(JsonRpcResponse
.Error
.INTERNAL_ERROR
,
246 e
.getMessage() + " (" + e
.getClass().getSimpleName() + ")",
247 getErrorDataNode(objectMapper
, result
)));
250 Object output
= result
[0] == null ? Map
.of() : result
[0];
251 return objectMapper
.valueToTree(output
);
254 private JsonNode
getErrorDataNode(final ObjectMapper objectMapper
, final Object
[] result
) {
255 if (result
[0] == null) {
258 return objectMapper
.valueToTree(Map
.of("response", result
[0]));
261 private <T
> void parseParamsAndRunCommand(
262 final ObjectMapper objectMapper
,
263 final TreeNode params
,
264 final JsonWriter jsonWriter
,
265 final CommandRunner
<T
> command
266 ) throws CommandException
, JsonMappingException
{
267 T requestParams
= null;
268 final var requestType
= command
.getRequestType();
269 if (params
!= null && requestType
!= null) {
271 requestParams
= objectMapper
.readValue(objectMapper
.treeAsTokens(params
), requestType
);
272 } catch (JsonMappingException e
) {
274 } catch (IOException e
) {
275 throw new AssertionError(e
);
278 command
.handleCommand(requestParams
, jsonWriter
);