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 a73158a718a..01ded57ddc9 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,15 @@ public class CommonParameter { @Getter @Setter public int jsonRpcMaxBlockFilterNum = 50000; + @Getter + @Setter + public int jsonRpcMaxBatchSize = 100; + @Getter + @Setter + public int jsonRpcMaxResponseSize = 25 * 1024 * 1024; + @Getter + @Setter + public int jsonRpcMaxAddressSize = 1000; @Getter @Setter diff --git a/common/src/main/java/org/tron/core/config/args/NodeConfig.java b/common/src/main/java/org/tron/core/config/args/NodeConfig.java index c3305e976de..72a4fdb5594 100644 --- a/common/src/main/java/org/tron/core/config/args/NodeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -302,6 +302,9 @@ public void setHttpPBFTPort(int v) { private int maxBlockRange = 5000; private int maxSubTopics = 1000; private int maxBlockFilterNum = 50000; + private int maxBatchSize = 100; + private int maxResponseSize = 25 * 1024 * 1024; + private int maxAddressSize = 1000; } @Getter diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 11970a0a673..dc1d4ee91ac 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -400,6 +400,15 @@ node { # Maximum number for blockFilter maxBlockFilterNum = 50000 + + # Maximum number of requests in a JSON-RPC batch, >0 otherwise no limit + maxBatchSize = 100 + + # Maximum response body size in bytes for JSON-RPC (default 25MB), >0 otherwise no limit + maxResponseSize = 26214400 + + # Maximum number of addresses in a single JSON-RPC request, >0 otherwise no limit + maxAddressSize = 1000 } # Disabled API list (works for http, rpc and pbft, not jsonrpc). Case insensitive. 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 f91c6a437ac..1094b04f2de 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 @@ -585,6 +585,9 @@ private static void applyNodeConfig(NodeConfig nc) { PARAMETER.jsonRpcMaxBlockRange = jsonrpc.getMaxBlockRange(); PARAMETER.jsonRpcMaxSubTopics = jsonrpc.getMaxSubTopics(); PARAMETER.jsonRpcMaxBlockFilterNum = jsonrpc.getMaxBlockFilterNum(); + PARAMETER.jsonRpcMaxBatchSize = jsonrpc.getMaxBatchSize(); + PARAMETER.jsonRpcMaxResponseSize = jsonrpc.getMaxResponseSize(); + PARAMETER.jsonRpcMaxAddressSize = jsonrpc.getMaxAddressSize(); // ---- P2P sub-bean ---- PARAMETER.nodeP2pVersion = nc.getP2p().getVersion(); 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..46872b15e21 --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java @@ -0,0 +1,133 @@ +package org.tron.core.services.filter; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import lombok.Getter; + +/** + * Buffers the response body without writing to the underlying response, + * so the caller can replay it after the handler returns. + * + *
If {@code maxBytes > 0} and the response would exceed that limit, the + * {@link #isOverflow()} flag is set instead of throwing. The caller should check this flag after + * the handler returns and write its own error response when true. + * + *
Header-mutating methods ({@code setStatus}, {@code setContentType}) are buffered here and
+ * only forwarded to the real response via {@link #commitToResponse()}, preventing a timed-out
+ * handler thread from racing with the timeout error writer.
+ */
+public class BufferedResponseWrapper extends HttpServletResponseWrapper {
+
+ private final HttpServletResponse actual;
+ private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ private final int maxBytes;
+ private int status = HttpServletResponse.SC_OK;
+ private String contentType;
+ @Getter
+ private boolean overflow = false;
+
+ private final ServletOutputStream outputStream = new ServletOutputStream() {
+ @Override
+ public void write(int b) {
+ if (overflow) {
+ return;
+ }
+ if (maxBytes > 0 && buffer.size() >= maxBytes) {
+ markOverflow();
+ return;
+ }
+ buffer.write(b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) {
+ if (overflow) {
+ return;
+ }
+ if (maxBytes > 0 && buffer.size() + len > maxBytes) {
+ markOverflow();
+ return;
+ }
+ buffer.write(b, off, len);
+ }
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public void setWriteListener(WriteListener writeListener) {
+ }
+ };
+
+ private final PrintWriter writer = new PrintWriter(outputStream, true);
+
+ /**
+ * @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.actual = response;
+ this.maxBytes = maxBytes;
+ }
+
+ private void markOverflow() {
+ overflow = true;
+ buffer.reset();
+ }
+
+ /**
+ * Early-detection path: if the framework reports the full content length before writing any
+ * bytes, we can flag overflow without buffering anything.
+ */
+ @Override
+ public void setContentLength(int len) {
+ if (maxBytes > 0 && len > maxBytes) {
+ markOverflow();
+ }
+ }
+
+ @Override
+ public void setContentLengthLong(long len) {
+ if (maxBytes > 0 && len > maxBytes) {
+ markOverflow();
+ }
+ }
+
+ @Override
+ public void setStatus(int sc) {
+ this.status = sc;
+ }
+
+ @Override
+ public void setContentType(String type) {
+ this.contentType = type;
+ }
+
+ @Override
+ public ServletOutputStream getOutputStream() {
+ return outputStream;
+ }
+
+ @Override
+ public PrintWriter getWriter() {
+ return writer;
+ }
+
+ public void commitToResponse() throws IOException {
+ if (contentType != null) {
+ actual.setContentType(contentType);
+ }
+ actual.setStatus(status);
+ actual.setContentLength(buffer.size());
+ buffer.writeTo(actual.getOutputStream());
+ actual.getOutputStream().flush();
+ }
+}
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..fcda7d34f86
--- /dev/null
+++ b/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java
@@ -0,0 +1,61 @@
+package org.tron.core.services.filter;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+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) {
+ }
+ };
+ }
+
+ @Override
+ public BufferedReader getReader() {
+ String encoding = getCharacterEncoding();
+ Charset charset = encoding != null ? Charset.forName(encoding) : StandardCharsets.UTF_8;
+ return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(body), charset));
+ }
+}
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..941961cd32b 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,10 +1,15 @@
package org.tron.core.services.jsonrpc;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
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.util.Collections;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
@@ -14,15 +19,28 @@
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.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();
+
+ enum JsonRpcError {
+ PARSE_ERROR(-32700),
+ EXCEED_LIMIT(-32005),
+ RESPONSE_TOO_LARGE(-32003);
+
+ final int code;
+
+ JsonRpcError(int code) {
+ this.code = code;
+ }
+ }
+
private JsonRpcServer rpcServer = null;
@Autowired
@@ -66,6 +84,71 @@ public Integer getJsonRpcCode(int httpStatusCode) {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
- rpcServer.handle(req, resp);
+ CommonParameter parameter = CommonParameter.getInstance();
+
+ byte[] body;
+ JsonNode rootNode;
+ try {
+ body = readBody(req.getInputStream());
+ rootNode = MAPPER.readTree(body);
+ } catch (IOException e) {
+ writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null);
+ return;
+ }
+ int batchSize = parameter.getJsonRpcMaxBatchSize();
+ if (rootNode.isArray() && batchSize > 0 && rootNode.size() > batchSize) {
+ writeJsonRpcError(resp, JsonRpcError.EXCEED_LIMIT,
+ "Batch size " + rootNode.size() + " exceeds the limit of " + batchSize, null);
+ return;
+ }
+
+ CachedBodyRequestWrapper cachedReq = new CachedBodyRequestWrapper(req, body);
+ BufferedResponseWrapper bufferedResp = new BufferedResponseWrapper(
+ resp, parameter.getJsonRpcMaxResponseSize());
+
+ try {
+ rpcServer.handle(cachedReq, bufferedResp);
+ } catch (Exception e) {
+ throw new IOException("RPC execution failed", e);
+ }
+
+ if (bufferedResp.isOverflow()) {
+ JsonNode idNode = !rootNode.isArray() ? rootNode.get("id") : null;
+ writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE,
+ "Response exceeds the limit of " + parameter.getJsonRpcMaxResponseSize() + " bytes",
+ idNode);
+ return;
+ }
+ bufferedResp.commitToResponse();
+ }
+
+ 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 {
+ ObjectNode root = MAPPER.createObjectNode();
+ root.put("jsonrpc", "2.0");
+ ObjectNode errNode = root.putObject("error");
+ errNode.put("code", error.code);
+ errNode.put("message", message);
+ if (id != null && !id.isNull() && !id.isMissingNode()) {
+ root.set("id", id);
+ } else {
+ root.putNull("id");
+ }
+ byte[] bytes = MAPPER.writeValueAsBytes(root);
+ resp.setContentType("application/json; charset=utf-8");
+ 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