diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/config/verification/ConfigVerificationService.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/config/verification/ConfigVerificationService.java index bb14f2f11..d9ca1330f 100644 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/config/verification/ConfigVerificationService.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/config/verification/ConfigVerificationService.java @@ -27,8 +27,10 @@ import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; import java.net.URI; +import java.net.InetAddress; import java.net.UnknownHostException; import java.time.Duration; import java.util.Objects; @@ -74,37 +76,53 @@ public Mono doBasePathValidate(Object basePath) { return Mono.empty(); } - URI uri = URI.create(CastUtils.castString(CastUtils.castString(basePath).concat(PATH_VERIFICATION_URI))); - if (Objects.equals(uri.getHost(), "127.0.0.1")){ - return Mono.error(new BusinessException("error.base_path_host_error", 500, "127.0.0.1")); - } - if (Objects.equals(uri.getHost(), "localhost")){ - return Mono.error(new BusinessException("error.base_path_host_error", 500, "localhost")); - } + URI uri = URI.create(CastUtils.castString(basePath).concat(PATH_VERIFICATION_URI)); - return webClient - .get() - .uri(uri) - .exchangeToMono(cr -> { - if (cr.statusCode().is2xxSuccessful()) { - return cr.bodyToMono(String.class) - .filter(r-> r.contains("auth:"+PATH_VERIFICATION_URI)) - .switchIfEmpty(Mono.error(()-> new BusinessException("error.base_path_error"))); - } - return Mono.defer(() -> Mono.error(new BusinessException("error.base_path_error"))); - }) - .timeout(Duration.ofSeconds(3), Mono.error(TimeoutException::new)) - .onErrorResume(err -> { - while (err != null) { - if (err instanceof TimeoutException) { - return Mono.error(() -> new BusinessException("error.base_path_validate_request_timeout")); - } else if (err instanceof UnknownHostException) { - return Mono.error(() -> new BusinessException("error.base_path_DNS_resolution_failed")); + return validateHost(uri) + .then( + webClient + .get() + .uri(uri) + .exchangeToMono(cr -> { + if (cr.statusCode().is2xxSuccessful()) { + return cr.bodyToMono(String.class) + .filter(r -> r.contains("auth:" + PATH_VERIFICATION_URI)) + .switchIfEmpty(Mono.error(() -> new BusinessException("error.base_path_error"))); + } + return Mono.defer(() -> Mono.error(new BusinessException("error.base_path_error"))); + }) + .timeout(Duration.ofSeconds(3), Mono.error(TimeoutException::new)) + .onErrorResume(err -> { + while (err != null) { + if (err instanceof TimeoutException) { + return Mono.error(() -> new BusinessException("error.base_path_validate_request_timeout")); + } else if (err instanceof UnknownHostException) { + return Mono.error(() -> new BusinessException("error.base_path_DNS_resolution_failed")); + } + err = err.getCause(); + } + return Mono.error(() -> new BusinessException("error.base_path_error")); + }) + .then() + ); + } + + private Mono validateHost(URI uri) { + String host = uri.getHost(); + if (host == null) { + return Mono.error(new BusinessException("error.base_path_host_error", 500, "unknown")); + } + return Mono + .fromCallable(() -> InetAddress.getAllByName(host)) + .subscribeOn(Schedulers.boundedElastic()) + .flatMap(addresses -> { + for (InetAddress address : addresses) { + // base-path 本身可以指向内网服务,但不能回连当前主机的环回接口。 + if (address.isLoopbackAddress()) { + return Mono.error(new BusinessException("error.base_path_host_error", 500, host)); } - err = err.getCause(); } - return Mono.error(() -> new BusinessException("error.base_path_error")); - }) - .then(); + return Mono.empty(); + }); } } diff --git a/jetlinks-components/common-component/src/test/java/org/jetlinks/community/config/verification/ConfigVerificationServiceTest.java b/jetlinks-components/common-component/src/test/java/org/jetlinks/community/config/verification/ConfigVerificationServiceTest.java new file mode 100644 index 000000000..df5493b8f --- /dev/null +++ b/jetlinks-components/common-component/src/test/java/org/jetlinks/community/config/verification/ConfigVerificationServiceTest.java @@ -0,0 +1,40 @@ +/* + * Copyright 2026 JetLinks https://www.jetlinks.cn + * + * Licensed 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.jetlinks.community.config.verification; + +import org.hswebframework.web.exception.BusinessException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import reactor.test.StepVerifier; + +class ConfigVerificationServiceTest { + + private final ConfigVerificationService service = new ConfigVerificationService(); + + @ParameterizedTest + @ValueSource(strings = { + "http://127.0.0.1:28081", + "http://localhost:28081", + "http://localhost.:28081", + "http://[::1]:28081" + }) + void shouldRejectLoopbackBasePath(String basePath) { + StepVerifier + .create(service.doBasePathValidate(basePath)) + .expectError(BusinessException.class) + .verify(); + } +}