diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index fbb39a13288..9c98b4d6491 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -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 diff --git a/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcResponseTooLargeException.java b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcResponseTooLargeException.java new file mode 100644 index 00000000000..65c7bc28cf8 --- /dev/null +++ b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcResponseTooLargeException.java @@ -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); + } + +} diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index 0e71294d786..2ae4dd7d0b8 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -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); } diff --git a/framework/src/main/java/org/tron/core/config/args/ConfigKey.java b/framework/src/main/java/org/tron/core/config/args/ConfigKey.java index dbb872febce..8a507ce037e 100644 --- a/framework/src/main/java/org/tron/core/config/args/ConfigKey.java +++ b/framework/src/main/java/org/tron/core/config/args/ConfigKey.java @@ -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"; diff --git a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java new file mode 100644 index 00000000000..fefea565c3c --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java @@ -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. + * + *
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();
+ }
+
+}
diff --git a/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java b/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java
new file mode 100644
index 00000000000..efee8d9574e
--- /dev/null
+++ b/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java
@@ -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) {
+ }
+ };
+ }
+}
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 104a0e9e470..f7c830cbd45 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,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;
@@ -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
@@ -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();
}
-}
\ No newline at end of file
+}
diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java
index 42bc123d4bc..d2bd58f6c56 100644
--- a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java
+++ b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java
@@ -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