]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/jsonrpc/JsonRpcReader.java
Extract JSON-RPC command handler
[signal-cli] / src / main / java / org / asamk / signal / jsonrpc / JsonRpcReader.java
1 package org.asamk.signal.jsonrpc;
2
3 import com.fasterxml.jackson.core.JsonParseException;
4 import com.fasterxml.jackson.databind.JsonMappingException;
5 import com.fasterxml.jackson.databind.JsonNode;
6 import com.fasterxml.jackson.databind.ObjectMapper;
7 import com.fasterxml.jackson.databind.node.ContainerNode;
8 import com.fasterxml.jackson.databind.node.ObjectNode;
9 import com.fasterxml.jackson.databind.node.ValueNode;
10
11 import org.asamk.signal.util.Util;
12 import org.slf4j.Logger;
13 import org.slf4j.LoggerFactory;
14
15 import java.io.IOException;
16 import java.util.Objects;
17 import java.util.function.Consumer;
18 import java.util.function.Supplier;
19 import java.util.stream.StreamSupport;
20
21 public class JsonRpcReader {
22
23 private final static Logger logger = LoggerFactory.getLogger(JsonRpcReader.class);
24
25 private final JsonRpcSender jsonRpcSender;
26 private final ObjectMapper objectMapper;
27 private final Supplier<String> lineSupplier;
28
29 public JsonRpcReader(
30 final JsonRpcSender jsonRpcSender, final Supplier<String> lineSupplier
31 ) {
32 this.jsonRpcSender = jsonRpcSender;
33 this.lineSupplier = lineSupplier;
34 this.objectMapper = Util.createJsonObjectMapper();
35 }
36
37 public void readMessages(final RequestHandler requestHandler, final Consumer<JsonRpcResponse> responseHandler) {
38 while (!Thread.interrupted()) {
39 JsonRpcMessage message = readMessage();
40 if (message == null) break;
41
42 if (message instanceof final JsonRpcRequest jsonRpcRequest) {
43 logger.debug("Received json rpc request, method: " + jsonRpcRequest.getMethod());
44 final var response = handleRequest(requestHandler, jsonRpcRequest);
45 if (response != null) {
46 jsonRpcSender.sendResponse(response);
47 }
48 } else if (message instanceof JsonRpcResponse jsonRpcResponse) {
49 responseHandler.accept(jsonRpcResponse);
50 } else {
51 final var responseList = ((JsonRpcBatchMessage) message).getMessages().stream().map(jsonNode -> {
52 final JsonRpcRequest request;
53 try {
54 request = parseJsonRpcRequest(jsonNode);
55 } catch (JsonRpcException e) {
56 return JsonRpcResponse.forError(e.getError(), getId(jsonNode));
57 }
58
59 return handleRequest(requestHandler, request);
60 }).filter(Objects::nonNull).toList();
61
62 jsonRpcSender.sendBatchResponses(responseList);
63 }
64 }
65 }
66
67 private JsonRpcResponse handleRequest(final RequestHandler requestHandler, final JsonRpcRequest request) {
68 try {
69 final var result = requestHandler.apply(request.getMethod(), request.getParams());
70 if (request.getId() != null) {
71 return JsonRpcResponse.forSuccess(result, request.getId());
72 } else {
73 logger.debug("Command '{}' succeeded but client didn't specify an id, dropping response",
74 request.getMethod());
75 }
76 } catch (JsonRpcException e) {
77 if (request.getId() != null) {
78 return JsonRpcResponse.forError(e.getError(), request.getId());
79 } else {
80 logger.debug("Command '{}' failed but client didn't specify an id, dropping error: {}",
81 request.getMethod(),
82 e.getMessage());
83 }
84 }
85 return null;
86 }
87
88 private JsonRpcMessage readMessage() {
89 while (!Thread.interrupted()) {
90 String input = lineSupplier.get();
91
92 if (input == null) {
93 logger.trace("Reached end of JSON-RPC input stream.");
94 break;
95 }
96
97 logger.trace("Incoming JSON-RPC message: {}", input);
98 JsonRpcMessage message = parseJsonRpcMessage(input);
99 if (message == null) continue;
100
101 return message;
102 }
103
104 return null;
105 }
106
107 private JsonRpcMessage parseJsonRpcMessage(final String input) {
108 final JsonNode jsonNode;
109 try {
110 jsonNode = objectMapper.readTree(input);
111 } catch (JsonParseException e) {
112 jsonRpcSender.sendResponse(JsonRpcResponse.forError(new JsonRpcResponse.Error(JsonRpcResponse.Error.PARSE_ERROR,
113 e.getMessage(),
114 null), null));
115 return null;
116 } catch (IOException e) {
117 throw new AssertionError(e);
118 }
119
120 if (jsonNode == null) {
121 jsonRpcSender.sendResponse(JsonRpcResponse.forError(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
122 "invalid request",
123 null), null));
124 return null;
125 } else if (jsonNode.isArray()) {
126 if (jsonNode.size() == 0) {
127 jsonRpcSender.sendResponse(JsonRpcResponse.forError(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
128 "invalid request",
129 null), null));
130 return null;
131 }
132 return new JsonRpcBatchMessage(StreamSupport.stream(jsonNode.spliterator(), false).toList());
133 } else if (jsonNode.isObject()) {
134 if (jsonNode.has("result") || jsonNode.has("error")) {
135 return parseJsonRpcResponse(jsonNode);
136 } else {
137 try {
138 return parseJsonRpcRequest(jsonNode);
139 } catch (JsonRpcException e) {
140 jsonRpcSender.sendResponse(JsonRpcResponse.forError(e.getError(), getId(jsonNode)));
141 return null;
142 }
143 }
144 } else {
145 jsonRpcSender.sendResponse(JsonRpcResponse.forError(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
146 "unexpected type: " + jsonNode.getNodeType().name(),
147 null), null));
148 return null;
149 }
150 }
151
152 private ValueNode getId(JsonNode jsonNode) {
153 final var id = jsonNode.get("id");
154 return id instanceof ValueNode ? (ValueNode) id : null;
155 }
156
157 private JsonRpcRequest parseJsonRpcRequest(final JsonNode input) throws JsonRpcException {
158 if (input instanceof ObjectNode i && input.has("params") && input.get("params").isNull()) {
159 // Workaround for clients that send a null params field instead of omitting it
160 i.remove("params");
161 }
162 JsonRpcRequest request;
163 try {
164 request = objectMapper.treeToValue(input, JsonRpcRequest.class);
165 } catch (JsonMappingException e) {
166 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
167 e.getMessage(),
168 null));
169 } catch (IOException e) {
170 throw new AssertionError(e);
171 }
172
173 if (!"2.0".equals(request.getJsonrpc())) {
174 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
175 "only jsonrpc version 2.0 is supported",
176 null));
177 }
178
179 if (request.getMethod() == null) {
180 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
181 "method field must be set",
182 null));
183 }
184
185 return request;
186 }
187
188 private JsonRpcResponse parseJsonRpcResponse(final JsonNode input) {
189 JsonRpcResponse response;
190 try {
191 response = objectMapper.treeToValue(input, JsonRpcResponse.class);
192 } catch (JsonParseException | JsonMappingException e) {
193 logger.debug("Received invalid jsonrpc response {}", e.getMessage());
194 return null;
195 } catch (IOException e) {
196 throw new AssertionError(e);
197 }
198
199 if (!"2.0".equals(response.getJsonrpc())) {
200 logger.debug("Received invalid jsonrpc response with invalid version {}", response.getJsonrpc());
201 return null;
202 }
203
204 if (response.getResult() != null && response.getError() != null) {
205 logger.debug("Received invalid jsonrpc response with both result and error");
206 return null;
207 }
208
209 if (response.getResult() == null && response.getError() == null) {
210 logger.debug("Received invalid jsonrpc response without result and error");
211 return null;
212 }
213
214 if (response.getId() == null || response.getId().isNull()) {
215 logger.debug("Received invalid jsonrpc response without id");
216 return null;
217 }
218
219 return response;
220 }
221
222 public interface RequestHandler {
223
224 JsonNode apply(String method, ContainerNode<?> params) throws JsonRpcException;
225 }
226 }