Skip to content
Closed
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 @@ -459,6 +459,18 @@ public class CommonParameter {
@Getter
@Setter
public int jsonRpcMaxBlockFilterNum = 50000;
@Getter
@Setter
public int jsonRpcMaxBatchSize = 1;
@Getter
@Setter
public int jsonRpcMaxResponseSize = 25 * 1024 * 1024;
@Getter
@Setter
public int jsonRpcMaxRequestTimeout = 30;
@Getter
@Setter
public int jsonRpcMaxAddressSize = 1000;

@Getter
@Setter
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.tron.core.exception.jsonrpc;

public class JsonRpcResponseTooLargeException extends RuntimeException {

public JsonRpcResponseTooLargeException() {
super();
}

public JsonRpcResponseTooLargeException(String message) {
super(message);
}

public JsonRpcResponseTooLargeException(String message, Throwable cause) {
super(message, cause);
}

}
20 changes: 20 additions & 0 deletions framework/src/main/java/org/tron/core/config/args/Args.java
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,26 @@ public static void applyConfigParams(
config.getInt(ConfigKey.NODE_JSONRPC_MAX_BLOCK_FILTER_NUM);
}

if (config.hasPath(ConfigKey.NODE_JSONRPC_MAX_BATCH_SIZE)) {
PARAMETER.jsonRpcMaxBatchSize =
config.getInt(ConfigKey.NODE_JSONRPC_MAX_BATCH_SIZE);
}

if (config.hasPath(ConfigKey.NODE_JSONRPC_MAX_RESPONSE_SIZE)) {
PARAMETER.jsonRpcMaxResponseSize =
config.getInt(ConfigKey.NODE_JSONRPC_MAX_RESPONSE_SIZE);
}

if (config.hasPath(ConfigKey.NODE_JSONRPC_MAX_REQUEST_TIMEOUT)) {
PARAMETER.jsonRpcMaxRequestTimeout =
config.getInt(ConfigKey.NODE_JSONRPC_MAX_REQUEST_TIMEOUT);
}

if (config.hasPath(ConfigKey.NODE_JSONRPC_MAX_ADDRESS_SIZE)) {
PARAMETER.jsonRpcMaxAddressSize =
config.getInt(ConfigKey.NODE_JSONRPC_MAX_ADDRESS_SIZE);
}

if (config.hasPath(ConfigKey.VM_MIN_TIME_RATIO)) {
PARAMETER.minTimeRatio = config.getDouble(ConfigKey.VM_MIN_TIME_RATIO);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,13 @@ private ConfigKey() {
public static final String NODE_JSONRPC_MAX_SUB_TOPICS = "node.jsonrpc.maxSubTopics";
public static final String NODE_JSONRPC_MAX_BLOCK_FILTER_NUM =
"node.jsonrpc.maxBlockFilterNum";
public static final String NODE_JSONRPC_MAX_BATCH_SIZE = "node.jsonrpc.maxBatchSize";
public static final String NODE_JSONRPC_MAX_RESPONSE_SIZE =
"node.jsonrpc.maxResponseSize";
public static final String NODE_JSONRPC_MAX_REQUEST_TIMEOUT =
"node.jsonrpc.maxRequestTimeout";
public static final String NODE_JSONRPC_MAX_ADDRESS_SIZE =
"node.jsonrpc.maxAddressSize";

// node - dns
public static final String NODE_DNS_TREE_URLS = "node.dns.treeUrls";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package org.tron.core.services.filter;

import java.io.ByteArrayOutputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.tron.core.exception.jsonrpc.JsonRpcResponseTooLargeException;

/**
* Buffers the response body without writing to the underlying response,
* so the caller can inspect the size before committing.
*
* <p>If {@code maxBytes > 0}, writes that would push the buffer past {@code maxBytes} throw
* {@link JsonRpcResponseTooLargeException} immediately, bounding memory usage to at most
* {@code maxBytes} rather than the full response size.
*/
public class BufferedResponseWrapper extends HttpServletResponseWrapper {

private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
private final int maxBytes;
private final ServletOutputStream outputStream = new ServletOutputStream() {
@Override
public void write(int b) {
checkLimit(1);
buffer.write(b);
}

@Override
public void write(byte[] b, int off, int len) {
checkLimit(len);
buffer.write(b, off, len);
}

@Override
public boolean isReady() {
return true;
}

@Override
public void setWriteListener(WriteListener writeListener) {
}
};

/**
* @param response the wrapped response
* @param maxBytes max allowed response bytes; {@code 0} means no limit
*/
public BufferedResponseWrapper(HttpServletResponse response, int maxBytes) {
super(response);
this.maxBytes = maxBytes;
}

private void checkLimit(int incoming) {
if (maxBytes > 0 && buffer.size() + incoming > maxBytes) {
throw new JsonRpcResponseTooLargeException(
"Response byte size exceeds the limit of " + maxBytes);
}
}

@Override
public ServletOutputStream getOutputStream() {
return outputStream;
}

/**
* Suppress forwarding Content-Length to the real response; caller sets it after size check.
*/
@Override
public void setContentLength(int len) {
}

@Override
public void setContentLengthLong(long len) {
}

public byte[] toByteArray() {
return buffer.toByteArray();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package org.tron.core.services.filter;

import java.io.ByteArrayInputStream;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

/**
* Wraps a request and replays a pre-read body from a byte array.
*/
public class CachedBodyRequestWrapper extends HttpServletRequestWrapper {

private final byte[] body;

public CachedBodyRequestWrapper(HttpServletRequest request, byte[] body) {
super(request);
this.body = body;
}

@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() {
return bais.read();
}

@Override
public int read(byte[] b, int off, int len) {
return bais.read(b, off, len);
}

@Override
public boolean isFinished() {
return bais.available() == 0;
}

@Override
public boolean isReady() {
return true;
}

@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
package org.tron.core.services.jsonrpc;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.googlecode.jsonrpc4j.HttpStatusCodeProvider;
import com.googlecode.jsonrpc4j.JsonRpcInterceptor;
import com.googlecode.jsonrpc4j.JsonRpcServer;
import com.googlecode.jsonrpc4j.ProxyUtil;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
Expand All @@ -14,15 +26,32 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.tron.common.parameter.CommonParameter;
import org.tron.core.Wallet;
import org.tron.core.db.Manager;
import org.tron.core.services.NodeInfoService;
import org.tron.core.exception.jsonrpc.JsonRpcResponseTooLargeException;
import org.tron.core.services.filter.BufferedResponseWrapper;
import org.tron.core.services.filter.CachedBodyRequestWrapper;
import org.tron.core.services.http.RateLimiterServlet;

@Component
@Slf4j(topic = "API")
public class JsonRpcServlet extends RateLimiterServlet {

private static final ObjectMapper MAPPER = new ObjectMapper();

private static final ExecutorService RPC_EXECUTOR = Executors.newCachedThreadPool(
new ThreadFactoryBuilder().setNameFormat("jsonrpc-timeout-%d").setDaemon(true).build());

enum JsonRpcError {
EXCEED_LIMIT(-32005),
RESPONSE_TOO_LARGE(-32003),
TIMEOUT(-32002);

final int code;

JsonRpcError(int code) {
this.code = code;
}
}

private JsonRpcServer rpcServer = null;

@Autowired
Expand Down Expand Up @@ -66,6 +95,83 @@ public Integer getJsonRpcCode(int httpStatusCode) {

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
rpcServer.handle(req, resp);
CommonParameter parameter = CommonParameter.getInstance();

// Read request body so we can inspect and replay it
byte[] body = readBody(req.getInputStream());

// Check batch request array length
JsonNode rootNode = MAPPER.readTree(body);
if (rootNode.isArray() && rootNode.size() > parameter.getJsonRpcMaxBatchSize()) {
writeJsonRpcError(resp, JsonRpcError.EXCEED_LIMIT,
"Batch size " + rootNode.size() + " exceeds the limit of "
+ parameter.getJsonRpcMaxBatchSize(), null);
return;
}

// Buffer the response; limit is enforced eagerly during writes to bound memory usage
int maxResponseSize = parameter.getJsonRpcMaxResponseSize();
CachedBodyRequestWrapper cachedReq = new CachedBodyRequestWrapper(req, body);
BufferedResponseWrapper bufferedResp = new BufferedResponseWrapper(resp, maxResponseSize);

int timeoutSec = parameter.getJsonRpcMaxRequestTimeout();
Future<?> future = RPC_EXECUTOR.submit(() -> {
try {
rpcServer.handle(cachedReq, bufferedResp);
} catch (Exception e) {
throw new RuntimeException(e);
}
});

try {
future.get(timeoutSec, TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true);
JsonNode idNode = (!rootNode.isArray()) ? rootNode.get("id") : null;
writeJsonRpcError(resp, JsonRpcError.TIMEOUT, "Request timeout after " + timeoutSec + "s",
idNode);
return;
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException
&& cause.getCause() instanceof JsonRpcResponseTooLargeException) {
JsonNode idNode = (!rootNode.isArray()) ? rootNode.get("id") : null;
writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE, cause.getCause().getMessage(),
idNode);
return;
}
throw new IOException("RPC execution failed", cause);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("RPC interrupted", e);
}

byte[] responseBytes = bufferedResp.toByteArray();
resp.setContentLength(responseBytes.length);
resp.getOutputStream().write(responseBytes);
resp.getOutputStream().flush();
}

private byte[] readBody(InputStream in) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] tmp = new byte[4096];
int n;
while ((n = in.read(tmp)) != -1) {
buffer.write(tmp, 0, n);
}
return buffer.toByteArray();
}

private void writeJsonRpcError(HttpServletResponse resp, JsonRpcError error, String message,
JsonNode id) throws IOException {
String idStr = (id != null && !id.isNull() && !id.isMissingNode()) ? id.toString() : "null";
String body = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":" + error.code
+ ",\"message\":\"" + message + "\"},\"id\":" + idStr + "}";
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
resp.setContentType("application/json");
resp.setStatus(HttpServletResponse.SC_OK);
resp.setContentLength(bytes.length);
resp.getOutputStream().write(bytes);
resp.getOutputStream().flush();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ public LogFilter(FilterRequest fr) throws JsonRpcInvalidParamsException {
withContractAddress(addressToByteArray((String) fr.getAddress()));

} else if (fr.getAddress() instanceof ArrayList) {
int maxAddressSize = Args.getInstance().getJsonRpcMaxAddressSize();
if (maxAddressSize > 0 && ((ArrayList<?>) fr.getAddress()).size() > maxAddressSize) {
throw new JsonRpcInvalidParamsException("exceed max addresses: " + maxAddressSize);
}
List<byte[]> addr = new ArrayList<>();
int i = 0;
for (Object s : (ArrayList) fr.getAddress()) {
Expand Down
10 changes: 9 additions & 1 deletion framework/src/main/resources/config.conf
Original file line number Diff line number Diff line change
Expand Up @@ -369,12 +369,20 @@ node {
# The maximum blocks range to retrieve logs for eth_getLogs, default value is 5000,
# should be > 0, otherwise means no limit.
maxBlockRange = 5000

# Allowed max address count in filter request, default value is 1000,
# should be > 0, otherwise means no limit.
maxAddressSize = 1000
# The maximum number of allowed topics within a topic criteria, default value is 1000,
# should be > 0, otherwise means no limit.
maxSubTopics = 1000
# Allowed maximum number for blockFilter
maxBlockFilterNum = 50000
# Allowed batch size
maxBatchSize = 1
# Allowed max response byte size
maxResponseSize = 26214400 // 25 MB = 25 * 1024 * 1024 B
# Allowed max request processing time in seconds
maxRequestTimeout = 30
}

# Disabled api list, it will work for http, rpc and pbft, both FullNode and SolidityNode,
Expand Down
Loading