mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
Support InetAddress filtering for HTTP Clients
Add `InetAddressFilter` interface which can be provided by `HttpSettings` to filter out addresses in order to harden applications against SSRF attacks. Closes gh-49687
This commit is contained in:
+19
@@ -446,3 +446,22 @@ spring:
|
||||
read-timeout: 1s
|
||||
redirects: dont-follow
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[io.rest-client.global-configuration.inetaddress-filtering]]
|
||||
=== InetAddress Filtering and SSRF Protection
|
||||
|
||||
It's sometimes useful to limit the remote addresses that an HTTP client is permitted to call.
|
||||
This technique can be especially useful when hardening your application against Server-Side Request Forgery (SSRF) attacks.
|
||||
|
||||
To limit the address that a client can call, you can use an javadoc:org.springframework.boot.http.client.InetAddressFilter[] which will only allow outgoing calls to addresses that match the filter.
|
||||
The filter is a functional interface and you can either create your own implementation, or use one of the convenient factory methods.
|
||||
|
||||
Filters may be applied to the javadoc:org.springframework.boot.http.client.HttpClientSettings[] you used when building a client:
|
||||
|
||||
include-code::MyService[]
|
||||
|
||||
Or you can also define one as a javadoc:org.springframework.context.annotation.Bean[format=annotation] if you want to apply it to all auto-confugured HTTP client builders:
|
||||
|
||||
include-code::MyHttpClientConfiguration[]
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.docs.io.restclient.globalconfiguration.inetaddressfiltering;
|
||||
|
||||
public class Details {
|
||||
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.docs.io.restclient.globalconfiguration.inetaddressfiltering;
|
||||
|
||||
import org.springframework.boot.http.client.InetAddressFilter;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class MyHttpClientConfiguration {
|
||||
|
||||
@Bean
|
||||
public InetAddressFilter httpClientInetAddressFilter() {
|
||||
return InetAddressFilter.of("192.168.1.0/24").andNot("192.168.1.1", "192.168.1.10");
|
||||
}
|
||||
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.docs.io.restclient.globalconfiguration.inetaddressfiltering;
|
||||
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.HttpClientSettings;
|
||||
import org.springframework.boot.http.client.InetAddressFilter;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
@Service
|
||||
public class MyService {
|
||||
|
||||
private final RestClient restClient;
|
||||
|
||||
public MyService() {
|
||||
InetAddressFilter onlyExternalAddresses = InetAddressFilter.externalAddresses();
|
||||
HttpClientSettings settings = HttpClientSettings.defaults().withInetAddressFilter(onlyExternalAddresses);
|
||||
ClientHttpRequestFactory requestFactory = ClientHttpRequestFactoryBuilder.jdk().build(settings);
|
||||
this.restClient = RestClient.builder().requestFactory(requestFactory).baseUrl("https://example.org").build();
|
||||
}
|
||||
|
||||
public Details someRestCall(String name) {
|
||||
return this.restClient.get().uri("/{name}/details", name).retrieve().body(Details.class);
|
||||
}
|
||||
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package org.springframework.boot.docs.io.restclient.globalconfiguration.inetaddressfiltering
|
||||
|
||||
class Details {
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package org.springframework.boot.docs.io.restclient.globalconfiguration.inetaddressfiltering
|
||||
|
||||
import org.springframework.boot.http.client.InetAddressFilter
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class MyHttpClientConfiguration {
|
||||
|
||||
@Bean
|
||||
fun httpClientInetAddressFilter(): InetAddressFilter {
|
||||
return InetAddressFilter.of("192.168.1.0/24").andNot("192.168.1.1", "192.168.1.10")
|
||||
}
|
||||
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package org.springframework.boot.docs.io.restclient.globalconfiguration.inetaddressfiltering
|
||||
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder
|
||||
import org.springframework.boot.http.client.HttpClientSettings
|
||||
import org.springframework.boot.http.client.InetAddressFilter
|
||||
import org.springframework.http.client.ClientHttpRequestFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.client.RestClient
|
||||
|
||||
@Service
|
||||
class MyService {
|
||||
|
||||
private val restClient: RestClient
|
||||
|
||||
init {
|
||||
val onlyExternalAddresses = InetAddressFilter.externalAddresses()
|
||||
val settings = HttpClientSettings.defaults().withInetAddressFilter(onlyExternalAddresses)
|
||||
val requestFactory: ClientHttpRequestFactory = ClientHttpRequestFactoryBuilder.jdk().build(settings)
|
||||
restClient = RestClient.builder().requestFactory(requestFactory).baseUrl("https://example.org").build()
|
||||
}
|
||||
|
||||
fun someRestCall(name: String?): Details {
|
||||
return restClient.get().uri("/{name}/details", name)
|
||||
.retrieve().body(Details::class.java)!!
|
||||
}
|
||||
|
||||
}
|
||||
@@ -42,6 +42,7 @@ dependencies {
|
||||
testImplementation(project(":module:spring-boot-tomcat"))
|
||||
testImplementation(project(":test-support:spring-boot-test-support"))
|
||||
testImplementation("org.springframework:spring-webflux")
|
||||
testImplementation("org.springframework.security:spring-security-core")
|
||||
testImplementation("io.micrometer:micrometer-observation-test")
|
||||
|
||||
testCompileOnly("com.google.code.findbugs:jsr305")
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.http.client;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.IntFunction;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Internal utility to handle filtering of addresses and possibly throwing a
|
||||
* {@link FilteredHostException}.
|
||||
*
|
||||
* @param <T> the address type
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class FilteredAddresses<T> {
|
||||
|
||||
private final Stream<T> stream;
|
||||
|
||||
private FilteredAddresses(Stream<T> stream) {
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
Filtered<List<T>> toList() {
|
||||
return new Filtered<>(this.stream.toList(), List::isEmpty);
|
||||
}
|
||||
|
||||
Filtered<T[]> toArray(IntFunction<T[]> generator) {
|
||||
return new Filtered<>(this.stream.toArray(generator), ObjectUtils::isEmpty);
|
||||
}
|
||||
|
||||
Filtered<T> get() {
|
||||
return new Filtered<>(this.stream.findAny().orElse(null), Objects::isNull);
|
||||
}
|
||||
|
||||
static <T> FilteredAddresses<T> of(Stream<T> stream, Predicate<? super T> predicate) {
|
||||
return new FilteredAddresses<>(stream.filter(Objects::nonNull).filter(predicate));
|
||||
}
|
||||
|
||||
static final class Filtered<T> {
|
||||
|
||||
private final @Nullable T result;
|
||||
|
||||
private final Predicate<T> check;
|
||||
|
||||
Filtered(@Nullable T result, Predicate<T> check) {
|
||||
this.result = result;
|
||||
this.check = check;
|
||||
}
|
||||
|
||||
T orElseThrow(String host, InetAddressFilter filter) {
|
||||
return orElseThrow(() -> host, filter);
|
||||
}
|
||||
|
||||
T orElseThrow(Supplier<String> host, InetAddressFilter filter) {
|
||||
if (this.result == null || this.check.test(this.result)) {
|
||||
throw new FilteredHostException(host.get(), filter);
|
||||
}
|
||||
return this.result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.http.client;
|
||||
|
||||
/**
|
||||
* Exception thrown when a host was not used due to an {@link InetAddressFilter}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public class FilteredHostException extends RuntimeException {
|
||||
|
||||
private final String host;
|
||||
|
||||
private final InetAddressFilter filter;
|
||||
|
||||
FilteredHostException(String host, InetAddressFilter filter) {
|
||||
super("Filtered host '%s'".formatted(host));
|
||||
this.host = host;
|
||||
this.filter = filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the host that was not matched.
|
||||
* @return the unmatched host
|
||||
*/
|
||||
public String getHost() {
|
||||
return this.host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the filter that was used.
|
||||
* @return the filter that didn't match
|
||||
*/
|
||||
public InetAddressFilter getFilter() {
|
||||
return this.filter;
|
||||
}
|
||||
|
||||
}
|
||||
+42
-9
@@ -32,11 +32,13 @@ import org.springframework.boot.ssl.SslBundle;
|
||||
* @param connectTimeout the connect timeout
|
||||
* @param readTimeout the read timeout
|
||||
* @param sslBundle the SSL bundle providing SSL configuration
|
||||
* @param inetAddressFilter the inetAddress filter used to filter out matching requests
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public record HttpClientSettings(@Nullable HttpCookieHandling cookieHandling, @Nullable HttpRedirects redirects,
|
||||
@Nullable Duration connectTimeout, @Nullable Duration readTimeout, @Nullable SslBundle sslBundle) {
|
||||
@Nullable Duration connectTimeout, @Nullable Duration readTimeout, @Nullable SslBundle sslBundle,
|
||||
@Nullable InetAddressFilter inetAddressFilter) {
|
||||
|
||||
/**
|
||||
* Create a new {@link HttpClientSettings} instance.
|
||||
@@ -53,7 +55,22 @@ public record HttpClientSettings(@Nullable HttpCookieHandling cookieHandling, @N
|
||||
this(null, redirects, connectTimeout, readTimeout, sslBundle);
|
||||
}
|
||||
|
||||
private static final HttpClientSettings defaults = new HttpClientSettings(null, null, null, null, null);
|
||||
/**
|
||||
* Create a new {@link HttpClientSettings} instance.
|
||||
* @param cookieHandling the cookie handling strategy to use or null to use the
|
||||
* underlying library's default
|
||||
* @param redirects the follow redirect strategy to use
|
||||
* @param connectTimeout the connect timeout
|
||||
* @param readTimeout the read timeout
|
||||
* @param sslBundle the SSL bundle providing SSL configuration
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public HttpClientSettings(@Nullable HttpCookieHandling cookieHandling, @Nullable HttpRedirects redirects,
|
||||
@Nullable Duration connectTimeout, @Nullable Duration readTimeout, @Nullable SslBundle sslBundle) {
|
||||
this(cookieHandling, redirects, connectTimeout, readTimeout, sslBundle, null);
|
||||
}
|
||||
|
||||
private static final HttpClientSettings defaults = new HttpClientSettings(null, null, null, null, null, null);
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpClientSettings} instance with an updated cookie handling
|
||||
@@ -64,7 +81,7 @@ public record HttpClientSettings(@Nullable HttpCookieHandling cookieHandling, @N
|
||||
*/
|
||||
public HttpClientSettings withCookieHandling(@Nullable HttpCookieHandling cookieHandling) {
|
||||
return new HttpClientSettings(cookieHandling, this.redirects, this.connectTimeout, this.readTimeout,
|
||||
this.sslBundle);
|
||||
this.sslBundle, this.inetAddressFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,7 +93,7 @@ public record HttpClientSettings(@Nullable HttpCookieHandling cookieHandling, @N
|
||||
*/
|
||||
public HttpClientSettings withConnectTimeout(@Nullable Duration connectTimeout) {
|
||||
return new HttpClientSettings(this.cookieHandling, this.redirects, connectTimeout, this.readTimeout,
|
||||
this.sslBundle);
|
||||
this.sslBundle, this.inetAddressFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,7 +105,7 @@ public record HttpClientSettings(@Nullable HttpCookieHandling cookieHandling, @N
|
||||
*/
|
||||
public HttpClientSettings withReadTimeout(@Nullable Duration readTimeout) {
|
||||
return new HttpClientSettings(this.cookieHandling, this.redirects, this.connectTimeout, readTimeout,
|
||||
this.sslBundle);
|
||||
this.sslBundle, this.inetAddressFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,7 +117,8 @@ public record HttpClientSettings(@Nullable HttpCookieHandling cookieHandling, @N
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public HttpClientSettings withTimeouts(@Nullable Duration connectTimeout, @Nullable Duration readTimeout) {
|
||||
return new HttpClientSettings(this.cookieHandling, this.redirects, connectTimeout, readTimeout, this.sslBundle);
|
||||
return new HttpClientSettings(this.cookieHandling, this.redirects, connectTimeout, readTimeout, this.sslBundle,
|
||||
this.inetAddressFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,7 +130,7 @@ public record HttpClientSettings(@Nullable HttpCookieHandling cookieHandling, @N
|
||||
*/
|
||||
public HttpClientSettings withSslBundle(@Nullable SslBundle sslBundle) {
|
||||
return new HttpClientSettings(this.cookieHandling, this.redirects, this.connectTimeout, this.readTimeout,
|
||||
sslBundle);
|
||||
sslBundle, this.inetAddressFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,7 +141,19 @@ public record HttpClientSettings(@Nullable HttpCookieHandling cookieHandling, @N
|
||||
*/
|
||||
public HttpClientSettings withRedirects(@Nullable HttpRedirects redirects) {
|
||||
return new HttpClientSettings(this.cookieHandling, redirects, this.connectTimeout, this.readTimeout,
|
||||
this.sslBundle);
|
||||
this.sslBundle, this.inetAddressFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpClientSettings} instance with an updated inetAddress
|
||||
* filter.
|
||||
* @param inetAddressFilter the new inetAddress filter
|
||||
* @return a new {@link HttpClientSettings} instance
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public HttpClientSettings withInetAddressFilter(@Nullable InetAddressFilter inetAddressFilter) {
|
||||
return new HttpClientSettings(this.cookieHandling, this.redirects, this.connectTimeout, this.readTimeout,
|
||||
this.sslBundle, inetAddressFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,7 +172,10 @@ public record HttpClientSettings(@Nullable HttpCookieHandling cookieHandling, @N
|
||||
Duration connectTimeout = (connectTimeout() != null) ? connectTimeout() : other.connectTimeout();
|
||||
Duration readTimeout = (readTimeout() != null) ? readTimeout() : other.readTimeout();
|
||||
SslBundle sslBundle = (sslBundle() != null) ? sslBundle() : other.sslBundle();
|
||||
return new HttpClientSettings(cookieHandling, redirects, connectTimeout, readTimeout, sslBundle);
|
||||
InetAddressFilter inetAddressFilter = (inetAddressFilter() != null) ? inetAddressFilter()
|
||||
: other.inetAddressFilter();
|
||||
return new HttpClientSettings(cookieHandling, redirects, connectTimeout, readTimeout, sslBundle,
|
||||
inetAddressFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+14
@@ -21,6 +21,7 @@ import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import org.apache.hc.client5.http.DnsResolver;
|
||||
import org.apache.hc.client5.http.classic.HttpClient;
|
||||
import org.apache.hc.client5.http.config.ConnectionConfig;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
@@ -157,6 +158,19 @@ public final class HttpComponentsClientHttpRequestFactoryBuilder
|
||||
this.httpClientBuilder.withDefaultRequestConfigCustomizer(defaultRequestConfigCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} with a
|
||||
* replacement {@link DnsResolver}.
|
||||
* @param dnsResolver the new DNS resolver
|
||||
* @return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} instance
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public HttpComponentsClientHttpRequestFactoryBuilder withDnsResolver(DnsResolver dnsResolver) {
|
||||
Assert.notNull(dnsResolver, "'dnsResolver' must not be null");
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withDnsResolver(dnsResolver));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} that applies the
|
||||
* given customizer. This can be useful for applying pre-packaged customizations.
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.http.client;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.hc.client5.http.DnsResolver;
|
||||
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* HTTP Components {@link DnsResolver} that filters using a {@link InetAddressFilter}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class HttpComponentsFilteredDnsResolver implements DnsResolver {
|
||||
|
||||
private final DnsResolver delegate;
|
||||
|
||||
private final InetAddressFilter filter;
|
||||
|
||||
HttpComponentsFilteredDnsResolver(DnsResolver delegate, InetAddressFilter filter) {
|
||||
this.delegate = delegate;
|
||||
this.filter = filter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetAddress[] resolve(String host) throws UnknownHostException {
|
||||
InetAddress[] resolved = this.delegate.resolve(host);
|
||||
if (ObjectUtils.isEmpty(resolved)) {
|
||||
return resolved;
|
||||
}
|
||||
return FilteredAddresses.of(Arrays.stream(resolved), this.filter::matches)
|
||||
.toArray(InetAddress[]::new)
|
||||
.orElseThrow(host, this.filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<InetSocketAddress> resolve(String host, int port) throws UnknownHostException {
|
||||
List<InetSocketAddress> resolved = this.delegate.resolve(host, port);
|
||||
if (resolved.isEmpty()) {
|
||||
return resolved;
|
||||
}
|
||||
return FilteredAddresses.of(resolved.stream(), this.filter::matches).toList().orElseThrow(host, this.filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolveCanonicalHostname(String host) throws UnknownHostException {
|
||||
return this.delegate.resolveCanonicalHostname(host);
|
||||
}
|
||||
|
||||
}
|
||||
+33
-7
@@ -21,6 +21,8 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.hc.client5.http.DnsResolver;
|
||||
import org.apache.hc.client5.http.SystemDefaultDnsResolver;
|
||||
import org.apache.hc.client5.http.async.HttpAsyncClient;
|
||||
import org.apache.hc.client5.http.config.ConnectionConfig;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
@@ -57,21 +59,24 @@ public final class HttpComponentsHttpAsyncClientBuilder {
|
||||
|
||||
private final Function<@Nullable SslBundle, @Nullable TlsStrategy> tlsStrategyFactory;
|
||||
|
||||
private final DnsResolver dnsResolver;
|
||||
|
||||
public HttpComponentsHttpAsyncClientBuilder() {
|
||||
this(Empty.consumer(), Empty.consumer(), Empty.consumer(), Empty.consumer(),
|
||||
HttpComponentsSslBundleTlsStrategy::get);
|
||||
HttpComponentsSslBundleTlsStrategy::get, SystemDefaultDnsResolver.INSTANCE);
|
||||
}
|
||||
|
||||
private HttpComponentsHttpAsyncClientBuilder(Consumer<HttpAsyncClientBuilder> customizer,
|
||||
Consumer<PoolingAsyncClientConnectionManagerBuilder> connectionManagerCustomizer,
|
||||
Consumer<ConnectionConfig.Builder> connectionConfigCustomizer,
|
||||
Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer,
|
||||
Function<@Nullable SslBundle, @Nullable TlsStrategy> tlsStrategyFactory) {
|
||||
Function<@Nullable SslBundle, @Nullable TlsStrategy> tlsStrategyFactory, DnsResolver dnsResolver) {
|
||||
this.customizer = customizer;
|
||||
this.connectionManagerCustomizer = connectionManagerCustomizer;
|
||||
this.connectionConfigCustomizer = connectionConfigCustomizer;
|
||||
this.defaultRequestConfigCustomizer = defaultRequestConfigCustomizer;
|
||||
this.tlsStrategyFactory = tlsStrategyFactory;
|
||||
this.dnsResolver = dnsResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +89,7 @@ public final class HttpComponentsHttpAsyncClientBuilder {
|
||||
Assert.notNull(customizer, "'customizer' must not be null");
|
||||
return new HttpComponentsHttpAsyncClientBuilder(this.customizer.andThen(customizer),
|
||||
this.connectionManagerCustomizer, this.connectionConfigCustomizer, this.defaultRequestConfigCustomizer,
|
||||
this.tlsStrategyFactory);
|
||||
this.tlsStrategyFactory, this.dnsResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,7 +103,7 @@ public final class HttpComponentsHttpAsyncClientBuilder {
|
||||
Assert.notNull(connectionManagerCustomizer, "'connectionManagerCustomizer' must not be null");
|
||||
return new HttpComponentsHttpAsyncClientBuilder(this.customizer,
|
||||
this.connectionManagerCustomizer.andThen(connectionManagerCustomizer), this.connectionConfigCustomizer,
|
||||
this.defaultRequestConfigCustomizer, this.tlsStrategyFactory);
|
||||
this.defaultRequestConfigCustomizer, this.tlsStrategyFactory, this.dnsResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,7 +118,7 @@ public final class HttpComponentsHttpAsyncClientBuilder {
|
||||
Assert.notNull(connectionConfigCustomizer, "'connectionConfigCustomizer' must not be null");
|
||||
return new HttpComponentsHttpAsyncClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.connectionConfigCustomizer.andThen(connectionConfigCustomizer),
|
||||
this.defaultRequestConfigCustomizer, this.tlsStrategyFactory);
|
||||
this.defaultRequestConfigCustomizer, this.tlsStrategyFactory, this.dnsResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,7 +132,8 @@ public final class HttpComponentsHttpAsyncClientBuilder {
|
||||
Function<@Nullable SslBundle, @Nullable TlsStrategy> tlsStrategyFactory) {
|
||||
Assert.notNull(tlsStrategyFactory, "'tlsStrategyFactory' must not be null");
|
||||
return new HttpComponentsHttpAsyncClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.connectionConfigCustomizer, this.defaultRequestConfigCustomizer, tlsStrategyFactory);
|
||||
this.connectionConfigCustomizer, this.defaultRequestConfigCustomizer, tlsStrategyFactory,
|
||||
this.dnsResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,7 +149,22 @@ public final class HttpComponentsHttpAsyncClientBuilder {
|
||||
Assert.notNull(defaultRequestConfigCustomizer, "'defaultRequestConfigCustomizer' must not be null");
|
||||
return new HttpComponentsHttpAsyncClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.connectionConfigCustomizer,
|
||||
this.defaultRequestConfigCustomizer.andThen(defaultRequestConfigCustomizer), this.tlsStrategyFactory);
|
||||
this.defaultRequestConfigCustomizer.andThen(defaultRequestConfigCustomizer), this.tlsStrategyFactory,
|
||||
this.dnsResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpAsyncClientBuilder} with a replacement
|
||||
* {@link DnsResolver}.
|
||||
* @param dnsResolver the new DNS resolver
|
||||
* @return a new {@link HttpComponentsHttpAsyncClientBuilder} instance
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public HttpComponentsHttpAsyncClientBuilder withDnsResolver(DnsResolver dnsResolver) {
|
||||
Assert.notNull(dnsResolver, "'dnsResolver' must not be null");
|
||||
return new HttpComponentsHttpAsyncClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.connectionConfigCustomizer, this.defaultRequestConfigCustomizer, this.tlsStrategyFactory,
|
||||
dnsResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,6 +189,11 @@ public final class HttpComponentsHttpAsyncClientBuilder {
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
builder.setDefaultConnectionConfig(createConnectionConfig(settings));
|
||||
map.from(settings::sslBundle).as(this.tlsStrategyFactory::apply).to(builder::setTlsStrategy);
|
||||
DnsResolver dnsResolver = this.dnsResolver;
|
||||
if (settings.inetAddressFilter() != null) {
|
||||
dnsResolver = new HttpComponentsFilteredDnsResolver(dnsResolver, settings.inetAddressFilter());
|
||||
}
|
||||
builder.setDnsResolver(dnsResolver);
|
||||
this.connectionManagerCustomizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
+33
-8
@@ -21,6 +21,8 @@ import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.apache.hc.client5.http.DnsResolver;
|
||||
import org.apache.hc.client5.http.SystemDefaultDnsResolver;
|
||||
import org.apache.hc.client5.http.config.ConnectionConfig;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.cookie.StandardCookieSpec;
|
||||
@@ -60,9 +62,11 @@ public final class HttpComponentsHttpClientBuilder {
|
||||
|
||||
private final TlsSocketStrategyFactory tlsSocketStrategyFactory;
|
||||
|
||||
private final DnsResolver dnsResolver;
|
||||
|
||||
public HttpComponentsHttpClientBuilder() {
|
||||
this(Empty.consumer(), Empty.consumer(), Empty.consumer(), Empty.consumer(), Empty.consumer(),
|
||||
HttpComponentsSslBundleTlsStrategy::get);
|
||||
HttpComponentsSslBundleTlsStrategy::get, SystemDefaultDnsResolver.INSTANCE);
|
||||
}
|
||||
|
||||
private HttpComponentsHttpClientBuilder(Consumer<HttpClientBuilder> customizer,
|
||||
@@ -70,13 +74,14 @@ public final class HttpComponentsHttpClientBuilder {
|
||||
Consumer<SocketConfig.Builder> socketConfigCustomizer,
|
||||
Consumer<ConnectionConfig.Builder> connectionConfigCustomizer,
|
||||
Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer,
|
||||
TlsSocketStrategyFactory tlsSocketStrategyFactory) {
|
||||
TlsSocketStrategyFactory tlsSocketStrategyFactory, DnsResolver dnsResolver) {
|
||||
this.customizer = customizer;
|
||||
this.connectionManagerCustomizer = connectionManagerCustomizer;
|
||||
this.socketConfigCustomizer = socketConfigCustomizer;
|
||||
this.connectionConfigCustomizer = connectionConfigCustomizer;
|
||||
this.defaultRequestConfigCustomizer = defaultRequestConfigCustomizer;
|
||||
this.tlsSocketStrategyFactory = tlsSocketStrategyFactory;
|
||||
this.dnsResolver = dnsResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,7 +94,7 @@ public final class HttpComponentsHttpClientBuilder {
|
||||
Assert.notNull(customizer, "'customizer' must not be null");
|
||||
return new HttpComponentsHttpClientBuilder(this.customizer.andThen(customizer),
|
||||
this.connectionManagerCustomizer, this.socketConfigCustomizer, this.connectionConfigCustomizer,
|
||||
this.defaultRequestConfigCustomizer, this.tlsSocketStrategyFactory);
|
||||
this.defaultRequestConfigCustomizer, this.tlsSocketStrategyFactory, this.dnsResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,7 +108,8 @@ public final class HttpComponentsHttpClientBuilder {
|
||||
Assert.notNull(connectionManagerCustomizer, "'connectionManagerCustomizer' must not be null");
|
||||
return new HttpComponentsHttpClientBuilder(this.customizer,
|
||||
this.connectionManagerCustomizer.andThen(connectionManagerCustomizer), this.socketConfigCustomizer,
|
||||
this.connectionConfigCustomizer, this.defaultRequestConfigCustomizer, this.tlsSocketStrategyFactory);
|
||||
this.connectionConfigCustomizer, this.defaultRequestConfigCustomizer, this.tlsSocketStrategyFactory,
|
||||
this.dnsResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,7 +124,7 @@ public final class HttpComponentsHttpClientBuilder {
|
||||
Assert.notNull(socketConfigCustomizer, "'socketConfigCustomizer' must not be null");
|
||||
return new HttpComponentsHttpClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.socketConfigCustomizer.andThen(socketConfigCustomizer), this.connectionConfigCustomizer,
|
||||
this.defaultRequestConfigCustomizer, this.tlsSocketStrategyFactory);
|
||||
this.defaultRequestConfigCustomizer, this.tlsSocketStrategyFactory, this.dnsResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,7 +139,7 @@ public final class HttpComponentsHttpClientBuilder {
|
||||
Assert.notNull(connectionConfigCustomizer, "'connectionConfigCustomizer' must not be null");
|
||||
return new HttpComponentsHttpClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.socketConfigCustomizer, this.connectionConfigCustomizer.andThen(connectionConfigCustomizer),
|
||||
this.defaultRequestConfigCustomizer, this.tlsSocketStrategyFactory);
|
||||
this.defaultRequestConfigCustomizer, this.tlsSocketStrategyFactory, this.dnsResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,7 +156,7 @@ public final class HttpComponentsHttpClientBuilder {
|
||||
Assert.notNull(tlsSocketStrategyFactory, "'tlsSocketStrategyFactory' must not be null");
|
||||
return new HttpComponentsHttpClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.socketConfigCustomizer, this.connectionConfigCustomizer, this.defaultRequestConfigCustomizer,
|
||||
tlsSocketStrategyFactory);
|
||||
tlsSocketStrategyFactory, this.dnsResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,7 +173,21 @@ public final class HttpComponentsHttpClientBuilder {
|
||||
return new HttpComponentsHttpClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.socketConfigCustomizer, this.connectionConfigCustomizer,
|
||||
this.defaultRequestConfigCustomizer.andThen(defaultRequestConfigCustomizer),
|
||||
this.tlsSocketStrategyFactory);
|
||||
this.tlsSocketStrategyFactory, this.dnsResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpClientBuilder} with a replacement
|
||||
* {@link DnsResolver}.
|
||||
* @param dnsResolver the new DNS resolver
|
||||
* @return a new {@link HttpComponentsHttpClientBuilder} instance
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public HttpComponentsHttpClientBuilder withDnsResolver(DnsResolver dnsResolver) {
|
||||
Assert.notNull(dnsResolver, "'dnsResolver' must not be null");
|
||||
return new HttpComponentsHttpClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.socketConfigCustomizer, this.connectionConfigCustomizer, this.defaultRequestConfigCustomizer,
|
||||
this.tlsSocketStrategyFactory, dnsResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,6 +216,11 @@ public final class HttpComponentsHttpClientBuilder {
|
||||
.always()
|
||||
.as(this.tlsSocketStrategyFactory::getTlsSocketStrategy)
|
||||
.to(builder::setTlsSocketStrategy);
|
||||
DnsResolver dnsResolver = this.dnsResolver;
|
||||
if (settings.inetAddressFilter() != null) {
|
||||
dnsResolver = new HttpComponentsFilteredDnsResolver(dnsResolver, settings.inetAddressFilter());
|
||||
}
|
||||
builder.setDnsResolver(dnsResolver);
|
||||
this.connectionManagerCustomizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.http.client;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Strategy interface used for {@link InetAddress}-based filtering.
|
||||
* <p>
|
||||
* Allow HTTP clients to offer Server-Side Request Forgery (SSRF) mitigation features, for
|
||||
* example by only allowing local addresses to be called.
|
||||
* <p>
|
||||
* Filters are typically built using the static factory methods on this interface,
|
||||
* optionally combined with one or more of the logic methods. For example:
|
||||
* <pre class="code">
|
||||
* InetAddressFilter.of("192.168.0.0/24")
|
||||
* .andNot("192.168.0.1");
|
||||
* </pre>
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Rob Winch
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
* @see FilteredHostException
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface InetAddressFilter {
|
||||
|
||||
/**
|
||||
* Determine whether the given socket address matches.
|
||||
* @param address the socket address string to check
|
||||
* @return if the address matches
|
||||
*/
|
||||
default boolean matches(InetSocketAddress address) {
|
||||
Assert.notNull(address, "'address' must not be null");
|
||||
return matches(address.getAddress());
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the given address matches.
|
||||
* @param address the address to check
|
||||
* @return if the address matches
|
||||
*/
|
||||
boolean matches(InetAddress address);
|
||||
|
||||
/**
|
||||
* Return a composed filter that represents a short-circuiting logical AND of this
|
||||
* filter and other IP addresses.
|
||||
* @param addresses the addresses that will be logically-ANDed with this filter in any
|
||||
* form supported by {@link #of(String...)}
|
||||
* @return a new composed filter instance
|
||||
*/
|
||||
default InetAddressFilter and(String... addresses) {
|
||||
return and(Arrays.stream(addresses).map(IpAddress::of).map(IpAddress::filter).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a composed filter that represents a short-circuiting logical AND of this
|
||||
* filter and other filters.
|
||||
* @param filters the filters that will be logically-ANDed with this filter
|
||||
* @return a new composed filter instance
|
||||
*/
|
||||
default InetAddressFilter and(InetAddressFilter... filters) {
|
||||
return and(Arrays.asList(filters));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a composed filter that represents a short-circuiting logical AND of this
|
||||
* filter and other filters.
|
||||
* @param filters the filters that will be logically-ANDed with this filter
|
||||
* @return a new composed filter instance
|
||||
*/
|
||||
default InetAddressFilter and(Collection<? extends InetAddressFilter> filters) {
|
||||
InetAddressFilter result = this;
|
||||
for (InetAddressFilter filter : filters) {
|
||||
InetAddressFilter ours = result;
|
||||
result = (address) -> ours.matches(address) && filter.matches(address);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a composed filter that represents a short-circuiting logical AND of this
|
||||
* filter and other {@link #negate() negated} IP addresses.
|
||||
* @param addresses the addresses that will be {@link #negate() negated} and
|
||||
* logically-ANDed with this filter in any form supported by {@link #of(String...)}
|
||||
* @return a new composed filter instance
|
||||
*/
|
||||
default InetAddressFilter andNot(String... addresses) {
|
||||
return andNot(Arrays.stream(addresses).map(IpAddress::of).map(IpAddress::filter).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a composed filter that represents a short-circuiting logical AND of this
|
||||
* filter and other {@link #negate() negated} filters.
|
||||
* @param filters the filters that will be {@link #negate() negated} and
|
||||
* logically-ANDed with this filter
|
||||
* @return a new composed filter instance
|
||||
*/
|
||||
default InetAddressFilter andNot(InetAddressFilter... filters) {
|
||||
return andNot(Arrays.asList(filters));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a composed filter that represents a short-circuiting logical AND of this
|
||||
* filter and other {@link #negate() negated} filters.
|
||||
* @param filters the filters that will be {@link #negate() negated} and
|
||||
* logically-ANDed with this filter
|
||||
* @return a new composed filter instance
|
||||
*/
|
||||
default InetAddressFilter andNot(Collection<? extends InetAddressFilter> filters) {
|
||||
InetAddressFilter result = this;
|
||||
for (InetAddressFilter filter : filters) {
|
||||
InetAddressFilter ours = result;
|
||||
result = (address) -> ours.matches(address) && !filter.matches(address);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a composed filter that represents a short-circuiting logical OR of this
|
||||
* filter and other IP addresses.
|
||||
* @param addresses the addresses that will be logically-ORed with this filter in any
|
||||
* form supported by {@link #of(String...)}
|
||||
* @return a new composed filter instance
|
||||
*/
|
||||
default InetAddressFilter or(String... addresses) {
|
||||
return or(Arrays.stream(addresses).map(IpAddress::of).map(IpAddress::filter).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a composed filter that represents a short-circuiting logical OR of this
|
||||
* filter and other filters.
|
||||
* @param filters the matchers that will be logically-ORed with this filter
|
||||
* @return a new composed filter instance
|
||||
*/
|
||||
default InetAddressFilter or(InetAddressFilter... filters) {
|
||||
return or(Arrays.asList(filters));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a composed filter that represents a short-circuiting logical OR of this
|
||||
* filter and other filters.
|
||||
* @param filters the filters that will be logically-ORed with this filter
|
||||
* @return a new composed filter instance
|
||||
*/
|
||||
default InetAddressFilter or(Collection<? extends InetAddressFilter> filters) {
|
||||
InetAddressFilter result = this;
|
||||
for (InetAddressFilter matcher : filters) {
|
||||
InetAddressFilter ours = result;
|
||||
result = (address) -> ours.matches(address) || matcher.matches(address);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new filter that represents the logical negation of this filter.
|
||||
* @return the negated filter
|
||||
*/
|
||||
default InetAddressFilter negate() {
|
||||
return (address) -> !matches(address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a filter that will match external (non-private) IP addresses. External
|
||||
* addresses are all non-{@link #internalAddresses() internal addresses}
|
||||
* @return a filter for external IP addresses
|
||||
* @see #internalAddresses()
|
||||
*/
|
||||
static InetAddressFilter externalAddresses() {
|
||||
return routable().andNot(InternalInetAddressFilter.instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a filter that will match internal (private) IP addresses.
|
||||
* <p>
|
||||
* Internal addresses include loopback addresses ({@code 127.0.0.0/8} for IPv4,
|
||||
* {@code ::1} for IPv6), private IPv4 address ranges ({@code 10.0.0.0/8},
|
||||
* {@code 172.16.0.0/12}, {@code 192.168.0.0/16}), and IPv6 Unique Local Addresses
|
||||
* ({@code fc00::/7}).
|
||||
* @return a filter for external IP addresses
|
||||
* @see #externalAddresses()
|
||||
*/
|
||||
static InetAddressFilter internalAddresses() {
|
||||
return routable().and(InternalInetAddressFilter.instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a filter that will match all routable addresses (not all zeros).
|
||||
* @return a filter for routable IP addresses
|
||||
*/
|
||||
static InetAddressFilter routable() {
|
||||
return (address) -> {
|
||||
Assert.notNull(address, "'address' must not be null");
|
||||
byte[] bytes = address.getAddress();
|
||||
for (byte b : bytes) {
|
||||
if (b != 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a filter that is the negation of all the given addresses.
|
||||
* @param addresses the addresses to negate in any form supported by
|
||||
* {@link #of(String...)}
|
||||
* @return a negated filter
|
||||
* @see #negate()
|
||||
*/
|
||||
static InetAddressFilter not(String... addresses) {
|
||||
return all().andNot(addresses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a filter that is the negation of all the given matchers.
|
||||
* @param filters the filters to negate
|
||||
* @return a negated filter
|
||||
* @see #negate()
|
||||
*/
|
||||
static InetAddressFilter not(InetAddressFilter... filters) {
|
||||
return all().andNot(filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a filter that is the negation of all the given filters.
|
||||
* @param filters the filters to negate
|
||||
* @return a negated filter
|
||||
* @see #negate()
|
||||
*/
|
||||
static InetAddressFilter not(Collection<? extends InetAddressFilter> filters) {
|
||||
return all().andNot(filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a filter that matches any of the given IP addresses. Address may be either a
|
||||
* full IP address (e.g. {@code 192.168.1.1}) or an IP address block spcified using
|
||||
* CIDR notations (for example {@code 192.168.1.0/24}). Both IPv4 and IPv6 addresses
|
||||
* are supported.
|
||||
* @param addresses the IP addresses to match
|
||||
* @return a filter that matches any of the given addresses
|
||||
*/
|
||||
static InetAddressFilter of(String... addresses) {
|
||||
return none().or(addresses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a filter that matches any of the given filters.
|
||||
* @param filters the filters to include
|
||||
* @return a filter that matches any of the filters
|
||||
*/
|
||||
static InetAddressFilter of(InetAddressFilter... filters) {
|
||||
return none().or(filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a filter that matches any of the given filters.
|
||||
* @param filters the filters to include
|
||||
* @return a filter that matches any of the filters
|
||||
*/
|
||||
static InetAddressFilter of(Collection<? extends InetAddressFilter> filters) {
|
||||
return none().or(filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt the given {@link Predicate} into an {@link InetAddressFilter}.
|
||||
* @param predicate the predicate to adapt
|
||||
* @return a filter that matches using the predicate
|
||||
*/
|
||||
static InetAddressFilter adapt(Predicate<@Nullable InetAddress> predicate) {
|
||||
Assert.notNull(predicate, "'predicate' must not be null");
|
||||
return (address) -> address != null && predicate.test(address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a filter that matches all addresses.
|
||||
* @return a filter that matches all
|
||||
*/
|
||||
static InetAddressFilter all() {
|
||||
return (address) -> {
|
||||
Assert.notNull(address, "'address' must not be null");
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a filter that matches no addresses.
|
||||
* @return a filter that matches none
|
||||
*/
|
||||
static InetAddressFilter none() {
|
||||
return (address) -> {
|
||||
Assert.notNull(address, "'address' must not be null");
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.http.client;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link InetAddressFilter} that matches internal (private) addresses.
|
||||
* <p>
|
||||
* Internal addresses include loopback addresses (127.0.0.0/8 for IPv4, ::1 for IPv6),
|
||||
* private IPv4 address ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), and IPv6
|
||||
* Unique Local Addresses (fc00::/7).
|
||||
*
|
||||
* @author Gábor Vaspöri
|
||||
* @author Kian Jamali
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Rob Winch
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class InternalInetAddressFilter implements InetAddressFilter {
|
||||
|
||||
private static final byte[] NAT64_PREFIX = { (byte) 0x00, (byte) 0x64, (byte) 0xff, (byte) 0x9b };
|
||||
|
||||
static final InternalInetAddressFilter instance = new InternalInetAddressFilter();
|
||||
|
||||
private InternalInetAddressFilter() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(InetAddress address) {
|
||||
Assert.notNull(address, "'address' must not be null");
|
||||
return isLocal(address) || isSiteLocalIpv6Address(address.getAddress());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for Unique Local IPv6 Addresses. We cannot rely on
|
||||
* {@code Inet6Address.isSiteLocalAddress()} because the JVM implementation dictates
|
||||
* that {@code fec0::/10} is the only site-local IPv6 address space, based on the
|
||||
* outdated RFC 2373. That RFC was deprecated by the IETF in 2004 in favor of
|
||||
* {@code fc00::/7} (RFC 4193). To keep our private network checking accurate to
|
||||
* modern subnets, we maintain manual parsing.
|
||||
* @param address the address to check
|
||||
* @return if the addess is site local
|
||||
*/
|
||||
private boolean isSiteLocalIpv6Address(byte[] address) {
|
||||
return (address.length == 16)
|
||||
&& (address[0] == (byte) 0xfc || address[0] == (byte) 0xfd || isNat64Local(address));
|
||||
}
|
||||
|
||||
private boolean isNat64Local(byte[] address) {
|
||||
if (!Arrays.equals(address, 0, NAT64_PREFIX.length, NAT64_PREFIX, 0, NAT64_PREFIX.length)) {
|
||||
return false;
|
||||
}
|
||||
try { // IPv4/IPv6 translation, 64:ff9b
|
||||
return isLocal(InetAddress.getByAddress(Arrays.copyOfRange(address, 12, 16)));
|
||||
}
|
||||
catch (UnknownHostException ex) {
|
||||
return false; // Should not happen for 4-byte array
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLocal(InetAddress address) {
|
||||
return address.isLoopbackAddress() || address.isLinkLocalAddress() || address.isSiteLocalAddress();
|
||||
}
|
||||
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.http.client;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* An IP address and optional mask as used in Classless Inter-Domain Routing (CIDR).
|
||||
*
|
||||
* @author Luke Taylor
|
||||
* @author Steve Riesenberg
|
||||
* @author Andrey Litvitski
|
||||
* @author Rob Winch
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class IpAddress {
|
||||
|
||||
private static Pattern IPV4 = Pattern.compile("^\\d{1,3}(?:\\.\\d{1,3}){0,3}(?:/\\d{1,2})?$");
|
||||
|
||||
private final InetAddress address;
|
||||
|
||||
private int maskBitSize;
|
||||
|
||||
private IpAddress(InetAddress address, int maskBitSize) {
|
||||
Assert.notNull(address, "'address' must not be null");
|
||||
Assert.isTrue(maskBitSize >= -1, "'maskBitSize' must be positive or -1");
|
||||
Assert.isTrue(address.getAddress().length * 8 >= maskBitSize, () -> String
|
||||
.format("IP address %s is too short for bitmask of length %d", address.getHostAddress(), maskBitSize));
|
||||
this.address = address;
|
||||
this.maskBitSize = maskBitSize;
|
||||
}
|
||||
|
||||
InetAddressFilter filter() {
|
||||
return (address) -> {
|
||||
Assert.notNull(address, "'address' must not be null");
|
||||
if (this.maskBitSize == -1) {
|
||||
return this.address.equals(address);
|
||||
}
|
||||
if (this.maskBitSize == 0) {
|
||||
return true;
|
||||
}
|
||||
byte[] ours = this.address.getAddress();
|
||||
byte[] theirs = address.getAddress();
|
||||
return (ours.length == theirs.length) && matchesMasked(ours, theirs);
|
||||
};
|
||||
}
|
||||
|
||||
private boolean matchesMasked(byte[] ours, byte[] theirs) {
|
||||
boolean result = true;
|
||||
for (int i = 0; i < ours.length; i++) {
|
||||
int remain = Math.max(this.maskBitSize - (i * 8), 0);
|
||||
byte mask = (byte) ((remain < 8) ? 0xFF << (8 - remain) : 0xFF);
|
||||
result = result && (ours[i] & mask) == (theirs[i] & mask);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String hostAddress = this.address.getHostAddress();
|
||||
String suffix = (this.maskBitSize != -1) ? "/" + this.maskBitSize : "";
|
||||
return hostAddress + suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link IpAddress} from a string.
|
||||
* @param address the IP address (plain or in CIDR notation)
|
||||
* @return a new {@link IpAddress} instance
|
||||
*/
|
||||
static IpAddress of(String address) {
|
||||
Assert.hasText(address, "'address' must not be empty");
|
||||
int slash = address.indexOf('/');
|
||||
if (slash == -1) {
|
||||
return new IpAddress(parseInetAddress(address), -1);
|
||||
}
|
||||
InetAddress parsedAddress = parseInetAddress(address.substring(0, slash));
|
||||
Assert.state(parsedAddress != null, "'address' [%s] did not parse".formatted(address));
|
||||
int parseMaskBitSize = parseMaskBitSize(address.substring(slash + 1));
|
||||
return new IpAddress(parsedAddress, parseMaskBitSize);
|
||||
|
||||
}
|
||||
|
||||
private static int parseMaskBitSize(String maskBitSize) {
|
||||
try {
|
||||
return Integer.parseInt(maskBitSize);
|
||||
}
|
||||
catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException("'address' subnet mask must be a number", ex);
|
||||
}
|
||||
}
|
||||
|
||||
static InetAddress parseInetAddress(String address) {
|
||||
Assert.isTrue(isLikelyIpAddress(address),
|
||||
() -> "'address' [%s] must be an IP address and not a host name".formatted(address));
|
||||
try {
|
||||
return InetAddress.getByName(address);
|
||||
}
|
||||
catch (UnknownHostException ex) {
|
||||
throw new IllegalArgumentException("'address' [%s] must be parsable to an InetAddress".formatted(address),
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isLikelyIpAddress(String address) {
|
||||
return StringUtils.hasText(address) && (IPV4.matcher(address).matches() || isLikelyIpv6Address(address));
|
||||
}
|
||||
|
||||
private static boolean isLikelyIpv6Address(String address) {
|
||||
char firstChar = address.charAt(0);
|
||||
return (firstChar == '[' || firstChar == ':') || (isHexDigit(firstChar) && address.contains(":"));
|
||||
}
|
||||
|
||||
private static boolean isHexDigit(char ch) {
|
||||
return Character.digit(ch, 16) != -1;
|
||||
}
|
||||
|
||||
}
|
||||
+14
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.net.ProxySelector;
|
||||
import java.net.http.HttpClient;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@@ -89,6 +90,19 @@ public final class JdkClientHttpRequestFactoryBuilder
|
||||
this.httpClientBuilder.withCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JdkClientHttpRequestFactoryBuilder} with a replacement
|
||||
* {@link ProxySelector}.
|
||||
* @param proxySelector the new proxy selector
|
||||
* @return a new {@link JdkClientHttpRequestFactoryBuilder} instance
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public JdkClientHttpRequestFactoryBuilder withProxySelector(ProxySelector proxySelector) {
|
||||
Assert.notNull(proxySelector, "'proxySelector' must not be null");
|
||||
return new JdkClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withProxySelector(proxySelector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JdkClientHttpRequestFactoryBuilder} that applies the given
|
||||
* customizer. This can be useful for applying pre-packaged customizations.
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.http.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.URI;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* JDK {@link ProxySelector} to check a URL is not filtered by a
|
||||
* {@link InetAddressFilter}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class JdkFilteredProxySelector extends ProxySelector {
|
||||
|
||||
private final ProxySelector delegate;
|
||||
|
||||
private final InetAddressFilter filter;
|
||||
|
||||
JdkFilteredProxySelector(ProxySelector delegate, InetAddressFilter filter) {
|
||||
this.delegate = delegate;
|
||||
this.filter = filter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Proxy> select(URI uri) {
|
||||
String host = uri.getHost();
|
||||
FilteredAddresses.of(Stream.of(host), this::matchesResolvedHost).get().orElseThrow(host, this.filter);
|
||||
return this.delegate.select(uri);
|
||||
}
|
||||
|
||||
private boolean matchesResolvedHost(String host) {
|
||||
InetAddress resolved = resolve(host);
|
||||
return (resolved != null) && this.filter.matches(resolved);
|
||||
}
|
||||
|
||||
private @Nullable InetAddress resolve(String host) {
|
||||
try {
|
||||
// We follow the same resolution logic as
|
||||
// jdk.internal.net.http.HttpRequestImpl.getAddress()
|
||||
return InetAddress.getByName(host);
|
||||
}
|
||||
catch (UnknownHostException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectFailed(URI uri, SocketAddress address, IOException ex) {
|
||||
this.delegate.connectFailed(uri, address, ex);
|
||||
}
|
||||
|
||||
}
|
||||
+23
-3
@@ -18,6 +18,7 @@ package org.springframework.boot.http.client;
|
||||
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpClient.Redirect;
|
||||
import java.util.concurrent.Executor;
|
||||
@@ -44,12 +45,15 @@ public final class JdkHttpClientBuilder {
|
||||
|
||||
private final Consumer<HttpClient.Builder> customizer;
|
||||
|
||||
private final ProxySelector proxySelector;
|
||||
|
||||
public JdkHttpClientBuilder() {
|
||||
this(Empty.consumer());
|
||||
this(Empty.consumer(), ProxySelector.getDefault());
|
||||
}
|
||||
|
||||
private JdkHttpClientBuilder(Consumer<HttpClient.Builder> customizer) {
|
||||
private JdkHttpClientBuilder(Consumer<HttpClient.Builder> customizer, ProxySelector proxySelector) {
|
||||
this.customizer = customizer;
|
||||
this.proxySelector = proxySelector;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,7 +76,18 @@ public final class JdkHttpClientBuilder {
|
||||
*/
|
||||
public JdkHttpClientBuilder withCustomizer(Consumer<HttpClient.Builder> customizer) {
|
||||
Assert.notNull(customizer, "'customizer' must not be null");
|
||||
return new JdkHttpClientBuilder(this.customizer.andThen(customizer));
|
||||
return new JdkHttpClientBuilder(this.customizer.andThen(customizer), this.proxySelector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JdkHttpClientBuilder} with a replacement {@link ProxySelector}.
|
||||
* @param proxySelector the new proxy selector
|
||||
* @return a new {@link JdkHttpClientBuilder} instance
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public JdkHttpClientBuilder withProxySelector(ProxySelector proxySelector) {
|
||||
Assert.notNull(proxySelector, "'proxySelector' must not be null");
|
||||
return new JdkHttpClientBuilder(this.customizer, proxySelector);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,6 +105,7 @@ public final class JdkHttpClientBuilder {
|
||||
map.from(settings::connectTimeout).to(builder::connectTimeout);
|
||||
map.from(settings::sslBundle).as(SslBundle::createSslContext).to(builder::sslContext);
|
||||
map.from(settings::sslBundle).as(this::asSslParameters).to(builder::sslParameters);
|
||||
map.from(proxySelector(settings.inetAddressFilter())).to(builder::proxy);
|
||||
this.customizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
@@ -119,4 +135,8 @@ public final class JdkHttpClientBuilder {
|
||||
};
|
||||
}
|
||||
|
||||
private @Nullable ProxySelector proxySelector(@Nullable InetAddressFilter filter) {
|
||||
return (filter != null) ? new JdkFilteredProxySelector(this.proxySelector, filter) : this.proxySelector;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+14
@@ -26,6 +26,7 @@ import java.util.function.UnaryOperator;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpClientTransport;
|
||||
import org.eclipse.jetty.io.ClientConnector;
|
||||
import org.eclipse.jetty.util.SocketAddressResolver;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
@@ -119,6 +120,19 @@ public final class JettyClientHttpRequestFactoryBuilder
|
||||
this.httpClientBuilder.withClientConnectorCustomizerCustomizer(clientConnectorCustomizerCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JettyClientHttpRequestFactoryBuilder} with a replacement
|
||||
* {@link SocketAddressResolver}.
|
||||
* @param socketAddressResolver the new socket address resolver
|
||||
* @return a new {@link JettyClientHttpRequestFactoryBuilder} instance
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public JettyClientHttpRequestFactoryBuilder withSocketAddressResolver(SocketAddressResolver socketAddressResolver) {
|
||||
Assert.notNull(socketAddressResolver, "'socketAddressResolver' must not be null");
|
||||
return new JettyClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withSocketAddressResolver(socketAddressResolver));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JettyClientHttpRequestFactoryBuilder} that applies the given
|
||||
* customizer. This can be useful for applying pre-packaged customizations.
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.http.client;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jetty.util.Promise;
|
||||
import org.eclipse.jetty.util.SocketAddressResolver;
|
||||
|
||||
/**
|
||||
* Jetty {@link SocketAddressResolver} that filters using a {@link InetAddressFilter}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class JettyFilteredSocketAddressResolver implements SocketAddressResolver {
|
||||
|
||||
private final SocketAddressResolver delegate;
|
||||
|
||||
private final InetAddressFilter filter;
|
||||
|
||||
JettyFilteredSocketAddressResolver(SocketAddressResolver delegate, InetAddressFilter filter) {
|
||||
this.delegate = delegate;
|
||||
this.filter = filter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resolve(String host, int port, Map<String, Object> context, Promise<List<InetSocketAddress>> promise) {
|
||||
this.delegate.resolve(host, port, context, new FilteredPromise(host, promise));
|
||||
}
|
||||
|
||||
class FilteredPromise implements Promise<List<InetSocketAddress>> {
|
||||
|
||||
private final String host;
|
||||
|
||||
private final Promise<List<InetSocketAddress>> delegate;
|
||||
|
||||
FilteredPromise(String host, Promise<List<InetSocketAddress>> delegate) {
|
||||
this.host = host;
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void succeeded(List<InetSocketAddress> result) {
|
||||
try {
|
||||
this.delegate.succeeded(filter(result));
|
||||
}
|
||||
catch (FilteredHostException ex) {
|
||||
failed(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private List<InetSocketAddress> filter(List<InetSocketAddress> result) {
|
||||
InetAddressFilter filter = JettyFilteredSocketAddressResolver.this.filter;
|
||||
return FilteredAddresses.of(result.stream(), filter::matches).toList().orElseThrow(this.host, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(Throwable ex) {
|
||||
this.delegate.failed(ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+50
-16
@@ -30,6 +30,7 @@ import org.eclipse.jetty.client.transport.HttpClientTransportDynamic;
|
||||
import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP;
|
||||
import org.eclipse.jetty.http.HttpCookieStore;
|
||||
import org.eclipse.jetty.io.ClientConnector;
|
||||
import org.eclipse.jetty.util.SocketAddressResolver;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
@@ -56,18 +57,23 @@ public final class JettyHttpClientBuilder {
|
||||
|
||||
private final Consumer<ClientConnector> clientConnectorCustomizerCustomizer;
|
||||
|
||||
private final @Nullable SocketAddressResolver socketAddressResolver;
|
||||
|
||||
public JettyHttpClientBuilder() {
|
||||
this(Empty.consumer(), JettyHttpClientBuilder::createHttpClientTransport, Empty.consumer(), Empty.consumer());
|
||||
this(Empty.consumer(), JettyHttpClientBuilder::createHttpClientTransport, Empty.consumer(), Empty.consumer(),
|
||||
null);
|
||||
}
|
||||
|
||||
private JettyHttpClientBuilder(Consumer<HttpClient> customizer,
|
||||
Function<ClientConnector, HttpClientTransport> httpClientTransportFactory,
|
||||
Consumer<HttpClientTransport> httpClientTransportCustomizer,
|
||||
Consumer<ClientConnector> clientConnectorCustomizerCustomizer) {
|
||||
Consumer<ClientConnector> clientConnectorCustomizerCustomizer,
|
||||
@Nullable SocketAddressResolver socketAddressResolver) {
|
||||
this.customizer = customizer;
|
||||
this.httpClientTransportFactory = httpClientTransportFactory;
|
||||
this.httpClientTransportCustomizer = httpClientTransportCustomizer;
|
||||
this.clientConnectorCustomizerCustomizer = clientConnectorCustomizerCustomizer;
|
||||
this.socketAddressResolver = socketAddressResolver;
|
||||
}
|
||||
|
||||
private static HttpClientTransport createHttpClientTransport(ClientConnector connector) {
|
||||
@@ -84,7 +90,8 @@ public final class JettyHttpClientBuilder {
|
||||
public JettyHttpClientBuilder withCustomizer(Consumer<HttpClient> customizer) {
|
||||
Assert.notNull(customizer, "'customizer' must not be null");
|
||||
return new JettyHttpClientBuilder(this.customizer.andThen(customizer), this.httpClientTransportFactory,
|
||||
this.httpClientTransportCustomizer, this.clientConnectorCustomizerCustomizer);
|
||||
this.httpClientTransportCustomizer, this.clientConnectorCustomizerCustomizer,
|
||||
this.socketAddressResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,7 +105,8 @@ public final class JettyHttpClientBuilder {
|
||||
Function<ClientConnector, HttpClientTransport> httpClientTransportFactory) {
|
||||
Assert.notNull(httpClientTransportFactory, "'httpClientTransportFactory' must not be null");
|
||||
return new JettyHttpClientBuilder(this.customizer, httpClientTransportFactory,
|
||||
this.httpClientTransportCustomizer, this.clientConnectorCustomizerCustomizer);
|
||||
this.httpClientTransportCustomizer, this.clientConnectorCustomizerCustomizer,
|
||||
this.socketAddressResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,7 +120,7 @@ public final class JettyHttpClientBuilder {
|
||||
Assert.notNull(httpClientTransportCustomizer, "'httpClientTransportCustomizer' must not be null");
|
||||
return new JettyHttpClientBuilder(this.customizer, this.httpClientTransportFactory,
|
||||
this.httpClientTransportCustomizer.andThen(httpClientTransportCustomizer),
|
||||
this.clientConnectorCustomizerCustomizer);
|
||||
this.clientConnectorCustomizerCustomizer, this.socketAddressResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,7 +134,21 @@ public final class JettyHttpClientBuilder {
|
||||
Assert.notNull(clientConnectorCustomizerCustomizer, "'clientConnectorCustomizerCustomizer' must not be null");
|
||||
return new JettyHttpClientBuilder(this.customizer, this.httpClientTransportFactory,
|
||||
this.httpClientTransportCustomizer,
|
||||
this.clientConnectorCustomizerCustomizer.andThen(clientConnectorCustomizerCustomizer));
|
||||
this.clientConnectorCustomizerCustomizer.andThen(clientConnectorCustomizerCustomizer),
|
||||
this.socketAddressResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JettyHttpClientBuilder} with a replacement
|
||||
* {@link SocketAddressResolver}.
|
||||
* @param socketAddressResolver the new socket address resolver
|
||||
* @return a new {@link JettyHttpClientBuilder} instance
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public JettyHttpClientBuilder withSocketAddressResolver(SocketAddressResolver socketAddressResolver) {
|
||||
Assert.notNull(socketAddressResolver, "'socketAddressResolver' must not be null");
|
||||
return new JettyHttpClientBuilder(this.customizer, this.httpClientTransportFactory,
|
||||
this.httpClientTransportCustomizer, this.clientConnectorCustomizerCustomizer, socketAddressResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,18 +160,19 @@ public final class JettyHttpClientBuilder {
|
||||
settings = (settings != null) ? settings : HttpClientSettings.defaults();
|
||||
HttpClientTransport transport = createTransport(settings);
|
||||
this.httpClientTransportCustomizer.accept(transport);
|
||||
HttpClient httpClient = createHttpClient(settings.readTimeout(), transport);
|
||||
HttpClient httpClient = createHttpClient(settings, transport);
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(settings::connectTimeout).as(Duration::toMillis).to(httpClient::setConnectTimeout);
|
||||
map.from(settings::cookieHandling).as(this::asCookieStore).to(httpClient::setHttpCookieStore);
|
||||
map.from(settings::redirects).always().as(this::followRedirects).to(httpClient::setFollowRedirects);
|
||||
map.from(this.socketAddressResolver).to(httpClient::setSocketAddressResolver);
|
||||
this.customizer.accept(httpClient);
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
private HttpClient createHttpClient(@Nullable Duration readTimeout, HttpClientTransport transport) {
|
||||
return (readTimeout != null) ? new HttpClientWithReadTimeout(transport, readTimeout)
|
||||
: new HttpClient(transport);
|
||||
private HttpClient createHttpClient(HttpClientSettings settings, HttpClientTransport transport) {
|
||||
return (settings.readTimeout() != null || settings.inetAddressFilter() != null)
|
||||
? new CustomizedHttpClient(transport, settings) : new HttpClient(transport);
|
||||
}
|
||||
|
||||
private HttpClientTransport createTransport(HttpClientSettings settings) {
|
||||
@@ -202,21 +225,32 @@ public final class JettyHttpClientBuilder {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link HttpClient} subclass that sets the read timeout.
|
||||
* {@link HttpClient} subclass to support customization.
|
||||
*/
|
||||
static class HttpClientWithReadTimeout extends HttpClient {
|
||||
static class CustomizedHttpClient extends HttpClient {
|
||||
|
||||
private final Duration readTimeout;
|
||||
private final HttpClientSettings settings;
|
||||
|
||||
HttpClientWithReadTimeout(HttpClientTransport transport, Duration readTimeout) {
|
||||
CustomizedHttpClient(HttpClientTransport transport, HttpClientSettings settings) {
|
||||
super(transport);
|
||||
this.readTimeout = readTimeout;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSocketAddressResolver(SocketAddressResolver resolver) {
|
||||
if (this.settings.inetAddressFilter() != null) {
|
||||
Assert.notNull(resolver, "'resolver' must not be null when addresses are filtered");
|
||||
resolver = new JettyFilteredSocketAddressResolver(resolver, this.settings.inetAddressFilter());
|
||||
}
|
||||
super.setSocketAddressResolver(resolver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.jetty.client.Request newRequest(java.net.URI uri) {
|
||||
Request request = super.newRequest(uri);
|
||||
request.timeout(this.readTimeout.toMillis(), TimeUnit.MILLISECONDS);
|
||||
if (this.settings.readTimeout() != null) {
|
||||
request.timeout(this.settings.readTimeout().toMillis(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
|
||||
+17
@@ -25,6 +25,8 @@ import java.util.function.UnaryOperator;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
import reactor.netty.http.client.HttpClientConfig;
|
||||
import reactor.netty.transport.ClientTransport.ResolvedAddressSelector;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.http.client.ReactorClientHttpRequestFactory;
|
||||
@@ -107,6 +109,21 @@ public final class ReactorClientHttpRequestFactoryBuilder
|
||||
this.httpClientBuilder.withHttpClientCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ReactorHttpClientBuilder} that uses the
|
||||
* {@link ResolvedAddressSelector}. This method should be used in favor of a
|
||||
* customizer so that {@link HttpClientSettings#inetAddressFilter()} can be applied.
|
||||
* @param resolvedAddressSelector the resolved address selector to use
|
||||
* @return a new {@link ReactorHttpClientBuilder} instance
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public ReactorClientHttpRequestFactoryBuilder withResolvedAddressSelector(
|
||||
ResolvedAddressSelector<? super HttpClientConfig> resolvedAddressSelector) {
|
||||
Assert.notNull(resolvedAddressSelector, "'resolvedAddressSelector' must not be null");
|
||||
return new ReactorClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withResolvedAddressSelector(resolvedAddressSelector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ReactorClientHttpRequestFactoryBuilder} that applies the given
|
||||
* customizer. This can be useful for applying pre-packaged customizations.
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.http.client;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.List;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import reactor.netty.transport.ClientTransport.ResolvedAddressSelector;
|
||||
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Reactor Netty {@link ResolvedAddressSelector} that filters using a
|
||||
* {@link InetAddressFilter}.
|
||||
*
|
||||
* @param <C> the client configuration implementation
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ReactorFilteredResolvedAddressSelector<C> implements ResolvedAddressSelector<C> {
|
||||
|
||||
private final @Nullable ResolvedAddressSelector<? super C> delegate;
|
||||
|
||||
private final InetAddressFilter filter;
|
||||
|
||||
ReactorFilteredResolvedAddressSelector(@Nullable ResolvedAddressSelector<? super C> delegate,
|
||||
InetAddressFilter filter) {
|
||||
this.delegate = delegate;
|
||||
this.filter = filter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<? extends SocketAddress> apply(C config, List<? extends SocketAddress> resolvedAddresses) {
|
||||
return filter((this.delegate != null) ? this.delegate.apply(config, resolvedAddresses) : resolvedAddresses);
|
||||
}
|
||||
|
||||
private @Nullable List<? extends SocketAddress> filter(@Nullable List<? extends SocketAddress> resolvedAddresses) {
|
||||
if (CollectionUtils.isEmpty(resolvedAddresses)) {
|
||||
return resolvedAddresses;
|
||||
}
|
||||
return FilteredAddresses.of(resolvedAddresses.stream(), this::matches)
|
||||
.toList()
|
||||
.orElseThrow(() -> hostString(resolvedAddresses), this.filter);
|
||||
}
|
||||
|
||||
private boolean matches(SocketAddress address) {
|
||||
return (address instanceof InetSocketAddress socketAddress) ? this.filter.matches(socketAddress) : true;
|
||||
}
|
||||
|
||||
private String hostString(List<? extends SocketAddress> resolvedAddresses) {
|
||||
List<String> hosts = resolvedAddresses.stream()
|
||||
.filter(InetSocketAddress.class::isInstance)
|
||||
.map(InetSocketAddress.class::cast)
|
||||
.map(InetSocketAddress::getAddress)
|
||||
.map(InetAddress::getHostAddress)
|
||||
.toList();
|
||||
if (hosts.isEmpty()) {
|
||||
return "unknown";
|
||||
}
|
||||
if (hosts.size() == 1) {
|
||||
return hosts.get(0);
|
||||
}
|
||||
return hosts.toString();
|
||||
}
|
||||
|
||||
}
|
||||
+35
-5
@@ -26,7 +26,9 @@ import io.netty.channel.ChannelOption;
|
||||
import io.netty.handler.ssl.SslContextBuilder;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
import reactor.netty.http.client.HttpClientConfig;
|
||||
import reactor.netty.tcp.SslProvider.SslContextSpec;
|
||||
import reactor.netty.transport.ClientTransport.ResolvedAddressSelector;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
@@ -50,13 +52,17 @@ public final class ReactorHttpClientBuilder {
|
||||
|
||||
private final UnaryOperator<HttpClient> customizer;
|
||||
|
||||
private final @Nullable ResolvedAddressSelector<? super HttpClientConfig> resolvedAddressSelector;
|
||||
|
||||
public ReactorHttpClientBuilder() {
|
||||
this(HttpClient::create, UnaryOperator.identity());
|
||||
this(HttpClient::create, UnaryOperator.identity(), null);
|
||||
}
|
||||
|
||||
private ReactorHttpClientBuilder(Supplier<HttpClient> httpClientFactory, UnaryOperator<HttpClient> customizer) {
|
||||
private ReactorHttpClientBuilder(Supplier<HttpClient> httpClientFactory, UnaryOperator<HttpClient> customizer,
|
||||
@Nullable ResolvedAddressSelector<? super HttpClientConfig> resolvedAddressSelector) {
|
||||
this.factory = httpClientFactory;
|
||||
this.customizer = customizer;
|
||||
this.resolvedAddressSelector = resolvedAddressSelector;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,7 +74,8 @@ public final class ReactorHttpClientBuilder {
|
||||
public ReactorHttpClientBuilder withReactorResourceFactory(ReactorResourceFactory reactorResourceFactory) {
|
||||
Assert.notNull(reactorResourceFactory, "'reactorResourceFactory' must not be null");
|
||||
return new ReactorHttpClientBuilder(() -> HttpClient.create(reactorResourceFactory.getConnectionProvider()),
|
||||
(httpClient) -> this.customizer.apply(httpClient).runOn(reactorResourceFactory.getLoopResources()));
|
||||
(httpClient) -> this.customizer.apply(httpClient).runOn(reactorResourceFactory.getLoopResources()),
|
||||
this.resolvedAddressSelector);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,7 +86,7 @@ public final class ReactorHttpClientBuilder {
|
||||
*/
|
||||
public ReactorHttpClientBuilder withHttpClientFactory(Supplier<HttpClient> factory) {
|
||||
Assert.notNull(factory, "'factory' must not be null");
|
||||
return new ReactorHttpClientBuilder(factory, this.customizer);
|
||||
return new ReactorHttpClientBuilder(factory, this.customizer, this.resolvedAddressSelector);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,7 +98,21 @@ public final class ReactorHttpClientBuilder {
|
||||
public ReactorHttpClientBuilder withHttpClientCustomizer(UnaryOperator<HttpClient> customizer) {
|
||||
Assert.notNull(customizer, "'customizer' must not be null");
|
||||
return new ReactorHttpClientBuilder(this.factory,
|
||||
(httpClient) -> customizer.apply(this.customizer.apply(httpClient)));
|
||||
(httpClient) -> customizer.apply(this.customizer.apply(httpClient)), this.resolvedAddressSelector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ReactorHttpClientBuilder} that uses the
|
||||
* {@link ResolvedAddressSelector}. This method should be used in favor of a
|
||||
* customizer so that {@link HttpClientSettings#inetAddressFilter()} can be applied.
|
||||
* @param resolvedAddressSelector the resolved address selector to use
|
||||
* @return a new {@link ReactorHttpClientBuilder} instance
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public ReactorHttpClientBuilder withResolvedAddressSelector(
|
||||
ResolvedAddressSelector<? super HttpClientConfig> resolvedAddressSelector) {
|
||||
Assert.notNull(resolvedAddressSelector, "'resolvedAddressSelector' must not be null");
|
||||
return new ReactorHttpClientBuilder(this.factory, this.customizer, resolvedAddressSelector);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,9 +134,18 @@ public final class ReactorHttpClientBuilder {
|
||||
throw new IllegalArgumentException("Reactor Netty HTTP client does not support cookie handling");
|
||||
}
|
||||
httpClient = map.from(settings::sslBundle).to(httpClient, this::secure);
|
||||
httpClient = map.from(resolvedAddressSelector(settings.inetAddressFilter()))
|
||||
.to(httpClient, HttpClient::resolvedAddressesSelector);
|
||||
return this.customizer.apply(httpClient);
|
||||
}
|
||||
|
||||
private @Nullable ResolvedAddressSelector<? super HttpClientConfig> resolvedAddressSelector(
|
||||
@Nullable InetAddressFilter inetAddressFilter) {
|
||||
return (inetAddressFilter != null)
|
||||
? new ReactorFilteredResolvedAddressSelector<>(this.resolvedAddressSelector, inetAddressFilter)
|
||||
: this.resolvedAddressSelector;
|
||||
}
|
||||
|
||||
HttpClient applyDefaults(HttpClient httpClient) {
|
||||
// Aligns with Spring Framework defaults
|
||||
return httpClient.compress(true);
|
||||
|
||||
+1
@@ -82,6 +82,7 @@ final class ReflectiveComponentsClientHttpRequestFactoryBuilder<T extends Client
|
||||
settings.cookieHandling() == null
|
||||
|| settings.cookieHandling() == HttpCookieHandling.ENABLE_WHEN_POSSIBLE,
|
||||
"Unable to set HTTP cookie handling using reflection");
|
||||
Assert.state(settings.inetAddressFilter() == null, "Unable to set InetAddress filter using reflection");
|
||||
ClientHttpRequestFactory unwrapped = unwrapRequestFactoryIfNecessary(requestFactory);
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(settings::connectTimeout).to((connectTimeout) -> setConnectTimeout(unwrapped, connectTimeout));
|
||||
|
||||
+4
-3
@@ -78,9 +78,10 @@ public final class SimpleClientHttpRequestFactoryBuilder
|
||||
|
||||
@Override
|
||||
protected SimpleClientHttpRequestFactory createClientHttpRequestFactory(HttpClientSettings settings) {
|
||||
if (settings.cookieHandling() == HttpCookieHandling.ENABLE) {
|
||||
throw new IllegalArgumentException("Simple HTTP request factory does not support HTTP cookie handling");
|
||||
}
|
||||
Assert.state(settings.cookieHandling() != HttpCookieHandling.ENABLE,
|
||||
"Simple HTTP request factory does not support HTTP cookie handling");
|
||||
Assert.state(settings.inetAddressFilter() == null,
|
||||
"Simple HTTP request factory does not support InetAddress filtering");
|
||||
SslBundle sslBundle = settings.sslBundle();
|
||||
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpsRequestFactory(settings);
|
||||
Assert.state(sslBundle == null || !sslBundle.getOptions().isSpecified(),
|
||||
|
||||
+9
-2
@@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.http.client.HttpClientSettings;
|
||||
import org.springframework.boot.http.client.InetAddressFilter;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
@@ -38,9 +39,15 @@ public final class HttpClientAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
HttpClientSettings httpClientSettings(ObjectProvider<SslBundles> sslBundles, HttpClientsProperties properties) {
|
||||
HttpClientSettings httpClientSettings(HttpClientsProperties properties,
|
||||
ObjectProvider<SslBundles> sslBundlesProvider,
|
||||
ObjectProvider<InetAddressFilter> inetAddressFilterProvider) {
|
||||
InetAddressFilter inetAddressFilter = inetAddressFilterProvider.getIfAvailable();
|
||||
HttpClientSettings settings = (inetAddressFilter != null)
|
||||
? HttpClientSettings.defaults().withInetAddressFilter(inetAddressFilter)
|
||||
: HttpClientSettings.defaults();
|
||||
HttpClientSettingsPropertyMapper propertyMapper = new HttpClientSettingsPropertyMapper(
|
||||
sslBundles.getIfAvailable(), null);
|
||||
sslBundlesProvider.getIfAvailable(), settings);
|
||||
return propertyMapper.map(properties);
|
||||
}
|
||||
|
||||
|
||||
+14
@@ -21,6 +21,7 @@ import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import org.apache.hc.client5.http.DnsResolver;
|
||||
import org.apache.hc.client5.http.config.ConnectionConfig;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
|
||||
@@ -128,6 +129,19 @@ public final class HttpComponentsClientHttpConnectorBuilder
|
||||
this.httpClientBuilder.withDefaultRequestConfigCustomizer(defaultRequestConfigCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsClientHttpConnectorBuilder} with a replacement
|
||||
* {@link DnsResolver}.
|
||||
* @param dnsResolver the new DNS resolver
|
||||
* @return a new {@link HttpComponentsClientHttpConnectorBuilder} instance
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public HttpComponentsClientHttpConnectorBuilder withDnsResolver(DnsResolver dnsResolver) {
|
||||
Assert.notNull(dnsResolver, "'dnsResolver' must not be null");
|
||||
return new HttpComponentsClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withDnsResolver(dnsResolver));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsClientHttpConnectorBuilder} that applies the
|
||||
* given customizer. This can be useful for applying pre-packaged customizations.
|
||||
|
||||
+14
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.boot.http.client.reactive;
|
||||
|
||||
import java.net.ProxySelector;
|
||||
import java.net.http.HttpClient;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@@ -85,6 +86,19 @@ public final class JdkClientHttpConnectorBuilder extends AbstractClientHttpConne
|
||||
this.httpClientBuilder.withCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JdkClientHttpConnectorBuilder} with a replacement
|
||||
* {@link ProxySelector}.
|
||||
* @param proxySelector the new proxy selector
|
||||
* @return a new {@link JdkClientHttpConnectorBuilder} instance
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public JdkClientHttpConnectorBuilder withProxySelector(ProxySelector proxySelector) {
|
||||
Assert.notNull(proxySelector, "'proxySelector' must not be null");
|
||||
return new JdkClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withProxySelector(proxySelector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JdkClientHttpConnectorBuilder} that applies the given
|
||||
* customizer. This can be useful for applying pre-packaged customizations.
|
||||
|
||||
+14
@@ -25,6 +25,7 @@ import java.util.function.UnaryOperator;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpClientTransport;
|
||||
import org.eclipse.jetty.io.ClientConnector;
|
||||
import org.eclipse.jetty.util.SocketAddressResolver;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.http.client.HttpClientSettings;
|
||||
@@ -116,6 +117,19 @@ public final class JettyClientHttpConnectorBuilder
|
||||
this.httpClientBuilder.withClientConnectorCustomizerCustomizer(clientConnectorCustomizerCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JettyClientHttpConnectorBuilder} with a replacement
|
||||
* {@link SocketAddressResolver}.
|
||||
* @param socketAddressResolver the new socket address resolver
|
||||
* @return a new {@link JettyClientHttpConnectorBuilder} instance
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public JettyClientHttpConnectorBuilder withSocketAddressResolver(SocketAddressResolver socketAddressResolver) {
|
||||
Assert.notNull(socketAddressResolver, "'socketAddressResolver' must not be null");
|
||||
return new JettyClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withSocketAddressResolver(socketAddressResolver));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JettyClientHttpConnectorBuilder} that applies the given
|
||||
* customizer. This can be useful for applying pre-packaged customizations.
|
||||
|
||||
+17
@@ -24,6 +24,8 @@ import java.util.function.UnaryOperator;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
import reactor.netty.http.client.HttpClientConfig;
|
||||
import reactor.netty.transport.ClientTransport.ResolvedAddressSelector;
|
||||
|
||||
import org.springframework.boot.http.client.HttpClientSettings;
|
||||
import org.springframework.boot.http.client.ReactorHttpClientBuilder;
|
||||
@@ -100,6 +102,21 @@ public final class ReactorClientHttpConnectorBuilder
|
||||
this.httpClientBuilder.withHttpClientCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ReactorHttpClientBuilder} that uses the
|
||||
* {@link ResolvedAddressSelector}. This method should be used in favor of a
|
||||
* customizer so that {@link HttpClientSettings#inetAddressFilter()} can be applied.
|
||||
* @param resolvedAddressSelector the resolved address selector to use
|
||||
* @return a new {@link ReactorHttpClientBuilder} instance
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public ReactorClientHttpConnectorBuilder withResolvedAddressSelector(
|
||||
ResolvedAddressSelector<? super HttpClientConfig> resolvedAddressSelector) {
|
||||
Assert.notNull(resolvedAddressSelector, "'resolvedAddressSelector' must not be null");
|
||||
return new ReactorClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withResolvedAddressSelector(resolvedAddressSelector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ReactorClientHttpConnectorBuilder} that applies the given
|
||||
* customizer. This can be useful for applying pre-packaged customizations.
|
||||
|
||||
+25
@@ -53,6 +53,7 @@ import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatException;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
@@ -188,6 +189,26 @@ abstract class AbstractClientHttpRequestFactoryBuilderTests<T extends ClientHttp
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void filteredInetAddress() throws Exception {
|
||||
TomcatServletWebServerFactory webServerFactory = new TomcatServletWebServerFactory(0);
|
||||
WebServer webServer = webServerFactory
|
||||
.getWebServer((context) -> context.addServlet("test", TestServlet.class).addMapping("/"));
|
||||
try {
|
||||
webServer.start();
|
||||
int port = webServer.getPort();
|
||||
URI uri = new URI("http://localhost:%s".formatted(port) + "/redirect");
|
||||
ClientHttpRequestFactory requestFactory = this.builder
|
||||
.build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses()));
|
||||
ClientHttpRequest request = requestFactory.createRequest(uri, HttpMethod.GET);
|
||||
assertThatException().isThrownBy(request::execute)
|
||||
.matches((ex) -> ex instanceof FilteredHostException || ex.getCause() instanceof FilteredHostException);
|
||||
}
|
||||
finally {
|
||||
webServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private ClientHttpRequest request(ClientHttpRequestFactory factory, URI uri, String method) throws IOException {
|
||||
return factory.createRequest(uri, HttpMethod.valueOf(method));
|
||||
}
|
||||
@@ -222,6 +243,10 @@ abstract class AbstractClientHttpRequestFactoryBuilderTests<T extends ClientHttp
|
||||
|
||||
protected abstract long readTimeout(T requestFactory);
|
||||
|
||||
protected final ClientHttpRequestFactoryBuilder<T> getBuilder() {
|
||||
return this.builder;
|
||||
}
|
||||
|
||||
public static class TestServlet extends HttpServlet {
|
||||
|
||||
@Override
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.http.client;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link FilteredAddresses}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class FilteredAddressesTests {
|
||||
|
||||
@Test
|
||||
void toListOrElseThrowWhenNotEmptyReturnsResult() {
|
||||
FilteredAddresses<String> result = FilteredAddresses.of(Stream.of("127.0.0.1"), (address) -> true);
|
||||
assertThat(result.toList().orElseThrow("localhost", mock())).containsExactly("127.0.0.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toListOrElseThrowWhenEmptyThrowsException() {
|
||||
FilteredAddresses<String> result = FilteredAddresses.of(Stream.of("127.0.0.1"), (address) -> false);
|
||||
assertThatExceptionOfType(FilteredHostException.class)
|
||||
.isThrownBy(() -> result.toList().orElseThrow("localhost", mock()))
|
||||
.withMessage("Filtered host 'localhost'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toArrayOrElseThrowWhenNotEmptyReturnsResult() {
|
||||
FilteredAddresses<String> result = FilteredAddresses.of(Stream.of("127.0.0.1"), (address) -> true);
|
||||
assertThat(result.toArray(String[]::new).orElseThrow("localhost", mock())).containsExactly("127.0.0.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toArrayOrElseThrowWhenEmptyThrowsException() {
|
||||
FilteredAddresses<String> result = FilteredAddresses.of(Stream.of("127.0.0.1"), (address) -> false);
|
||||
assertThatExceptionOfType(FilteredHostException.class)
|
||||
.isThrownBy(() -> result.toArray(String[]::new).orElseThrow("localhost", mock()))
|
||||
.withMessage("Filtered host 'localhost'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOrElseThrowWhenNotEmptyReturnsResult() {
|
||||
FilteredAddresses<String> result = FilteredAddresses.of(Stream.of("127.0.0.1"), (address) -> true);
|
||||
assertThat(result.get().orElseThrow("localhost", mock())).isEqualTo("127.0.0.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOrElseThrowWhenEmptyThrowsException() {
|
||||
FilteredAddresses<String> result = FilteredAddresses.of(Stream.of("127.0.0.1"), (address) -> false);
|
||||
assertThatExceptionOfType(FilteredHostException.class)
|
||||
.isThrownBy(() -> result.get().orElseThrow("localhost", mock()))
|
||||
.withMessage("Filtered host 'localhost'");
|
||||
}
|
||||
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.http.client;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link FilteredHostException}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class FilteredHostExceptionTests {
|
||||
|
||||
@Test
|
||||
void create() {
|
||||
InetAddressFilter matcher = (address) -> false;
|
||||
FilteredHostException exception = new FilteredHostException("localhost", matcher);
|
||||
assertThat(exception).hasMessage("Filtered host 'localhost'");
|
||||
assertThat(exception.getHost()).isEqualTo("localhost");
|
||||
assertThat(exception.getFilter()).isSameAs(matcher);
|
||||
}
|
||||
|
||||
}
|
||||
+35
-3
@@ -43,16 +43,29 @@ class HttpClientSettingsTests {
|
||||
assertThat(settings.connectTimeout()).isNull();
|
||||
assertThat(settings.readTimeout()).isNull();
|
||||
assertThat(settings.sslBundle()).isNull();
|
||||
assertThat(settings.inetAddressFilter()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWithNulls() {
|
||||
HttpClientSettings settings = new HttpClientSettings(null, null, null, null, null, null);
|
||||
assertThat(settings.cookieHandling()).isNull();
|
||||
assertThat(settings.redirects()).isNull();
|
||||
assertThat(settings.connectTimeout()).isNull();
|
||||
assertThat(settings.readTimeout()).isNull();
|
||||
assertThat(settings.sslBundle()).isNull();
|
||||
assertThat(settings.inetAddressFilter()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWithNullsWhenSpringBoot41() {
|
||||
HttpClientSettings settings = new HttpClientSettings(null, null, null, null, null);
|
||||
assertThat(settings.cookieHandling()).isNull();
|
||||
assertThat(settings.redirects()).isNull();
|
||||
assertThat(settings.connectTimeout()).isNull();
|
||||
assertThat(settings.readTimeout()).isNull();
|
||||
assertThat(settings.sslBundle()).isNull();
|
||||
assertThat(settings.inetAddressFilter()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -62,6 +75,7 @@ class HttpClientSettingsTests {
|
||||
assertThat(settings.connectTimeout()).isEqualTo(ONE_SECOND);
|
||||
assertThat(settings.readTimeout()).isNull();
|
||||
assertThat(settings.sslBundle()).isNull();
|
||||
assertThat(settings.inetAddressFilter()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -71,6 +85,7 @@ class HttpClientSettingsTests {
|
||||
assertThat(settings.connectTimeout()).isNull();
|
||||
assertThat(settings.readTimeout()).isEqualTo(ONE_SECOND);
|
||||
assertThat(settings.sslBundle()).isNull();
|
||||
assertThat(settings.inetAddressFilter()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -81,6 +96,7 @@ class HttpClientSettingsTests {
|
||||
assertThat(settings.connectTimeout()).isNull();
|
||||
assertThat(settings.readTimeout()).isNull();
|
||||
assertThat(settings.sslBundle()).isSameAs(sslBundle);
|
||||
assertThat(settings.inetAddressFilter()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -91,6 +107,7 @@ class HttpClientSettingsTests {
|
||||
assertThat(settings.connectTimeout()).isNull();
|
||||
assertThat(settings.readTimeout()).isNull();
|
||||
assertThat(settings.sslBundle()).isNull();
|
||||
assertThat(settings.inetAddressFilter()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -100,19 +117,33 @@ class HttpClientSettingsTests {
|
||||
assertThat(settings.connectTimeout()).isNull();
|
||||
assertThat(settings.readTimeout()).isNull();
|
||||
assertThat(settings.sslBundle()).isNull();
|
||||
assertThat(settings.inetAddressFilter()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void withInetAddressMatcherReturnsInstanceWithUpdatedInetAddressMatcher() {
|
||||
InetAddressFilter inetAddressMatcher = mock();
|
||||
HttpClientSettings settings = HttpClientSettings.defaults().withInetAddressFilter(inetAddressMatcher);
|
||||
assertThat(settings.redirects()).isNull();
|
||||
assertThat(settings.connectTimeout()).isNull();
|
||||
assertThat(settings.readTimeout()).isNull();
|
||||
assertThat(settings.sslBundle()).isNull();
|
||||
assertThat(settings.inetAddressFilter()).isEqualTo(inetAddressMatcher);
|
||||
}
|
||||
|
||||
@Test
|
||||
void orElseReturnsNewInstanceWithUpdatedValues() {
|
||||
SslBundle sslBundle = mock(SslBundle.class);
|
||||
HttpClientSettings settings = new HttpClientSettings(null, null, ONE_SECOND, null, null)
|
||||
SslBundle sslBundle = mock();
|
||||
InetAddressFilter inetAddressMatcher = mock();
|
||||
HttpClientSettings settings = new HttpClientSettings(null, null, ONE_SECOND, null, null, null)
|
||||
.orElse(new HttpClientSettings(HttpCookieHandling.ENABLE, HttpRedirects.FOLLOW_WHEN_POSSIBLE, TWO_SECONDS,
|
||||
TWO_SECONDS, sslBundle));
|
||||
TWO_SECONDS, sslBundle, inetAddressMatcher));
|
||||
assertThat(settings.cookieHandling()).isEqualTo(HttpCookieHandling.ENABLE);
|
||||
assertThat(settings.redirects()).isEqualTo(HttpRedirects.FOLLOW_WHEN_POSSIBLE);
|
||||
assertThat(settings.connectTimeout()).isEqualTo(ONE_SECOND);
|
||||
assertThat(settings.readTimeout()).isEqualTo(TWO_SECONDS);
|
||||
assertThat(settings.sslBundle()).isEqualTo(sslBundle);
|
||||
assertThat(settings.inetAddressFilter()).isEqualTo(inetAddressMatcher);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -123,6 +154,7 @@ class HttpClientSettingsTests {
|
||||
assertThat(settings.connectTimeout()).isNull();
|
||||
assertThat(settings.readTimeout()).isNull();
|
||||
assertThat(settings.sslBundle()).isSameAs(sslBundle);
|
||||
assertThat(settings.inetAddressFilter()).isNull();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+24
@@ -19,6 +19,7 @@ package org.springframework.boot.http.client;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.hc.client5.http.DnsResolver;
|
||||
import org.apache.hc.client5.http.HttpRoute;
|
||||
import org.apache.hc.client5.http.config.ConnectionConfig;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
@@ -35,10 +36,12 @@ import org.junit.jupiter.params.provider.EnumSource;
|
||||
import org.springframework.boot.http.client.HttpComponentsHttpClientBuilder.TlsSocketStrategyFactory;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpComponentsClientHttpRequestFactoryBuilder} and
|
||||
@@ -105,6 +108,27 @@ class HttpComponentsClientHttpRequestFactoryBuilderTests
|
||||
customizer.assertCalled();
|
||||
}
|
||||
|
||||
@Test
|
||||
void withDnsResolver() {
|
||||
DnsResolver dnsResolver = mock();
|
||||
ClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.httpComponents()
|
||||
.withDnsResolver(dnsResolver)
|
||||
.build();
|
||||
assertThat(factory).extracting("httpClient.connManager.connectionOperator.dnsResolver").isSameAs(dnsResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withDnsResolverWhenHasInetAddressMatcher() {
|
||||
DnsResolver dnsResolver = mock();
|
||||
ClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.httpComponents()
|
||||
.withDnsResolver(dnsResolver)
|
||||
.build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses()));
|
||||
assertThat(factory).extracting("httpClient.connManager.connectionOperator.dnsResolver")
|
||||
.matches((resolver) -> resolver.getClass().getName().contains("HttpComponentsFiltered"));
|
||||
assertThat(factory).extracting("httpClient.connManager.connectionOperator.dnsResolver.delegate")
|
||||
.isSameAs(dnsResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultCookieHandling() {
|
||||
HttpComponentsClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.httpComponents()
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.http.client;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.hc.client5.http.DnsResolver;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpComponentsFilteredDnsResolver}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class HttpComponentsFilteredDnsResolverTests {
|
||||
|
||||
@Test
|
||||
void resolveHostWhenMatchedOnlyElement() throws Exception {
|
||||
DnsResolver delegate = mock();
|
||||
InetAddress localhost = InetAddress.getLocalHost();
|
||||
given(delegate.resolve("localhost")).willReturn(new InetAddress[] { localhost });
|
||||
HttpComponentsFilteredDnsResolver dnsResolver = new HttpComponentsFilteredDnsResolver(delegate,
|
||||
InetAddressFilter.internalAddresses());
|
||||
assertThat(dnsResolver.resolve("localhost")).containsExactly(localhost);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveHostWhenMatchedOneOfManyElements() throws Exception {
|
||||
DnsResolver delegate = mock();
|
||||
InetAddress localhost = InetAddress.getLocalHost();
|
||||
InetAddress remote = InetAddress.getByName("8.8.8.8");
|
||||
given(delegate.resolve("localhost")).willReturn(new InetAddress[] { localhost, remote });
|
||||
HttpComponentsFilteredDnsResolver dnsResolver = new HttpComponentsFilteredDnsResolver(delegate,
|
||||
InetAddressFilter.internalAddresses());
|
||||
assertThat(dnsResolver.resolve("localhost")).containsExactly(localhost);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveHostWhenMatchedNoElements() throws UnknownHostException {
|
||||
DnsResolver delegate = mock();
|
||||
InetAddress localhost = InetAddress.getLocalHost();
|
||||
InetAddress remote = InetAddress.getByName("8.8.8.8");
|
||||
given(delegate.resolve("localhost")).willReturn(new InetAddress[] { localhost, remote });
|
||||
HttpComponentsFilteredDnsResolver dnsResolver = new HttpComponentsFilteredDnsResolver(delegate,
|
||||
InetAddressFilter.externalAddresses().andNot("8.8.8.8"));
|
||||
assertThatExceptionOfType(FilteredHostException.class).isThrownBy(() -> dnsResolver.resolve("localhost"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveHostAndPortWhenMatchedOnlyElement() throws Exception {
|
||||
DnsResolver delegate = mock();
|
||||
InetAddress localhost = InetAddress.getLocalHost();
|
||||
given(delegate.resolve("localhost", 8080)).willReturn(List.of(new InetSocketAddress(localhost, 8080)));
|
||||
HttpComponentsFilteredDnsResolver dnsResolver = new HttpComponentsFilteredDnsResolver(delegate,
|
||||
InetAddressFilter.internalAddresses());
|
||||
assertThat(dnsResolver.resolve("localhost", 8080)).containsExactly(new InetSocketAddress(localhost, 8080));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveHostAndPortWhenMatchedOneOfManyElements() throws Exception {
|
||||
DnsResolver delegate = mock();
|
||||
InetAddress localhost = InetAddress.getLocalHost();
|
||||
InetAddress remote = InetAddress.getByName("8.8.8.8");
|
||||
given(delegate.resolve("localhost", 8080))
|
||||
.willReturn(List.of(new InetSocketAddress(localhost, 8080), new InetSocketAddress(remote, 8080)));
|
||||
HttpComponentsFilteredDnsResolver dnsResolver = new HttpComponentsFilteredDnsResolver(delegate,
|
||||
InetAddressFilter.internalAddresses());
|
||||
assertThat(dnsResolver.resolve("localhost", 8080)).containsExactly(new InetSocketAddress(localhost, 8080));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveHostAndPortWhenMatchedNoElements() throws UnknownHostException {
|
||||
DnsResolver delegate = mock();
|
||||
InetAddress localhost = InetAddress.getLocalHost();
|
||||
InetAddress remote = InetAddress.getByName("8.8.8.8");
|
||||
given(delegate.resolve("localhost", 8080))
|
||||
.willReturn(List.of(new InetSocketAddress(localhost, 8080), new InetSocketAddress(remote, 8080)));
|
||||
HttpComponentsFilteredDnsResolver dnsResolver = new HttpComponentsFilteredDnsResolver(delegate,
|
||||
InetAddressFilter.externalAddresses().andNot("8.8.8.8"));
|
||||
assertThatExceptionOfType(FilteredHostException.class).isThrownBy(() -> dnsResolver.resolve("localhost", 8080));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveCanonicalHostnameDelegates() throws Exception {
|
||||
DnsResolver delegate = mock();
|
||||
given(delegate.resolveCanonicalHostname("spring")).willReturn("boot");
|
||||
HttpComponentsFilteredDnsResolver dnsResolver = new HttpComponentsFilteredDnsResolver(delegate,
|
||||
InetAddressFilter.internalAddresses());
|
||||
assertThat(dnsResolver.resolveCanonicalHostname("spring")).isEqualTo("boot");
|
||||
}
|
||||
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.http.client;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import org.assertj.core.api.AbstractObjectAssert;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Assertions for an {@link InetAddressFilter}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class InetAddressFilterAssert extends AbstractObjectAssert<InetAddressFilterAssert, InetAddressFilter> {
|
||||
|
||||
InetAddressFilterAssert(InetAddressFilter actual) {
|
||||
super(actual, InetAddressFilterAssert.class);
|
||||
}
|
||||
|
||||
InetAddressFilterAssert matches(String address) {
|
||||
return matches(asInetAddress(address));
|
||||
}
|
||||
|
||||
InetAddressFilterAssert matches(InetAddress address) {
|
||||
isNotNull();
|
||||
assertThat(this.actual.matches(address)).as("Matches address %s", address).isTrue();
|
||||
return this;
|
||||
}
|
||||
|
||||
InetAddressFilterAssert matches(InetSocketAddress address) {
|
||||
isNotNull();
|
||||
assertThat(this.actual.matches(address)).as("Matches socket address %s", address).isTrue();
|
||||
return this;
|
||||
}
|
||||
|
||||
InetAddressFilterAssert doesNotMatch(String address) {
|
||||
return doesNotMatch(asInetAddress(address));
|
||||
}
|
||||
|
||||
InetAddressFilterAssert doesNotMatch(InetAddress address) {
|
||||
isNotNull();
|
||||
assertThat(this.actual.matches(address)).as("Does not match address %s", address).isFalse();
|
||||
return this;
|
||||
}
|
||||
|
||||
InetAddressFilterAssert doesNotMatch(InetSocketAddress address) {
|
||||
isNotNull();
|
||||
assertThat(this.actual.matches(address)).as("Does not match socket address %s", address).isFalse();
|
||||
return this;
|
||||
}
|
||||
|
||||
private InetAddress asInetAddress(String address) {
|
||||
try {
|
||||
return InetAddress.getByName(address);
|
||||
}
|
||||
catch (UnknownHostException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+723
@@ -0,0 +1,723 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.http.client;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.security.util.matcher.InetAddressMatcher;
|
||||
import org.springframework.security.util.matcher.InetAddressMatchers;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link InetAddressFilter} and {@link InternalInetAddressFilter}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class InetAddressFilterTests {
|
||||
|
||||
private static InetAddressFilterAssert assertThat(InetAddressFilter filter) {
|
||||
return new InetAddressFilterAssert(filter);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class MatchesSocketAddressTests {
|
||||
|
||||
@Test
|
||||
void whenIpv4() {
|
||||
InetAddressFilter filter = InetAddressFilter.of("192.168.1.1");
|
||||
assertThat(filter).matches(new InetSocketAddress("192.168.1.1", 8080));
|
||||
assertThat(filter).doesNotMatch(new InetSocketAddress("192.168.1.2", 8080));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenIpv6() {
|
||||
InetAddressFilter filter = InetAddressFilter.of("fe80:0:0:0:21f:5bff:fe33:bd68");
|
||||
assertThat(filter).matches(new InetSocketAddress("fe80::21f:5bff:fe33:bd68", 8080));
|
||||
assertThat(filter).doesNotMatch(new InetSocketAddress("fe90::21f:5bff:fe33:bd68", 8080));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("NullAway") // Test null check
|
||||
void whenNull() {
|
||||
InetAddressFilter filter = (address) -> address != null;
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> filter.matches((InetSocketAddress) null))
|
||||
.withMessage("'address' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenLambda() {
|
||||
InetAddressFilter filter = (address) -> address.getHostAddress().startsWith("192.168");
|
||||
assertThat(filter).matches(new InetSocketAddress("192.168.1.1", 8080));
|
||||
assertThat(filter).matches(new InetSocketAddress("192.168.100.200", 8080));
|
||||
assertThat(filter).doesNotMatch(new InetSocketAddress("10.0.0.1", 8080));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class AndTests {
|
||||
|
||||
@Test
|
||||
void stringsWhenEmpty() {
|
||||
InetAddressFilter originalFilter = (address) -> true;
|
||||
InetAddressFilter filter = originalFilter.and(new String[] {});
|
||||
assertThat(filter).isSameAs(originalFilter);
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringsWhenSingle() {
|
||||
InetAddressFilter originalFilter = InetAddressFilter.of("192.168.1.1/24");
|
||||
InetAddressFilter filter = originalFilter.and("192.168.1.1");
|
||||
assertThat(originalFilter).matches("192.168.1.1");
|
||||
assertThat(originalFilter).matches("192.168.1.2");
|
||||
assertThat(filter).matches("192.168.1.1");
|
||||
assertThat(filter).doesNotMatch("192.168.1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringsWhenMultiple() {
|
||||
InetAddressFilter originalFilter = InetAddressFilter.of("192.168.1.1/16");
|
||||
InetAddressFilter filter = originalFilter.and("192.168.1.1/24", "192.168.1.1");
|
||||
assertThat(originalFilter).matches("192.168.1.1");
|
||||
assertThat(originalFilter).matches("192.168.1.2");
|
||||
assertThat(originalFilter).matches("192.168.2.1");
|
||||
assertThat(filter).matches("192.168.1.1");
|
||||
assertThat(filter).doesNotMatch("192.168.1.2");
|
||||
assertThat(filter).doesNotMatch("192.168.2.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void filters() {
|
||||
InetAddressFilter startsWithTen = (address) -> address.getHostAddress().startsWith("10.");
|
||||
InetAddressFilter endsWithOne = (address) -> address.getHostAddress().endsWith(".1");
|
||||
InetAddressFilter filter = startsWithTen.and(endsWithOne);
|
||||
assertThat(filter).matches("10.0.0.1");
|
||||
assertThat(filter).doesNotMatch("10.0.0.2");
|
||||
assertThat(filter).doesNotMatch("192.168.1.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void collection() {
|
||||
InetAddressFilter originalfilter = InetAddressFilter.of("192.168.1.1/24");
|
||||
InetAddressFilter filter = originalfilter.and(List.of(InetAddressFilter.of("192.168.1.1")));
|
||||
assertThat(originalfilter).matches("192.168.1.1");
|
||||
assertThat(originalfilter).matches("192.168.1.2");
|
||||
assertThat(filter).matches("192.168.1.1");
|
||||
assertThat(filter).doesNotMatch("192.168.1.2");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class AndNotTests {
|
||||
|
||||
@Test
|
||||
void stringsWhenEmpty() {
|
||||
InetAddressFilter originalFilter = (address) -> true;
|
||||
InetAddressFilter filter = originalFilter.andNot(new String[] {});
|
||||
assertThat(filter).isSameAs(originalFilter);
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringsWhenSingle() {
|
||||
InetAddressFilter originalFilter = InetAddressFilter.of("192.168.1.1/24");
|
||||
InetAddressFilter filter = originalFilter.andNot("192.168.1.1");
|
||||
assertThat(originalFilter).matches("192.168.1.1");
|
||||
assertThat(originalFilter).matches("192.168.1.2");
|
||||
assertThat(filter).doesNotMatch("192.168.1.1");
|
||||
assertThat(filter).matches("192.168.1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringsWhenMultiple() {
|
||||
InetAddressFilter originalFilter = InetAddressFilter.of("192.168.1.1/24");
|
||||
InetAddressFilter filter = originalFilter.andNot("192.168.1.1", "192.168.1.2");
|
||||
assertThat(originalFilter).matches("192.168.1.1");
|
||||
assertThat(originalFilter).matches("192.168.1.2");
|
||||
assertThat(originalFilter).matches("192.168.1.3");
|
||||
assertThat(filter).doesNotMatch("192.168.1.1");
|
||||
assertThat(filter).doesNotMatch("192.168.1.2");
|
||||
assertThat(filter).matches("192.168.1.3");
|
||||
}
|
||||
|
||||
@Test
|
||||
void filters() {
|
||||
InetAddressFilter startsWithTen = (address) -> address.getHostAddress().startsWith("10.");
|
||||
InetAddressFilter endsWithOne = (address) -> address.getHostAddress().endsWith(".1");
|
||||
InetAddressFilter filter = startsWithTen.andNot(endsWithOne);
|
||||
assertThat(filter).doesNotMatch("10.0.0.1");
|
||||
assertThat(filter).matches("10.0.0.2");
|
||||
assertThat(filter).doesNotMatch("192.168.1.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void collection() {
|
||||
InetAddressFilter originalFilter = InetAddressFilter.of("192.168.1.1/24");
|
||||
InetAddressFilter filter = originalFilter.andNot(List.of(InetAddressFilter.of("192.168.1.1")));
|
||||
assertThat(originalFilter).matches("192.168.1.1");
|
||||
assertThat(originalFilter).matches("192.168.1.2");
|
||||
assertThat(filter).doesNotMatch("192.168.1.1");
|
||||
assertThat(filter).matches("192.168.1.2");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class OrTests {
|
||||
|
||||
@Test
|
||||
void stringsWhenEmpty() {
|
||||
InetAddressFilter originalFilter = (address) -> true;
|
||||
InetAddressFilter filter = originalFilter.or(new String[] {});
|
||||
assertThat(filter).isSameAs(originalFilter);
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringsWhenSingle() {
|
||||
InetAddressFilter originalFilter = InetAddressFilter.of("192.168.1.1");
|
||||
InetAddressFilter filter = originalFilter.or("192.168.1.2");
|
||||
assertThat(originalFilter).matches("192.168.1.1");
|
||||
assertThat(originalFilter).doesNotMatch("192.168.1.2");
|
||||
assertThat(filter).matches("192.168.1.1");
|
||||
assertThat(filter).matches("192.168.1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringsWhenMultiple() {
|
||||
InetAddressFilter originalFilter = InetAddressFilter.of("192.168.1.1");
|
||||
InetAddressFilter filter = originalFilter.or("192.168.1.2", "192.168.1.3");
|
||||
assertThat(originalFilter).matches("192.168.1.1");
|
||||
assertThat(originalFilter).doesNotMatch("192.168.1.2");
|
||||
assertThat(originalFilter).doesNotMatch("192.168.1.3");
|
||||
assertThat(filter).matches("192.168.1.1");
|
||||
assertThat(filter).matches("192.168.1.2");
|
||||
assertThat(filter).matches("192.168.1.3");
|
||||
}
|
||||
|
||||
@Test
|
||||
void filter() {
|
||||
InetAddressFilter originalFilter = InetAddressFilter.of("192.168.1.1");
|
||||
InetAddressFilter filter = originalFilter.or(InetAddressFilter.of("192.168.1.2"));
|
||||
assertThat(originalFilter).matches("192.168.1.1");
|
||||
assertThat(originalFilter).doesNotMatch("192.168.1.2");
|
||||
assertThat(filter).matches("192.168.1.1");
|
||||
assertThat(filter).matches("192.168.1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void collection() {
|
||||
InetAddressFilter originalFilter = InetAddressFilter.of("192.168.1.1");
|
||||
InetAddressFilter filter = originalFilter.or(List.of(InetAddressFilter.of("192.168.1.2")));
|
||||
assertThat(originalFilter).matches("192.168.1.1");
|
||||
assertThat(originalFilter).doesNotMatch("192.168.1.2");
|
||||
assertThat(filter).matches("192.168.1.1");
|
||||
assertThat(filter).matches("192.168.1.2");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class NegateTests {
|
||||
|
||||
@Test
|
||||
void negate() {
|
||||
InetAddressFilter originalFilter = InetAddressFilter.of("192.168.1.1");
|
||||
InetAddressFilter filter = originalFilter.negate();
|
||||
assertThat(originalFilter).matches("192.168.1.1");
|
||||
assertThat(originalFilter).doesNotMatch("192.168.1.2");
|
||||
assertThat(filter).doesNotMatch("192.168.1.1");
|
||||
assertThat(filter).matches("192.168.1.2");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ExternalAddressesTests {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("NullAway") // Test null check
|
||||
void nullInetAddress() {
|
||||
InetAddressFilter filter = InetAddressFilter.externalAddresses();
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> filter.matches((InetAddress) null))
|
||||
.withMessage("'address' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv4Public() {
|
||||
InetAddressFilter filter = InetAddressFilter.externalAddresses();
|
||||
assertThat(filter).matches("8.8.8.8");
|
||||
assertThat(filter).matches("1.1.1.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv6Public() {
|
||||
InetAddressFilter filter = InetAddressFilter.externalAddresses();
|
||||
assertThat(filter).matches("2001:4860:4860::8888");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv4Private() {
|
||||
InetAddressFilter filter = InetAddressFilter.externalAddresses();
|
||||
assertThat(filter).doesNotMatch("192.168.1.1");
|
||||
assertThat(filter).doesNotMatch("10.0.0.1");
|
||||
assertThat(filter).doesNotMatch("172.16.0.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv4Loopback() {
|
||||
InetAddressFilter filter = InetAddressFilter.externalAddresses();
|
||||
assertThat(filter).doesNotMatch("127.0.0.1");
|
||||
assertThat(filter).doesNotMatch("127.1.1.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv4LinkLocal() {
|
||||
InetAddressFilter filter = InetAddressFilter.externalAddresses();
|
||||
assertThat(filter).doesNotMatch("169.254.0.0");
|
||||
assertThat(filter).doesNotMatch("169.254.169.254");
|
||||
assertThat(filter).doesNotMatch("169.254.255.255");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv6Loopback() {
|
||||
InetAddressFilter filter = InetAddressFilter.externalAddresses();
|
||||
assertThat(filter).doesNotMatch("::1");
|
||||
assertThat(filter).doesNotMatch("0000::1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv6UniqueLocal() {
|
||||
InetAddressFilter filter = InetAddressFilter.externalAddresses();
|
||||
assertThat(filter).doesNotMatch("fc00::1");
|
||||
assertThat(filter).doesNotMatch("fd00::1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv4NonRoutable() {
|
||||
InetAddressFilter filter = InetAddressFilter.externalAddresses();
|
||||
assertThat(filter).doesNotMatch("0.0.0.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv6NonRoutable() {
|
||||
InetAddressFilter filter = InetAddressFilter.externalAddresses();
|
||||
assertThat(filter).doesNotMatch("0000:0000:0000:0000:0000:0000:0000:0000");
|
||||
assertThat(filter).doesNotMatch("::");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class InternalAddresses {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("NullAway") // Test null check
|
||||
void nullInetAddress() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> filter.matches((InetAddress) null))
|
||||
.withMessage("'address' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv4Loopback() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).matches("127.0.0.1");
|
||||
assertThat(filter).matches("127.1.1.1");
|
||||
assertThat(filter).matches("127.0.0.255");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv6Loopback() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).matches("::1");
|
||||
assertThat(filter).matches("0000::1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv4PrivateClass10() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).matches("10.0.0.1");
|
||||
assertThat(filter).matches("10.255.255.255");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv4PrivateClass192() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).matches("192.168.0.1");
|
||||
assertThat(filter).matches("192.168.255.255");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv4LinkLocal() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).matches("169.254.0.0");
|
||||
assertThat(filter).matches("169.254.169.254");
|
||||
assertThat(filter).matches("169.254.255.255");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv4PrivateClass172() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).matches("172.16.0.1");
|
||||
assertThat(filter).matches("172.16.255.255");
|
||||
assertThat(filter).matches("172.17.1.1");
|
||||
assertThat(filter).matches("172.31.255.255");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv4MappedIpv6Internal() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).matches("::ffff:127.0.0.1");
|
||||
assertThat(filter).matches("::ffff:192.168.1.1");
|
||||
assertThat(filter).matches("::ffff:169.254.169.254");
|
||||
assertThat(filter).matches("::ffff:10.0.0.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv6UniqueLocal() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).matches("fc00::1");
|
||||
assertThat(filter).matches("fd00::1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv6TranslationWithInternalIpv4() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).matches("64:ff9b::10.0.0.1");
|
||||
assertThat(filter).matches("64:ff9b::127.0.0.1");
|
||||
assertThat(filter).matches("64:ff9b::192.168.1.1");
|
||||
assertThat(filter).matches("64:ff9b::172.16.0.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv6TranslationWithIpv4StartsWith192ButNot168() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).doesNotMatch("64:ff9b::192.0.2.1");
|
||||
assertThat(filter).doesNotMatch("64:ff9b::192.167.1.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv6TranslationWithIpv4StartsWith172And16() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).matches("64:ff9b::172.16.0.1");
|
||||
assertThat(filter).matches("64:ff9b::172.16.255.255");
|
||||
}
|
||||
|
||||
@Test
|
||||
@ValueSource(strings = {})
|
||||
void ipv6TranslationWithExternalIpv4() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).doesNotMatch("64:ff9b::8.8.8.8");
|
||||
assertThat(filter).doesNotMatch("64:ff9b::1.1.1.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ppv6NonTranslationPrefixByte0() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).doesNotMatch("65:ff9b::10.0.0.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv6NonTranslationPrefixByte1() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).doesNotMatch("64:fe9b::10.0.0.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv6NonTranslationPrefixByte2() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).doesNotMatch("64:ff9a::10.0.0.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv6NonTranslationPrefixByte3() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).doesNotMatch("64:ff9c::10.0.0.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv4Public() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).doesNotMatch("8.8.8.8");
|
||||
assertThat(filter).doesNotMatch("1.1.1.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv4StartsWith192ButNot168() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).doesNotMatch("192.0.2.1");
|
||||
assertThat(filter).doesNotMatch("192.167.1.1");
|
||||
assertThat(filter).doesNotMatch("192.169.1.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv4StartsWith172ButNotPrivate16To31() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).doesNotMatch("172.15.1.1");
|
||||
assertThat(filter).doesNotMatch("172.32.1.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv6Public() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).doesNotMatch("2001:4860:4860::8888");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv4NonRoutable() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).doesNotMatch("0.0.0.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ipv6NonRoutable() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses();
|
||||
assertThat(filter).doesNotMatch("0000:0000:0000:0000:0000:0000:0000:0000");
|
||||
assertThat(filter).doesNotMatch("::");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Routable {
|
||||
|
||||
@Test
|
||||
void nonRoutable() {
|
||||
InetAddressFilter filter = InetAddressFilter.routable();
|
||||
assertThat(filter).doesNotMatch("0.0.0.0");
|
||||
assertThat(filter).doesNotMatch("0000:0000:0000:0000:0000:0000:0000:0000");
|
||||
assertThat(filter).doesNotMatch("::");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void routable() {
|
||||
InetAddressFilter filter = InetAddressFilter.routable();
|
||||
assertThat(filter).matches("0.0.0.1");
|
||||
assertThat(filter).matches("0000:0000:0000:0000:0000:0000:0000:0001");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class NotTests {
|
||||
|
||||
@Test
|
||||
void stringsWhenEmpty() {
|
||||
InetAddressFilter filter = InetAddressFilter.not(new String[] {});
|
||||
assertThat(filter).matches("192.168.1.1");
|
||||
assertThat(filter).matches("8.8.8.8");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringsWhenSingle() {
|
||||
InetAddressFilter filter = InetAddressFilter.not("192.168.1.1");
|
||||
assertThat(filter).doesNotMatch("192.168.1.1");
|
||||
assertThat(filter).matches("192.168.1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringsWhenMultiple() {
|
||||
InetAddressFilter filter = InetAddressFilter.not("192.168.1.1", "10.0.0.1");
|
||||
assertThat(filter).doesNotMatch("192.168.1.1");
|
||||
assertThat(filter).doesNotMatch("10.0.0.1");
|
||||
assertThat(filter).matches("8.8.8.8");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringsWhenCidr() {
|
||||
InetAddressFilter filter = InetAddressFilter.not("192.168.1.0/24");
|
||||
assertThat(filter).matches("192.168.2.1");
|
||||
assertThat(filter).doesNotMatch("192.168.1.1");
|
||||
assertThat(filter).doesNotMatch("192.168.1.255");
|
||||
}
|
||||
|
||||
@Test
|
||||
void filters() {
|
||||
InetAddressFilter filter = InetAddressFilter.not(InetAddressFilter.of("192.168.1.1"));
|
||||
assertThat(filter).doesNotMatch("192.168.1.1");
|
||||
assertThat(filter).matches("192.168.1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void collection() {
|
||||
InetAddressFilter filter = InetAddressFilter.not(List.of(InetAddressFilter.of("192.168.1.1")));
|
||||
assertThat(filter).doesNotMatch("192.168.1.1");
|
||||
assertThat(filter).matches("192.168.1.2");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class OfTests {
|
||||
|
||||
@Test
|
||||
void stringsWhenEmpty() {
|
||||
InetAddressFilter filter = InetAddressFilter.of(new String[] {});
|
||||
assertThat(filter).doesNotMatch("192.168.1.1");
|
||||
assertThat(filter).doesNotMatch("8.8.8.8");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringsWhenSingle() {
|
||||
InetAddressFilter filter = InetAddressFilter.of("192.168.1.1");
|
||||
assertThat(filter).matches("192.168.1.1");
|
||||
assertThat(filter).doesNotMatch("192.168.1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringsWhenMultiple() {
|
||||
InetAddressFilter filter = InetAddressFilter.of("192.168.1.1", "10.0.0.1");
|
||||
assertThat(filter).matches("192.168.1.1");
|
||||
assertThat(filter).matches("10.0.0.1");
|
||||
assertThat(filter).doesNotMatch("8.8.8.8");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringsWhenCidr() {
|
||||
InetAddressFilter filter = InetAddressFilter.of("192.168.1.0/24");
|
||||
assertThat(filter).matches("192.168.1.1");
|
||||
assertThat(filter).matches("192.168.1.255");
|
||||
assertThat(filter).doesNotMatch("192.168.2.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void filter() {
|
||||
InetAddressFilter originalfilter = (address) -> address.getHostAddress().startsWith("10.");
|
||||
InetAddressFilter filter = InetAddressFilter.of(originalfilter);
|
||||
assertThat(filter).matches("10.0.0.1");
|
||||
assertThat(filter).doesNotMatch("192.168.1.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void collection() {
|
||||
InetAddressFilter originalfilter = (address) -> address.getHostAddress().startsWith("10.");
|
||||
InetAddressFilter filter = InetAddressFilter.of(List.of(originalfilter));
|
||||
assertThat(filter).matches("10.0.0.1");
|
||||
assertThat(filter).doesNotMatch("192.168.1.1");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class CompositeTests {
|
||||
|
||||
@Test
|
||||
void ofAndNot() {
|
||||
InetAddressFilter filter = InetAddressFilter.of("192.168.1.0/24").andNot("192.168.1.100");
|
||||
assertThat(filter).matches("192.168.1.1");
|
||||
assertThat(filter).doesNotMatch("192.168.1.100");
|
||||
assertThat(filter).doesNotMatch("192.168.2.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofOr() {
|
||||
InetAddressFilter filter = InetAddressFilter.of("192.168.1.100").or("192.168.1.101");
|
||||
assertThat(filter).matches("192.168.1.100");
|
||||
assertThat(filter).matches("192.168.1.101");
|
||||
assertThat(filter).doesNotMatch("192.168.1.102");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofAnd() {
|
||||
InetAddressFilter filter = InetAddressFilter.of("192.168.1.0/24")
|
||||
.and((address) -> address.getHostAddress().endsWith(".1"));
|
||||
assertThat(filter).matches("192.168.1.1");
|
||||
assertThat(filter).doesNotMatch("192.168.1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofInternalAddressOrAndNot() {
|
||||
InetAddressFilter filter = InetAddressFilter.internalAddresses()
|
||||
.or("8.8.8.8", "8.8.4.4")
|
||||
.andNot("192.168.2.0/24");
|
||||
assertThat(filter).matches("192.168.1.1");
|
||||
assertThat(filter).matches("8.8.8.8");
|
||||
assertThat(filter).matches("8.8.4.4");
|
||||
assertThat(filter).doesNotMatch("192.168.2.1");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class AllTests {
|
||||
|
||||
@Test
|
||||
void all() {
|
||||
InetAddressFilter filter = InetAddressFilter.all();
|
||||
assertThat(filter).matches("192.168.1.1");
|
||||
assertThat(filter).matches("8.8.8.8");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("NullAway") // Test null check
|
||||
void allWhenNull() {
|
||||
InetAddressFilter filter = InetAddressFilter.all();
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> filter.matches((InetAddress) null))
|
||||
.withMessage("'address' must not be null");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class NoneTests {
|
||||
|
||||
@Test
|
||||
void none() {
|
||||
InetAddressFilter filter = InetAddressFilter.none();
|
||||
assertThat(filter).doesNotMatch("192.168.1.1");
|
||||
assertThat(filter).doesNotMatch("8.8.8.8");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("NullAway") // Test null check
|
||||
void noneWhenNull() {
|
||||
InetAddressFilter filter = InetAddressFilter.none();
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> filter.matches((InetAddress) null))
|
||||
.withMessage("'address' must not be null");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class AdaptTests {
|
||||
|
||||
@Test
|
||||
void adaptsSpringSecurity() {
|
||||
InetAddressMatcher securityMatcher = InetAddressMatchers.matchInternal().build();
|
||||
InetAddressFilter filter = InetAddressFilter.adapt(securityMatcher::matches);
|
||||
assertThat(filter).matches("127.0.0.1");
|
||||
assertThat(filter).doesNotMatch("8.8.8.8");
|
||||
assertThatFilterDoesNotMatchNull(filter);
|
||||
}
|
||||
|
||||
@SuppressWarnings("NullAway") // Test null check
|
||||
private void assertThatFilterDoesNotMatchNull(InetAddressFilter filter) {
|
||||
assertThat(filter).doesNotMatch((InetAddress) null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.http.client;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link IpAddress}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class IpAddressTests {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("NullAway") // Test null check
|
||||
void ofWhenAddressIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> IpAddress.of(null))
|
||||
.withMessage("'address' must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenAddressIsEmptyThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> IpAddress.of(""))
|
||||
.withMessage("'address' must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWithMaskWhenAddressIsEmptyThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> IpAddress.of("192.168.1.1/"))
|
||||
.withMessage("'address' subnet mask must be a number");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenUnmaskedIpAddress() throws Exception {
|
||||
IpAddress address = IpAddress.of("192.168.1.1");
|
||||
assertThat(address).extracting("address").isEqualTo(InetAddress.getByName("192.168.1.1"));
|
||||
assertThat(address).extracting("maskBitSize").isEqualTo(-1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenMaskedIpAddress() throws Exception {
|
||||
IpAddress address = IpAddress.of("192.168.1.1/24");
|
||||
assertThat(address).extracting("address").isEqualTo(InetAddress.getByName("192.168.1.1"));
|
||||
assertThat(address).extracting("maskBitSize").isEqualTo(24);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseInetAddressWhenIpv4() throws Exception {
|
||||
InetAddress parsed = IpAddress.parseInetAddress("192.168.1.1");
|
||||
assertThat(parsed).isEqualTo(InetAddress.getByName("192.168.1.1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseInetAddressWhenIpv6InUrl() {
|
||||
InetAddress parsed = IpAddress.parseInetAddress("[::1]");
|
||||
assertThat(parsed.isLoopbackAddress()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseInetAddressWhenIpv6Shortcut() {
|
||||
InetAddress parsed = IpAddress.parseInetAddress("::1");
|
||||
assertThat(parsed.isLoopbackAddress()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseInetAddressWhenLikelyHost() {
|
||||
String message = "must be an IP address and not a host name";
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> IpAddress.parseInetAddress("https://example.com"))
|
||||
.withMessageContaining(message);
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> IpAddress.parseInetAddress("192.168.1.2.3"))
|
||||
.withMessageContaining(message);
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> IpAddress.parseInetAddress("G001:0db8:0000:0000:0000:0000:0000:0000"))
|
||||
.withMessageContaining(message);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseInetAddressWhenCannotBeParsed() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> IpAddress.parseInetAddress("2001:0db8:0000:0000:0000:0000:0000:000G"))
|
||||
.withMessageContaining("must be parsable to an InetAddress");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWithHostnameThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> IpAddress.of("example.com"))
|
||||
.withMessage("'address' [example.com] must be an IP address and not a host name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void matcherWhenUnmaskedIpv4() {
|
||||
IpAddress address = IpAddress.of("192.168.1.1");
|
||||
assertThatFilter(address).matches("192.168.1.1");
|
||||
assertThatFilter(address).doesNotMatch("192.168.1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void matcherWhenUnmaskedIpv6() {
|
||||
IpAddress address = IpAddress.of("fe80::21f:5bff:fe33:bd68");
|
||||
assertThatFilter(address).matches("fe80::21f:5bff:fe33:bd68");
|
||||
assertThatFilter(address).doesNotMatch("fe80::21f:5bff:fe33:bd69");
|
||||
}
|
||||
|
||||
@Test
|
||||
void matcherWhenMaskedIpv4() {
|
||||
IpAddress address = IpAddress.of("192.168.1.0/24");
|
||||
assertThatFilter(address).doesNotMatch("192.168.2.1");
|
||||
assertThatFilter(address).doesNotMatch("192.168.0.255");
|
||||
}
|
||||
|
||||
@Test
|
||||
void matcherWhenMaskedWithZero() {
|
||||
IpAddress address = IpAddress.of("192.168.1.0/0");
|
||||
assertThatFilter(address).matches("192.168.1.1");
|
||||
assertThatFilter(address).matches("192.168.1.255");
|
||||
assertThatFilter(address).matches("8.8.8.8");
|
||||
}
|
||||
|
||||
@Test
|
||||
void matcherWhenMaskedIpv6() {
|
||||
IpAddress address = IpAddress.of("2001:db8::/48");
|
||||
assertThatFilter(address).matches("2001:db8:0:0:0:0:0:0");
|
||||
assertThatFilter(address).matches("2001:db8:0:ffff:ffff:ffff:ffff:ffff");
|
||||
assertThatFilter(address).doesNotMatch("2001:db8:1:0:0:0:0:0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void matcherWhenMaskedIpv4OutsideOfByteBoundary() {
|
||||
IpAddress address = IpAddress.of("192.168.1.0/30");
|
||||
assertThatFilter(address).matches("192.168.1.0");
|
||||
assertThatFilter(address).matches("192.168.1.1");
|
||||
assertThatFilter(address).matches("192.168.1.2");
|
||||
assertThatFilter(address).matches("192.168.1.3");
|
||||
assertThatFilter(address).doesNotMatch("192.168.1.4");
|
||||
}
|
||||
|
||||
@Test
|
||||
void matcherWhenIpv4DoesNotMatchIpv6() {
|
||||
assertThatFilter(IpAddress.of("192.168.1.1")).doesNotMatch("fe80::21f:5bff:fe33:bd68");
|
||||
assertThatFilter(IpAddress.of("8.8.8.8")).doesNotMatch("0808:0808::");
|
||||
}
|
||||
|
||||
@Test
|
||||
void matcherWhenIpv6DoesNotMatchIpv4() {
|
||||
assertThatFilter(IpAddress.of("fe80::21f:5bff:fe33:bd68")).doesNotMatch("192.168.1.1");
|
||||
assertThatFilter(IpAddress.of("0808:0808::/32")).doesNotMatch("8.8.8.8");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("NullAway") // Test null check
|
||||
void matcherWhenCheckingNullThrowsException() {
|
||||
IpAddress address = IpAddress.of("192.168.1.1");
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> address.filter().matches((InetAddress) null))
|
||||
.withMessage("'address' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void addressesInIpRangeMatch() {
|
||||
for (int i = 0; i < 255; i++) {
|
||||
assertThatFilter(IpAddress.of("192.168.1.0/24")).matches("192.168.1." + i);
|
||||
}
|
||||
assertThatFilter(IpAddress.of("192.168.1.0/25")).matches("192.168.1.127").doesNotMatch("192.168.1.128");
|
||||
assertThatFilter(IpAddress.of("192.168.1.128/25")).matches("192.168.1.255");
|
||||
assertThatFilter(IpAddress.of("192.168.1.192/26")).matches("192.168.1.255");
|
||||
assertThatFilter(IpAddress.of("192.168.1.224/27")).matches("192.168.1.255");
|
||||
assertThatFilter(IpAddress.of("192.168.1.240/27")).matches("192.168.1.255");
|
||||
assertThatFilter(IpAddress.of("192.168.1.255/32")).matches("192.168.1.255");
|
||||
assertThatFilter(IpAddress.of("202.24.0.0/14")).matches("202.24.199.127");
|
||||
assertThatFilter(IpAddress.of("202.24.0.0/14")).matches("202.25.179.135");
|
||||
assertThatFilter(IpAddress.of("202.24.0.0/14")).matches("202.26.179.135");
|
||||
}
|
||||
|
||||
private static InetAddressFilterAssert assertThatFilter(IpAddress address) {
|
||||
return new InetAddressFilterAssert(address.filter());
|
||||
}
|
||||
|
||||
}
|
||||
+26
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.net.ProxySelector;
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.Executor;
|
||||
@@ -29,6 +30,7 @@ import org.springframework.http.client.JdkClientHttpRequestFactory;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link JdkClientHttpRequestFactoryBuilder} and {@link JdkHttpClientBuilder}.
|
||||
@@ -70,6 +72,30 @@ class JdkClientHttpRequestFactoryBuilderTests
|
||||
customizer.assertCalled();
|
||||
}
|
||||
|
||||
@Test
|
||||
void withProxySelector() {
|
||||
ProxySelector proxySelector = mock();
|
||||
JdkClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.jdk()
|
||||
.withProxySelector(proxySelector)
|
||||
.build();
|
||||
HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(factory, "httpClient");
|
||||
assertThat(httpClient).isNotNull();
|
||||
assertThat(httpClient.proxy()).contains(proxySelector);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withProxySelectorWhenHasInetAddressMatcher() {
|
||||
ProxySelector proxySelector = mock();
|
||||
JdkClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.jdk()
|
||||
.withProxySelector(proxySelector)
|
||||
.build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses()));
|
||||
HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(factory, "httpClient");
|
||||
assertThat(httpClient).isNotNull();
|
||||
ProxySelector actual = httpClient.proxy().get();
|
||||
assertThat(actual).matches((proxy) -> proxy.getClass().getName().contains("JdkFiltered"));
|
||||
assertThat(actual).extracting("delegate").isEqualTo(proxySelector);
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultCookieHandling() {
|
||||
JdkClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.jdk()
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.http.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Proxy;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link JdkFilteredProxySelector}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class JdkFilteredProxySelectorTests {
|
||||
|
||||
@Test
|
||||
void selectWhenMatchesResolvedHost() throws Exception {
|
||||
URI localhost = new URI("http://localhost");
|
||||
ProxySelector delegate = mock(ProxySelector.class);
|
||||
Proxy proxy = mock();
|
||||
given(delegate.select(localhost)).willReturn(List.of(proxy));
|
||||
JdkFilteredProxySelector proxySelector = new JdkFilteredProxySelector(delegate,
|
||||
InetAddressFilter.internalAddresses());
|
||||
assertThat(proxySelector.select(localhost)).containsExactly(proxy);
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectWhenDoesNotMatchResolvedHost() throws Exception {
|
||||
URI localhost = new URI("http://localhost");
|
||||
ProxySelector delegate = mock(ProxySelector.class);
|
||||
Proxy proxy = mock();
|
||||
given(delegate.select(localhost)).willReturn(List.of(proxy));
|
||||
JdkFilteredProxySelector proxySelector = new JdkFilteredProxySelector(delegate,
|
||||
InetAddressFilter.externalAddresses());
|
||||
assertThatExceptionOfType(FilteredHostException.class).isThrownBy(() -> proxySelector.select(localhost));
|
||||
}
|
||||
|
||||
@Test
|
||||
void connectFailDelegates() throws Exception {
|
||||
URI localhost = new URI("http://localhost");
|
||||
ProxySelector delegate = mock(ProxySelector.class);
|
||||
JdkFilteredProxySelector proxySelector = new JdkFilteredProxySelector(delegate,
|
||||
InetAddressFilter.externalAddresses());
|
||||
SocketAddress address = mock();
|
||||
IOException ex = new IOException();
|
||||
proxySelector.connectFailed(localhost, address, ex);
|
||||
then(delegate).should().connectFailed(localhost, address, ex);
|
||||
}
|
||||
|
||||
}
|
||||
+22
@@ -21,6 +21,7 @@ import org.eclipse.jetty.client.HttpClientTransport;
|
||||
import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP;
|
||||
import org.eclipse.jetty.http.HttpCookieStore;
|
||||
import org.eclipse.jetty.io.ClientConnector;
|
||||
import org.eclipse.jetty.util.SocketAddressResolver;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
@@ -29,6 +30,7 @@ import org.springframework.http.client.JettyClientHttpRequestFactory;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link JettyClientHttpRequestFactoryBuilder} and
|
||||
@@ -78,6 +80,26 @@ class JettyClientHttpRequestFactoryBuilderTests
|
||||
.isInstanceOf(TestHttpClientTransport.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withSocketAddressResolver() {
|
||||
SocketAddressResolver socketAddressResolver = mock();
|
||||
JettyClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.jetty()
|
||||
.withSocketAddressResolver(socketAddressResolver)
|
||||
.build();
|
||||
assertThat(factory).extracting("httpClient.resolver").isSameAs(socketAddressResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withSocketAddressResolverWhenHasInetAddressMatcher() {
|
||||
SocketAddressResolver socketAddressResolver = mock();
|
||||
JettyClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.jetty()
|
||||
.withSocketAddressResolver(socketAddressResolver)
|
||||
.build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses()));
|
||||
assertThat(factory).extracting("httpClient.resolver")
|
||||
.matches((resolver) -> resolver.getClass().getName().contains("JettyFiltered"));
|
||||
assertThat(factory).extracting("httpClient.resolver.delegate").isSameAs(socketAddressResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultCookieHandling() {
|
||||
JettyClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.jetty()
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.http.client;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jetty.util.Promise;
|
||||
import org.eclipse.jetty.util.SocketAddressResolver;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link JettyFilteredSocketAddressResolver}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class JettyFilteredSocketAddressResolverTests {
|
||||
|
||||
@Test
|
||||
void resolveWhenMatchedOnlyElement() {
|
||||
InetSocketAddress localhost = inetSocketAddress("localhost", 8080);
|
||||
SocketAddressResolver delegate = (host, port, context, promise) -> promise.succeeded(List.of(localhost));
|
||||
JettyFilteredSocketAddressResolver resolver = new JettyFilteredSocketAddressResolver(delegate,
|
||||
InetAddressFilter.internalAddresses());
|
||||
Promise<List<InetSocketAddress>> promise = mock();
|
||||
resolver.resolve("localhost", 8080, Collections.emptyMap(), promise);
|
||||
then(promise).should().succeeded(List.of(localhost));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveWhenMatchedOneOfManyElements() {
|
||||
InetSocketAddress localhost = inetSocketAddress("localhost", 8080);
|
||||
InetSocketAddress remote = inetSocketAddress("8.8.8.8", 8080);
|
||||
SocketAddressResolver delegate = (host, port, context, promise) -> promise
|
||||
.succeeded(List.of(localhost, remote));
|
||||
JettyFilteredSocketAddressResolver resolver = new JettyFilteredSocketAddressResolver(delegate,
|
||||
InetAddressFilter.internalAddresses());
|
||||
Promise<List<InetSocketAddress>> promise = mock();
|
||||
resolver.resolve("localhost", 8080, Collections.emptyMap(), promise);
|
||||
then(promise).should().succeeded(List.of(localhost));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveWhenMatchedNoElements() {
|
||||
InetSocketAddress localhost = inetSocketAddress("localhost", 8080);
|
||||
InetSocketAddress remote = inetSocketAddress("8.8.8.8", 8080);
|
||||
SocketAddressResolver delegate = (host, port, context, promise) -> promise
|
||||
.succeeded(List.of(localhost, remote));
|
||||
JettyFilteredSocketAddressResolver resolver = new JettyFilteredSocketAddressResolver(delegate,
|
||||
InetAddressFilter.externalAddresses().andNot("8.8.8.8"));
|
||||
Promise<List<InetSocketAddress>> promise = mock();
|
||||
resolver.resolve("localhost", 8080, Collections.emptyMap(), promise);
|
||||
ArgumentCaptor<Throwable> failure = ArgumentCaptor.captor();
|
||||
then(promise).should().failed(failure.capture());
|
||||
assertThat(failure.getValue()).isInstanceOf(FilteredHostException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveWhenDelegateFails() {
|
||||
Throwable ex = new RuntimeException();
|
||||
SocketAddressResolver delegate = (host, port, context, promise) -> promise.failed(ex);
|
||||
JettyFilteredSocketAddressResolver resolver = new JettyFilteredSocketAddressResolver(delegate,
|
||||
InetAddressFilter.externalAddresses());
|
||||
Promise<List<InetSocketAddress>> promise = mock();
|
||||
resolver.resolve("localhost", 8080, Collections.emptyMap(), promise);
|
||||
then(promise).should().failed(ex);
|
||||
}
|
||||
|
||||
private InetSocketAddress inetSocketAddress(String host, int port) {
|
||||
try {
|
||||
return new InetSocketAddress(InetAddress.getByName(host), port);
|
||||
}
|
||||
catch (UnknownHostException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.http.client;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.netty.http.client.HttpClientConfig;
|
||||
import reactor.netty.transport.ClientTransport.ResolvedAddressSelector;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReactorFilteredResolvedAddressSelector}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ReactorFilteredResolvedAddressSelectorTests {
|
||||
|
||||
@Test
|
||||
void applyWhenMatchedOnlyElement() {
|
||||
InetSocketAddress localhost = inetSocketAddress("localhost", 8080);
|
||||
ResolvedAddressSelector<HttpClientConfig> delegate = (config, resolvedAddresses) -> resolvedAddresses;
|
||||
HttpClientConfig config = mock();
|
||||
ReactorFilteredResolvedAddressSelector<HttpClientConfig> addressSelector = new ReactorFilteredResolvedAddressSelector<>(
|
||||
delegate, InetAddressFilter.internalAddresses());
|
||||
assertThat(addressSelector.apply(config, List.of(localhost))).isEqualTo(List.of(localhost));
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenMatchedOneOfManyElements() {
|
||||
InetSocketAddress localhost = inetSocketAddress("localhost", 8080);
|
||||
InetSocketAddress remote = inetSocketAddress("8.8.8.8", 8080);
|
||||
ResolvedAddressSelector<HttpClientConfig> delegate = (config, resolvedAddresses) -> resolvedAddresses;
|
||||
HttpClientConfig config = mock();
|
||||
ReactorFilteredResolvedAddressSelector<HttpClientConfig> addressSelector = new ReactorFilteredResolvedAddressSelector<>(
|
||||
delegate, InetAddressFilter.internalAddresses());
|
||||
assertThat(addressSelector.apply(config, List.of(localhost, remote))).isEqualTo(List.of(localhost));
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenMatchedNoElements() {
|
||||
InetSocketAddress localhost = inetSocketAddress("localhost", 8080);
|
||||
InetSocketAddress remote = inetSocketAddress("8.8.8.8", 8080);
|
||||
ResolvedAddressSelector<HttpClientConfig> delegate = (config, resolvedAddresses) -> resolvedAddresses;
|
||||
HttpClientConfig config = mock();
|
||||
ReactorFilteredResolvedAddressSelector<HttpClientConfig> addressSelector = new ReactorFilteredResolvedAddressSelector<>(
|
||||
delegate, InetAddressFilter.externalAddresses().andNot("8.8.8.8"));
|
||||
assertThatExceptionOfType(FilteredHostException.class)
|
||||
.isThrownBy(() -> addressSelector.apply(config, List.of(localhost, remote)))
|
||||
.withMessage("Filtered host '[127.0.0.1, 8.8.8.8]'");
|
||||
|
||||
}
|
||||
|
||||
private InetSocketAddress inetSocketAddress(String host, int port) {
|
||||
try {
|
||||
return new InetSocketAddress(InetAddress.getByName(host), port);
|
||||
}
|
||||
catch (UnknownHostException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+9
@@ -46,6 +46,15 @@ class ReflectiveComponentsClientHttpRequestFactoryBuilderTests
|
||||
super(ClientHttpRequestFactory.class, ClientHttpRequestFactoryBuilder.of(JettyClientHttpRequestFactory::new));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Override
|
||||
void filteredInetAddress() throws Exception {
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> getBuilder()
|
||||
.build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses())))
|
||||
.withMessage("Unable to set InetAddress filter using reflection");
|
||||
}
|
||||
|
||||
@Override
|
||||
void connectWithSslBundle(String httpMethod) throws Exception {
|
||||
HttpClientSettings settings = HttpClientSettings.ofSslBundle(sslBundle());
|
||||
|
||||
+10
-2
@@ -26,7 +26,6 @@ import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
@@ -83,9 +82,18 @@ class SimpleClientHttpRequestFactoryBuilderTests
|
||||
super.redirectDontFollow(httpMethod);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Override
|
||||
void filteredInetAddress() throws Exception {
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> getBuilder()
|
||||
.build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses())))
|
||||
.withMessage("Simple HTTP request factory does not support InetAddress filtering");
|
||||
}
|
||||
|
||||
@Test
|
||||
void throwsWhenCookieHandlingEnabled() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> ClientHttpRequestFactoryBuilder.simple()
|
||||
assertThatIllegalStateException().isThrownBy(() -> ClientHttpRequestFactoryBuilder.simple()
|
||||
.build(HttpClientSettings.defaults().withCookieHandling(HttpCookieHandling.ENABLE)));
|
||||
}
|
||||
|
||||
|
||||
+10
@@ -24,11 +24,13 @@ import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
|
||||
import org.springframework.boot.http.client.HttpClientSettings;
|
||||
import org.springframework.boot.http.client.HttpRedirects;
|
||||
import org.springframework.boot.http.client.InetAddressFilter;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpClientAutoConfiguration}.
|
||||
@@ -62,6 +64,14 @@ class HttpClientAutoConfigurationTests {
|
||||
.isEqualTo(new HttpClientSettings(null, null, Duration.ofSeconds(1), Duration.ofSeconds(2), null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void injectsInetAddressMatcher() {
|
||||
InetAddressFilter matcher = mock();
|
||||
this.contextRunner.withBean(InetAddressFilter.class, () -> matcher)
|
||||
.run((context) -> assertThat(context.getBean(HttpClientSettings.class).inetAddressFilter())
|
||||
.isEqualTo(matcher));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestHttpClientConfiguration {
|
||||
|
||||
|
||||
+24
@@ -34,8 +34,10 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.boot.http.client.FilteredHostException;
|
||||
import org.springframework.boot.http.client.HttpClientSettings;
|
||||
import org.springframework.boot.http.client.HttpRedirects;
|
||||
import org.springframework.boot.http.client.InetAddressFilter;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundleKey;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
@@ -55,6 +57,7 @@ import org.springframework.web.reactive.function.client.ExchangeFunctions;
|
||||
import org.springframework.web.reactive.function.client.WebClientRequestException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatException;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
@@ -192,6 +195,27 @@ abstract class AbstractClientHttpConnectorBuilderTests<T extends ClientHttpConne
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void filteredInetAddress() throws Exception {
|
||||
TomcatServletWebServerFactory webServerFactory = new TomcatServletWebServerFactory(0);
|
||||
WebServer webServer = webServerFactory
|
||||
.getWebServer((context) -> context.addServlet("test", TestServlet.class).addMapping("/"));
|
||||
try {
|
||||
webServer.start();
|
||||
int port = webServer.getPort();
|
||||
URI uri = new URI("http://localhost:%s".formatted(port) + "/redirect");
|
||||
ClientHttpConnector connector = this.builder
|
||||
.build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses()));
|
||||
ClientRequest request = createRequest("GET", uri);
|
||||
assertThatException().isThrownBy(() -> getResponse(connector, request))
|
||||
.matches((ex) -> ex instanceof FilteredHostException || ex.getCause() instanceof FilteredHostException);
|
||||
|
||||
}
|
||||
finally {
|
||||
webServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private ClientRequest createRequest(String httpMethod, URI uri) {
|
||||
return createRequest(HttpMethod.valueOf(httpMethod), uri);
|
||||
}
|
||||
|
||||
+25
@@ -20,6 +20,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.hc.client5.http.DnsResolver;
|
||||
import org.apache.hc.client5.http.HttpRoute;
|
||||
import org.apache.hc.client5.http.async.HttpAsyncClient;
|
||||
import org.apache.hc.client5.http.config.ConnectionConfig;
|
||||
@@ -33,12 +34,14 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.http.client.HttpClientSettings;
|
||||
import org.springframework.boot.http.client.HttpComponentsHttpAsyncClientBuilder;
|
||||
import org.springframework.boot.http.client.InetAddressFilter;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpComponentsClientHttpConnectorBuilder} and
|
||||
@@ -104,6 +107,28 @@ class HttpComponentsClientHttpConnectorBuilderTests
|
||||
customizer.assertCalled();
|
||||
}
|
||||
|
||||
@Test
|
||||
void withDnsResolver() {
|
||||
DnsResolver dnsResolver = mock();
|
||||
HttpComponentsClientHttpConnector connector = ClientHttpConnectorBuilder.httpComponents()
|
||||
.withDnsResolver(dnsResolver)
|
||||
.build();
|
||||
assertThat(connector).extracting("client.manager.connectionOperator.sessionRequester.dnsResolver")
|
||||
.isSameAs(dnsResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withDnsResolverWhenHasInetAddressMatcher() {
|
||||
DnsResolver dnsResolver = mock();
|
||||
HttpComponentsClientHttpConnector connector = ClientHttpConnectorBuilder.httpComponents()
|
||||
.withDnsResolver(dnsResolver)
|
||||
.build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses()));
|
||||
assertThat(connector).extracting("client.manager.connectionOperator.sessionRequester.dnsResolver")
|
||||
.matches((resolver) -> resolver.getClass().getName().contains("HttpComponentsFiltered"));
|
||||
assertThat(connector).extracting("client.manager.connectionOperator.sessionRequester.dnsResolver.delegate")
|
||||
.isSameAs(dnsResolver);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long connectTimeout(HttpComponentsClientHttpConnector connector) {
|
||||
return getConnectorConfig(connector).getConnectTimeout().toMilliseconds();
|
||||
|
||||
+27
@@ -16,18 +16,22 @@
|
||||
|
||||
package org.springframework.boot.http.client.reactive;
|
||||
|
||||
import java.net.ProxySelector;
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.http.client.HttpClientSettings;
|
||||
import org.springframework.boot.http.client.InetAddressFilter;
|
||||
import org.springframework.boot.http.client.JdkHttpClientBuilder;
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.http.client.reactive.JdkClientHttpConnector;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link JdkClientHttpConnectorBuilder} and {@link JdkHttpClientBuilder}.
|
||||
@@ -68,6 +72,29 @@ class JdkClientHttpConnectorBuilderTests extends AbstractClientHttpConnectorBuil
|
||||
customizer.assertCalled();
|
||||
}
|
||||
|
||||
@Test
|
||||
void withProxySelector() {
|
||||
ProxySelector proxySelector = mock();
|
||||
JdkClientHttpConnector connector = ClientHttpConnectorBuilder.jdk().withProxySelector(proxySelector).build();
|
||||
HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(connector, "httpClient");
|
||||
assertThat(httpClient).isNotNull();
|
||||
assertThat(httpClient.proxy()).contains(proxySelector);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withProxySelectorWhenHasInetAddressMatcher() {
|
||||
ProxySelector proxySelector = mock();
|
||||
JdkClientHttpConnector connector = ClientHttpConnectorBuilder.jdk()
|
||||
.withProxySelector(proxySelector)
|
||||
.build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses()));
|
||||
HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(connector, "httpClient");
|
||||
assertThat(httpClient).isNotNull();
|
||||
assertThat(httpClient.proxy()).isNotNull();
|
||||
ProxySelector actual = httpClient.proxy().get();
|
||||
assertThat(actual).matches((proxy) -> proxy.getClass().getName().contains("JdkFiltered"));
|
||||
assertThat(actual).extracting("delegate").isEqualTo(proxySelector);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long connectTimeout(JdkClientHttpConnector connector) {
|
||||
HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(connector, "httpClient");
|
||||
|
||||
+28
-5
@@ -16,19 +16,21 @@
|
||||
|
||||
package org.springframework.boot.http.client.reactive;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpClientTransport;
|
||||
import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP;
|
||||
import org.eclipse.jetty.io.ClientConnector;
|
||||
import org.eclipse.jetty.util.SocketAddressResolver;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.http.client.HttpClientSettings;
|
||||
import org.springframework.boot.http.client.InetAddressFilter;
|
||||
import org.springframework.boot.http.client.JettyHttpClientBuilder;
|
||||
import org.springframework.http.client.reactive.JettyClientHttpConnector;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link JettyClientHttpConnectorBuilder} and {@link JettyHttpClientBuilder}.
|
||||
@@ -76,6 +78,26 @@ class JettyClientHttpConnectorBuilderTests extends AbstractClientHttpConnectorBu
|
||||
.isInstanceOf(TestHttpClientTransport.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withSocketAddressResolver() {
|
||||
SocketAddressResolver socketAddressResolver = mock();
|
||||
JettyClientHttpConnector connector = ClientHttpConnectorBuilder.jetty()
|
||||
.withSocketAddressResolver(socketAddressResolver)
|
||||
.build();
|
||||
assertThat(connector).extracting("httpClient.resolver").isSameAs(socketAddressResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withSocketAddressResolverWhenHasInetAddressMatcher() {
|
||||
SocketAddressResolver socketAddressResolver = mock();
|
||||
JettyClientHttpConnector connector = ClientHttpConnectorBuilder.jetty()
|
||||
.withSocketAddressResolver(socketAddressResolver)
|
||||
.build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses()));
|
||||
assertThat(connector).extracting("httpClient.resolver")
|
||||
.matches((resolver) -> resolver.getClass().getName().contains("JettyFiltered"));
|
||||
assertThat(connector).extracting("httpClient.resolver.delegate").isSameAs(socketAddressResolver);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long connectTimeout(JettyClientHttpConnector connector) {
|
||||
HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(connector, "httpClient");
|
||||
@@ -87,9 +109,10 @@ class JettyClientHttpConnectorBuilderTests extends AbstractClientHttpConnectorBu
|
||||
protected long readTimeout(JettyClientHttpConnector connector) {
|
||||
HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(connector, "httpClient");
|
||||
assertThat(httpClient).isNotNull();
|
||||
Object field = ReflectionTestUtils.getField(httpClient, "readTimeout");
|
||||
assertThat(field).isNotNull();
|
||||
return ((Duration) field).toMillis();
|
||||
HttpClientSettings settings = (HttpClientSettings) ReflectionTestUtils.getField(httpClient, "settings");
|
||||
assertThat(settings).isNotNull();
|
||||
assertThat(settings.readTimeout()).isNotNull();
|
||||
return settings.readTimeout().toMillis();
|
||||
}
|
||||
|
||||
static class TestHttpClientTransport extends HttpClientTransportOverHTTP {
|
||||
|
||||
+27
@@ -25,7 +25,11 @@ import java.util.function.UnaryOperator;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
import reactor.netty.http.client.HttpClientConfig;
|
||||
import reactor.netty.transport.ClientTransport.ResolvedAddressSelector;
|
||||
|
||||
import org.springframework.boot.http.client.HttpClientSettings;
|
||||
import org.springframework.boot.http.client.InetAddressFilter;
|
||||
import org.springframework.boot.http.client.ReactorHttpClientBuilder;
|
||||
import org.springframework.http.client.ReactorResourceFactory;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
@@ -33,6 +37,7 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
||||
/**
|
||||
@@ -98,6 +103,28 @@ class ReactorClientHttpConnectorBuilderTests
|
||||
assertThat(called).containsExactly(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withResolvedAddressSelector() {
|
||||
ResolvedAddressSelector<? super HttpClientConfig> resolvedAddressSelector = mock();
|
||||
ReactorClientHttpConnector connector = ClientHttpConnectorBuilder.reactor()
|
||||
.withResolvedAddressSelector(resolvedAddressSelector)
|
||||
.build();
|
||||
assertThat(connector).extracting("httpClient.config.resolvedAddressesSelector")
|
||||
.isEqualTo(resolvedAddressSelector);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withResolvedAddressSelectorWhenHasInetAddressFilter() {
|
||||
ResolvedAddressSelector<? super HttpClientConfig> resolvedAddressSelector = mock();
|
||||
ReactorClientHttpConnector connector = ClientHttpConnectorBuilder.reactor()
|
||||
.withResolvedAddressSelector(resolvedAddressSelector)
|
||||
.build(HttpClientSettings.defaults().withInetAddressFilter(InetAddressFilter.externalAddresses()));
|
||||
assertThat(connector).extracting("httpClient.config.resolvedAddressesSelector")
|
||||
.matches((selector) -> selector.getClass().getName().contains("ReactorFiltered"));
|
||||
assertThat(connector).extracting("httpClient.config.resolvedAddressesSelector.delegate")
|
||||
.isEqualTo(resolvedAddressSelector);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long connectTimeout(ReactorClientHttpConnector connector) {
|
||||
HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(connector, "httpClient");
|
||||
|
||||
Reference in New Issue
Block a user