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..33428fed0b9 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -48,6 +48,9 @@ public class CommonParameter { public long maxEnergyLimitForConstant = 100_000_000L; @Getter @Setter + public int maxConcurrentConstantCalls = 8; + @Getter + @Setter public int lruCacheSize = 500; @Getter @Setter @@ -216,6 +219,9 @@ public class CommonParameter { public int maxMessageSize; @Getter @Setter + public int maxHttpRequestBodySize = 5 * 1024 * 1024; + @Getter + @Setter public int maxHeaderListSize; @Getter @Setter @@ -459,6 +465,18 @@ public class CommonParameter { @Getter @Setter public int jsonRpcMaxBlockFilterNum = 50000; + @Getter + @Setter + public int jsonRpcMaxBatchSize = 1000; + @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/framework/src/main/java/org/tron/common/application/HttpService.java b/framework/src/main/java/org/tron/common/application/HttpService.java index e9a902002ba..c4a27701b99 100644 --- a/framework/src/main/java/org/tron/common/application/HttpService.java +++ b/framework/src/main/java/org/tron/common/application/HttpService.java @@ -15,12 +15,16 @@ package org.tron.common.application; +import java.util.EnumSet; import java.util.concurrent.CompletableFuture; +import javax.servlet.DispatcherType; import lombok.extern.slf4j.Slf4j; import org.eclipse.jetty.server.ConnectionLimit; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletContextHandler; import org.tron.core.config.args.Args; +import org.tron.core.services.filter.RequestBodySizeLimitFilter; @Slf4j(topic = "rpc") public abstract class HttpService extends AbstractService { @@ -70,6 +74,7 @@ protected ServletContextHandler initContextHandler() { protected abstract void addServlet(ServletContextHandler context); protected void addFilter(ServletContextHandler context) { - + context.addFilter(new FilterHolder(new RequestBodySizeLimitFilter()), + "/*", EnumSet.allOf(DispatcherType.class)); } } diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 39e8f06c281..264ce997856 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -61,6 +61,7 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.Semaphore; import java.util.stream.Collectors; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -263,6 +264,9 @@ @Component public class Wallet { + private static final Semaphore CONSTANT_CALL_SEMAPHORE = new Semaphore( + Args.getInstance().getMaxConcurrentConstantCalls()); + private static final String SHIELDED_ID_NOT_ALLOWED = "ShieldedTransactionApi is not allowed"; private static final String PAYMENT_ADDRESS_FORMAT_WRONG = "paymentAddress format is wrong"; private static final String SHIELDED_TRANSACTION_SCAN_RANGE = @@ -743,6 +747,9 @@ public Block getByJsonBlockId(String id) throws JsonRpcInvalidParamsException { } else if (PENDING_STR.equalsIgnoreCase(id)) { throw new JsonRpcInvalidParamsException(TAG_PENDING_SUPPORT_ERROR); } else { + if (id.length() > 128) { + throw new JsonRpcInvalidParamsException("invalid block number"); + } long blockNumber; try { blockNumber = ByteArray.hexToBigInteger(id).longValue(); @@ -3124,6 +3131,26 @@ public Transaction triggerConstantContract(TriggerSmartContract triggerSmartCont TransactionCapsule trxCap, Builder builder, Return.Builder retBuilder, boolean isEstimating) throws ContractValidateException, ContractExeException, HeaderNotFound, VMIllegalException { + if (!CONSTANT_CALL_SEMAPHORE.tryAcquire()) { + throw new ContractValidateException( + "Too many concurrent constant calls, max allowed: " + + Args.getInstance().getMaxConcurrentConstantCalls()); + } + try { + return doTriggerConstantContract( + triggerSmartContract, trxCap, builder, retBuilder, isEstimating); + } finally { + CONSTANT_CALL_SEMAPHORE.release(); + } + } + + private Transaction doTriggerConstantContract( + TriggerSmartContract triggerSmartContract, + TransactionCapsule trxCap, Builder builder, Return.Builder retBuilder, + boolean isEstimating) + throws ContractValidateException, ContractExeException, + HeaderNotFound, VMIllegalException { + if (triggerSmartContract.getContractAddress().isEmpty()) { // deploy contract CreateSmartContract.Builder deployBuilder = CreateSmartContract.newBuilder(); deployBuilder.setOwnerAddress(triggerSmartContract.getOwnerAddress()); 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 83d7fd2c63d..02350228287 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 @@ -207,6 +207,11 @@ public static void applyConfigParams( PARAMETER.maxEnergyLimitForConstant = max(ENERGY_LIMIT_IN_CONSTANT_TX, configLimit, true); } + if (config.hasPath(ConfigKey.VM_MAX_CONCURRENT_CONSTANT_CALLS)) { + PARAMETER.maxConcurrentConstantCalls = + config.getInt(ConfigKey.VM_MAX_CONCURRENT_CONSTANT_CALLS); + } + if (config.hasPath(ConfigKey.VM_LRU_CACHE_SIZE)) { PARAMETER.lruCacheSize = config.getInt(ConfigKey.VM_LRU_CACHE_SIZE); } @@ -265,6 +270,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); } @@ -526,6 +551,11 @@ public static void applyConfigParams( PARAMETER.maxMessageSize = config.hasPath(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) ? config.getInt(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) : GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; + if (config.hasPath(ConfigKey.NODE_HTTP_MAX_REQUEST_BODY_SIZE)) { + PARAMETER.maxHttpRequestBodySize = + config.getInt(ConfigKey.NODE_HTTP_MAX_REQUEST_BODY_SIZE); + } + PARAMETER.maxHeaderListSize = config.hasPath(ConfigKey.NODE_RPC_MAX_HEADER_LIST_SIZE) ? config.getInt(ConfigKey.NODE_RPC_MAX_HEADER_LIST_SIZE) : GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE; 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 b21c9c440a4..b2acc69dee8 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 @@ -20,6 +20,8 @@ private ConfigKey() { // vm public static final String VM_SUPPORT_CONSTANT = "vm.supportConstant"; public static final String VM_MAX_ENERGY_LIMIT_FOR_CONSTANT = "vm.maxEnergyLimitForConstant"; + public static final String VM_MAX_CONCURRENT_CONSTANT_CALLS = + "vm.maxConcurrentConstantCalls"; public static final String VM_LRU_CACHE_SIZE = "vm.lruCacheSize"; public static final String VM_MIN_TIME_RATIO = "vm.minTimeRatio"; public static final String VM_MAX_TIME_RATIO = "vm.maxTimeRatio"; @@ -123,6 +125,8 @@ private ConfigKey() { public static final String NODE_RPC_MAX_CONNECTION_AGE_IN_MILLIS = "node.rpc.maxConnectionAgeInMillis"; public static final String NODE_RPC_MAX_MESSAGE_SIZE = "node.rpc.maxMessageSize"; + public static final String NODE_HTTP_MAX_REQUEST_BODY_SIZE = + "node.http.maxRequestBodySize"; public static final String NODE_RPC_MAX_HEADER_LIST_SIZE = "node.rpc.maxHeaderListSize"; public static final String NODE_RPC_REFLECTION_SERVICE = "node.rpc.reflectionService"; public static final String NODE_RPC_MIN_EFFECTIVE_CONNECTION = @@ -150,6 +154,14 @@ 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..940fba72c41 --- /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; + +/** + * Buffers the response body so the caller can inspect the size + * before committing. If maxBytes > 0, writes that push the buffer + * past maxBytes throw ResponseTooLargeException immediately. + */ +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) { + } + }; + + 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 ResponseTooLargeException( + "Response size exceeds the limit of " + maxBytes + + " bytes"); + } + } + + @Override + public ServletOutputStream getOutputStream() { + return outputStream; + } + + @Override + public void setContentLength(int len) { + } + + @Override + public void setContentLengthLong(long len) { + } + + public byte[] toByteArray() { + return buffer.toByteArray(); + } + + public static class ResponseTooLargeException + extends RuntimeException { + + public ResponseTooLargeException(String message) { + super(message); + } + } +} 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..721eb3adaa1 --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java @@ -0,0 +1,51 @@ +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/filter/RequestBodySizeLimitFilter.java b/framework/src/main/java/org/tron/core/services/filter/RequestBodySizeLimitFilter.java new file mode 100644 index 00000000000..1b20a46bc01 --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/filter/RequestBodySizeLimitFilter.java @@ -0,0 +1,48 @@ +package org.tron.core.services.filter; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.tron.common.parameter.CommonParameter; + +@Slf4j(topic = "API") +public class RequestBodySizeLimitFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) { + try { + if (request instanceof HttpServletRequest) { + HttpServletRequest httpReq = (HttpServletRequest) request; + int maxBodySize = CommonParameter.getInstance() + .getMaxHttpRequestBodySize(); + if (maxBodySize > 0 && httpReq.getContentLength() > maxBodySize) { + HttpServletResponse resp = (HttpServletResponse) response; + resp.setStatus(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE); + resp.setContentType("application/json; charset=utf-8"); + resp.getWriter().println( + "{\"Error\":\"request body too large, limit is " + + maxBodySize + " bytes\"}"); + return; + } + } + chain.doFilter(request, response); + } catch (Exception e) { + logger.error("RequestBodySizeLimitFilter exception: {}", + e.getMessage()); + } + } + + @Override + public void destroy() { + } +} diff --git a/framework/src/main/java/org/tron/core/services/http/FullNodeHttpApiService.java b/framework/src/main/java/org/tron/core/services/http/FullNodeHttpApiService.java index 3ad4ace62fc..34c76279ee9 100644 --- a/framework/src/main/java/org/tron/core/services/http/FullNodeHttpApiService.java +++ b/framework/src/main/java/org/tron/core/services/http/FullNodeHttpApiService.java @@ -518,6 +518,8 @@ protected void addServlet(ServletContextHandler context) { @Override protected void addFilter(ServletContextHandler context) { + super.addFilter(context); + // filters the specified APIs // when node is lite fullnode and openHistoryQueryWhenLiteFN is false context.addFilter(new FilterHolder(liteFnQueryHttpFilter), "/*", diff --git a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java index 7a66aed34f6..08c9bc672f7 100644 --- a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java @@ -1,9 +1,11 @@ package org.tron.core.services.http; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; import io.prometheus.client.Histogram; import java.io.IOException; import java.lang.reflect.Constructor; +import java.util.Set; import javax.annotation.PostConstruct; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -32,6 +34,12 @@ public abstract class RateLimiterServlet extends HttpServlet { private static final String KEY_PREFIX_HTTP = "http_"; private static final String ADAPTER_PREFIX = "org.tron.core.services.ratelimiter.adapter."; + private static final Set ALLOWED_ADAPTERS = ImmutableSet.of( + "GlobalPreemptibleAdapter", + "QpsRateLimiterAdapter", + "IPQPSRateLimiterAdapter", + "DefaultBaseQqsAdapter" + ); @Autowired private RateLimiterContainer container; @@ -49,6 +57,10 @@ private void addRateContainer() { try { cName = item.getStrategy(); params = item.getParams(); + if (!ALLOWED_ADAPTERS.contains(cName)) { + throw new IllegalArgumentException( + "Unknown rate limiter adapter: " + cName); + } // add the specific rate limiter strategy of servlet. Class c = Class.forName(ADAPTER_PREFIX + cName); Constructor constructor; diff --git a/framework/src/main/java/org/tron/core/services/http/solidity/SolidityNodeHttpApiService.java b/framework/src/main/java/org/tron/core/services/http/solidity/SolidityNodeHttpApiService.java index 359adfc2b39..e3775e3792e 100644 --- a/framework/src/main/java/org/tron/core/services/http/solidity/SolidityNodeHttpApiService.java +++ b/framework/src/main/java/org/tron/core/services/http/solidity/SolidityNodeHttpApiService.java @@ -280,6 +280,8 @@ protected void addServlet(ServletContextHandler context) { @Override protected void addFilter(ServletContextHandler context) { + super.addFilter(context); + // http access filter context.addFilter(new FilterHolder(httpApiAccessFilter), "/walletsolidity/*", EnumSet.allOf(DispatcherType.class)); diff --git a/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/http/PBFT/HttpApiOnPBFTService.java b/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/http/PBFT/HttpApiOnPBFTService.java index a77b45353c9..52bd9778a96 100644 --- a/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/http/PBFT/HttpApiOnPBFTService.java +++ b/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/http/PBFT/HttpApiOnPBFTService.java @@ -265,6 +265,8 @@ protected void addServlet(ServletContextHandler context) { @Override protected void addFilter(ServletContextHandler context) { + super.addFilter(context); + // filters the specified APIs // when node is lite fullnode and openHistoryQueryWhenLiteFN is false context.addFilter(new FilterHolder(liteFnQueryHttpFilter), "/*", diff --git a/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/http/solidity/HttpApiOnSolidityService.java b/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/http/solidity/HttpApiOnSolidityService.java index f69597959f8..ddc0ca08525 100644 --- a/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/http/solidity/HttpApiOnSolidityService.java +++ b/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/http/solidity/HttpApiOnSolidityService.java @@ -292,6 +292,8 @@ protected void addServlet(ServletContextHandler context) { @Override protected void addFilter(ServletContextHandler context) { + super.addFilter(context); + // filters the specified APIs // when node is lite fullnode and openHistoryQueryWhenLiteFN is false context.addFilter(new FilterHolder(liteFnQueryHttpFilter), "/*", diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/FullNodeJsonRpcHttpService.java b/framework/src/main/java/org/tron/core/services/jsonrpc/FullNodeJsonRpcHttpService.java index 566ad33a722..79311403894 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/FullNodeJsonRpcHttpService.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/FullNodeJsonRpcHttpService.java @@ -33,6 +33,8 @@ protected void addServlet(ServletContextHandler context) { @Override protected void addFilter(ServletContextHandler context) { + super.addFilter(context); + // filter ServletHandler handler = new ServletHandler(); FilterHolder fh = handler 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..036d4d92e5d 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,22 @@ package org.tron.core.services.jsonrpc; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; 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.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +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 +25,30 @@ 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(); + + private static final int ERR_BATCH_TOO_LARGE = -32600; + private static final int ERR_RESPONSE_TOO_LARGE = -32003; + private static final int ERR_TIMEOUT = -32002; + + private static final ExecutorService RPC_EXECUTOR = + new ThreadPoolExecutor(4, 16, 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(256), + r -> { + Thread t = new Thread(r, "jsonrpc-worker"); + t.setDaemon(true); + return t; + }, + new ThreadPoolExecutor.AbortPolicy()); + private JsonRpcServer rpcServer = null; @Autowired @@ -45,27 +71,112 @@ public void init(ServletConfig config) throws ServletException { rpcServer = new JsonRpcServer(compositeService); rpcServer.setErrorResolver(JsonRpcErrorResolver.INSTANCE); - HttpStatusCodeProvider httpStatusCodeProvider = new HttpStatusCodeProvider() { - @Override - public int getHttpStatusCode(int resultCode) { - return 200; - } + HttpStatusCodeProvider httpStatusCodeProvider = + new HttpStatusCodeProvider() { + @Override + public int getHttpStatusCode(int resultCode) { + return 200; + } - @Override - public Integer getJsonRpcCode(int httpStatusCode) { - return null; - } - }; + @Override + public Integer getJsonRpcCode(int httpStatusCode) { + return null; + } + }; rpcServer.setHttpStatusCodeProvider(httpStatusCodeProvider); rpcServer.setShouldLogInvocationErrors(false); if (CommonParameter.getInstance().isMetricsPrometheusEnable()) { - rpcServer.setInterceptorList(Collections.singletonList(interceptor)); + rpcServer.setInterceptorList( + Collections.singletonList(interceptor)); } } @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { - rpcServer.handle(req, resp); + protected void doPost(HttpServletRequest req, + HttpServletResponse resp) throws IOException { + CommonParameter parameter = CommonParameter.getInstance(); + + byte[] body = readBody(req.getInputStream()); + + JsonNode rootNode = MAPPER.readTree(body); + int maxBatchSize = parameter.getJsonRpcMaxBatchSize(); + if (rootNode.isArray() && maxBatchSize > 0 + && rootNode.size() > maxBatchSize) { + writeJsonRpcError(resp, ERR_BATCH_TOO_LARGE, + "Batch size " + rootNode.size() + + " exceeds limit of " + maxBatchSize, null); + return; + } + + int maxResponseSize = parameter.getJsonRpcMaxResponseSize(); + CachedBodyRequestWrapper cachedReq = + new CachedBodyRequestWrapper(req, body); + BufferedResponseWrapper bufferedResp = + new BufferedResponseWrapper(resp, maxResponseSize); + + int timeoutSec = parameter.getJsonRpcMaxRequestTimeout(); + try { + RPC_EXECUTOR.submit(() -> { + try { + rpcServer.handle(cachedReq, bufferedResp); + } catch (Exception e) { + throw new RuntimeException(e); + } + }).get(timeoutSec, TimeUnit.SECONDS); + } catch (TimeoutException e) { + JsonNode idNode = + !rootNode.isArray() ? rootNode.get("id") : null; + writeJsonRpcError(resp, ERR_TIMEOUT, + "Request timeout after " + timeoutSec + "s", idNode); + return; + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException + && cause.getCause() + instanceof BufferedResponseWrapper + .ResponseTooLargeException) { + JsonNode idNode = + !rootNode.isArray() ? rootNode.get("id") : null; + writeJsonRpcError(resp, ERR_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, int code, + String message, JsonNode id) throws IOException { + String idStr = + (id != null && !id.isNull() && !id.isMissingNode()) + ? id.toString() : "null"; + String body = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":" + + 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/TronJsonRpcImpl.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java index de939bdfff4..a5dbafa7ae5 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java @@ -161,6 +161,7 @@ public enum RequestSource { private static final String JSON_ERROR = "invalid json request"; private static final String BLOCK_NUM_ERROR = "invalid block number"; + private static final int MAX_HEX_PARAM_LENGTH = 128; private static final String TAG_NOT_SUPPORT_ERROR = "TAG [earliest | pending | finalized] not supported"; private static final String QUANTITY_NOT_SUPPORT_ERROR = @@ -409,6 +410,9 @@ public String getTrxBalance(String address, String blockNumOrTag) } return ByteArray.toJsonHex(balance); } else { + if (blockNumOrTag.length() > MAX_HEX_PARAM_LENGTH) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } try { ByteArray.hexToBigInteger(blockNumOrTag); } catch (Exception e) { @@ -558,6 +562,9 @@ public String getStorageAt(String address, String storageIdx, String blockNumOrT DataWord value = storage.getValue(new DataWord(ByteArray.fromHexString(storageIdx))); return ByteArray.toJsonHex(value == null ? new byte[32] : value.getData()); } else { + if (blockNumOrTag.length() > MAX_HEX_PARAM_LENGTH) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } try { ByteArray.hexToBigInteger(blockNumOrTag); } catch (Exception e) { @@ -589,6 +596,9 @@ public String getABIOfSmartContract(String contractAddress, String blockNumOrTag } } else { + if (blockNumOrTag.length() > MAX_HEX_PARAM_LENGTH) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } try { ByteArray.hexToBigInteger(blockNumOrTag); } catch (Exception e) { @@ -971,6 +981,9 @@ public String getCall(CallArguments transactionCall, Object blockParamObj) throw new JsonRpcInvalidParamsException(JSON_ERROR); } + if (blockNumOrTag.length() > MAX_HEX_PARAM_LENGTH) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } long blockNumber; try { blockNumber = ByteArray.hexToBigInteger(blockNumOrTag).longValue(); @@ -1014,6 +1027,9 @@ public String getCall(CallArguments transactionCall, Object blockParamObj) return call(addressData, contractAddressData, transactionCall.parseValue(), ByteArray.fromHexString(transactionCall.getData())); } else { + if (blockNumOrTag.length() > MAX_HEX_PARAM_LENGTH) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } try { ByteArray.hexToBigInteger(blockNumOrTag); } catch (Exception e) { 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..4dde098d4de 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,12 @@ 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 369924074bc..7e3fc401aa0 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -176,6 +176,12 @@ node { maxHttpConnectNumber = 50 + http { + # Maximum HTTP request body size in bytes, default 5MB (aligned with geth). + # Rejects oversized requests before buffering. Set to 0 to disable. + # maxRequestBodySize = 5242880 + } + minParticipationRate = 15 # allowShieldedTransactionApi = true @@ -375,6 +381,17 @@ node { maxSubTopics = 1000 # Allowed maximum number for blockFilter maxBlockFilterNum = 50000 + # Maximum number of requests in a JSON-RPC batch, default 1000 (aligned with geth). + # Set to 0 to disable limit. + # maxBatchSize = 1000 + # Maximum response size in bytes, default 25MB (aligned with geth). + # Set to 0 to disable limit. + # maxResponseSize = 26214400 + # Maximum request processing time in seconds, default 30 (aligned with geth). + # maxRequestTimeout = 30 + # Maximum number of addresses in eth_getLogs filter, default 1000 (aligned with geth). + # Set to 0 to disable limit. + # maxAddressSize = 1000 } # Disabled api list, it will work for http, rpc and pbft, both FullNode and SolidityNode, @@ -413,6 +430,19 @@ rate.limiter = { # component = "ListWitnessesServlet", # strategy = "QpsRateLimiterAdapter", # paramString = "qps=1" + # }, + + # Recommended: rate limit constant call endpoints to mitigate DoS. + # constant calls are free (no TRX cost) and can consume significant CPU. + # { + # component = "TriggerConstantContractServlet", + # strategy = "QpsRateLimiterAdapter", + # paramString = "qps=20" + # }, + # { + # component = "EstimateEnergyServlet", + # strategy = "QpsRateLimiterAdapter", + # paramString = "qps=10" # } ], @@ -442,9 +472,9 @@ rate.limiter = { # disconnect = 1.0 } - # global qps, default 50000 + # global qps, default 50000. Recommended: 10000 for public-facing nodes. global.qps = 50000 - # IP-based global qps, default 10000 + # IP-based global qps, default 10000. Recommended: 1000 for public-facing nodes. global.ip.qps = 10000 } @@ -688,7 +718,11 @@ trx.reference.block = "solid" // "head" or "solid" vm = { supportConstant = false + # Maximum energy for constant calls. Recommended: 10000000 for public-facing nodes. + # Default 100000000 (~100s CPU per call). Lower values reduce DoS attack surface. maxEnergyLimitForConstant = 100000000 + # Maximum concurrent constant calls. Default 8. Set to 0 to disable limit. + # maxConcurrentConstantCalls = 8 minTimeRatio = 0.0 maxTimeRatio = 5.0 saveInternalTx = false diff --git a/framework/src/test/java/org/tron/core/services/filter/RequestBodySizeLimitFilterTest.java b/framework/src/test/java/org/tron/core/services/filter/RequestBodySizeLimitFilterTest.java new file mode 100644 index 00000000000..5c89bdf2e01 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/filter/RequestBodySizeLimitFilterTest.java @@ -0,0 +1,72 @@ +package org.tron.core.services.filter; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.PrintWriter; +import java.io.StringWriter; +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.tron.common.parameter.CommonParameter; + +public class RequestBodySizeLimitFilterTest { + + private RequestBodySizeLimitFilter filter; + private HttpServletRequest request; + private HttpServletResponse response; + private FilterChain chain; + private StringWriter responseBody; + + @Before + public void setUp() throws Exception { + filter = new RequestBodySizeLimitFilter(); + request = mock(HttpServletRequest.class); + response = mock(HttpServletResponse.class); + chain = mock(FilterChain.class); + responseBody = new StringWriter(); + when(response.getWriter()) + .thenReturn(new PrintWriter(responseBody)); + } + + @Test + public void testNormalRequest() throws Exception { + when(request.getContentLength()).thenReturn(1024); + filter.doFilter(request, response, chain); + verify(chain).doFilter(request, response); + } + + @Test + public void testOversizedRequest() throws Exception { + int limit = CommonParameter.getInstance() + .getMaxHttpRequestBodySize(); + when(request.getContentLength()).thenReturn(limit + 1); + filter.doFilter(request, response, chain); + verify(response).setStatus( + HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE); + verify(chain, never()).doFilter(request, response); + Assert.assertTrue( + responseBody.toString().contains("request body too large")); + } + + @Test + public void testExactLimit() throws Exception { + int limit = CommonParameter.getInstance() + .getMaxHttpRequestBodySize(); + when(request.getContentLength()).thenReturn(limit); + filter.doFilter(request, response, chain); + verify(chain).doFilter(request, response); + } + + @Test + public void testMissingContentLength() throws Exception { + when(request.getContentLength()).thenReturn(-1); + filter.doFilter(request, response, chain); + verify(chain).doFilter(request, response); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/RateLimiterWhitelistTest.java b/framework/src/test/java/org/tron/core/services/http/RateLimiterWhitelistTest.java new file mode 100644 index 00000000000..e98a88ed1cf --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/RateLimiterWhitelistTest.java @@ -0,0 +1,35 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Field; +import java.util.Set; +import org.junit.Test; + +public class RateLimiterWhitelistTest { + + @SuppressWarnings("unchecked") + private Set getAllowedAdapters() throws Exception { + Field field = RateLimiterServlet.class + .getDeclaredField("ALLOWED_ADAPTERS"); + field.setAccessible(true); + return (Set) field.get(null); + } + + @Test + public void testAllowedAdapters() throws Exception { + Set allowed = getAllowedAdapters(); + assertTrue(allowed.contains("GlobalPreemptibleAdapter")); + assertTrue(allowed.contains("QpsRateLimiterAdapter")); + assertTrue(allowed.contains("IPQPSRateLimiterAdapter")); + assertTrue(allowed.contains("DefaultBaseQqsAdapter")); + } + + @Test + public void testUnknownAdapterBlocked() throws Exception { + Set allowed = getAllowedAdapters(); + assertFalse(allowed.contains("EvilAdapter")); + assertFalse(allowed.contains("java.lang.Runtime")); + } +} diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/HexParamValidationTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/HexParamValidationTest.java new file mode 100644 index 00000000000..13d446b8268 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/HexParamValidationTest.java @@ -0,0 +1,32 @@ +package org.tron.core.services.jsonrpc; + +import org.junit.Test; +import org.tron.common.utils.ByteArray; + +public class HexParamValidationTest { + + @Test + public void testNormalHex() { + ByteArray.hexToBigInteger("0xffffffffffffffff"); + } + + @Test + public void testMaxLengthHex() { + StringBuilder sb = new StringBuilder("0x"); + for (int i = 0; i < 126; i++) { + sb.append("a"); + } + assert sb.toString().length() == 128; + ByteArray.hexToBigInteger(sb.toString()); + } + + @Test + public void testOversizedHex() { + StringBuilder sb = new StringBuilder("0x"); + for (int i = 0; i < 198; i++) { + sb.append("f"); + } + assert sb.toString().length() == 200; + ByteArray.hexToBigInteger(sb.toString()); + } +} diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcLimitsTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcLimitsTest.java new file mode 100644 index 00000000000..26fafa06974 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcLimitsTest.java @@ -0,0 +1,57 @@ +package org.tron.core.services.jsonrpc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; + +import java.nio.charset.StandardCharsets; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.Test; +import org.tron.core.services.filter.BufferedResponseWrapper; +import org.tron.core.services.filter.CachedBodyRequestWrapper; + +public class JsonRpcLimitsTest { + + @Test + public void testCachedBodyReplay() throws Exception { + HttpServletRequest req = mock(HttpServletRequest.class); + byte[] body = "{\"method\":\"eth_blockNumber\"}" + .getBytes(StandardCharsets.UTF_8); + CachedBodyRequestWrapper wrapper = + new CachedBodyRequestWrapper(req, body); + byte[] buf = new byte[body.length]; + wrapper.getInputStream().read(buf); + assertEquals(new String(body), new String(buf)); + } + + @Test + public void testBufferedResponseNormal() throws Exception { + HttpServletResponse resp = mock(HttpServletResponse.class); + BufferedResponseWrapper wrapper = + new BufferedResponseWrapper(resp, 1024); + wrapper.getOutputStream().write("hello".getBytes()); + assertEquals("hello", new String(wrapper.toByteArray())); + } + + @Test + public void testBufferedResponseExceedsLimit() { + HttpServletResponse resp = mock(HttpServletResponse.class); + BufferedResponseWrapper wrapper = + new BufferedResponseWrapper(resp, 10); + assertThrows( + BufferedResponseWrapper.ResponseTooLargeException.class, + () -> wrapper.getOutputStream() + .write("this exceeds 10 bytes".getBytes())); + } + + @Test + public void testBufferedResponseNoLimit() throws Exception { + HttpServletResponse resp = mock(HttpServletResponse.class); + BufferedResponseWrapper wrapper = + new BufferedResponseWrapper(resp, 0); + byte[] data = new byte[10_000]; + wrapper.getOutputStream().write(data); + assertEquals(10_000, wrapper.toByteArray().length); + } +}