]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/jsonrpc/JsonRpcReader.java
Extend logging in JsonRpcReader
[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 final JsonRpcRequest jsonRpcRequest) {
45 logger.debug("Received json rpc request, method: " + jsonRpcRequest.method);
46 final var response = handleRequest(requestHandler, jsonRpcRequest);
47 if (response != null) {
48 jsonRpcSender.sendResponse(response);
49 }
50 } else if (message instanceof JsonRpcResponse jsonRpcResponse) {
51 responseHandler.accept(jsonRpcResponse);
52 } else {
53 final var responseList = ((JsonRpcBulkMessage) message).getMessages().stream().map(jsonNode -> {
54 final JsonRpcRequest request;
55 try {
56 request = parseJsonRpcRequest(jsonNode);
57 } catch (JsonRpcException e) {
58 return JsonRpcResponse.forError(e.getError(), getId(jsonNode));
59 }
60
61 return handleRequest(requestHandler, request);
62 }).filter(Objects::nonNull).collect(Collectors.toList());
63
64 jsonRpcSender.sendBulkResponses(responseList);
65 }
66 }
67 }
68
69 private JsonRpcResponse handleRequest(final RequestHandler requestHandler, final JsonRpcRequest request) {
70 try {
71 final var result = requestHandler.apply(request.getMethod(), request.getParams());
72 if (request.getId() != null) {
73 return JsonRpcResponse.forSuccess(result, request.getId());
74 } else {
75 logger.debug("Command '{}' succeeded but client didn't specify an id, dropping response",
76 request.getMethod());
77 }
78 } catch (JsonRpcException e) {
79 if (request.getId() != null) {
80 return JsonRpcResponse.forError(e.getError(), request.getId());
81 } else {
82 logger.debug("Command '{}' failed but client didn't specify an id, dropping error: {}",
83 request.getMethod(),
84 e.getMessage());
85 }
86 }
87 return null;
88 }
89
90 private JsonRpcMessage readMessage() {
91 while (!Thread.interrupted()) {
92 String input = lineSupplier.get();
93
94 if (input == null) {
95 // Reached end of input stream
96 break;
97 }
98
99 JsonRpcMessage message = parseJsonRpcMessage(input);
100 if (message == null) continue;
101
102 return message;
103 }
104
105 return null;
106 }
107
108 private JsonRpcMessage parseJsonRpcMessage(final String input) {
109 final JsonNode jsonNode;
110 try {
111 jsonNode = objectMapper.readTree(input);
112 } catch (JsonParseException e) {
113 jsonRpcSender.sendResponse(JsonRpcResponse.forError(new JsonRpcResponse.Error(JsonRpcResponse.Error.PARSE_ERROR,
114 e.getMessage(),
115 null), null));
116 return null;
117 } catch (IOException e) {
118 throw new AssertionError(e);
119 }
120
121 if (jsonNode == null) {
122 jsonRpcSender.sendResponse(JsonRpcResponse.forError(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
123 "invalid request",
124 null), null));
125 return null;
126 } else if (jsonNode.isArray()) {
127 if (jsonNode.size() == 0) {
128 jsonRpcSender.sendResponse(JsonRpcResponse.forError(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
129 "invalid request",
130 null), null));
131 return null;
132 }
133 return new JsonRpcBulkMessage(StreamSupport.stream(jsonNode.spliterator(), false)
134 .collect(Collectors.toList()));
135 } else if (jsonNode.isObject()) {
136 if (jsonNode.has("result") || jsonNode.has("error")) {
137 return parseJsonRpcResponse(jsonNode);
138 } else {
139 try {
140 return parseJsonRpcRequest(jsonNode);
141 } catch (JsonRpcException e) {
142 jsonRpcSender.sendResponse(JsonRpcResponse.forError(e.getError(), getId(jsonNode)));
143 return null;
144 }
145 }
146 } else {
147 jsonRpcSender.sendResponse(JsonRpcResponse.forError(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
148 "unexpected type: " + jsonNode.getNodeType().name(),
149 null), null));
150 return null;
151 }
152 }
153
154 private ValueNode getId(JsonNode jsonNode) {
155 final var id = jsonNode.get("id");
156 return id instanceof ValueNode ? (ValueNode) id : null;
157 }
158
159 private JsonRpcRequest parseJsonRpcRequest(final JsonNode input) throws JsonRpcException {
160 JsonRpcRequest request;
161 try {
162 request = objectMapper.treeToValue(input, JsonRpcRequest.class);
163 } catch (JsonMappingException e) {
164 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
165 e.getMessage(),
166 null));
167 } catch (IOException e) {
168 throw new AssertionError(e);
169 }
170
171 if (!"2.0".equals(request.getJsonrpc())) {
172 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
173 "only jsonrpc version 2.0 is supported",
174 null));
175 }
176
177 if (request.getMethod() == null) {
178 throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
179 "method field must be set",
180 null));
181 }
182
183 return request;
184 }
185
186 private JsonRpcResponse parseJsonRpcResponse(final JsonNode input) {
187 JsonRpcResponse response;
188 try {
189 response = objectMapper.treeToValue(input, JsonRpcResponse.class);
190 } catch (JsonParseException | JsonMappingException e) {
191 logger.debug("Received invalid jsonrpc response {}", e.getMessage());
192 return null;
193 } catch (IOException e) {
194 throw new AssertionError(e);
195 }
196
197 if (!"2.0".equals(response.getJsonrpc())) {
198 logger.debug("Received invalid jsonrpc response with invalid version {}", response.getJsonrpc());
199 return null;
200 }
201
202 if (response.getResult() != null && response.getError() != null) {
203 logger.debug("Received invalid jsonrpc response with both result and error");
204 return null;
205 }
206
207 if (response.getResult() == null && response.getError() == null) {
208 logger.debug("Received invalid jsonrpc response without result and error");
209 return null;
210 }
211
212 if (response.getId() == null || response.getId().isNull()) {
213 logger.debug("Received invalid jsonrpc response without id");
214 return null;
215 }
216
217 return response;
218 }
219
220 public interface RequestHandler {
221
222 JsonNode apply(String method, ContainerNode<?> params) throws JsonRpcException;
223 }
224 }