This is an automated email from the ASF dual-hosted git repository.
dengliming pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new f9a67c9516 fix(admin): enforce response body size limit on automatic
swagger doc pull (#6458)
f9a67c9516 is described below
commit f9a67c95164da6796ba8f32a084d03252922400b
Author: wy471x <[email protected]>
AuthorDate: Thu Jul 30 22:59:57 2026 +0800
fix(admin): enforce response body size limit on automatic swagger doc pull
(#6458)
Co-authored-by: aias00 <[email protected]>
Co-authored-by: xiaoyu <[email protected]>
---
.../service/impl/SwaggerImportServiceImpl.java | 54 +----------
.../manager/impl/PullSwaggerDocServiceImpl.java | 6 +-
.../org/apache/shenyu/admin/utils/HttpUtils.java | 49 ++++++++++
.../service/impl/SwaggerImportServiceImplTest.java | 91 ++----------------
.../apache/shenyu/admin/utils/HttpUtilsTest.java | 106 +++++++++++++++++++++
5 files changed, 170 insertions(+), 136 deletions(-)
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImpl.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImpl.java
index e2965b2c9c..62846158ed 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImpl.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImpl.java
@@ -26,7 +26,7 @@ import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.parser.OpenAPIV3Parser;
import okhttp3.Response;
-import okhttp3.ResponseBody;
+
import org.apache.shenyu.admin.model.bean.UpstreamInstance;
import org.apache.shenyu.admin.model.dto.SwaggerImportRequest;
import org.apache.shenyu.admin.service.SwaggerImportService;
@@ -47,13 +47,9 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
-import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -71,17 +67,12 @@ public class SwaggerImportServiceImpl implements
SwaggerImportService {
private static final Logger LOG =
LoggerFactory.getLogger(SwaggerImportServiceImpl.class);
- private static final int READ_BUFFER_SIZE = 8192;
-
- private static final long DEFAULT_MAX_SWAGGER_BODY_SIZE = 10L * 1024 *
1024;
-
private final DocManager docManager;
-
+
private final HttpUtils httpUtils;
- // Default is 10 MB and can be overridden by shenyu.swagger.max-body-size.
@Value("${shenyu.swagger.max-body-size:10485760}")
- private long maxSwaggerBodySize = DEFAULT_MAX_SWAGGER_BODY_SIZE;
+ private long maxSwaggerBodySize;
@Resource
private ShenyuClientRegisterMcpServiceImpl shenyuClientRegisterMcpService;
@@ -281,49 +272,12 @@ public class SwaggerImportServiceImpl implements
SwaggerImportService {
throw new RuntimeException("Failed to get Swagger document,
HTTP status code: " + response.code());
}
- return readLimitedResponseBody(response.body(),
maxSwaggerBodySize);
+ return HttpUtils.readLimitedResponseBody(response.body(),
maxSwaggerBodySize);
}
}
- private String readLimitedResponseBody(final ResponseBody responseBody,
final long maxBodySize) throws IOException {
- if (Objects.isNull(responseBody)) {
- throw new IllegalArgumentException("Swagger document response body
is empty");
- }
- if (maxBodySize < 0) {
- throw new IllegalArgumentException("Max Swagger response body size
must not be negative");
- }
-
- long contentLength = responseBody.contentLength();
- // Reject early when the server declares a body larger than the
configured limit.
- if (contentLength > maxBodySize) {
- throw new IllegalArgumentException(String.format(
- "Swagger document response body exceeds maximum size of %d
bytes", maxBodySize));
- }
-
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- byte[] buffer = new byte[READ_BUFFER_SIZE];
- long totalBytes = 0;
- try (InputStream inputStream = responseBody.byteStream()) {
- int bytesRead;
- while ((bytesRead = inputStream.read(buffer)) != -1) {
- totalBytes += bytesRead;
- // Content-Length can be missing or wrong, so enforce the
limit while streaming.
- if (totalBytes > maxBodySize) {
- throw new IllegalArgumentException(String.format(
- "Swagger document response body exceeds maximum
size of %d bytes", maxBodySize));
- }
- outputStream.write(buffer, 0, bytesRead);
- }
- }
- // Preserve the server-declared charset; default to UTF-8 when it is
absent.
- Charset charset = Objects.isNull(responseBody.contentType())
- ? StandardCharsets.UTF_8
- : responseBody.contentType().charset(StandardCharsets.UTF_8);
- return outputStream.toString(charset.name());
- }
-
private void validateSwaggerContent(final String swaggerJson) {
try {
JsonObject docRoot = GsonUtils.getInstance().fromJson(swaggerJson,
JsonObject.class);
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/impl/PullSwaggerDocServiceImpl.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/impl/PullSwaggerDocServiceImpl.java
index f71db31bc3..bbdb392fa5 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/impl/PullSwaggerDocServiceImpl.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/impl/PullSwaggerDocServiceImpl.java
@@ -36,6 +36,7 @@ import org.apache.shenyu.common.constant.AdminConstants;
import org.apache.shenyu.common.utils.GsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder;
@@ -62,6 +63,9 @@ public class PullSwaggerDocServiceImpl implements
PullSwaggerDocService {
private static final long DOC_LOCK_EXPIRED_TIME = 60 * 1000;
+ @Value("${shenyu.swagger.max-body-size:10485760}")
+ private long maxSwaggerBodySize;
+
private final Interner<Object> interner = Interners.newWeakInterner();
@Resource
@@ -102,7 +106,7 @@ public class PullSwaggerDocServiceImpl implements
PullSwaggerDocService {
if (response.code() != HttpStatus.SC_OK) {
throw new IOException(response.toString());
}
- final String body = response.body().string();
+ final String body =
HttpUtils.readLimitedResponseBody(response.body(), maxSwaggerBodySize);
docManager.addDocInfo(
instance,
body,
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/HttpUtils.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/HttpUtils.java
index c8bbf0b637..977b7ec9a7 100644
--- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/HttpUtils.java
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/HttpUtils.java
@@ -43,6 +43,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
+import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
@@ -63,6 +64,8 @@ public class HttpUtils {
private static final MediaType MEDIA_TYPE_JSON =
MediaType.parse("application/json; charset=utf-8");
+ private static final int READ_BUFFER_SIZE = 8192;
+
private Map<String, List<Cookie>> cookieStore = new HashMap<>();
private OkHttpClient httpClient;
@@ -460,6 +463,52 @@ public class HttpUtils {
}
}
+ /**
+ * Read response body with a size limit to prevent excessive memory usage.
+ *
+ * @param responseBody the response body to read
+ * @param maxBodySize maximum allowed body size in bytes
+ * @return the response body as a string
+ * @throws IOException if an I/O error occurs
+ * @throws IllegalArgumentException if the body exceeds maxBodySize
+ */
+ public static String readLimitedResponseBody(final ResponseBody
responseBody, final long maxBodySize) throws IOException {
+ if (Objects.isNull(responseBody)) {
+ throw new IllegalArgumentException("Response body is empty");
+ }
+ if (maxBodySize < 0) {
+ throw new IllegalArgumentException("Max response body size must
not be negative");
+ }
+
+ long contentLength = responseBody.contentLength();
+ if (contentLength > maxBodySize) {
+ throw new IllegalArgumentException(String.format(
+ "Response body exceeds maximum size of %d bytes",
maxBodySize));
+ }
+
+ ByteArrayOutputStream outputStream = contentLength > 0
+ ? new ByteArrayOutputStream((int) Math.min(contentLength,
Integer.MAX_VALUE))
+ : new ByteArrayOutputStream();
+ byte[] buffer = new byte[READ_BUFFER_SIZE];
+ long totalBytes = 0;
+ try (InputStream inputStream = responseBody.byteStream()) {
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ totalBytes += bytesRead;
+ if (totalBytes > maxBodySize) {
+ throw new IllegalArgumentException(String.format(
+ "Response body exceeds maximum size of %d bytes",
maxBodySize));
+ }
+ outputStream.write(buffer, 0, bytesRead);
+ }
+ }
+
+ Charset charset = Objects.isNull(responseBody.contentType())
+ ? StandardCharsets.UTF_8
+ : responseBody.contentType().charset(StandardCharsets.UTF_8);
+ return outputStream.toString(charset.name());
+ }
+
public static class HttpToolConfig {
private int connectTimeoutSeconds = 10;
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImplTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImplTest.java
index 86a43ac42e..231c01aa8a 100644
---
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImplTest.java
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImplTest.java
@@ -17,10 +17,6 @@
package org.apache.shenyu.admin.service.impl;
-import io.swagger.v3.oas.models.OpenAPI;
-import io.swagger.v3.oas.models.Operation;
-import io.swagger.v3.oas.models.info.Info;
-import io.swagger.v3.oas.models.servers.Server;
import okhttp3.MediaType;
import okhttp3.Protocol;
import okhttp3.Request;
@@ -33,8 +29,6 @@ import org.apache.shenyu.admin.model.bean.UpstreamInstance;
import org.apache.shenyu.admin.model.dto.SwaggerImportRequest;
import org.apache.shenyu.admin.service.manager.DocManager;
import org.apache.shenyu.admin.utils.HttpUtils;
-import org.apache.shenyu.client.mcp.common.dto.ShenyuMcpTool;
-import org.apache.shenyu.register.common.dto.MetaDataRegisterDTO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
@@ -44,12 +38,10 @@ import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
-import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
@@ -63,19 +55,24 @@ public class SwaggerImportServiceImplTest {
private static final MediaType JSON_UTF_8 =
MediaType.parse("application/json; charset=utf-8");
+ private static final long DEFAULT_MAX_SWAGGER_BODY_SIZE = 10L * 1024 *
1024;
+
private RecordingDocManager docManager;
private StubHttpUtils httpUtils;
+ private SwaggerImportServiceImpl service;
+
@BeforeEach
public void setUp() {
docManager = new RecordingDocManager();
httpUtils = new StubHttpUtils();
+ service = new SwaggerImportServiceImpl(docManager, httpUtils);
+ ReflectionTestUtils.setField(service, "maxSwaggerBodySize",
DEFAULT_MAX_SWAGGER_BODY_SIZE);
}
@Test
public void importSwaggerShouldReadSmallSwaggerBody() throws IOException {
- SwaggerImportServiceImpl service = new
SwaggerImportServiceImpl(docManager, httpUtils);
httpUtils.setResponse(response(responseBody(
SWAGGER_JSON,
SWAGGER_JSON.getBytes(StandardCharsets.UTF_8).length, JSON_UTF_8)));
@@ -87,7 +84,6 @@ public class SwaggerImportServiceImplTest {
@Test
public void importSwaggerShouldRejectKnownContentLengthGreaterThanLimit()
throws IOException {
- SwaggerImportServiceImpl service = new
SwaggerImportServiceImpl(docManager, httpUtils);
ReflectionTestUtils.setField(service, "maxSwaggerBodySize", 10L);
httpUtils.setResponse(response(responseBody("small", 11L,
JSON_UTF_8)));
@@ -96,87 +92,12 @@ public class SwaggerImportServiceImplTest {
@Test
public void
importSwaggerShouldRejectUnknownContentLengthWhenActualBodyExceedsLimit()
throws IOException {
- SwaggerImportServiceImpl service = new
SwaggerImportServiceImpl(docManager, httpUtils);
ReflectionTestUtils.setField(service, "maxSwaggerBodySize", 10L);
httpUtils.setResponse(response(responseBody("01234567890", -1L,
JSON_UTF_8)));
assertThrows(IllegalArgumentException.class, () ->
service.importSwagger(request()));
}
- @Test
- public void readLimitedResponseBodyShouldAllowBodyExactlyEqualToLimit()
throws IOException {
- String body = "0123456789";
-
- String result = ReflectionTestUtils.invokeMethod(new
SwaggerImportServiceImpl(docManager, httpUtils),
- "readLimitedResponseBody", responseBody(body, -1L,
JSON_UTF_8), 10L);
-
- assertEquals(body, result);
- }
-
- @Test
- public void readLimitedResponseBodyShouldHandleNullBodySafely() {
- SwaggerImportServiceImpl service = new
SwaggerImportServiceImpl(docManager, httpUtils);
-
- assertThrows(IllegalArgumentException.class, () ->
- ReflectionTestUtils.invokeMethod(service,
"readLimitedResponseBody", null, 10L));
- }
-
- @Test
- public void readLimitedResponseBodyShouldUseResponseCharset() throws
IOException {
- Charset charset = StandardCharsets.ISO_8859_1;
- byte[] bytes = new byte[] {'c', 'a', 'f', (byte) 0xE9};
- String body = new String(bytes, charset);
- ResponseBody responseBody = responseBody(bytes, -1L,
MediaType.parse("text/plain; charset=iso-8859-1"));
-
- String result = ReflectionTestUtils.invokeMethod(new
SwaggerImportServiceImpl(docManager, httpUtils),
- "readLimitedResponseBody", responseBody, 10L);
-
- assertEquals(body, result);
- }
-
- @Test
- public void buildMetaDataRegisterDTOShouldThrowWhenServersIsNull() {
- SwaggerImportServiceImpl service = new
SwaggerImportServiceImpl(docManager, httpUtils);
- OpenAPI openapi = new OpenAPI()
- .info(new Info().title("test"));
-
- assertThrows(IllegalArgumentException.class, () ->
- ReflectionTestUtils.invokeMethod(service,
"buildMetaDataRegisterDTO",
- openapi, "test", new ShenyuMcpTool(), "/ping", "ns1"));
- }
-
- @Test
- public void buildMetaDataRegisterDTOShouldThrowWhenServersIsEmpty() {
- SwaggerImportServiceImpl service = new
SwaggerImportServiceImpl(docManager, httpUtils);
- OpenAPI openapi = new OpenAPI()
- .info(new Info().title("test"))
- .servers(Collections.emptyList());
-
- assertThrows(IllegalArgumentException.class, () ->
- ReflectionTestUtils.invokeMethod(service,
"buildMetaDataRegisterDTO",
- openapi, "test", new ShenyuMcpTool(), "/ping", "ns1"));
- }
-
- @Test
- public void buildMetaDataRegisterDTOShouldHandleNullParameters() {
- SwaggerImportServiceImpl service = new
SwaggerImportServiceImpl(docManager, httpUtils);
- OpenAPI openapi = new OpenAPI()
- .info(new Info().title("test"))
- .servers(List.of(new Server().url("http://localhost:8080")));
- Operation operation = new Operation()
- .operationId("ping")
- .parameters(null);
- ShenyuMcpTool tool = new ShenyuMcpTool();
- tool.setOperation(operation);
- tool.setToolName("ping");
-
- MetaDataRegisterDTO result = ReflectionTestUtils.invokeMethod(service,
"buildMetaDataRegisterDTO",
- openapi, "test", tool, "/ping", "ns1");
-
- assertNotNull(result);
- assertEquals("", result.getParameterTypes());
- }
-
private SwaggerImportRequest request() {
SwaggerImportRequest request = new SwaggerImportRequest();
request.setSwaggerUrl(SWAGGER_URL);
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/HttpUtilsTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/HttpUtilsTest.java
index a70870ae33..07f5634dde 100644
---
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/HttpUtilsTest.java
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/HttpUtilsTest.java
@@ -23,6 +23,7 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
+import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
@@ -30,8 +31,12 @@ import java.util.List;
import java.util.Map;
import okhttp3.FormBody;
import okhttp3.HttpUrl;
+import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.Response;
+import okhttp3.ResponseBody;
+import okio.Buffer;
+import okio.BufferedSource;
import org.junit.Assert;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
@@ -420,6 +425,107 @@ public class HttpUtilsTest {
}
}
+ @Test
+ public void readLimitedResponseBodyShouldReadWithinLimit() throws
IOException {
+ String body = "hello";
+ ResponseBody responseBody =
responseBody(body.getBytes(StandardCharsets.UTF_8),
+ body.getBytes(StandardCharsets.UTF_8).length,
+ MediaType.parse("application/json; charset=utf-8"));
+
+ String result = HttpUtils.readLimitedResponseBody(responseBody, 1024);
+
+ Assert.assertEquals(body, result);
+ }
+
+ @Test
+ public void readLimitedResponseBodyShouldAllowBodyExactlyEqualToLimit()
throws IOException {
+ String body = "0123456789";
+
+ String result = HttpUtils.readLimitedResponseBody(
+ responseBody(body.getBytes(StandardCharsets.UTF_8), -1L,
+ MediaType.parse("text/plain")), 10L);
+
+ Assert.assertEquals(body, result);
+ }
+
+ @Test
+ public void
readLimitedResponseBodyShouldRejectContentLengthGreaterThanLimit() {
+ ResponseBody responseBody =
responseBody("small".getBytes(StandardCharsets.UTF_8), 11L,
+ MediaType.parse("application/json"));
+
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> HttpUtils.readLimitedResponseBody(responseBody, 10L));
+ }
+
+ @Test
+ public void readLimitedResponseBodyShouldRejectActualBodyExceedingLimit() {
+ ResponseBody responseBody =
responseBody("01234567890".getBytes(StandardCharsets.UTF_8), -1L,
+ MediaType.parse("application/json"));
+
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> HttpUtils.readLimitedResponseBody(responseBody, 10L));
+ }
+
+ @Test
+ public void readLimitedResponseBodyShouldThrowOnNullBody() {
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> HttpUtils.readLimitedResponseBody(null, 1024));
+ }
+
+ @Test
+ public void readLimitedResponseBodyShouldThrowOnNegativeMaxSize() {
+ ResponseBody responseBody =
responseBody("test".getBytes(StandardCharsets.UTF_8), 4L,
+ MediaType.parse("text/plain"));
+
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> HttpUtils.readLimitedResponseBody(responseBody, -1));
+ }
+
+ @Test
+ public void readLimitedResponseBodyShouldUseResponseCharset() throws
IOException {
+ Charset charset = StandardCharsets.ISO_8859_1;
+ byte[] bytes = new byte[] {'c', 'a', 'f', (byte) 0xE9};
+ String expected = new String(bytes, charset);
+ ResponseBody responseBody = responseBody(bytes, -1L,
+ MediaType.parse("text/plain; charset=iso-8859-1"));
+
+ String result = HttpUtils.readLimitedResponseBody(responseBody, 100);
+
+ Assert.assertEquals(expected, result);
+ }
+
+ @Test
+ public void readLimitedResponseBodyShouldDefaultToUtf8WhenCharsetMissing()
throws IOException {
+ String body = "hello";
+ ResponseBody responseBody =
responseBody(body.getBytes(StandardCharsets.UTF_8),
+ body.getBytes(StandardCharsets.UTF_8).length,
+ MediaType.parse("application/json"));
+
+ String result = HttpUtils.readLimitedResponseBody(responseBody, 1024);
+
+ Assert.assertEquals(body, result);
+ }
+
+ private static ResponseBody responseBody(final byte[] bytes, final long
contentLength,
+ final MediaType mediaType) {
+ return new ResponseBody() {
+ @Override
+ public MediaType contentType() {
+ return mediaType;
+ }
+
+ @Override
+ public long contentLength() {
+ return contentLength;
+ }
+
+ @Override
+ public BufferedSource source() {
+ return new Buffer().write(bytes);
+ }
+ };
+ }
+
@Test
public void downloadFileMethodTest() throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);