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 addr = new ArrayList<>(); int i = 0; for (Object s : (ArrayList) fr.getAddress()) { diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 661a592e431..5c375c51e57 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -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,