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:
Phillip Webb
2026-04-13 22:03:50 -07:00
parent 39ee6098a6
commit 52cef18686
56 changed files with 3272 additions and 63 deletions
@@ -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[]
@@ -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 {
}
@@ -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");
}
}
@@ -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);
}
}
@@ -0,0 +1,4 @@
package org.springframework.boot.docs.io.restclient.globalconfiguration.inetaddressfiltering
class Details {
}
@@ -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")
}
}
@@ -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")
@@ -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;
}
}
}
@@ -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;
}
}
@@ -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);
}
/**
@@ -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.
@@ -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);
}
}
@@ -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();
}
@@ -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();
}
@@ -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;
};
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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.
@@ -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);
}
}
@@ -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;
}
}
@@ -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.
@@ -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);
}
}
}
@@ -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;
}
@@ -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.
@@ -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();
}
}
@@ -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);
@@ -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));
@@ -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(),
@@ -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);
}
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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
@@ -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'");
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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()
@@ -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");
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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());
}
}
@@ -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()
@@ -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);
}
}
@@ -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()
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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());
@@ -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)));
}
@@ -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 {
@@ -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);
}
@@ -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();
@@ -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");
@@ -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 {
@@ -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");