]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/jsonrpc/JsonRpcReader.java
Log a debug message when dropping json rpc response for request without id
[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.ValueNode;
9
10 import org.asamk.signal.util.Util;
11 import org.slf4j.Logger;
12 import org.slf4j.LoggerFactory;
13
14 import java.io.IOException;
15 import java.util.Objects;
16 import java.util.function.Consumer;
17 import java.util.function.Supplier;
18 import java.util.stream.Collectors;
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 readRequests(
38 final RequestHandler requestHandler, final Consumer<JsonRpcResponse> responseHandler
39 ) {
40 while (!Thread.interrupted()) {
41 JsonRpcMessage message = readMessage();
42 if (message == null) break;
43
44 if (message instanceof JsonRpcRequest) {
45 final var response = handleRequest(requestHandler, (JsonRpcRequest) message);
46 if (response != null) {
47 jsonRpcSender.sendResponse(response);
48 }
49 } else if (message instanceof JsonRpcResponse) {
50 responseHandler.accept((JsonRpcResponse) message);
51 } else {
52 final var responseList = ((JsonRpcBulkMessage) message).getMessages().stream().map(jsonNode -> {
53 final JsonRpcRequest request;
54 try {
55 request = parseJsonRpcRequest(jsonNode);
56 } catch (JsonRpcException e) {
57 return JsonRpcResponse.forError(e.getError(), getId(jsonNode));
58 }
59
60 return handleRequest(requestHandler, request);
61 }).filter(Objects::nonNull).collect(Collectors.toList());
62
63 jsonRpcSender.sendBulkResponses(responseList);
64 }
65 }
66 }
67
68 private JsonRpcResponse handleRequest(final RequestHandler requestHandler, final JsonRpcRequest request) {
69 try {
70 final var result = requestHandler.apply(request.getMethod(), request.getParams());
71 if (request.getId() != null) {
72 return JsonRpcResponse.forSuccess(result, request.getId());
73 } else {
74 logger.debug("Command '{}' succeeded but client didn't specify an id, dropping response",
75 request.getMethod());
76 }
77 } catch (JsonRpcException e) {
78 if (request.getId() != null) {
79 return JsonRpcResponse.forError(e.getError(), request.getId());
80 } else {
81 logger.debug("Command '{}' failed but client didn't specify an id, dropping error: {}",
82 request.getMethod(),
83 e.getMessage());
84 }
85 }
86 return null;
87 }
88
89 private JsonRpcMessage readMessage() {
90 while (!Thread.interrupted()) {
91 String input = lineSupplier.get();
92
93 if (input == null) {
94 // Reached end of input stream
95 break;
96 }
97
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 JsonRpcBulkMessage(StreamSupport.stream(jsonNode.spliterator(), false)
133 .collect(Collectors.toList()));
134 } else if (jsonNode.isObject()) {
135 if (jsonNode.has("result") || jsonNode.has("error")) {
136 return parseJsonRpcResponse(jsonNode);
137 } else {
138 try {
139 return parseJsonRpcRequest(jsonNode);
140 } catch (JsonRpcException e) {
141 jsonRpcSender.sendResponse(JsonRpcResponse.forError(e.getError(), getId(jsonNode)));
142 return null;
143 }
144 }
145 } else {
146 jsonRpcSender.sendResponse(JsonRpcResponse.forError(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
147 "unexpected type: " + jsonNode.getNodeType().name(),
148 null), null));
149 return null;
150 }
151 }
152
153 private ValueNode getId(JsonNode jsonNode) {
154 final var id = jsonNode.get("id");
155 return id instanceof ValueNode ? (ValueNode) id : null;
156 }
157
158 private JsonRpcRequest parseJsonRpcRequest(final JsonNode input) throws JsonRpcException {
159 JsonRpcRequest request;
160 try {
161 request = objectMapper.treeToValue(input, JsonRpcRequest.class);
162 } catch (JsonMappingException e) {
163 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
164 e.getMessage(),
165 null));
166 } catch (IOException e) {
167 throw new AssertionError(e);
168 }
169
170 if (!"2.0".equals(request.getJsonrpc())) {
171 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
172 "only jsonrpc version 2.0 is supported",
173 null));
174 }
175
176 if (request.getMethod() == null) {
177 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
178 "method field must be set",
179 null));
180 }
181
182 return request;
183 }
184
185 private JsonRpcResponse parseJsonRpcResponse(final JsonNode input) {
186 JsonRpcResponse response;
187 try {
188 response = objectMapper.treeToValue(input, JsonRpcResponse.class);
189 } catch (JsonParseException | JsonMappingException e) {
190 logger.debug("Received invalid jsonrpc response {}", e.getMessage());
191 return null;
192 } catch (IOException e) {
193 throw new AssertionError(e);
194 }
195
196 if (!"2.0".equals(response.getJsonrpc())) {
197 logger.debug("Received invalid jsonrpc response with invalid version {}", response.getJsonrpc());
198 return null;
199 }
200
201 if (response.getResult() != null && response.getError() != null) {
202 logger.debug("Received invalid jsonrpc response with both result and error");
203 return null;
204 }
205
206 if (response.getResult() == null && response.getError() == null) {
207 logger.debug("Received invalid jsonrpc response without result and error");
208 return null;
209 }
210
211 if (response.getId() == null || response.getId().isNull()) {
212 logger.debug("Received invalid jsonrpc response without id");
213 return null;
214 }
215
216 return response;
217 }
218
219 public interface RequestHandler {
220
221 JsonNode apply(String method, ContainerNode<?> params) throws JsonRpcException;
222 }
223 }