This is an automated email from the ASF dual-hosted git repository.

Aias00 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 620dcb1102 fix(admin): allow application ports and pin Swagger import 
DNS (#7868)
620dcb1102 is described below

commit 620dcb11022849491c3121309bd99f1bd509c95c
Author: redyyang <[email protected]>
AuthorDate: Fri Oct 23 10:07:11 2026 +0800

    fix(admin): allow application ports and pin Swagger import DNS (#7868)
    
    * fix(admin): allow application ports and pin Swagger import DNS
    
    * fix(admin): validate IPv4 ranges in mapped IPv6 DNS answers
    
    Apply the same IPv4 address policy when a resolver preserves ::ffff:a.b.c.d 
as an explicit Inet6Address. Regression cases reject internal mapped answers in 
mixed DNS results and retain public addresses at range boundaries.
    
    * test(ci): align cryptor failures and wait for usable SDK routes
---
 .../admin/config/HttpUtilsConfiguration.java       |   3 +
 .../config/properties/SwaggerImportProperties.java |  78 +++++++
 .../service/impl/SwaggerImportServiceImpl.java     |  34 ++--
 .../org/apache/shenyu/admin/utils/HttpUtils.java   |  23 +++
 .../shenyu/admin/utils/UrlSecurityUtils.java       | 225 +++++++--------------
 shenyu-admin/src/main/resources/application.yml    |   6 +
 .../properties/SwaggerImportPropertiesTest.java    |  76 +++++++
 .../service/impl/SwaggerImportServiceImplTest.java |  51 ++++-
 .../apache/shenyu/admin/utils/HttpUtilsTest.java   |   6 +-
 .../shenyu/admin/utils/UrlSecurityUtilsTest.java   | 120 +++++++++++
 .../admin/utils/ValidatedHttpRequestTest.java      | 148 ++++++++++++++
 11 files changed, 596 insertions(+), 174 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
index 6b5990eb2c..69575d7fd2 100644
--- 
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
@@ -18,7 +18,9 @@
 package org.apache.shenyu.admin.config;
 
 import org.apache.shenyu.admin.utils.HttpUtils;
+import org.apache.shenyu.admin.config.properties.SwaggerImportProperties;
 import 
org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import 
org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
@@ -26,6 +28,7 @@ import org.springframework.context.annotation.Configuration;
  * HTTP utilities configuration.
  */
 @Configuration
+@EnableConfigurationProperties(SwaggerImportProperties.class)
 public class HttpUtilsConfiguration {
 
     /**
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/properties/SwaggerImportProperties.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/properties/SwaggerImportProperties.java
new file mode 100644
index 0000000000..ab17bb5cf9
--- /dev/null
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/properties/SwaggerImportProperties.java
@@ -0,0 +1,78 @@
+/*
+ * 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.properties;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Port policy for Swagger import, MCP import and connection testing.
+ * This policy never permits private or internal destination addresses.
+ */
+@ConfigurationProperties(prefix = "shenyu.swagger")
+public class SwaggerImportProperties {
+
+    private Set<Integer> allowedPorts = Collections.emptySet();
+
+    private Set<Integer> deniedPorts = Set.of(22, 23, 3389, 25, 53, 135, 139, 
445,
+            5432, 3306, 1433, 6379, 11211, 5984, 9200, 2181, 9092, 9093,
+            2375, 2376, 25672, 5672, 15672, 4369);
+
+    /**
+     * Get allowed ports. An empty set permits any port not denied.
+     * @return allowed ports
+     */
+    public Set<Integer> getAllowedPorts() {
+        return allowedPorts;
+    }
+
+    /**
+     * Set allowed ports. Denied ports take precedence.
+     * @param allowedPorts allowed ports
+     */
+    public void setAllowedPorts(final Set<Integer> allowedPorts) {
+        this.allowedPorts = validatedPorts(allowedPorts);
+    }
+
+    /**
+     * Get denied ports.
+     * @return denied ports
+     */
+    public Set<Integer> getDeniedPorts() {
+        return deniedPorts;
+    }
+
+    /**
+     * Replace denied ports. An empty set disables only the port deny list.
+     * @param deniedPorts denied ports
+     */
+    public void setDeniedPorts(final Set<Integer> deniedPorts) {
+        this.deniedPorts = validatedPorts(deniedPorts);
+    }
+
+    private Set<Integer> validatedPorts(final Set<Integer> ports) {
+        if (Objects.isNull(ports) || ports.stream().anyMatch(port -> 
Objects.isNull(port) || port < 1 || port > 65535)) {
+            throw new IllegalArgumentException("Swagger ports must be between 
1 and 65535");
+        }
+        return Collections.unmodifiableSet(new HashSet<>(ports));
+    }
+}
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 d1ccd07b80..69e9bde1f2 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,9 +26,11 @@ 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.Dns;
 import org.apache.commons.lang3.StringUtils;
 
 import org.apache.shenyu.admin.model.bean.UpstreamInstance;
+import org.apache.shenyu.admin.config.properties.SwaggerImportProperties;
 import org.apache.shenyu.admin.model.dto.SwaggerImportRequest;
 import org.apache.shenyu.admin.service.SwaggerImportService;
 import org.apache.shenyu.admin.service.manager.DocManager;
@@ -45,6 +47,7 @@ import 
org.apache.shenyu.register.common.dto.MetaDataRegisterDTO;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
@@ -74,6 +77,8 @@ public class SwaggerImportServiceImpl implements 
SwaggerImportService {
 
     private final HttpUtils httpUtils;
 
+    private final SwaggerImportProperties swaggerImportProperties;
+
     @Value("${shenyu.swagger.max-body-size:10485760}")
     private long maxSwaggerBodySize;
 
@@ -81,8 +86,14 @@ public class SwaggerImportServiceImpl implements 
SwaggerImportService {
     private ShenyuClientRegisterMcpServiceImpl shenyuClientRegisterMcpService;
 
     public SwaggerImportServiceImpl(final DocManager docManager, final 
HttpUtils httpUtils) {
+        this(docManager, httpUtils, new SwaggerImportProperties());
+    }
+
+    @Autowired
+    public SwaggerImportServiceImpl(final DocManager docManager, final 
HttpUtils httpUtils, final SwaggerImportProperties swaggerImportProperties) {
         this.docManager = docManager;
         this.httpUtils = httpUtils;
+        this.swaggerImportProperties = swaggerImportProperties;
     }
     
     @Override
@@ -91,10 +102,10 @@ public class SwaggerImportServiceImpl implements 
SwaggerImportService {
         
         try {
             // 1. Validate URL
-            validateSwaggerUrl(request.getSwaggerUrl());
+            Dns validatedDns = validateSwaggerUrl(request.getSwaggerUrl());
             
             // 2. Get swagger document
-            String swaggerJson = fetchSwaggerDoc(request.getSwaggerUrl());
+            String swaggerJson = fetchSwaggerDoc(request.getSwaggerUrl(), 
validatedDns);
             
             // 3. Validate Swagger content and version
             validateSwaggerContent(swaggerJson);
@@ -125,9 +136,9 @@ public class SwaggerImportServiceImpl implements 
SwaggerImportService {
         LOG.info("Start importing Mcp config: {}", request);
 
         try {
-            validateSwaggerUrl(request.getSwaggerUrl());
+            Dns validatedDns = validateSwaggerUrl(request.getSwaggerUrl());
 
-            String swaggerJson = fetchSwaggerDoc(request.getSwaggerUrl());
+            String swaggerJson = fetchSwaggerDoc(request.getSwaggerUrl(), 
validatedDns);
 
             String namespaceId = 
StringUtils.defaultIfEmpty(request.getNamespaceId(), SYS_DEFAULT_NAMESPACE_ID);
             List<McpToolsRegisterDTO> mcpToolsRegisterDTOList = 
buildMcpToolRegisterDTO(swaggerJson, namespaceId);
@@ -253,9 +264,8 @@ public class SwaggerImportServiceImpl implements 
SwaggerImportService {
     @Override
     public boolean testConnection(final String swaggerUrl) {
         try {
-            validateSwaggerUrl(swaggerUrl);
-            try (Response response = httpUtils.requestForResponse(swaggerUrl, 
-                    Collections.emptyMap(), Collections.emptyMap(), 
HttpUtils.HTTPMethod.GET, false)) {
+            Dns validatedDns = validateSwaggerUrl(swaggerUrl);
+            try (Response response = 
httpUtils.requestForValidatedResponse(swaggerUrl, validatedDns)) {
                 return response.code() == 200;
             }
         } catch (Exception e) {
@@ -264,14 +274,12 @@ public class SwaggerImportServiceImpl implements 
SwaggerImportService {
         }
     }
     
-    private void validateSwaggerUrl(final String swaggerUrl) {
-        // Use UrlSecurityUtils for SSRF protection
-        UrlSecurityUtils.validateUrlForSSRF(swaggerUrl);
+    private Dns validateSwaggerUrl(final String swaggerUrl) {
+        return UrlSecurityUtils.validatedDns(swaggerUrl, 
swaggerImportProperties, Dns.SYSTEM);
     }
     
-    private String fetchSwaggerDoc(final String swaggerUrl) throws IOException 
{
-        try (Response response = httpUtils.requestForResponse(swaggerUrl,
-                Collections.emptyMap(), Collections.emptyMap(), 
HttpUtils.HTTPMethod.GET, false)) {
+    private String fetchSwaggerDoc(final String swaggerUrl, final Dns 
validatedDns) throws IOException {
+        try (Response response = 
httpUtils.requestForValidatedResponse(swaggerUrl, validatedDns)) {
             
             if (response.code() != 200) {
                 throw new RuntimeException("Failed to get Swagger document, 
HTTP status code: " + response.code());
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 977b7ec9a7..5bb1a8a370 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
@@ -18,6 +18,8 @@
 package org.apache.shenyu.admin.utils;
 
 import okhttp3.Cookie;
+import okhttp3.ConnectionPool;
+import okhttp3.Dns;
 import okhttp3.CookieJar;
 import okhttp3.FormBody;
 import okhttp3.HttpUrl;
@@ -43,6 +45,7 @@ import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.Serializable;
+import java.net.Proxy;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
@@ -356,6 +359,26 @@ public class HttpUtils {
             .execute();
     }
 
+    /**
+     * Fetch an external document using DNS returned by {@link 
UrlSecurityUtils#validatedDns}.
+     * The URL must be the same URL that was validated. Isolate connections 
from the ordinary
+     * client, bypass proxies and reject redirects so only validated 
destinations are used.
+     * @param url validated URL
+     * @param validatedDns pinned DNS for the validated URL
+     * @return response which the caller must close
+     * @throws IOException if the request fails
+     */
+    public Response requestForValidatedResponse(final String url, final Dns 
validatedDns) throws IOException {
+        OkHttpClient client = httpClient.newBuilder()
+                .dns(validatedDns)
+                .proxy(Proxy.NO_PROXY)
+                .connectionPool(new ConnectionPool(0, 1, TimeUnit.SECONDS))
+                .followRedirects(false)
+                .followSslRedirects(false)
+                .build();
+        return client.newCall(new 
Request.Builder().url(url).get().build()).execute();
+    }
+
     /**
      * download file.
      *
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
index cbdad61455..fcfbd18441 100644
--- 
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
@@ -17,204 +17,121 @@
 
 package org.apache.shenyu.admin.utils;
 
+import okhttp3.Dns;
 import okhttp3.HttpUrl;
+import org.apache.shenyu.admin.config.properties.SwaggerImportProperties;
 
 import java.net.InetAddress;
 import java.net.UnknownHostException;
-import java.util.Arrays;
-import java.util.HashSet;
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
 
 /**
  * URL security utilities for preventing SSRF and other URL-based attacks.
  */
 public final class UrlSecurityUtils {
 
-    private static final String HTTP_PROTOCOL = "http";
-
-    private static final String HTTPS_PROTOCOL = "https";
-
-    /**
-     * Private constructor to prevent instantiation.
-     */
     private UrlSecurityUtils() {
     }
 
     /**
-     * Validate URL to prevent SSRF attacks.
-     *
-     * @param url the URL to validate
+     * Validate a URL using the default Swagger import policy.
+     * @param url 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");
-        }
-
-        HttpUrl parsedUrl = HttpUrl.parse(url);
-        if (Objects.isNull(parsedUrl)) {
-            throw new IllegalArgumentException("Invalid URL format");
-        }
-
-        String protocol = parsedUrl.scheme();
-
-        // Only allow HTTP and HTTPS protocols
-        if (!HTTP_PROTOCOL.equals(protocol) && 
!HTTPS_PROTOCOL.equals(protocol)) {
-            throw new IllegalArgumentException("Only HTTP and HTTPS protocols 
are allowed");
-        }
-
-        // Validate host for SSRF protection using the same URL parser as 
request execution.
-        validateHostForSSRF(parsedUrl.host(), parsedUrl.port());
+        validatedDns(url, new SwaggerImportProperties(), Dns.SYSTEM);
     }
 
     /**
-     * Validate host to prevent SSRF attacks.
-     *
-     * @param host the host to validate
-     * @param port the port to validate
+     * Validate a host using the default Swagger import policy.
+     * @param host host to validate
+     * @param port port to validate, or -1 for the default HTTP port
      * @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");
         }
+        validateUrlForSSRF(new 
HttpUrl.Builder().scheme("http").host(host.trim())
+                .port(port == -1 ? 80 : port).build().toString());
+    }
 
-        String normalizedHost = host.toLowerCase().trim();
-
-        // Check for localhost variations
-        if (isLocalhost(normalizedHost)) {
-            throw new IllegalArgumentException("Access to localhost is not 
allowed");
+    /**
+     * Resolve and validate every destination once, then pin that result for 
request execution.
+     * The returned DNS rejects other hostnames; callers must also disable 
redirects and proxies
+     * and isolate their connection pool so a previous connection cannot 
bypass this resolution.
+     * @param url URL to validate, parsed using the same parser as OkHttp 
requests
+     * @param properties allowed and denied port policy
+     * @param resolver resolver used for this request
+     * @return DNS restricted to an immutable copy of the validated addresses
+     * @throws IllegalArgumentException if the URL, port or any resolved 
address is not allowed
+     */
+    public static Dns validatedDns(final String url, final 
SwaggerImportProperties properties, final Dns resolver) {
+        if (Objects.isNull(url) || url.trim().isEmpty()) {
+            throw new IllegalArgumentException("URL cannot be empty");
         }
-
-        // Check for private IP addresses
-        if (isPrivateOrInternalIP(normalizedHost)) {
-            throw new IllegalArgumentException("Access to private or internal 
IP addresses is not allowed");
+        HttpUrl parsedUrl = HttpUrl.parse(url);
+        if (Objects.isNull(parsedUrl)) {
+            throw new IllegalArgumentException("Invalid HTTP or HTTPS URL");
         }
-
-        // Check for sensitive ports
-        if (isSensitivePort(port)) {
-            throw new IllegalArgumentException("Access to sensitive ports is 
not allowed");
+        int port = parsedUrl.port();
+        if (properties.getDeniedPorts().contains(port)
+                || !properties.getAllowedPorts().isEmpty() && 
!properties.getAllowedPorts().contains(port)) {
+            throw new IllegalArgumentException("Access to this port is not 
allowed");
         }
-
-        // Additional validation for DNS resolution
+        List<InetAddress> addresses;
         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());
-                }
-            }
+            addresses = resolver.lookup(parsedUrl.host());
         } catch (UnknownHostException e) {
-            throw new IllegalArgumentException("Cannot resolve host: " + host);
+            throw new IllegalArgumentException("Cannot resolve host: " + 
parsedUrl.host(), e);
         }
+        if (Objects.isNull(addresses) || addresses.isEmpty() || 
addresses.stream().anyMatch(UrlSecurityUtils::isInternalAddress)) {
+            throw new IllegalArgumentException("Resolved IP address is private 
or internal");
+        }
+        List<InetAddress> validatedAddresses = List.copyOf(addresses);
+        return hostname -> {
+            if (!parsedUrl.host().equals(hostname)) {
+                throw new UnknownHostException("Host differs from validated 
request");
+            }
+            return validatedAddresses;
+        };
     }
 
-    /**
-     * 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\\..*")) {
+    private static boolean isInternalAddress(final InetAddress address) {
+        if (Objects.isNull(address) || address.isLoopbackAddress() || 
address.isLinkLocalAddress()
+                || address.isSiteLocalAddress() || address.isAnyLocalAddress() 
|| address.isMulticastAddress()) {
             return true;
         }
-
-        // Check for IPv6 private ranges
-        if (host.startsWith("fc") || host.startsWith("fd")
-                || host.startsWith("fe80") || "::1".equals(host)) {
-            return true;
+        byte[] bytes = address.getAddress();
+        int first = bytes[0] & 0xff;
+        if (bytes.length == 4) {
+            return isInternalIpv4Address(bytes, 0);
         }
-
-        // Check for other internal addresses
-        if (host.matches("^169\\.254\\..*")
-                || host.matches("^224\\..*")
-                || host.matches("^255\\..*")) {
-            return true;
+        if (isIpv4MappedAddress(bytes)) {
+            return isInternalIpv4Address(bytes, 12);
         }
-
-        return false;
+        // Java does not classify IPv6 unique-local addresses as site-local.
+        return (first & 0xfe) == 0xfc;
     }
 
-    /**
-     * 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) {
+    private static boolean isIpv4MappedAddress(final byte[] bytes) {
+        if (bytes.length != 16 || bytes[10] != (byte) 0xff || bytes[11] != 
(byte) 0xff) {
             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;
+        for (int index = 0; index < 10; index++) {
+            if (bytes[index] != 0) {
+                return false;
+            }
         }
+        return true;
+    }
 
-        // 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);
+    private static boolean isInternalIpv4Address(final byte[] bytes, final int 
offset) {
+        int first = bytes[offset] & 0xff;
+        int second = bytes[offset + 1] & 0xff;
+        return first == 0 || first == 10 || first == 127 || first >= 224
+                || first == 169 && second == 254 || first == 172 && second >= 
16 && second <= 31
+                || first == 192 && second == 168 || first == 100 && second >= 
64 && second <= 127;
     }
-} 
+}
diff --git a/shenyu-admin/src/main/resources/application.yml 
b/shenyu-admin/src/main/resources/application.yml
index 015396d931..19d88e671b 100755
--- a/shenyu-admin/src/main/resources/application.yml
+++ b/shenyu-admin/src/main/resources/application.yml
@@ -215,6 +215,12 @@ shenyu:
       - /alert/report
   swagger:
     max-body-size: 10485760
+    # Applies to Swagger import, MCP import and test-connection. Empty allows 
any non-denied port.
+    allowed-ports: []
+    # Replaces the default deny list; denied ports take precedence over 
allowed-ports.
+    # 8080/8081/9090/9091 are allowed by default. All DNS answers must still 
be public addresses.
+    # Redirects and proxies are disabled; the request connects only to 
validated DNS addresses.
+    denied-ports: [22, 23, 3389, 25, 53, 135, 139, 445, 5432, 3306, 1433, 
6379, 11211, 5984, 9200, 2181, 9092, 9093, 2375, 2376, 25672, 5672, 15672, 4369]
   dashboard:
     core:
       onlySuperAdminPermission:
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/config/properties/SwaggerImportPropertiesTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/config/properties/SwaggerImportPropertiesTest.java
new file mode 100644
index 0000000000..513d3674fb
--- /dev/null
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/config/properties/SwaggerImportPropertiesTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.properties;
+
+import org.apache.shenyu.admin.config.HttpUtilsConfiguration;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Test Swagger import port policy binding.
+ */
+public class SwaggerImportPropertiesTest {
+
+    private final ApplicationContextRunner contextRunner = new 
ApplicationContextRunner().withUserConfiguration(HttpUtilsConfiguration.class);
+
+    @Test
+    public void shouldBindAllowedAndDeniedPorts() {
+        
contextRunner.withPropertyValues("shenyu.swagger.allowed-ports=80,443,8080", 
"shenyu.swagger.denied-ports=8080,9090")
+                .run(context -> {
+                    SwaggerImportProperties properties = 
context.getBean(SwaggerImportProperties.class);
+                    assertEquals(Set.of(80, 443, 8080), 
properties.getAllowedPorts());
+                    assertEquals(Set.of(8080, 9090), 
properties.getDeniedPorts());
+                });
+    }
+
+    @Test
+    public void shouldAllowExplicitEmptyDenyList() {
+        contextRunner.withPropertyValues("shenyu.swagger.denied-ports=")
+                .run(context -> 
assertTrue(context.getBean(SwaggerImportProperties.class).getDeniedPorts().isEmpty()));
+    }
+
+    @Test
+    public void shouldRejectInvalidBoundPorts() {
+        contextRunner.withPropertyValues("shenyu.swagger.allowed-ports=65536")
+                .run(context -> assertNotNull(context.getStartupFailure()));
+        contextRunner.withPropertyValues("shenyu.swagger.denied-ports=0")
+                .run(context -> assertNotNull(context.getStartupFailure()));
+    }
+
+    @Test
+    public void shouldCopyConfigurationAndRejectInvalidValues() {
+        SwaggerImportProperties properties = new SwaggerImportProperties();
+        Set<Integer> ports = new HashSet<>(Set.of(8080));
+        properties.setAllowedPorts(ports);
+        ports.clear();
+        assertEquals(Set.of(8080), properties.getAllowedPorts());
+        assertThrows(UnsupportedOperationException.class, () -> 
properties.getAllowedPorts().clear());
+        assertThrows(IllegalArgumentException.class, () -> 
properties.setDeniedPorts(null));
+        assertThrows(IllegalArgumentException.class, () -> 
properties.setDeniedPorts(Collections.singleton(null)));
+        assertThrows(IllegalArgumentException.class, () -> 
properties.setAllowedPorts(Set.of(-1)));
+    }
+}
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 313a87ad2a..ba631256f2 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
@@ -18,12 +18,14 @@
 package org.apache.shenyu.admin.service.impl;
 
 import okhttp3.MediaType;
+import okhttp3.Dns;
 import okhttp3.Protocol;
 import okhttp3.Request;
 import okhttp3.Response;
 import okhttp3.ResponseBody;
 import okio.Buffer;
 import okio.BufferedSource;
+import org.apache.shenyu.admin.config.properties.SwaggerImportProperties;
 import org.apache.shenyu.admin.model.bean.DocInfo;
 import org.apache.shenyu.admin.model.bean.UpstreamInstance;
 import org.apache.shenyu.admin.model.dto.SwaggerImportRequest;
@@ -33,6 +35,8 @@ import org.apache.shenyu.admin.utils.HttpUtils;
 import org.apache.shenyu.register.common.dto.McpToolsRegisterDTO;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
@@ -45,6 +49,9 @@ import 
org.apache.shenyu.register.mcp.common.dto.ShenyuMcpTool;
 import org.springframework.test.util.ReflectionTestUtils;
 
 import java.io.IOException;
+import java.net.InetAddress;
+import java.util.Collections;
+import java.util.Set;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
@@ -53,6 +60,8 @@ import java.util.function.Consumer;
 
 import static 
org.apache.shenyu.common.constant.Constants.SYS_DEFAULT_NAMESPACE_ID;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.Mockito.verify;
 
@@ -80,6 +89,8 @@ public class SwaggerImportServiceImplTest {
 
     private SwaggerImportServiceImpl service;
 
+    private SwaggerImportProperties properties;
+
     @Mock
     private ShenyuClientRegisterMcpServiceImpl mcpService;
 
@@ -87,7 +98,8 @@ public class SwaggerImportServiceImplTest {
     public void setUp() {
         docManager = new RecordingDocManager();
         httpUtils = new StubHttpUtils();
-        service = new SwaggerImportServiceImpl(docManager, httpUtils);
+        properties = new SwaggerImportProperties();
+        service = new SwaggerImportServiceImpl(docManager, httpUtils, 
properties);
         ReflectionTestUtils.setField(service, "maxSwaggerBodySize", 
DEFAULT_MAX_SWAGGER_BODY_SIZE);
         ReflectionTestUtils.setField(service, 
"shenyuClientRegisterMcpService", mcpService);
     }
@@ -133,6 +145,33 @@ public class SwaggerImportServiceImplTest {
                 captor.getValue().getMetaDataRegisterDTO().getNamespaceId());
     }
 
+    @ParameterizedTest
+    @ValueSource(ints = {8080, 8081, 9090, 9091})
+    public void shouldUseValidatedTransportForAllSwaggerEntryPoints(final int 
port) throws IOException {
+        SwaggerImportRequest request = request();
+        request.setSwaggerUrl("http://8.8.8.8:"; + port + "/v3/api-docs");
+        httpUtils.setResponse(response(responseBody(SWAGGER_JSON, -1, 
JSON_UTF_8)));
+        service.importSwagger(request);
+        assertEquals(SWAGGER_JSON, docManager.getDocJson());
+        httpUtils.setResponse(response(responseBody(MCP_SWAGGER_JSON, -1, 
JSON_UTF_8)));
+        service.importMcpConfig(request);
+        httpUtils.setResponse(response(responseBody(SWAGGER_JSON, -1, 
JSON_UTF_8)));
+        assertTrue(service.testConnection(request.getSwaggerUrl()));
+        assertEquals(3, httpUtils.requests);
+        
assertEquals(Collections.singletonList(InetAddress.getByName("8.8.8.8")), 
httpUtils.dns.lookup("8.8.8.8"));
+    }
+
+    @Test
+    public void shouldApplyConfiguredPolicyToAllSwaggerEntryPoints() {
+        properties.setDeniedPorts(Set.of(8080));
+        SwaggerImportRequest request = request();
+        request.setSwaggerUrl("http://8.8.8.8:8080/v3/api-docs";);
+        assertThrows(IllegalArgumentException.class, () -> 
service.importSwagger(request));
+        assertThrows(IllegalArgumentException.class, () -> 
service.importMcpConfig(request));
+        assertFalse(service.testConnection(request.getSwaggerUrl()));
+        assertEquals(0, httpUtils.requests);
+    }
+
     private SwaggerImportRequest request() {
         SwaggerImportRequest request = new SwaggerImportRequest();
         request.setSwaggerUrl(SWAGGER_URL);
@@ -223,14 +262,18 @@ public class SwaggerImportServiceImplTest {
 
         private Response response;
 
+        private Dns dns;
+
+        private int requests;
+
         private void setResponse(final Response response) {
             this.response = response;
         }
 
         @Override
-        public Response requestForResponse(final String url, final Map<String, 
?> form,
-                                           final Map<String, String> header, 
final HTTPMethod method,
-                                           final boolean followRedirects) {
+        public Response requestForValidatedResponse(final String url, final 
Dns validatedDns) {
+            dns = validatedDns;
+            requests++;
             return response;
         }
     }
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 07f5634dde..de1bc4e27d 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.net.URISyntaxException;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
@@ -127,9 +128,8 @@ public class HttpUtilsTest {
     }
 
     @Test
-    public void fileUtilsToBytesByFileExistsTest() {
-        String path = 
HttpUtilsTest.class.getClassLoader().getResource("./").getPath();
-        File file = new File(path + "application.yml");
+    public void fileUtilsToBytesByFileExistsTest() throws URISyntaxException {
+        File file = new 
File(HttpUtilsTest.class.getClassLoader().getResource("application.yml").toURI());
         Assertions.assertDoesNotThrow(() -> HttpUtils.FileUtils.toBytes(file));
     }
 
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/UrlSecurityUtilsTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/UrlSecurityUtilsTest.java
index 850055d47a..c963a5826b 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/UrlSecurityUtilsTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/UrlSecurityUtilsTest.java
@@ -17,8 +17,24 @@
 
 package org.apache.shenyu.admin.utils;
 
+import okhttp3.Dns;
+import org.apache.shenyu.admin.config.properties.SwaggerImportProperties;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
 /**
@@ -28,9 +44,113 @@ public class UrlSecurityUtilsTest {
 
     private static final String PARSER_CONFUSION_PAYLOAD = 
"http://127.0.0.1:6666\\@1.1.1.1";;
 
+    @ParameterizedTest
+    @ValueSource(ints = {8080, 8081, 9090, 9091})
+    public void shouldAllowPublicApplicationPorts(final int port) {
+        assertDoesNotThrow(() -> 
UrlSecurityUtils.validateUrlForSSRF("http://8.8.8.8:"; + port + "/v3/api-docs"));
+    }
+
     @Test
     public void validateUrlForSSRFShouldRejectParserConfusionPayload() {
         assertThrows(IllegalArgumentException.class,
                 () -> 
UrlSecurityUtils.validateUrlForSSRF(PARSER_CONFUSION_PAYLOAD));
     }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"127.0.0.1", "127.1", "2130706433", "0.0.0.0", 
"10.0.0.1", "172.16.0.1", "192.168.0.1",
+            "169.254.169.254", "100.64.0.1", "224.0.0.1", "239.1.2.3", 
"255.255.255.255", "[::1]", "[::]",
+            "[fe80::1]", "[fc00::1]", "[fd00::1]", "[ff02::1]", 
"[::ffff:127.0.0.1]"})
+    public void shouldRejectInternalLiteralsOnApplicationPorts(final String 
host) {
+        assertThrows(IllegalArgumentException.class, () -> 
UrlSecurityUtils.validateUrlForSSRF("http://"; + host + ":8080/docs"));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"127.0.0.1", "10.0.0.1", "169.254.169.254", 
"100.64.0.1", "239.1.2.3", "fc00::1", "fd00::1", "ff02::1"})
+    public void shouldRejectEveryInternalDnsAnswer(final String address) 
throws UnknownHostException {
+        List<InetAddress> answers = 
Arrays.asList(InetAddress.getByName("8.8.8.8"), InetAddress.getByName(address));
+        assertThrows(IllegalArgumentException.class, () -> 
UrlSecurityUtils.validatedDns("http://example.test:8080/docs";,
+                new SwaggerImportProperties(), hostname -> answers));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"0.0.0.0", "10.0.0.1", "127.0.0.1", 
"169.254.169.254", "172.16.0.1", "172.31.255.255",
+            "192.168.0.1", "100.64.0.1", "100.127.255.255", "224.0.0.1", 
"239.1.2.3", "240.0.0.1", "255.255.255.255"})
+    public void shouldRejectInternalIpv4MappedIpv6DnsAnswers(final String 
address) throws UnknownHostException {
+        InetAddress mapped = mappedAddress(address);
+        assertThrows(IllegalArgumentException.class, () -> 
UrlSecurityUtils.validatedDns("http://example.test:8080/docs";,
+                new SwaggerImportProperties(), hostname -> 
Arrays.asList(InetAddress.getByName("8.8.8.8"), mapped)));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"8.8.8.8", "100.63.255.255", "100.128.0.1", 
"172.15.255.255", "172.32.0.1", "192.169.0.1"})
+    public void shouldAllowPublicIpv4MappedIpv6DnsAnswers(final String 
address) throws UnknownHostException {
+        InetAddress mapped = mappedAddress(address);
+        Dns pinned = 
UrlSecurityUtils.validatedDns("http://example.test:8080/docs";,
+                new SwaggerImportProperties(), hostname -> 
Collections.singletonList(mapped));
+        assertEquals(Collections.singletonList(mapped), 
pinned.lookup("example.test"));
+    }
+
+    private static InetAddress mappedAddress(final String address) throws 
UnknownHostException {
+        byte[] bytes = new byte[16];
+        bytes[10] = (byte) 0xff;
+        bytes[11] = (byte) 0xff;
+        System.arraycopy(InetAddress.getByName(address).getAddress(), 0, 
bytes, 12, 4);
+        // Keep the 16-byte form: InetAddress.getByName normally collapses 
mapped literals to IPv4.
+        return Inet6Address.getByAddress(null, bytes, -1);
+    }
+
+    @Test
+    public void shouldResolveOnlyOnceAndFreezeValidatedAnswers() throws 
UnknownHostException {
+        AtomicInteger resolutions = new AtomicInteger();
+        InetAddress publicAddress = InetAddress.getByName("8.8.8.8");
+        List<InetAddress> answers = new 
ArrayList<>(Collections.singletonList(publicAddress));
+        Dns pinned = 
UrlSecurityUtils.validatedDns("http://fc-public.example:8080/docs";, new 
SwaggerImportProperties(), hostname -> {
+            resolutions.incrementAndGet();
+            return answers;
+        });
+        answers.set(0, InetAddress.getByName("127.0.0.1"));
+        assertEquals(Collections.singletonList(publicAddress), 
pinned.lookup("fc-public.example"));
+        assertEquals(Collections.singletonList(publicAddress), 
pinned.lookup("fc-public.example"));
+        assertEquals(1, resolutions.get());
+        assertThrows(UnsupportedOperationException.class, () -> 
pinned.lookup("fc-public.example").clear());
+        assertThrows(UnknownHostException.class, () -> 
pinned.lookup("other.example"));
+    }
+
+    @Test
+    public void shouldRejectEmptyNullAndUnresolvableDnsAnswers() {
+        SwaggerImportProperties policy = new SwaggerImportProperties();
+        assertThrows(IllegalArgumentException.class, () -> 
UrlSecurityUtils.validatedDns("http://example.test";, policy, host -> null));
+        assertThrows(IllegalArgumentException.class, () -> 
UrlSecurityUtils.validatedDns("http://example.test";, policy, host -> 
Collections.emptyList()));
+        assertThrows(IllegalArgumentException.class, () -> 
UrlSecurityUtils.validatedDns("http://example.test";, policy, host -> 
Collections.singletonList(null)));
+        assertThrows(IllegalArgumentException.class, () -> 
UrlSecurityUtils.validatedDns("http://example.test";, policy, host -> {
+            throw new UnknownHostException("unavailable");
+        }));
+    }
+
+    @Test
+    public void shouldApplyAllowAndDenyPolicyBeforeResolving() {
+        SwaggerImportProperties policy = new SwaggerImportProperties();
+        policy.setAllowedPorts(Set.of(8080, 3306));
+        Dns forbiddenLookup = host -> {
+            throw new AssertionError("Rejected port must not trigger DNS");
+        };
+        assertThrows(IllegalArgumentException.class, () -> 
UrlSecurityUtils.validatedDns("http://example.test:3306";, policy, 
forbiddenLookup));
+        assertThrows(IllegalArgumentException.class, () -> 
UrlSecurityUtils.validatedDns("http://example.test:8081";, policy, 
forbiddenLookup));
+        assertDoesNotThrow(() -> 
UrlSecurityUtils.validatedDns("http://8.8.8.8:8080";, policy, Dns.SYSTEM));
+        policy.setDeniedPorts(Collections.emptySet());
+        assertDoesNotThrow(() -> 
UrlSecurityUtils.validatedDns("http://8.8.8.8:3306";, policy, Dns.SYSTEM));
+        assertThrows(IllegalArgumentException.class, () -> 
UrlSecurityUtils.validatedDns("http://127.0.0.1:3306";, policy, Dns.SYSTEM));
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = {22, 25, 53, 3306, 5432, 6379, 2181, 9200})
+    public void shouldContinueDenyingInfrastructurePorts(final int port) {
+        assertThrows(IllegalArgumentException.class, () -> 
UrlSecurityUtils.validateUrlForSSRF("http://8.8.8.8:"; + port));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"http://8.8.8.8";, "https://8.8.8.8";, 
"https://[2606:4700:4700::1111]:9090"})
+    public void shouldAllowPublicAddressesAndDefaultPorts(final String url) {
+        assertDoesNotThrow(() -> UrlSecurityUtils.validateUrlForSSRF(url));
+    }
 }
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/ValidatedHttpRequestTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/ValidatedHttpRequestTest.java
new file mode 100644
index 0000000000..7c999bf257
--- /dev/null
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/ValidatedHttpRequestTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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 com.sun.net.httpserver.HttpServer;
+import okhttp3.Dns;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.apache.shenyu.admin.config.properties.SwaggerImportProperties;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import javax.net.SocketFactory;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Exercise real HTTP transport without contacting an external server.
+ */
+public class ValidatedHttpRequestTest {
+
+    private HttpServer server;
+
+    private OkHttpClient baseClient;
+
+    private HttpUtils httpUtils;
+
+    private InetAddress publicAddress;
+
+    private final List<InetAddress> connectedAddresses = new ArrayList<>();
+
+    private final AtomicInteger privateRequests = new AtomicInteger();
+
+    private final AtomicReference<String> hostHeader = new AtomicReference<>();
+
+    @BeforeEach
+    public void setUp() throws IOException {
+        publicAddress = InetAddress.getByName("8.8.8.8");
+        server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
+        server.createContext("/docs", exchange -> {
+            hostHeader.set(exchange.getRequestHeaders().getFirst("Host"));
+            byte[] body = "swagger document".getBytes(StandardCharsets.UTF_8);
+            exchange.sendResponseHeaders(200, body.length);
+            exchange.getResponseBody().write(body);
+            exchange.close();
+        });
+        server.createContext("/redirect", exchange -> {
+            exchange.getResponseHeaders().add("Location", "http://127.0.0.1:"; 
+ server.getAddress().getPort() + "/private");
+            exchange.sendResponseHeaders(302, -1);
+            exchange.close();
+        });
+        server.createContext("/private", exchange -> {
+            privateRequests.incrementAndGet();
+            exchange.sendResponseHeaders(200, -1);
+            exchange.close();
+        });
+        server.start();
+        SocketFactory socketFactory = mock(SocketFactory.class);
+        when(socketFactory.createSocket()).thenAnswer(invocation -> new 
Socket() {
+            @Override
+            public void connect(final SocketAddress endpoint, final int 
timeout) throws IOException {
+                connectedAddresses.add(((InetSocketAddress) 
endpoint).getAddress());
+                // Record OkHttp's actual selected destination, then route 
only this test socket to the fixture.
+                super.connect(server.getAddress(), timeout);
+            }
+        });
+        baseClient = new OkHttpClient.Builder().socketFactory(socketFactory)
+                .dns(host -> 
Collections.singletonList(InetAddress.getByName("127.0.0.1")))
+                .proxy(Proxy.NO_PROXY).build();
+        httpUtils = new HttpUtils();
+        httpUtils.setHttpClient(baseClient);
+    }
+
+    @AfterEach
+    public void tearDown() {
+        baseClient.connectionPool().evictAll();
+        server.stop(0);
+    }
+
+    @Test
+    public void 
shouldUsePinnedAddressesWithoutRepeatingDnsOrReusingUnvalidatedConnections() 
throws IOException {
+        String url = url("/docs");
+        // Populate the ordinary client's pool with a connection to an 
internal DNS answer.
+        try (Response response = baseClient.newCall(new 
Request.Builder().url(url).build()).execute()) {
+            assertEquals("swagger document", response.body().string());
+        }
+        AtomicInteger resolutions = new AtomicInteger();
+        Dns changingDns = host -> 
Collections.singletonList(resolutions.getAndIncrement() == 0
+                ? publicAddress : InetAddress.getByName("127.0.0.1"));
+        Dns validatedDns = UrlSecurityUtils.validatedDns(url, new 
SwaggerImportProperties(), changingDns);
+        try (Response response = httpUtils.requestForValidatedResponse(url, 
validatedDns)) {
+            assertEquals(200, response.code());
+            assertEquals("swagger document", response.body().string());
+        }
+        assertEquals(1, resolutions.get());
+        assertEquals(2, connectedAddresses.size());
+        assertEquals(publicAddress, connectedAddresses.get(1));
+        assertEquals("docs.example:" + server.getAddress().getPort(), 
hostHeader.get());
+    }
+
+    @Test
+    public void shouldBypassInheritedProxyAndNotFollowRedirects() throws 
IOException {
+        httpUtils.setHttpClient(baseClient.newBuilder()
+                .proxy(new Proxy(Proxy.Type.HTTP, new 
InetSocketAddress("127.0.0.1", 1))).build());
+        String url = url("/redirect");
+        Dns validatedDns = UrlSecurityUtils.validatedDns(url, new 
SwaggerImportProperties(), host -> Collections.singletonList(publicAddress));
+        try (Response response = httpUtils.requestForValidatedResponse(url, 
validatedDns)) {
+            assertEquals(302, response.code());
+        }
+        assertEquals(0, privateRequests.get());
+        assertEquals(Collections.singletonList(publicAddress), 
connectedAddresses);
+    }
+
+    private String url(final String path) {
+        return "http://docs.example:"; + server.getAddress().getPort() + path;
+    }
+}

Reply via email to