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