-
Notifications
You must be signed in to change notification settings - Fork 1
feat(jsonrpc): add resource restirct for jsonrpc #69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
df444c7
8930d76
27c593c
2cbcf6a
7b33166
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
| } | ||
|
|
||
| } |
| 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; | ||||||||||||
|
|
@@ -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 { | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: No limit on request body size. Prompt for AI agents |
||||||||||||
| 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 + "}"; | ||||||||||||
|
Comment on lines
+168
to
+169
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: The Prompt for AI agents
Suggested change
|
||||||||||||
| 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(); | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Race condition after timeout:
future.cancel(true)only sends an interrupt; the RPC handler may still be running. SinceBufferedResponseWrapperdoesn't override header methods (setStatus,setContentType, etc.), the handler thread writes headers directly to the underlyingrespconcurrently withwriteJsonRpcError. This can corrupt the HTTP response. Consider either wrapping header methods inBufferedResponseWrappertoo, or awaiting actual thread termination before writing the error.Prompt for AI agents