This is an automated email from the ASF dual-hosted git repository.
liuhongyu 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 aca751ae9a [type:feature] Add Swagger Import Functionality to ShenYu
Admin (#6050)
aca751ae9a is described below
commit aca751ae9ae1e075c66d5441956aff09da3efd53
Author: Jesen Kwan <[email protected]>
AuthorDate: Sun Jul 20 09:58:53 2025 +0800
[type:feature] Add Swagger Import Functionality to ShenYu Admin (#6050)
* feature (admin) : add swagger import functionality with support for
swagger 2.0 and openapi 3.0.
(cherry picked from commit 6cb050722cd85698ec5f62326fbec1fd802cd5ac)
* refactor(admin): Refactor Swagger-related code
- Add SwaggerVersion enum import in SwaggerDocParser
- Update test case descriptions in SwaggerImportServiceTest
- Move SwaggerVersion enum from the admin module to the common module
* refactor(admin): Optimize Swagger document parsing and import logic
- Refactor the property handling logic in SwaggerDocParser to improve code
readability and efficiency
- Optimize HTTP request handling in SwaggerImportServiceImpl to enhance
code flexibility
- Remove unnecessary static HttpUtils instances to reduce resource
consumption
- Adjust code formatting and indentation to improve code cleanliness
* refactor(admin): Optimize Swagger import-related code
- Use Objects.isNull() instead of direct equality checks to enhance code
readability and safety
- Improve the toString method of the SwaggerImportRequest class
- Remove unused imports and optimize parts of the code structure
* refactor(admin): Refactor Swagger document import functionality
- Extract base path method to support Swagger 2.0 and OpenAPI 3.0
- Optimize HTTP request handling, use Spring Bean to manage HttpUtils
- Improve log output, add document MD5 information
- Refactor code structure to enhance maintainability and testability
* feat(admin): Add URL security checks to prevent SSRF attacks
- Added UrlSecurityUtils utility class for URL security validation
- Integrated URL security checks into the Swagger import feature
- Implemented comprehensive validation for URL format, protocol, host, IP
address, and port
- Effectively prevents SSRF (Server-Side Request Forgery) and other
URL-based attacks
* feat(build): Update static resource version
- Update CSS file references in index.html
- Update JavaScript file references in index.html
* refactor(admin): Refactor Swagger import functionality and URL security
utility class
- Optimized the code structure of the SwaggerImportServiceImpl class to
improve code readability
- Refactored the UrlSecurityUtils class to enhance URL security check
functionality
- Adjusted the exception handling method to make error messages clearer
- Removed unused import statements to streamline the code
---------
Co-authored-by: aias00 <[email protected]>
---
.../admin/config/HttpUtilsConfiguration.java | 41 ++++
.../admin/controller/SwaggerImportController.java | 91 +++++++++
.../admin/model/dto/SwaggerImportRequest.java | 70 +++++++
.../shenyu/admin/service/SwaggerImportService.java | 42 ++++
.../service/impl/SwaggerImportServiceImpl.java | 151 ++++++++++++++
.../service/manager/impl/SwaggerDocParser.java | 223 +++++++++++++++++----
.../shenyu/admin/utils/UrlSecurityUtils.java | 217 ++++++++++++++++++++
.../{index.2a428c0d.css => index.7892d888.css} | 4 +-
.../src/main/resources/static/index.c72b2a38.js | 1 +
.../src/main/resources/static/index.db384a79.js | 1 -
shenyu-admin/src/main/resources/static/index.html | 4 +-
.../admin/service/SwaggerImportServiceTest.java | 54 +++++
.../apache/shenyu/common/enums/SwaggerVersion.java | 49 +++++
13 files changed, 907 insertions(+), 41 deletions(-)
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/HttpUtilsConfiguration.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/HttpUtilsConfiguration.java
new file mode 100644
index 0000000000..e4af4bce8a
--- /dev/null
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/HttpUtilsConfiguration.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.config;
+
+import org.apache.shenyu.admin.utils.HttpUtils;
+import
org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * HTTP utilities configuration.
+ */
+@Configuration
+public class HttpUtilsConfiguration {
+
+ /**
+ * Configure HttpUtils as a Spring Bean.
+ *
+ * @return HttpUtils instance
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ public HttpUtils httpUtils() {
+ return new HttpUtils();
+ }
+}
\ No newline at end of file
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/SwaggerImportController.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/SwaggerImportController.java
new file mode 100644
index 0000000000..c1fc306e10
--- /dev/null
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/SwaggerImportController.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.controller;
+
+import jakarta.validation.Valid;
+import org.apache.shenyu.admin.model.result.ShenyuAdminResult;
+import org.apache.shenyu.admin.model.dto.SwaggerImportRequest;
+import org.apache.shenyu.admin.service.SwaggerImportService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Swagger Import Controller.
+ */
+@RestController
+@RequestMapping("/swagger")
+@Validated
+public class SwaggerImportController {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(SwaggerImportController.class);
+
+ private final SwaggerImportService swaggerImportService;
+
+ public SwaggerImportController(final SwaggerImportService
swaggerImportService) {
+ this.swaggerImportService = swaggerImportService;
+ }
+
+ /**
+ * Import swagger documentation.
+ *
+ * @param request the swagger import request
+ * @return the result of swagger import
+ */
+ @PostMapping("/import")
+ public ShenyuAdminResult importSwagger(@Valid @RequestBody final
SwaggerImportRequest request) {
+ LOG.info("Received Swagger import request: {}", request);
+
+ try {
+ String result = swaggerImportService.importSwagger(request);
+ return ShenyuAdminResult.success(result);
+
+ } catch (Exception e) {
+ LOG.error("Failed to import swagger document", e);
+
+ return ShenyuAdminResult.error("Import failed: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Test connection to swagger URL.
+ *
+ * @param swaggerUrl the swagger URL to test
+ * @return the result of connection test
+ */
+ @PostMapping("/test-connection")
+ public ShenyuAdminResult testConnection(@RequestParam final String
swaggerUrl) {
+ LOG.info("Testing Swagger URL connection: {}", swaggerUrl);
+
+ try {
+ boolean isConnected =
swaggerImportService.testConnection(swaggerUrl);
+
+ return ShenyuAdminResult.success(isConnected ? "Connection
successful" : "Connection failed");
+
+ } catch (Exception e) {
+ LOG.error("Failed to test connection", e);
+
+ return ShenyuAdminResult.error("Connection failed: " +
e.getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/SwaggerImportRequest.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/SwaggerImportRequest.java
new file mode 100644
index 0000000000..4e232234c7
--- /dev/null
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/SwaggerImportRequest.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.model.dto;
+
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+
+/**
+ * Swagger Import Request.
+ */
+public class SwaggerImportRequest {
+
+ @NotBlank(message = "swagger URL cannot be empty")
+ @Pattern(regexp = "^https?://.*", message = "swagger URL must be a valid
HTTP/HTTPS address")
+ private String swaggerUrl;
+
+ @NotBlank(message = "project name cannot be empty")
+ private String projectName;
+
+ private String projectDescription;
+
+ public String getSwaggerUrl() {
+ return swaggerUrl;
+ }
+
+ public void setSwaggerUrl(final String swaggerUrl) {
+ this.swaggerUrl = swaggerUrl;
+ }
+
+ public String getProjectName() {
+ return projectName;
+ }
+
+ public void setProjectName(final String projectName) {
+ this.projectName = projectName;
+ }
+
+ public String getProjectDescription() {
+ return projectDescription;
+ }
+
+ public void setProjectDescription(final String projectDescription) {
+ this.projectDescription = projectDescription;
+ }
+
+ @Override
+ public String toString() {
+ return "SwaggerImportRequest{"
+ + "swaggerUrl='" + swaggerUrl + '\''
+ + ", projectName='" + projectName + '\''
+ + ", projectDescription='" + projectDescription + '\''
+ + '}';
+ }
+}
\ No newline at end of file
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/SwaggerImportService.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/SwaggerImportService.java
new file mode 100644
index 0000000000..bc18afd805
--- /dev/null
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/SwaggerImportService.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.service;
+
+import org.apache.shenyu.admin.model.dto.SwaggerImportRequest;
+
+/**
+ * Swagger Import Service.
+ */
+public interface SwaggerImportService {
+
+ /**
+ * Import swagger documentation.
+ *
+ * @param request swagger import request
+ * @return import result message
+ */
+ String importSwagger(SwaggerImportRequest request);
+
+ /**
+ * Test connection to swagger URL.
+ *
+ * @param swaggerUrl swagger URL to test
+ * @return true if connection is successful, false otherwise
+ */
+ boolean testConnection(String swaggerUrl);
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000..f2dda3307f
--- /dev/null
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImpl.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.service.impl;
+
+import com.google.gson.JsonObject;
+import okhttp3.Response;
+import org.apache.shenyu.admin.model.bean.UpstreamInstance;
+import org.apache.shenyu.admin.model.dto.SwaggerImportRequest;
+import org.apache.shenyu.admin.service.SwaggerImportService;
+import org.apache.shenyu.admin.service.manager.DocManager;
+import org.apache.shenyu.admin.utils.HttpUtils;
+import org.apache.shenyu.admin.utils.UrlSecurityUtils;
+import org.apache.shenyu.common.utils.GsonUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Collections;
+
+/**
+ * Implementation of the {@link
org.apache.shenyu.admin.service.SwaggerImportService}.
+ */
+@Service
+public class SwaggerImportServiceImpl implements SwaggerImportService {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(SwaggerImportServiceImpl.class);
+
+ private final DocManager docManager;
+
+ private final HttpUtils httpUtils;
+
+ public SwaggerImportServiceImpl(final DocManager docManager, final
HttpUtils httpUtils) {
+ this.docManager = docManager;
+ this.httpUtils = httpUtils;
+ }
+
+ @Override
+ public String importSwagger(final SwaggerImportRequest request) {
+ LOG.info("Start importing Swagger document: {}", request);
+
+ try {
+ // 1. Validate URL
+ validateSwaggerUrl(request.getSwaggerUrl());
+
+ // 2. Get swagger document
+ String swaggerJson = fetchSwaggerDoc(request.getSwaggerUrl());
+
+ // 3. Validate Swagger content and version
+ validateSwaggerContent(swaggerJson);
+
+ // 4. Create virtual instance
+ UpstreamInstance instance = createVirtualInstance(request);
+
+ // 5. Parse and save document
+ docManager.addDocInfo(instance, swaggerJson, null, docInfo -> {
+ LOG.info("Successfully imported swagger document: {} with MD5:
{}",
+ request.getProjectName(), docInfo.getDocMd5());
+ });
+
+ return "Import successful, supports Swagger 2.0 and OpenAPI 3.0
formats";
+
+ } catch (Exception e) {
+ LOG.error("Failed to import swagger document: {}",
request.getProjectName(), e);
+ throw new RuntimeException("Import failed: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public boolean testConnection(final String swaggerUrl) {
+ try {
+ validateSwaggerUrl(swaggerUrl);
+ try (Response response = httpUtils.requestForResponse(swaggerUrl,
+ Collections.emptyMap(), Collections.emptyMap(),
HttpUtils.HTTPMethod.GET)) {
+ return response.code() == 200;
+ }
+ } catch (Exception e) {
+ LOG.warn("Failed to test Swagger URL connection: {}", swaggerUrl,
e);
+ return false;
+ }
+ }
+
+ private void validateSwaggerUrl(final String swaggerUrl) {
+ // Use UrlSecurityUtils for SSRF protection
+ UrlSecurityUtils.validateUrlForSSRF(swaggerUrl);
+ }
+
+ private String fetchSwaggerDoc(final String swaggerUrl) throws IOException
{
+ try (Response response = httpUtils.requestForResponse(swaggerUrl,
+ Collections.emptyMap(), Collections.emptyMap(),
HttpUtils.HTTPMethod.GET)) {
+
+ if (response.code() != 200) {
+ throw new RuntimeException("Failed to get Swagger document,
HTTP status code: " + response.code());
+ }
+
+ return response.body().string();
+ }
+ }
+
+ private void validateSwaggerContent(final String swaggerJson) {
+ try {
+ JsonObject docRoot = GsonUtils.getInstance().fromJson(swaggerJson,
JsonObject.class);
+
+ // Detect version
+ boolean isV2 = docRoot.has("swagger") &&
docRoot.get("swagger").getAsString().startsWith("2.");
+ boolean isV3 = docRoot.has("openapi") &&
docRoot.get("openapi").getAsString().startsWith("3.");
+
+ if (!isV2 && !isV3) {
+ throw new IllegalArgumentException("Unsupported Swagger
version, only Swagger 2.0 and OpenAPI 3.0 formats are supported");
+ }
+
+ LOG.info("Detected Swagger version: {}", isV2 ? "2.0" : "3.0");
+
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Invalid Swagger JSON format: "
+ e.getMessage());
+ }
+ }
+
+ private UpstreamInstance createVirtualInstance(final SwaggerImportRequest
request) {
+ UpstreamInstance instance = new UpstreamInstance();
+ instance.setContextPath(request.getProjectName());
+
+ // Try to parse IP and port from URL
+ try {
+ URL url = new URL(request.getSwaggerUrl());
+ instance.setIp(url.getHost());
+ instance.setPort(url.getPort() == -1 ?
(url.getProtocol().equals("https") ? 443 : 80) : url.getPort());
+ } catch (Exception e) {
+ instance.setIp("unknown");
+ instance.setPort(80);
+ }
+
+ return instance;
+ }
+}
\ No newline at end of file
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/impl/SwaggerDocParser.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/impl/SwaggerDocParser.java
index fc8bbf97d0..5f82f92164 100755
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/impl/SwaggerDocParser.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/impl/SwaggerDocParser.java
@@ -30,8 +30,10 @@ import org.apache.shenyu.admin.model.bean.DocItem;
import org.apache.shenyu.admin.model.bean.DocModule;
import org.apache.shenyu.admin.model.bean.DocParameter;
import org.apache.shenyu.admin.service.manager.DocParser;
+import org.apache.shenyu.common.enums.SwaggerVersion;
import org.apache.shenyu.common.utils.GsonUtils;
+import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -56,7 +58,10 @@ public class SwaggerDocParser implements DocParser {
*/
@Override
public DocInfo parseJson(final JsonObject docRoot) {
- final String basePath = docRoot.get("basePath").getAsString();
+ // Detect Swagger version
+ SwaggerVersion version = detectSwaggerVersion(docRoot);
+
+ final String basePath = extractBasePath(docRoot, version);
final String title =
Optional.ofNullable(docRoot.getAsJsonObject("info")).map(jsonObject ->
jsonObject.get("title").getAsString()).orElse(basePath);
final List<DocItem> docItems = new ArrayList<>();
@@ -74,7 +79,7 @@ public class SwaggerDocParser implements DocParser {
JsonObject docInfo = pathInfo.getAsJsonObject(method);
docInfo.addProperty("real_req_path", apiPath);
docInfo.addProperty("basePath", basePath);
- DocItem docItem = buildDocItem(docInfo, docRoot);
+ DocItem docItem = buildDocItem(docInfo, docRoot, version);
if (Objects.isNull(docItem)) {
continue;
}
@@ -126,7 +131,7 @@ public class SwaggerDocParser implements DocParser {
return retList;
}
- protected DocItem buildDocItem(final JsonObject docInfo, final JsonObject
docRoot) {
+ protected DocItem buildDocItem(final JsonObject docInfo, final JsonObject
docRoot, final SwaggerVersion version) {
String apiName = docInfo.get("real_req_path").getAsString();
String basePath = docInfo.get("basePath").getAsString();
apiName = basePath + apiName;
@@ -158,8 +163,8 @@ public class SwaggerDocParser implements DocParser {
}
String moduleName = this.buildModuleName(docInfo, docRoot, basePath);
docItem.setModule(moduleName);
- this.buildRequestParameterList(docItem, docInfo, docRoot);
- List<DocParameter> responseParameterList =
this.buildResponseParameterList(docInfo, docRoot);
+ this.buildRequestParameterList(docItem, docInfo, docRoot, version);
+ List<DocParameter> responseParameterList =
this.buildResponseParameterList(docInfo, docRoot, version);
docItem.setResponseParameters(responseParameterList);
return docItem;
}
@@ -173,18 +178,20 @@ public class SwaggerDocParser implements DocParser {
}
protected void buildRequestParameterList(final DocItem docItem, final
JsonObject docInfo,
- final JsonObject docRoot) {
+ final JsonObject docRoot, final
SwaggerVersion version) {
Optional<JsonArray> parametersOptional =
Optional.ofNullable(docInfo.getAsJsonArray("parameters"));
JsonArray parameters = parametersOptional.orElse(new JsonArray());
List<DocParameter> docRequestParameterList = new ArrayList<>();
List<DocParameter> docHeaderParameterList = new ArrayList<>();
+
for (int i = 0; i < parameters.size(); i++) {
JsonObject fieldJson = parameters.get(i).getAsJsonObject();
JsonObject schema = fieldJson.getAsJsonObject("schema");
+
if (Objects.nonNull(schema)) {
- RefInfo refInfo = getRefInfo(schema);
+ RefInfo refInfo = getRefInfo(schema, version);
if (Objects.nonNull(refInfo)) {
- List<DocParameter> parameterList =
this.buildDocParameters(refInfo.ref, docRoot, true);
+ List<DocParameter> parameterList =
this.buildDocParameters(refInfo.ref, docRoot, true, version);
docRequestParameterList.addAll(parameterList);
}
} else {
@@ -228,13 +235,17 @@ public class SwaggerDocParser implements DocParser {
docItem.setRequestHeaders(docHeaderParameterList);
}
- protected List<DocParameter> buildResponseParameterList(final JsonObject
docInfo, final JsonObject docRoot) {
- RefInfo refInfo = getResponseRefInfo(docInfo);
+ protected List<DocParameter> buildResponseParameterList(final JsonObject
docInfo,
+ final JsonObject
docRoot,
+ final SwaggerVersion
version) {
+ RefInfo refInfo = getResponseRefInfo(docInfo, version);
List<DocParameter> respParameterList = Collections.emptyList();
+
if (Objects.nonNull(refInfo)) {
String responseRef = refInfo.ref;
- respParameterList = this.buildDocParameters(responseRef, docRoot,
true);
- // If an array is returned.
+ respParameterList = this.buildDocParameters(responseRef, docRoot,
true, version);
+
+ // If it returns an array
if (refInfo.isArray) {
DocParameter docParameter = new DocParameter();
docParameter.setName("items");
@@ -246,37 +257,48 @@ public class SwaggerDocParser implements DocParser {
return respParameterList;
}
- protected List<DocParameter> buildDocParameters(final String ref, final
JsonObject docRoot, final boolean doSubRef) {
- JsonObject responseObject =
docRoot.getAsJsonObject("components").getAsJsonObject("schemas").getAsJsonObject(ref);
- JsonObject extProperties =
responseObject.getAsJsonObject("properties");
+ protected List<DocParameter> buildDocParameters(final String ref, final
JsonObject docRoot,
+ final boolean doSubRef,
final SwaggerVersion version) {
+ JsonObject schemaDefinitions = getSchemaDefinitions(docRoot, version);
+ if (Objects.isNull(schemaDefinitions)) {
+ return Collections.emptyList();
+ }
+
+ JsonObject responseObject = schemaDefinitions.getAsJsonObject(ref);
+ if (Objects.isNull(responseObject)) {
+ return Collections.emptyList();
+ }
+
+ JsonObject properties = responseObject.getAsJsonObject("properties");
JsonArray requiredProperties =
responseObject.getAsJsonArray("required");
List<String> requiredFieldList =
this.jsonArrayToStringList(requiredProperties);
- JsonObject properties = responseObject.getAsJsonObject("properties");
List<DocParameter> docParameterList = new ArrayList<>();
+
if (Objects.isNull(properties)) {
return docParameterList;
}
+
Set<String> fieldNames = properties.keySet();
for (String fieldName : fieldNames) {
JsonObject fieldInfo = properties.getAsJsonObject(fieldName);
DocParameter docParameter =
GsonUtils.getInstance().fromJson(fieldInfo, DocParameter.class);
docParameter.setName(fieldName);
docParameter.setRequired(requiredFieldList.contains(fieldName));
- if (Objects.nonNull(extProperties)) {
- JsonObject prop = extProperties.getAsJsonObject(fieldName);
- if (Objects.nonNull(prop)) {
-
docParameter.setMaxLength(Objects.isNull(prop.get("maxLength")) ? "-" :
prop.get("maxLength").getAsString());
- if (Objects.nonNull(prop.get("required"))) {
-
docParameter.setRequired(Boolean.parseBoolean(prop.get("required").getAsString()));
- }
+
+ JsonObject prop = properties.getAsJsonObject(fieldName);
+ if (Objects.nonNull(prop)) {
+
docParameter.setMaxLength(Objects.isNull(prop.get("maxLength")) ? "-" :
prop.get("maxLength").getAsString());
+ if (Objects.nonNull(prop.get("required"))) {
+
docParameter.setRequired(Boolean.parseBoolean(prop.get("required").getAsString()));
}
}
+
docParameterList.add(docParameter);
- RefInfo refInfo = this.getRefInfo(fieldInfo);
+ RefInfo refInfo = this.getRefInfo(fieldInfo, version);
if (Objects.nonNull(refInfo) && doSubRef) {
String subRef = refInfo.ref;
boolean nextDoRef = !Objects.equals(ref, subRef);
- List<DocParameter> refs = buildDocParameters(subRef, docRoot,
nextDoRef);
+ List<DocParameter> refs = buildDocParameters(subRef, docRoot,
nextDoRef, version);
docParameter.setRefs(refs);
}
}
@@ -302,39 +324,168 @@ public class SwaggerDocParser implements DocParser {
* Simple object return, pure array return.
*
* @param docInfo docInfo
+ * @param version version
* @return RefInfo
*/
- protected RefInfo getResponseRefInfo(final JsonObject docInfo) {
- return Optional.ofNullable(docInfo.getAsJsonObject("responses"))
- .flatMap(jsonObject ->
Optional.ofNullable(jsonObject.getAsJsonObject("200")))
- .flatMap(jsonObject ->
Optional.ofNullable(jsonObject.getAsJsonObject("schema")))
- .map(this::getRefInfo)
- .orElse(null);
+ protected RefInfo getResponseRefInfo(final JsonObject docInfo, final
SwaggerVersion version) {
+ if (version == SwaggerVersion.V2) {
+ // v2: responses/200/schema
+ return Optional.ofNullable(docInfo.getAsJsonObject("responses"))
+ .flatMap(jsonObject ->
Optional.ofNullable(jsonObject.getAsJsonObject("200")))
+ .flatMap(jsonObject ->
Optional.ofNullable(jsonObject.getAsJsonObject("schema")))
+ .map(schema -> this.getRefInfo(schema, version))
+ .orElse(null);
+ } else {
+ // v3: responses/200/content/application/json/schema
+ return Optional.ofNullable(docInfo.getAsJsonObject("responses"))
+ .flatMap(jsonObject ->
Optional.ofNullable(jsonObject.getAsJsonObject("200")))
+ .flatMap(jsonObject ->
Optional.ofNullable(jsonObject.getAsJsonObject("content")))
+ .flatMap(jsonObject ->
Optional.ofNullable(jsonObject.getAsJsonObject("application/json")))
+ .flatMap(jsonObject ->
Optional.ofNullable(jsonObject.getAsJsonObject("schema")))
+ .map(schema -> this.getRefInfo(schema, version))
+ .orElse(null);
+ }
}
- private RefInfo getRefInfo(final JsonObject jsonObject) {
+ private RefInfo getRefInfo(final JsonObject jsonObject, final
SwaggerVersion version) {
JsonElement refElement;
boolean isArray = Objects.nonNull(jsonObject.get("type")) &&
"array".equals(jsonObject.get("type").getAsString());
+
if (isArray) {
refElement = jsonObject.getAsJsonObject("items").get("$ref");
} else {
- // #/definitions/xxx
refElement = jsonObject.get("$ref");
}
+
if (Objects.isNull(refElement)) {
return null;
}
+
String ref = refElement.getAsString();
- int index = ref.lastIndexOf("/");
- if (index > -1) {
- ref = ref.substring(index + 1);
+
+ // Parse reference path
+ if (version == SwaggerVersion.V2) {
+ // v2: #/definitions/ModelName
+ if (ref.startsWith("#/definitions/")) {
+ ref = ref.substring("#/definitions/".length());
+ }
+ } else {
+ // v3: #/components/schemas/ModelName
+ if (ref.startsWith("#/components/schemas/")) {
+ ref = ref.substring("#/components/schemas/".length());
+ }
}
+
RefInfo refInfo = new RefInfo();
refInfo.isArray = isArray;
refInfo.ref = ref;
return refInfo;
}
+ /**
+ * Extract base path based on Swagger version.
+ *
+ * @param docRoot docRoot
+ * @param version version
+ * @return base path
+ */
+ private String extractBasePath(final JsonObject docRoot, final
SwaggerVersion version) {
+ if (version == SwaggerVersion.V2) {
+ // Swagger 2.0: use basePath field
+ return Optional.ofNullable(docRoot.get("basePath"))
+ .map(JsonElement::getAsString)
+ .orElse("/");
+ } else {
+ // OpenAPI 3.0: use servers[0].url
+ return Optional.ofNullable(docRoot.getAsJsonArray("servers"))
+ .filter(servers -> !servers.isEmpty())
+ .map(servers -> servers.get(0).getAsJsonObject())
+ .map(server -> server.get("url"))
+ .map(JsonElement::getAsString)
+ .map(this::extractPathFromUrl)
+ .orElse("/");
+ }
+ }
+
+ /**
+ * Extract path from server URL.
+ * For example: "https://api.example.com/v1" -> "/v1"
+ *
+ * @param url server URL
+ * @return path part of URL
+ */
+ private String extractPathFromUrl(final String url) {
+ if (Objects.isNull(url) || url.trim().isEmpty()) {
+ return "/";
+ }
+
+ try {
+ // Handle relative URLs
+ if (url.startsWith("/")) {
+ return url;
+ }
+
+ // Handle absolute URLs
+ URL parsedUrl = new URL(url);
+ String path = parsedUrl.getPath();
+ return Objects.isNull(path) || path.trim().isEmpty() ? "/" : path;
+ } catch (Exception e) {
+ // If URL parsing fails, try to extract path manually
+ int protocolIndex = url.indexOf("://");
+ if (protocolIndex != -1) {
+ String afterProtocol = url.substring(protocolIndex + 3);
+ int pathIndex = afterProtocol.indexOf("/");
+ if (pathIndex != -1) {
+ return afterProtocol.substring(pathIndex);
+ }
+ }
+ return "/";
+ }
+ }
+
+ /**
+ * Detect Swagger version.
+ *
+ * @param docRoot docRoot
+ * @return SwaggerVersion
+ */
+ private SwaggerVersion detectSwaggerVersion(final JsonObject docRoot) {
+ // Check if openapi field exists (v3)
+ if (docRoot.has("openapi")) {
+ return SwaggerVersion.V3;
+ }
+
+ // Check if swagger field exists with value 2.0 (v2)
+ if (docRoot.has("swagger")) {
+ String swaggerVersion = docRoot.get("swagger").getAsString();
+ if (swaggerVersion.startsWith("2.")) {
+ return SwaggerVersion.V2;
+ }
+ }
+
+ // Default to v3
+ return SwaggerVersion.V3;
+ }
+
+ /**
+ * Get schema definitions by version.
+ *
+ * @param docRoot docRoot
+ * @param version version
+ * @return JsonObject
+ */
+ private JsonObject getSchemaDefinitions(final JsonObject docRoot, final
SwaggerVersion version) {
+ if (version == SwaggerVersion.V2) {
+ return docRoot.getAsJsonObject("definitions");
+ } else {
+ JsonObject components = docRoot.getAsJsonObject("components");
+ if (Objects.isNull(components)) {
+ return null;
+ }
+ return components.getAsJsonObject("schemas");
+ }
+ }
+
private static class RefInfo {
private boolean isArray;
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/UrlSecurityUtils.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/UrlSecurityUtils.java
new file mode 100644
index 0000000000..ef9568f1c0
--- /dev/null
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/UrlSecurityUtils.java
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.utils;
+
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * URL security utilities for preventing SSRF and other URL-based attacks.
+ */
+public final class UrlSecurityUtils {
+
+ /**
+ * Private constructor to prevent instantiation.
+ */
+ private UrlSecurityUtils() {
+ }
+
+ /**
+ * Validate URL to prevent SSRF attacks.
+ *
+ * @param url the URL to validate
+ * @throws IllegalArgumentException if the URL is not safe for external
requests
+ */
+ public static void validateUrlForSSRF(final String url) {
+ if (Objects.isNull(url) || url.trim().isEmpty()) {
+ throw new IllegalArgumentException("URL cannot be empty");
+ }
+
+ try {
+ URL parsedUrl = new URL(url);
+ String protocol = parsedUrl.getProtocol();
+
+ // Only allow HTTP and HTTPS protocols
+ if (!"http".equals(protocol) && !"https".equals(protocol)) {
+ throw new IllegalArgumentException("Only HTTP and HTTPS
protocols are allowed");
+ }
+
+ // Validate host for SSRF protection
+ validateHostForSSRF(parsedUrl.getHost(), parsedUrl.getPort());
+
+ } catch (MalformedURLException e) {
+ throw new IllegalArgumentException("Invalid URL format: " +
e.getMessage());
+ }
+ }
+
+ /**
+ * Validate host to prevent SSRF attacks.
+ *
+ * @param host the host to validate
+ * @param port the port to validate
+ * @throws IllegalArgumentException if the host is not allowed
+ */
+ public static void validateHostForSSRF(final String host, final int port) {
+ if (Objects.isNull(host) || host.trim().isEmpty()) {
+ throw new IllegalArgumentException("Host cannot be empty");
+ }
+
+ String normalizedHost = host.toLowerCase().trim();
+
+ // Check for localhost variations
+ if (isLocalhost(normalizedHost)) {
+ throw new IllegalArgumentException("Access to localhost is not
allowed");
+ }
+
+ // Check for private IP addresses
+ if (isPrivateOrInternalIP(normalizedHost)) {
+ throw new IllegalArgumentException("Access to private or internal
IP addresses is not allowed");
+ }
+
+ // Check for sensitive ports
+ if (isSensitivePort(port)) {
+ throw new IllegalArgumentException("Access to sensitive ports is
not allowed");
+ }
+
+ // Additional validation for DNS resolution
+ try {
+ InetAddress[] addresses = InetAddress.getAllByName(normalizedHost);
+ for (InetAddress address : addresses) {
+ if (address.isLoopbackAddress() || address.isLinkLocalAddress()
+ || address.isSiteLocalAddress() ||
address.isAnyLocalAddress()) {
+ throw new IllegalArgumentException("Resolved IP address is
not allowed: " + address.getHostAddress());
+ }
+
+ // Check resolved IP against private ranges
+ if (isPrivateIPAddress(address.getHostAddress())) {
+ throw new IllegalArgumentException("Resolved IP address is
private: " + address.getHostAddress());
+ }
+ }
+ } catch (UnknownHostException e) {
+ throw new IllegalArgumentException("Cannot resolve host: " + host);
+ }
+ }
+
+ /**
+ * Check if the host is localhost or localhost variations.
+ *
+ * @param host the host to check
+ * @return true if the host is localhost
+ */
+ private static boolean isLocalhost(final String host) {
+ Set<String> localhostVariations = new HashSet<>(Arrays.asList(
+ "localhost", "127.0.0.1", "::1", "0.0.0.0",
"0000:0000:0000:0000:0000:0000:0000:0001"
+ ));
+ return localhostVariations.contains(host);
+ }
+
+ /**
+ * Check if the host is a private or internal IP address.
+ *
+ * @param host the host to check
+ * @return true if the host is private or internal
+ */
+ private static boolean isPrivateOrInternalIP(final String host) {
+ // Check for IPv4 private ranges
+ if (host.matches("^10\\..*")
+ || host.matches("^172\\.(1[6-9]|2[0-9]|3[0-1])\\..*")
+ || host.matches("^192\\.168\\..*")) {
+ return true;
+ }
+
+ // Check for IPv6 private ranges
+ if (host.startsWith("fc") || host.startsWith("fd")
+ || host.startsWith("fe80") || "::1".equals(host)) {
+ return true;
+ }
+
+ // Check for other internal addresses
+ if (host.matches("^169\\.254\\..*")
+ || host.matches("^224\\..*")
+ || host.matches("^255\\..*")) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if the resolved IP address is private.
+ *
+ * @param ip the IP address to check
+ * @return true if the IP is private
+ */
+ private static boolean isPrivateIPAddress(final String ip) {
+ try {
+ InetAddress address = InetAddress.getByName(ip);
+ return address.isSiteLocalAddress() || address.isLoopbackAddress()
+ || address.isLinkLocalAddress() ||
address.isAnyLocalAddress();
+ } catch (UnknownHostException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Check if the port is sensitive (commonly used for internal services).
+ *
+ * @param port the port to check
+ * @return true if the port is sensitive
+ */
+ private static boolean isSensitivePort(final int port) {
+ if (port == -1) {
+ // Default port, will be 80 or 443
+ return false;
+ }
+
+ // Common sensitive ports
+ Set<Integer> sensitivePorts = new HashSet<>(Arrays.asList(
+ /* SSH and remote access */
+ 22, 23, 3389,
+ /* Email services */
+ 25,
+ /* DNS */
+ 53,
+ /* Windows services */
+ 135, 139, 445,
+ /* Database services */
+ 5432, 3306, 1433,
+ /* Cache services */
+ 6379, 11211,
+ /* Search and storage */
+ 5984, 9200,
+ /* Middleware */
+ 2181, 9092, 9093,
+ /* Common internal web services */
+ 8080, 8081, 9090, 9091,
+ /* Container services */
+ 2375, 2376,
+ /* Message queue */
+ 25672, 5672, 15672,
+ /* Other services */
+ 4369
+ ));
+
+ return sensitivePorts.contains(port);
+ }
+}
\ No newline at end of file
diff --git a/shenyu-admin/src/main/resources/static/index.2a428c0d.css
b/shenyu-admin/src/main/resources/static/index.7892d888.css
similarity index 76%
rename from shenyu-admin/src/main/resources/static/index.2a428c0d.css
rename to shenyu-admin/src/main/resources/static/index.7892d888.css
index a445febe77..81ee083ee0 100644
--- a/shenyu-admin/src/main/resources/static/index.2a428c0d.css
+++ b/shenyu-admin/src/main/resources/static/index.7892d888.css
@@ -1,5 +1,5 @@
-#root,body,html{height:100%}.plug-content-wrap{padding:24px}.open{color:#14c974}.close{color:#ff586d}.optionParts{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;gap:16px}.ant-layout{min-height:100%}ol,ul{list-style:none}.ant-table{background:#fff}.table-selected{background:#98cdff}.edit{cursor:pointer}.edit,.edit:hover{color:#1890ff}.searchblock{display:-ms-flexbox!important;display:flex!important}.searchblock
button{margin-left:30px}.ant-table table{padding [...]
- /*! autoprefixer: ignore next
*/-webkit-box-orient:vertical;overflow:hidden}.main___i2kiy{width:100%;height:100%;padding:24px}.header___1eXCR,.main___i2kiy{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.header___1eXCR
.titleBar___1m0IH{width:100%;display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between;-ms-flex-align:end;align-items:end;-ms-flex:1
1;flex:1 1}.header___1eXCR .titleBar___1m0IH .left___HAYeJ{display:-ms-flexbo
[...]
+#root,body,html{height:100%}.plug-content-wrap{padding:24px}.open{color:#14c974}.close{color:#ff586d}.optionParts{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;gap:16px}.ant-layout{min-height:100%}ol,ul{list-style:none}.ant-table{background:#fff}.table-selected{background:#98cdff}.edit{cursor:pointer}.edit,.edit:hover{color:#1890ff}.searchblock{display:-ms-flexbox!important;display:flex!important}.searchblock
button{margin-left:30px}.ant-table table{padding [...]
+ /*! autoprefixer: ignore next
*/-webkit-box-orient:vertical;overflow:hidden}.headerSearch___2Y3tE,.layout___1o3Ic{display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between}.headerSearch___2Y3tE,.headerSearch___2Y3tE
.search___3wHRQ{-ms-flex-align:center;align-items:center}.headerSearch___2Y3tE
.search___3wHRQ{margin-right:10px;display:-ms-flexbox;display:flex}.marginLeft10___p90P_{margin-left:10px}.condition___2uVb3,.springCloud___lnMsj{margin-top:8px}.condit
[...]
*
* antd v3.26.20
*
diff --git a/shenyu-admin/src/main/resources/static/index.c72b2a38.js
b/shenyu-admin/src/main/resources/static/index.c72b2a38.js
new file mode 100644
index 0000000000..fad8eb358c
--- /dev/null
+++ b/shenyu-admin/src/main/resources/static/index.c72b2a38.js
@@ -0,0 +1 @@
+!function(e){function t(r){if(n[r])return n[r].exports;var
o=n[r]={i:r,l:!1,exports:{}};return
e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var
n={};t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var
n=e&&e.__esModule?function(){return e.default}:function(){return e};return
t.d(n,"a",n),n},t.o=function(e,t){return
Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s="lVK7")}({"+0it":function(e,t,n){"us
[...]
\ No newline at end of file
diff --git a/shenyu-admin/src/main/resources/static/index.db384a79.js
b/shenyu-admin/src/main/resources/static/index.db384a79.js
deleted file mode 100644
index ac14c53528..0000000000
--- a/shenyu-admin/src/main/resources/static/index.db384a79.js
+++ /dev/null
@@ -1 +0,0 @@
-!function(e){function t(r){if(n[r])return n[r].exports;var
o=n[r]={i:r,l:!1,exports:{}};return
e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var
n={};t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var
n=e&&e.__esModule?function(){return e.default}:function(){return e};return
t.d(n,"a",n),n},t.o=function(e,t){return
Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s="lVK7")}({"+0it":function(e,t,n){"us
[...]
\ No newline at end of file
diff --git a/shenyu-admin/src/main/resources/static/index.html
b/shenyu-admin/src/main/resources/static/index.html
index 381e123bda..5319193245 100644
--- a/shenyu-admin/src/main/resources/static/index.html
+++ b/shenyu-admin/src/main/resources/static/index.html
@@ -24,11 +24,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Apache ShenYu Gateway</title>
<link rel="icon" href="favicon.ico" type="image/x-icon">
-<link href="index.2a428c0d.css" rel="stylesheet"></head>
+<link href="index.7892d888.css" rel="stylesheet"></head>
<body>
<div id="httpPath" style="display: none" th:text="${domain}"></div>
<div id="root"></div>
-<script type="text/javascript" src="index.db384a79.js"></script></body>
+<script type="text/javascript" src="index.c72b2a38.js"></script></body>
</html>
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/SwaggerImportServiceTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/SwaggerImportServiceTest.java
new file mode 100644
index 0000000000..28d929700c
--- /dev/null
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/SwaggerImportServiceTest.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.service;
+
+import org.apache.shenyu.admin.service.impl.SwaggerImportServiceImpl;
+import org.apache.shenyu.admin.utils.HttpUtils;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.apache.shenyu.admin.service.manager.DocManager;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+/**
+ * Test for SwaggerImportService.
+ */
+@ExtendWith(MockitoExtension.class)
+public class SwaggerImportServiceTest {
+
+ @Mock
+ private DocManager docManager;
+
+ @Mock
+ private HttpUtils httpUtils;
+
+ @Test
+ public void testConnection() {
+ SwaggerImportService service = new
SwaggerImportServiceImpl(docManager, httpUtils);
+
+ // Test invalid URLs
+ assertFalse(service.testConnection("invalid-url"));
+ assertFalse(service.testConnection(""));
+ assertFalse(service.testConnection(null));
+
+ // Test valid URL format but may fail to connect
+
assertFalse(service.testConnection("http://invalid.example.com/swagger.json"));
+ }
+}
\ No newline at end of file
diff --git
a/shenyu-common/src/main/java/org/apache/shenyu/common/enums/SwaggerVersion.java
b/shenyu-common/src/main/java/org/apache/shenyu/common/enums/SwaggerVersion.java
new file mode 100644
index 0000000000..8f97aac858
--- /dev/null
+++
b/shenyu-common/src/main/java/org/apache/shenyu/common/enums/SwaggerVersion.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.common.enums;
+
+/**
+ * Swagger version enumeration.
+ */
+public enum SwaggerVersion {
+
+ /**
+ * Swagger 2.0.
+ */
+ V2("2.0"),
+
+ /**
+ * OpenAPI 3.0.
+ */
+ V3("3.0");
+
+ private final String version;
+
+ SwaggerVersion(final String version) {
+ this.version = version;
+ }
+
+ /**
+ * get version.
+ *
+ * @return version
+ */
+ public String getVersion() {
+ return version;
+ }
+}
\ No newline at end of file