Skip to content

Commit 0a8289c

Browse files
authored
feat(jsonrpc): add resource restrict for jsonrpc (#6728)
1 parent 03eca29 commit 0a8289c

14 files changed

Lines changed: 1235 additions & 21 deletions

File tree

common/src/main/java/org/tron/common/parameter/CommonParameter.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,8 +494,16 @@ public class CommonParameter {
494494
public int jsonRpcMaxBlockFilterNum = 50000;
495495
@Getter
496496
@Setter
497+
public int jsonRpcMaxBatchSize = 100;
498+
@Getter
499+
@Setter
500+
public int jsonRpcMaxResponseSize = 25 * 1024 * 1024;
501+
@Getter
502+
@Setter
503+
public int jsonRpcMaxAddressSize = 1000;
504+
@Getter
505+
@Setter
497506
public int jsonRpcMaxLogFilterNum = 20000;
498-
499507
@Getter
500508
@Setter
501509
public int maxTransactionPendingSize;

common/src/main/java/org/tron/core/config/args/NodeConfig.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,9 @@ public void setHttpPBFTPort(int v) {
311311
private int maxBlockRange = 5000;
312312
private int maxSubTopics = 1000;
313313
private int maxBlockFilterNum = 50000;
314+
private int maxBatchSize = 100;
315+
private int maxResponseSize = 25 * 1024 * 1024;
316+
private int maxAddressSize = 1000;
314317
private int maxLogFilterNum = 20000;
315318
private long maxMessageSize = 4194304;
316319
}

common/src/main/resources/reference.conf

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -380,18 +380,20 @@ node {
380380
httpPBFTEnable = false
381381
httpPBFTPort = 8565
382382

383-
# Maximum blocks range for eth_getLogs, >0 otherwise no limit
383+
# The maximum blocks range to retrieve logs for eth_getLogs, default: 5000, <=0 means no limit
384384
maxBlockRange = 5000
385-
386-
# Maximum topics within a topic criteria, >0 otherwise no limit
385+
# Allowed max address count in filter request, default: 1000, <=0 means no limit
386+
maxAddressSize = 1000
387+
# The maximum number of allowed topics within a topic criteria, default: 1000, <=0 means no limit
387388
maxSubTopics = 1000
388-
389-
# Maximum number for blockFilter. >0 otherwise no limit
389+
# Allowed maximum number for blockFilter, default: 50000, <=0 means no limit
390390
maxBlockFilterNum = 50000
391-
392-
# Maximum number of concurrent eth_newFilter registrations, >0 otherwise no limit
391+
# Allowed batch size, default: 100, <=0 means no limit
392+
maxBatchSize = 100
393+
# Allowed max response byte size, default: 26214400 (25 MB), <=0 means no limit
394+
maxResponseSize = 26214400
395+
# Allowed maximum number for newFilter, <=0 means no limit
393396
maxLogFilterNum = 20000
394-
395397
# Maximum JSON-RPC request body size, default 4MB. Independent from rpc.maxMessageSize.
396398
maxMessageSize = 4M
397399
}

framework/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ dependencies {
5656
}
5757

5858
testImplementation group: 'org.springframework', name: 'spring-test', version: "${springVersion}"
59+
testImplementation group: 'javax.portlet', name: 'portlet-api', version: '3.0.1'
5960
implementation group: 'org.zeromq', name: 'jeromq', version: '0.5.3'
6061
api project(":chainbase")
6162
api project(":protocol")

framework/src/main/java/org/tron/core/config/args/Args.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,9 @@ private static void applyNodeConfig(NodeConfig nc) {
559559
PARAMETER.jsonRpcMaxBlockRange = jsonrpc.getMaxBlockRange();
560560
PARAMETER.jsonRpcMaxSubTopics = jsonrpc.getMaxSubTopics();
561561
PARAMETER.jsonRpcMaxBlockFilterNum = jsonrpc.getMaxBlockFilterNum();
562+
PARAMETER.jsonRpcMaxBatchSize = jsonrpc.getMaxBatchSize();
563+
PARAMETER.jsonRpcMaxResponseSize = jsonrpc.getMaxResponseSize();
564+
PARAMETER.jsonRpcMaxAddressSize = jsonrpc.getMaxAddressSize();
562565
PARAMETER.jsonRpcMaxLogFilterNum = jsonrpc.getMaxLogFilterNum();
563566
PARAMETER.jsonRpcMaxMessageSize = jsonrpc.getMaxMessageSize();
564567

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package org.tron.core.services.filter;
2+
3+
import java.io.ByteArrayOutputStream;
4+
import java.io.IOException;
5+
import java.io.OutputStreamWriter;
6+
import java.io.PrintWriter;
7+
import java.nio.charset.StandardCharsets;
8+
import javax.servlet.ServletOutputStream;
9+
import javax.servlet.WriteListener;
10+
import javax.servlet.http.HttpServletResponse;
11+
import javax.servlet.http.HttpServletResponseWrapper;
12+
import lombok.Getter;
13+
14+
/**
15+
* Buffers the response body without writing to the underlying response,
16+
* so the caller can replay it after the handler returns.
17+
*
18+
* <p>If {@code maxBytes > 0} and the response would exceed that limit, the
19+
* {@link #isOverflow()} flag is set instead of throwing. The caller should check this flag after
20+
* the handler returns and write its own error response when true.
21+
*
22+
* <p>Header-mutating methods ({@code setStatus}, {@code setContentType}) are buffered here and
23+
* only forwarded to the real response via {@link #commitToResponse()}.
24+
*/
25+
public class BufferedResponseWrapper extends HttpServletResponseWrapper {
26+
27+
private final HttpServletResponse actual;
28+
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
29+
private final int maxBytes;
30+
private int status = HttpServletResponse.SC_OK;
31+
private String contentType;
32+
private boolean committed = false;
33+
@Getter
34+
private volatile boolean overflow = false;
35+
36+
private final ServletOutputStream outputStream = new ServletOutputStream() {
37+
@Override
38+
public void write(int b) {
39+
if (overflow) {
40+
return;
41+
}
42+
if (maxBytes > 0 && buffer.size() >= maxBytes) {
43+
markOverflow();
44+
return;
45+
}
46+
buffer.write(b);
47+
}
48+
49+
@Override
50+
public void write(byte[] b, int off, int len) {
51+
if (overflow) {
52+
return;
53+
}
54+
if (maxBytes > 0 && buffer.size() + len > maxBytes) {
55+
markOverflow();
56+
return;
57+
}
58+
buffer.write(b, off, len);
59+
}
60+
61+
@Override
62+
public boolean isReady() {
63+
return true;
64+
}
65+
66+
@Override
67+
public void setWriteListener(WriteListener writeListener) {
68+
}
69+
};
70+
71+
private final PrintWriter writer =
72+
new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8), true);
73+
74+
/**
75+
* @param response the wrapped response
76+
* @param maxBytes max allowed response bytes; {@code 0} means no limit
77+
*/
78+
public BufferedResponseWrapper(HttpServletResponse response, int maxBytes) {
79+
super(response);
80+
this.actual = response;
81+
this.maxBytes = maxBytes;
82+
}
83+
84+
private void markOverflow() {
85+
overflow = true;
86+
buffer.reset();
87+
}
88+
89+
/**
90+
* Early-detection path: if the framework reports the full content length before writing any
91+
* bytes, we can flag overflow without buffering anything.
92+
*/
93+
@Override
94+
public void setContentLength(int len) {
95+
if (maxBytes > 0 && len > maxBytes) {
96+
markOverflow();
97+
}
98+
}
99+
100+
@Override
101+
public void setContentLengthLong(long len) {
102+
if (maxBytes > 0 && len > maxBytes) {
103+
markOverflow();
104+
}
105+
}
106+
107+
@Override
108+
public int getStatus() {
109+
return this.status;
110+
}
111+
112+
@Override
113+
public void setStatus(int sc) {
114+
this.status = sc;
115+
}
116+
117+
@Override
118+
public void setHeader(String name, String value) {
119+
if ("content-length".equalsIgnoreCase(name)) {
120+
try {
121+
setContentLengthLong(Long.parseLong(value));
122+
} catch (NumberFormatException ignored) {
123+
// malformed value, skip overflow check
124+
}
125+
} else {
126+
super.setHeader(name, value);
127+
}
128+
}
129+
130+
@Override
131+
public void addHeader(String name, String value) {
132+
if ("content-length".equalsIgnoreCase(name)) {
133+
try {
134+
setContentLengthLong(Long.parseLong(value));
135+
} catch (NumberFormatException ignored) {
136+
// malformed value, skip overflow check
137+
}
138+
} else {
139+
super.addHeader(name, value);
140+
}
141+
}
142+
143+
@Override
144+
public void setContentType(String type) {
145+
this.contentType = type;
146+
}
147+
148+
@Override
149+
public ServletOutputStream getOutputStream() {
150+
return outputStream;
151+
}
152+
153+
@Override
154+
public PrintWriter getWriter() {
155+
return writer;
156+
}
157+
158+
public void commitToResponse() throws IOException {
159+
if (committed) {
160+
throw new IllegalStateException("commitToResponse() already called");
161+
}
162+
committed = true;
163+
// Flush the PrintWriter's OutputStreamWriter encoder into our ByteArrayOutputStream.
164+
// PrintWriter(autoFlush=true) only auto-flushes on println/printf/format, not print/write,
165+
// so bytes can remain buffered in the encoder until an explicit flush.
166+
writer.flush();
167+
if (overflow) {
168+
return;
169+
}
170+
if (contentType != null) {
171+
actual.setContentType(contentType);
172+
}
173+
actual.setStatus(status);
174+
actual.setContentLength(buffer.size());
175+
buffer.writeTo(actual.getOutputStream());
176+
actual.getOutputStream().flush();
177+
}
178+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package org.tron.core.services.filter;
2+
3+
import java.io.BufferedReader;
4+
import java.io.ByteArrayInputStream;
5+
import java.io.InputStreamReader;
6+
import java.nio.charset.Charset;
7+
import java.nio.charset.IllegalCharsetNameException;
8+
import java.nio.charset.StandardCharsets;
9+
import java.nio.charset.UnsupportedCharsetException;
10+
import javax.servlet.ReadListener;
11+
import javax.servlet.ServletInputStream;
12+
import javax.servlet.http.HttpServletRequest;
13+
import javax.servlet.http.HttpServletRequestWrapper;
14+
15+
/**
16+
* Wraps a request to replay a pre-read body from a byte array,
17+
* allowing the body to be read more than once.
18+
*
19+
* <p><b>Scope:</b> designed for synchronous, raw-body POST endpoints
20+
* (e.g. JSON-RPC). It is NOT compatible with:
21+
* <ul>
22+
* <li>{@code application/x-www-form-urlencoded} — cached body cannot back
23+
* {@code getParameter*}.</li>
24+
* <li>multipart — {@code getPart()/getParts()} read from the original
25+
* (already-consumed) stream.</li>
26+
* <li>async non-blocking I/O — see {@code setReadListener}.</li>
27+
* <li>request dispatch / forward chains.</li>
28+
* </ul>
29+
*
30+
* <p>Multiple calls to {@code getInputStream()} (or {@code getReader()})
31+
* are allowed and each returns a fresh stream over the same cached body —
32+
* a deliberate extension of the standard servlet contract.
33+
*/
34+
public class CachedBodyRequestWrapper extends HttpServletRequestWrapper {
35+
36+
private enum BodyAccessor { NONE, STREAM, READER }
37+
38+
private final byte[] body;
39+
private BodyAccessor accessor = BodyAccessor.NONE;
40+
41+
public CachedBodyRequestWrapper(HttpServletRequest request, byte[] body) {
42+
super(request);
43+
this.body = body;
44+
}
45+
46+
@Override
47+
public ServletInputStream getInputStream() {
48+
if (accessor == BodyAccessor.READER) {
49+
throw new IllegalStateException("getReader() has already been called on this request");
50+
}
51+
accessor = BodyAccessor.STREAM;
52+
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
53+
return new ServletInputStream() {
54+
@Override
55+
public int read() {
56+
return bais.read();
57+
}
58+
59+
@Override
60+
public int read(byte[] b, int off, int len) {
61+
return bais.read(b, off, len);
62+
}
63+
64+
@Override
65+
public boolean isFinished() {
66+
return bais.available() == 0;
67+
}
68+
69+
@Override
70+
public boolean isReady() {
71+
return true;
72+
}
73+
74+
@Override
75+
public void setReadListener(ReadListener readListener) {
76+
throw new UnsupportedOperationException(
77+
"async I/O is not supported on cached body");
78+
}
79+
};
80+
}
81+
82+
@Override
83+
public BufferedReader getReader() {
84+
if (accessor == BodyAccessor.STREAM) {
85+
throw new IllegalStateException("getInputStream() has already been called on this request");
86+
}
87+
accessor = BodyAccessor.READER;
88+
String encoding = getCharacterEncoding();
89+
Charset charset;
90+
try {
91+
charset = encoding != null ? Charset.forName(encoding) : StandardCharsets.UTF_8;
92+
} catch (IllegalCharsetNameException | UnsupportedCharsetException ex) {
93+
charset = StandardCharsets.UTF_8;
94+
}
95+
return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(body), charset));
96+
}
97+
}

0 commit comments

Comments
 (0)