diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java index 2093930ca9..2986940398 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java @@ -1,6 +1,8 @@ package org.tron.core.services.jsonrpc; +import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.StreamReadConstraints; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -30,7 +32,19 @@ @Slf4j(topic = "API") public class JsonRpcServlet extends RateLimiterServlet { - private static final ObjectMapper MAPPER = new ObjectMapper(); + // Snapshot of node.http.maxNestingDepth / maxTokenCount at class-load time (after Args.setParam). + private static final ObjectMapper MAPPER = buildMapper(); + + private static ObjectMapper buildMapper() { + CommonParameter p = CommonParameter.getInstance(); + JsonFactory factory = JsonFactory.builder() + .streamReadConstraints(StreamReadConstraints.builder() + .maxNestingDepth(p.getMaxNestingDepth()) + .maxTokenCount(p.getMaxTokenCount()) + .build()) + .build(); + return new ObjectMapper(factory); + } private enum JsonRpcError { PARSE_ERROR(-32700), @@ -97,11 +111,16 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I try { rootNode = MAPPER.readTree(body); if (rootNode == null || rootNode.isMissingNode()) { - writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null, false); + writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "JSON parse error", null, false); return; } } catch (JsonProcessingException e) { - writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null, false); + writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "JSON parse error", null, false); + return; + } + + if (!rootNode.isObject() && !rootNode.isArray()) { + writeJsonRpcError(resp, JsonRpcError.INVALID_REQUEST, "Invalid Request", null, false); return; } @@ -159,8 +178,10 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes JsonNode subRequest = rootNode.get(i); if (overflow) { - // Notifications (no "id") do not get a response even on overflow. - if (subRequest.has("id")) { + if (!subRequest.isObject()) { + batchResult.add(buildErrorNode(JsonRpcError.INVALID_REQUEST, "Invalid Request", null)); + } else if (subRequest.has("id")) { + // Notifications (no "id") do not get a response even on overflow. batchResult.add(buildErrorNode(JsonRpcError.RESPONSE_TOO_LARGE, "Response exceeds the limit of " + maxResponseSize + " bytes", subRequest.get("id"))); @@ -168,6 +189,19 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes continue; } + if (!subRequest.isObject()) { + ObjectNode errNode = buildErrorNode(JsonRpcError.INVALID_REQUEST, "Invalid Request", null); + byte[] errBytes = MAPPER.writeValueAsBytes(errNode); + int addition = errBytes.length + (!batchResult.isEmpty() ? 1 : 0); + if (maxResponseSize > 0 && accumulatedSize + addition > maxResponseSize) { + overflow = true; + } else { + accumulatedSize += addition; + } + batchResult.add(errNode); + continue; + } + byte[] subBody; try { subBody = MAPPER.writeValueAsBytes(subRequest); @@ -213,13 +247,14 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes // JSON-RPC 2.0 §6: MUST NOT return an empty Array when there are no response objects. if (batchResult.isEmpty()) { + resp.setContentType("application/json-rpc"); resp.setStatus(HttpServletResponse.SC_OK); resp.setContentLength(0); return; } byte[] finalBytes = MAPPER.writeValueAsBytes(batchResult); - resp.setContentType("application/json-rpc; charset=utf-8"); + resp.setContentType("application/json-rpc"); resp.setStatus(HttpServletResponse.SC_OK); resp.setContentLength(finalBytes.length); resp.getOutputStream().write(finalBytes); @@ -261,7 +296,7 @@ private void writeJsonRpcError(HttpServletResponse resp, JsonRpcError error, Str } else { bytes = MAPPER.writeValueAsBytes(errorObj); } - resp.setContentType("application/json-rpc; charset=utf-8"); + resp.setContentType("application/json-rpc"); resp.setStatus(HttpServletResponse.SC_OK); resp.setContentLength(bytes.length); resp.getOutputStream().write(bytes); diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java index fa45ca4887..b66298d677 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java @@ -245,6 +245,160 @@ public void normalRequest_commitsRpcServerResponse() throws Exception { assertArrayEquals(rpcResp, resp.getContentAsByteArray()); } + // --- Content-Type header: must be application/json-rpc (no charset suffix) --- + + @Test + public void errorResponse_contentTypeIsApplicationJsonRpc() throws Exception { + MockHttpServletResponse resp = doPost("not valid json"); + assertEquals("application/json-rpc", resp.getContentType()); + } + + @Test + public void batchResponse_contentTypeIsApplicationJsonRpc() throws Exception { + byte[] singleResp = "{\"jsonrpc\":\"2.0\",\"result\":\"ok\",\"id\":1}" + .getBytes(StandardCharsets.UTF_8); + doAnswer(inv -> { + OutputStream out = inv.getArgument(1); + out.write(singleResp); + return 0; + }).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class)); + + MockHttpServletResponse resp = doPost("[{\"id\":1}]"); + assertEquals("application/json-rpc", resp.getContentType()); + } + + @Test + public void allNotificationBatch_contentTypeIsApplicationJsonRpc() throws Exception { + // notification: rpcServer returns 0 bytes → empty batchResult → early return path + doAnswer(inv -> 0).when(mockRpcServer) + .handleRequest(any(InputStream.class), any(OutputStream.class)); + + MockHttpServletResponse resp = doPost("[{\"method\":\"eth_blockNumber\"}]"); + assertEquals(200, resp.getStatus()); + assertEquals(0, resp.getContentLength()); + assertEquals("application/json-rpc", resp.getContentType()); + } + + // --- Primitive root node → Invalid Request (-32600), id must be JSON null --- + + @Test + public void primitiveRootNull_returnsInvalidRequestWithJsonNullId() throws Exception { + MockHttpServletResponse resp = doPost("null"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertFalse(body.isArray()); + assertEquals("2.0", body.get("jsonrpc").asText()); + assertEquals(-32600, body.get("error").get("code").asInt()); + assertTrue("id must be JSON null, not the string \"null\"", body.get("id").isNull()); + assertFalse("id must not be a string", body.get("id").isTextual()); + } + + @Test + public void primitiveRootBoolean_returnsInvalidRequest() throws Exception { + MockHttpServletResponse resp = doPost("true"); + assertEquals(200, resp.getStatus()); + assertEquals(-32600, + MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt()); + } + + @Test + public void primitiveRootNumber_returnsInvalidRequest() throws Exception { + MockHttpServletResponse resp = doPost("123"); + assertEquals(200, resp.getStatus()); + assertEquals(-32600, + MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt()); + } + + @Test + public void primitiveRootString_returnsInvalidRequest() throws Exception { + MockHttpServletResponse resp = doPost("\"hello\""); + assertEquals(200, resp.getStatus()); + assertEquals(-32600, + MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt()); + } + + // --- Non-object element inside a batch → Invalid Request per element --- + + @Test + public void batchWithNestedArray_returnsInvalidRequestArray() throws Exception { + MockHttpServletResponse resp = doPost("[[]]"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertTrue("response must be a JSON array", body.isArray()); + assertEquals(1, body.size()); + assertEquals(-32600, body.get(0).get("error").get("code").asInt()); + assertTrue("id in batch error must be JSON null", body.get(0).get("id").isNull()); + } + + @Test + public void batchWithMixedObjectAndArray_objectProcessedArrayRejected() throws Exception { + byte[] singleResp = "{\"jsonrpc\":\"2.0\",\"result\":\"ok\",\"id\":1}" + .getBytes(StandardCharsets.UTF_8); + doAnswer(inv -> { + OutputStream out = inv.getArgument(1); + out.write(singleResp); + return 0; + }).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class)); + + MockHttpServletResponse resp = doPost("[{\"id\":1}, []]"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertTrue("response must be a JSON array", body.isArray()); + assertEquals(2, body.size()); + assertEquals("ok", body.get(0).get("result").asText()); + assertEquals(-32600, body.get(1).get("error").get("code").asInt()); + } + + @Test + public void batchWithNumericAndStringElements_allGetInvalidRequest() throws Exception { + MockHttpServletResponse resp = doPost("[42, \"foo\", true]"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertTrue("response must be a JSON array", body.isArray()); + assertEquals(3, body.size()); + for (int i = 0; i < 3; i++) { + assertEquals(-32600, body.get(i).get("error").get("code").asInt()); + } + } + + // --- StreamReadConstraints: maxNestingDepth and maxTokenCount must be enforced --- + + @Test + public void excessivelyNestedRequest_returnsParseError() throws Exception { + int limit = CommonParameter.getInstance().getMaxNestingDepth(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i <= limit; i++) { + sb.append('['); + } + sb.append('0'); + for (int i = 0; i <= limit; i++) { + sb.append(']'); + } + + MockHttpServletResponse resp = doPost(sb.toString()); + assertEquals(200, resp.getStatus()); + assertEquals(-32700, + MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt()); + } + + @Test + public void tooManyTokens_returnsParseError() throws Exception { + int limit = CommonParameter.getInstance().getMaxTokenCount(); + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < limit; i++) { + if (i > 0) { + sb.append(','); + } + sb.append('0'); + } + sb.append(']'); + + MockHttpServletResponse resp = doPost(sb.toString()); + assertEquals(200, resp.getStatus()); + assertEquals(-32700, + MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt()); + } + // --- helpers --- private MockHttpServletResponse doPost(String body) throws Exception {