]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/jsonrpc/JsonRpcReader.java
Only output username if set
[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 if (responseList.size() > 0) {
99 jsonRpcSender.sendBatchResponses(responseList);
100 }
101 }
102 }
103
104 private JsonRpcResponse handleRequest(final RequestHandler requestHandler, final JsonRpcRequest request) {
105 try {
106 final var result = requestHandler.apply(request.getMethod(), request.getParams());
107 if (request.getId() != null) {
108 return JsonRpcResponse.forSuccess(result, request.getId());
109 } else {
110 logger.debug("Command '{}' succeeded but client didn't specify an id, dropping response",
111 request.getMethod());
112 }
113 } catch (JsonRpcException e) {
114 if (request.getId() != null) {
115 return JsonRpcResponse.forError(e.getError(), request.getId());
116 } else {
117 logger.debug("Command '{}' failed but client didn't specify an id, dropping error: {}",
118 request.getMethod(),
119 e.getMessage());
120 }
121 }
122 return null;
123 }
124
125 private JsonRpcMessage parseJsonRpcMessage(final String input) {
126 final JsonNode jsonNode;
127 try {
128 jsonNode = objectMapper.readTree(input);
129 } catch (JsonParseException e) {
130 jsonRpcSender.sendResponse(JsonRpcResponse.forError(new JsonRpcResponse.Error(JsonRpcResponse.Error.PARSE_ERROR,
131 e.getMessage(),
132 null), null));
133 return null;
134 } catch (IOException e) {
135 throw new AssertionError(e);
136 }
137
138 return parseJsonRpcMessage(jsonNode);
139 }
140
141 private JsonRpcMessage parseJsonRpcMessage(final InputStream input) {
142 final JsonNode jsonNode;
143 try {
144 jsonNode = objectMapper.readTree(input);
145 } catch (JsonParseException e) {
146 jsonRpcSender.sendResponse(JsonRpcResponse.forError(new JsonRpcResponse.Error(JsonRpcResponse.Error.PARSE_ERROR,
147 e.getMessage(),
148 null), null));
149 return null;
150 } catch (IOException e) {
151 throw new AssertionError(e);
152 }
153
154 return parseJsonRpcMessage(jsonNode);
155 }
156
157 private JsonRpcMessage parseJsonRpcMessage(final JsonNode jsonNode) {
158 if (jsonNode == null) {
159 jsonRpcSender.sendResponse(JsonRpcResponse.forError(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
160 "invalid request",
161 null), null));
162 return null;
163 } else if (jsonNode.isArray()) {
164 if (jsonNode.size() == 0) {
165 jsonRpcSender.sendResponse(JsonRpcResponse.forError(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
166 "invalid request",
167 null), null));
168 return null;
169 }
170 return new JsonRpcBatchMessage(StreamSupport.stream(jsonNode.spliterator(), false).toList());
171 } else if (jsonNode.isObject()) {
172 if (jsonNode.has("result") || jsonNode.has("error")) {
173 return parseJsonRpcResponse(jsonNode);
174 } else {
175 try {
176 return parseJsonRpcRequest(jsonNode);
177 } catch (JsonRpcException e) {
178 jsonRpcSender.sendResponse(JsonRpcResponse.forError(e.getError(), getId(jsonNode)));
179 return null;
180 }
181 }
182 } else {
183 jsonRpcSender.sendResponse(JsonRpcResponse.forError(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
184 "unexpected type: " + jsonNode.getNodeType().name(),
185 null), null));
186 return null;
187 }
188 }
189
190 private ValueNode getId(JsonNode jsonNode) {
191 final var id = jsonNode.get("id");
192 return id instanceof ValueNode ? (ValueNode) id : null;
193 }
194
195 private JsonRpcRequest parseJsonRpcRequest(final JsonNode input) throws JsonRpcException {
196 if (input instanceof ObjectNode i && input.has("params") && input.get("params").isNull()) {
197 // Workaround for clients that send a null params field instead of omitting it
198 i.remove("params");
199 }
200 JsonRpcRequest request;
201 try {
202 request = objectMapper.treeToValue(input, JsonRpcRequest.class);
203 } catch (JsonMappingException e) {
204 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
205 e.getMessage(),
206 null));
207 } catch (IOException e) {
208 throw new AssertionError(e);
209 }
210
211 if (!"2.0".equals(request.getJsonrpc())) {
212 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
213 "only jsonrpc version 2.0 is supported",
214 null));
215 }
216
217 if (request.getMethod() == null) {
218 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
219 "method field must be set",
220 null));
221 }
222
223 return request;
224 }
225
226 private JsonRpcResponse parseJsonRpcResponse(final JsonNode input) {
227 JsonRpcResponse response;
228 try {
229 response = objectMapper.treeToValue(input, JsonRpcResponse.class);
230 } catch (JsonParseException | JsonMappingException e) {
231 logger.debug("Received invalid jsonrpc response {}", e.getMessage());
232 return null;
233 } catch (IOException e) {
234 throw new AssertionError(e);
235 }
236
237 if (!"2.0".equals(response.getJsonrpc())) {
238 logger.debug("Received invalid jsonrpc response with invalid version {}", response.getJsonrpc());
239 return null;
240 }
241
242 if (response.getResult() != null && response.getError() != null) {
243 logger.debug("Received invalid jsonrpc response with both result and error");
244 return null;
245 }
246
247 if (response.getResult() == null && response.getError() == null) {
248 logger.debug("Received invalid jsonrpc response without result and error");
249 return null;
250 }
251
252 if (response.getId() == null || response.getId().isNull()) {
253 logger.debug("Received invalid jsonrpc response without id");
254 return null;
255 }
256
257 return response;
258 }
259
260 public interface RequestHandler {
261
262 JsonNode apply(String method, ContainerNode<?> params) throws JsonRpcException;
263 }
264 }