Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.exc.StreamConstraintsException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Utf8;
import com.google.common.primitives.Bytes;
import com.googlecode.jsonrpc4j.ErrorResolver.JsonError;
import com.googlecode.jsonrpc4j.JsonRpcInterceptor;
import com.googlecode.jsonrpc4j.JsonRpcMethod;
Expand Down Expand Up @@ -39,6 +43,8 @@ public class JsonRpcServlet extends RateLimiterServlet {

private static final ObjectMapper MAPPER = JsonRpcMapper.create();
private static final ObjectMapper OUTBOUND_MAPPER = new ObjectMapper();
private static final ObjectReader REQUEST_READER =
MAPPER.reader().with(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);

private enum JsonRpcError {
PARSE_ERROR(-32700),
Expand Down Expand Up @@ -119,19 +125,23 @@ private void doPostInternal(HttpServletRequest req, HttpServletResponse resp) th

// Transport IOException from readBody propagates as HTTP 500 (genuine IO failure).
byte[] body = readBody(req.getInputStream());
// Jackson accepts overlong, surrogate and above-U+10FFFF UTF-8 sequences and treats NUL
// bytes as a sign of UTF-16 or UTF-32, so check the bytes first.
if (!Utf8.isWellFormed(body) || Bytes.contains(body, (byte) 0)) {
writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "JSON parse error", null, false);
return;
}
JsonNode rootNode;
try {
rootNode = MAPPER.readTree(body);
if (rootNode == null || rootNode.isMissingNode()) {
writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "JSON parse error", null, false);
return;
}
rootNode = REQUEST_READER.readTree(body);
} catch (JsonProcessingException e) {
if (e instanceof StreamConstraintsException) {
writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, e.getMessage(), null, false);
} else {
writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "JSON parse error", null, false);
}
String message = e instanceof StreamConstraintsException
? e.getMessage() : "JSON parse error";
writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, message, null, false);
return;
}
if (rootNode == null || rootNode.isMissingNode()) {
writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "JSON parse error", null, false);
return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.tron.core.services.jsonrpc;

import static com.google.common.primitives.Bytes.concat;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.googlecode.jsonrpc4j.JsonRpcMethod;
Expand All @@ -15,6 +17,7 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
Expand All @@ -31,6 +34,7 @@
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
Expand Down Expand Up @@ -193,6 +197,43 @@ public void successfulRequestWithProductionFilterPreservesResponse() throws Exce
}
}

@Test
public void malformedUtf8BodyReturnsJsonParseErrorOverHttp() throws Exception {
URI endpoint = startServer(true);
// 0xC1 0xAF is an overlong encoding of 'o' in test_ok.
byte[] body = concat("{\"jsonrpc\":\"2.0\",\"method\":\"test_".getBytes(StandardCharsets.UTF_8),
new byte[] {(byte) 0xC1, (byte) 0xAF},
"k\",\"params\":[],\"id\":7}".getBytes(StandardCharsets.UTF_8));

assertParseErrorOverHttp(endpoint, body);
Assert.assertEquals(0, fatalService.okCalls.get());
}

@Test
public void trailingContentReturnsJsonParseErrorOverHttp() throws Exception {
URI endpoint = startServer(true);
String request = "{\"jsonrpc\":\"2.0\",\"method\":\"test_ok\",\"params\":[],\"id\":7}";

assertParseErrorOverHttp(endpoint,
(request + " " + request).getBytes(StandardCharsets.UTF_8));
Assert.assertEquals(0, fatalService.okCalls.get());
}

private void assertParseErrorOverHttp(URI endpoint, byte[] body) throws IOException {
HttpPost request = new HttpPost(endpoint);
request.setEntity(new ByteArrayEntity(body, ContentType.APPLICATION_JSON));

try (CloseableHttpResponse response = client.execute(request)) {
Assert.assertEquals(200, response.getStatusLine().getStatusCode());
Assert.assertEquals("application/json-rpc",
ContentType.get(response.getEntity()).getMimeType());
ObjectMapper mapper = new ObjectMapper();
Assert.assertEquals(mapper.readTree("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32700,"
+ "\"message\":\"JSON parse error\"},\"id\":null}"),
mapper.readTree(EntityUtils.toByteArray(response.getEntity())));
}
}

public interface FatalService {

@JsonRpcMethod("test_fatal")
Expand All @@ -205,6 +246,7 @@ public interface FatalService {
private static class FatalServiceImpl implements FatalService {

private final AtomicBoolean invoked = new AtomicBoolean();
private final AtomicInteger okCalls = new AtomicInteger();

@Override
public String fatal() {
Expand All @@ -214,6 +256,7 @@ public String fatal() {

@Override
public String ok() {
okCalls.incrementAndGet();
return "ok";
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.tron.core.services.jsonrpc;

import static com.google.common.primitives.Bytes.concat;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
Expand Down Expand Up @@ -39,6 +40,7 @@
import java.io.OutputStream;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -103,6 +105,158 @@ public void emptyBody_returnsParseError() throws Exception {
assertEquals(-32700, body.get("error").get("code").asInt());
}

// --- request body bytes ---

@Test
public void malformedUtf8_isRejectedBeforeDispatch() throws Exception {
TronJsonRpc rpc = useRealRpcServer();
// Each sequence replaces the leading 'w' of web3_clientVersion. The overlong forms decode
// to 'w' in a lenient parser, so they reach the real method unless rejected up front.
byte[][] sequences = {
{(byte) 0xC1, (byte) 0xB7},
{(byte) 0xE0, (byte) 0x81, (byte) 0xB7},
{(byte) 0xED, (byte) 0xA0, (byte) 0x80},
{(byte) 0xF4, (byte) 0x90, (byte) 0x80, (byte) 0x80}};
for (byte[] sequence : sequences) {
byte[] single = concat(utf8("{\"jsonrpc\":\"2.0\",\"method\":\""), sequence,
utf8("eb3_clientVersion\",\"id\":1}"));
byte[] notification = concat(utf8("{\"jsonrpc\":\"2.0\",\"method\":\""), sequence,
utf8("eb3_clientVersion\"}"));
byte[] batch = concat(utf8("["), single, utf8("]"));
for (byte[] body : Arrays.asList(single, notification, batch)) {
assertParseError(Arrays.toString(body), doPost(body));
}
}
verify(realRpcServer, never()).handleRequest(any(InputStream.class), any(OutputStream.class));
verify(rpc, never()).web3ClientVersion();
}

@Test
public void wellFormedNonAsciiInput_isDispatched() throws Exception {
TronJsonRpc rpc = useRealRpcServer();
when(rpc.web3Sha3(any())).thenReturn("0x1");
String twoBytes = new String(Character.toChars(0x7FF));
String chinese = "中文";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[NIT] Two small things:

  • wellFormedNonAsciiInput_isDispatched covers raw 3-byte and 4-byte UTF-8, but its U+0080 to U+07FF case is the JSON escape for U+00E9, which is plain ASCII on the wire. So no positive test sends a raw 2-byte sequence, and the negative test only has 2-byte overlongs. Consider adding one, e.g. new String(Character.toChars(0xE9)). Optionally, new String(Character.toChars(0x10FFFF)) instead of the raw literal on line 141 would make that case visible in the diff; plain Unicode escapes would trip AvoidEscapedUnicodeCharacters.
  • The comment on JsonRpcServlet.java:127 could also mention code points above U+10FFFF, as the description does.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. 1d841d4 adds a raw two-byte parameter. I used U+07FF rather than U+00E9 because the JSON escape case already decodes to U+00E9 and the per-iteration verify would count two calls. The 4-byte parameters are now built with Character.toChars. The comment at JsonRpcServlet.java:127 now mentions code points above U+10FFFF (bf6d23a).

String emoji = new String(Character.toChars(0x1F600));
String maxCodePoint = new String(Character.toChars(0x10FFFF));
String[] params = {twoBytes, chinese, emoji, maxCodePoint, "\\u00e9"};
String[] expected = {twoBytes, chinese, emoji, maxCodePoint, "é"};
for (int i = 0; i < params.length; i++) {
MockHttpServletResponse resp = doPost("{\"jsonrpc\":\"2.0\",\"method\":\"web3_sha3\","
+ "\"params\":[\"" + params[i] + "\"],\"id\":1}");
assertEquals(MAPPER.readTree("{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x1\"}"),
MAPPER.readTree(resp.getContentAsByteArray()));
verify(rpc).web3Sha3(expected[i]);
}
}

@Test
public void utf8ByteOrderMark_isAccepted() throws Exception {
TronJsonRpc rpc = useRealRpcServer();
byte[] body = concat(new byte[] {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF},
utf8("{\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\",\"id\":1}"));

MockHttpServletResponse resp = doPost(body);

assertEquals(MAPPER.readTree("{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x1\"}"),
MAPPER.readTree(resp.getContentAsByteArray()));
verify(rpc).getLatestBlockNum();
}

@Test
public void utf16AndUtf32Bodies_areRejected() throws Exception {
TronJsonRpc rpc = useRealRpcServer();
String request = "{\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\",\"id\":1}";
String chinese = "{\"jsonrpc\":\"2.0\",\"method\":\"web3_sha3\",\"params\":[\"中\"],\"id\":1}";
String accented = "{\"jsonrpc\":\"2.0\",\"method\":\"web3_sha3\",\"params\":[\"é\"],\"id\":1}";
for (String charset : Arrays.asList("UTF-16LE", "UTF-16BE", "UTF-32LE", "UTF-32BE")) {
for (String body : Arrays.asList(request, chinese, accented)) {
assertParseError(charset + " " + body, doPost(body.getBytes(Charset.forName(charset))));
}
}
List<byte[]> withByteOrderMark = Arrays.asList(
request.getBytes(StandardCharsets.UTF_16),
concat(new byte[] {(byte) 0xFF, (byte) 0xFE}, request.getBytes(StandardCharsets.UTF_16LE)));
for (byte[] body : withByteOrderMark) {
assertParseError(Arrays.toString(body), doPost(body));
}
verify(realRpcServer, never()).handleRequest(any(InputStream.class), any(OutputStream.class));
verify(rpc, never()).getLatestBlockNum();
verify(rpc, never()).web3Sha3(any());
}

@Test
public void rawNulByte_isRejected() throws Exception {
TronJsonRpc rpc = useRealRpcServer();
String prefix = "{\"jsonrpc\":\"2.0\",\"method\":\"web3_sha3\",\"params\":[\"a";
String suffix = "b\"],\"id\":1}";
List<byte[]> bodies = Arrays.asList(
concat(utf8(prefix), new byte[] {0}, utf8(suffix)),
concat(utf8(prefix + suffix), new byte[] {0}),
concat(new byte[] {0}, utf8(prefix + suffix)));
for (byte[] body : bodies) {
assertParseError(Arrays.toString(body), doPost(body));
}
verify(realRpcServer, never()).handleRequest(any(InputStream.class), any(OutputStream.class));
verify(rpc, never()).web3Sha3(any());
}

@Test
public void escapedNulCharacter_isDispatched() throws Exception {
TronJsonRpc rpc = useRealRpcServer();
when(rpc.web3Sha3(any())).thenReturn("0x1");

MockHttpServletResponse resp = doPost("{\"jsonrpc\":\"2.0\",\"method\":\"web3_sha3\","
+ "\"params\":[\"a\\u0000b\"],\"id\":1}");

assertEquals(MAPPER.readTree("{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x1\"}"),
MAPPER.readTree(resp.getContentAsByteArray()));
verify(rpc).web3Sha3("a" + (char) 0 + "b");
}

@Test
public void trailingContent_isRejectedBeforeDispatch() throws Exception {
TronJsonRpc rpc = useRealRpcServer();
String request = "{\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\",\"id\":1}";
String notification = "{\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\"}";
List<String> bodies = Arrays.asList(
request + " " + request,
request + "\n" + request,
request + request,
request + " xyz",
request + "}",
"[" + request + "] " + request,
"[" + request + "]]",
notification + " " + notification,
"1 2",
"\"x\" {");

for (String body : bodies) {
assertParseError(body, doPost(body));
}
verify(realRpcServer, never()).handleRequest(any(InputStream.class), any(OutputStream.class));
verify(rpc, never()).getLatestBlockNum();
}

@Test
public void trailingWhitespace_isAccepted() throws Exception {
TronJsonRpc rpc = useRealRpcServer();
String request = "{\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\",\"id\":1}";
JsonNode expected = MAPPER.readTree("{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x1\"}");

for (String whitespace : Arrays.asList(" ", "\n", "\r\n", "\t", " \r\n\t ")) {
MockHttpServletResponse resp = doPost(request + whitespace);
assertEquals(whitespace, expected, MAPPER.readTree(resp.getContentAsByteArray()));
}
MockHttpServletResponse batch = doPost("[" + request + "]\n");
assertEquals(MAPPER.createArrayNode().add(expected),
MAPPER.readTree(batch.getContentAsByteArray()));
MockHttpServletResponse withBom = doPost(concat(
new byte[] {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}, utf8(request + "\r\n")));
assertEquals(expected, MAPPER.readTree(withBom.getContentAsByteArray()));
verify(rpc, times(7)).getLatestBlockNum();
}

// --- batch size limit ---

@Test
Expand Down Expand Up @@ -322,14 +476,13 @@ public void methodNameVariants_areAnsweredWithoutDispatch() throws Exception {
public void allowedNameOutsideTopLevelMethod_isNotDispatched() throws Exception {
TronJsonRpc rpc = useRealRpcServer();
String unknown = "{\"jsonrpc\":\"2.0\",\"method\":\"eth_unknownMethod\"";
List<String> bodies = Arrays.asList(
unknown + ",\"params\":[{\"method\":\"eth_blockNumber\"}],\"id\":1}",
unknown + ",\"id\":1}{\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\",\"id\":2}");

for (String body : bodies) {
MockHttpServletResponse resp = doPost(body);
assertEquals(methodNotFound("1"), MAPPER.readTree(resp.getContentAsByteArray()));
}
MockHttpServletResponse nested = doPost(
unknown + ",\"params\":[{\"method\":\"eth_blockNumber\"}],\"id\":1}");
assertEquals(methodNotFound("1"), MAPPER.readTree(nested.getContentAsByteArray()));
MockHttpServletResponse trailing = doPost(
unknown + ",\"id\":1}{\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\",\"id\":2}");
assertParseError("trailing request", trailing);
verify(realRpcServer, never()).handleRequest(any(InputStream.class), any(OutputStream.class));
verify(rpc, never()).getLatestBlockNum();
}
Expand Down Expand Up @@ -1858,12 +2011,33 @@ private MockHttpServletResponse doPost(String body) throws Exception {
return resp;
}

private MockHttpServletResponse doPost(byte[] body) throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/jsonrpc");
request.setContent(body);
MockHttpServletResponse resp = new MockHttpServletResponse();
servlet.callDoPost(request, resp);
return resp;
}

private void doPost(String body, HttpServletResponse response) throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/jsonrpc");
request.setContent(body.getBytes(StandardCharsets.UTF_8));
servlet.callDoPost(request, response);
}

private static void assertParseError(String message, MockHttpServletResponse response)
throws IOException {
assertEquals(message, 200, response.getStatus());
assertEquals(message, "application/json-rpc", response.getContentType());
assertEquals(message, MAPPER.readTree("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32700,"
+ "\"message\":\"JSON parse error\"},\"id\":null}"),
MAPPER.readTree(response.getContentAsByteArray()));
}

private static byte[] utf8(String text) {
return text.getBytes(StandardCharsets.UTF_8);
}

private void assertCleanupFailureDoesNotReplaceFatal(HttpServletResponse response)
throws Exception {
StackOverflowError fatal = new StackOverflowError("original-fatal-marker");
Expand Down
Loading