diff --git a/commafeed-server/src/main/java/com/commafeed/backend/HttpClientFactory.java b/commafeed-server/src/main/java/com/commafeed/backend/HttpClientFactory.java index d0c62977..efe452b7 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/HttpClientFactory.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/HttpClientFactory.java @@ -1,8 +1,8 @@ package com.commafeed.backend; import java.net.InetAddress; +import java.net.URI; import java.net.UnknownHostException; -import java.time.Duration; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -18,13 +18,19 @@ import org.apache.hc.client5.http.config.TlsConfig; import org.apache.hc.client5.http.entity.DeflateInputStream; import org.apache.hc.client5.http.entity.InputStreamFactory; import org.apache.hc.client5.http.entity.compress.ContentCoding; +import org.apache.hc.client5.http.impl.DefaultRedirectStrategy; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.client5.http.protocol.RedirectStrategy; import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.util.TimeValue; import org.apache.hc.core5.util.Timeout; import org.brotli.dec.BrotliInputStream; @@ -41,6 +47,8 @@ import nl.altindag.ssl.apache5.util.Apache5SslUtils; @RequiredArgsConstructor public class HttpClientFactory { + private static final DnsResolver DNS_RESOLVER = SystemDefaultDnsResolver.INSTANCE; + private final CommaFeedConfiguration config; private final CommaFeedVersion version; @@ -49,11 +57,10 @@ public class HttpClientFactory { String userAgent = config.httpClient() .userAgent() .orElseGet(() -> String.format("CommaFeed/%s (https://github.com/Athou/commafeed)", version.getVersion())); - return newClient(connectionManager, userAgent, config.httpClient().idleConnectionsEvictionInterval()); + return newClient(config, connectionManager, userAgent); } - private CloseableHttpClient newClient(HttpClientConnectionManager connectionManager, String userAgent, - Duration idleConnectionsEvictionInterval) { + private CloseableHttpClient newClient(CommaFeedConfiguration config, HttpClientConnectionManager connectionManager, String userAgent) { List
headers = new ArrayList<>(); headers.add(new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en")); headers.add(new BasicHeader(HttpHeaders.PRAGMA, "No-cache")); @@ -64,6 +71,10 @@ public class HttpClientFactory { contentDecoderMap.put(ContentCoding.DEFLATE.token(), DeflateInputStream::new); contentDecoderMap.put(ContentCoding.BROTLI.token(), BrotliInputStream::new); + RedirectStrategy redirectStrategy = config.httpClient().blockLocalAddresses() + ? new BlockLocalAddressesRedirectStrategy(DNS_RESOLVER) + : new DefaultRedirectStrategy(); + return HttpClientBuilder.create() .useSystemProperties() .disableAutomaticRetries() @@ -72,16 +83,16 @@ public class HttpClientFactory { .setDefaultHeaders(headers) .setConnectionManager(connectionManager) .evictExpiredConnections() - .evictIdleConnections(TimeValue.of(idleConnectionsEvictionInterval)) + .evictIdleConnections(TimeValue.of(config.httpClient().idleConnectionsEvictionInterval())) .setContentDecoderRegistry(new LinkedHashMap<>(contentDecoderMap)) + .setRedirectStrategy(redirectStrategy) .build(); } private PoolingHttpClientConnectionManager newConnectionManager(CommaFeedConfiguration config, int poolSize) { SSLFactory sslFactory = SSLFactory.builder().withUnsafeTrustMaterial().withUnsafeHostnameVerifier().build(); - DnsResolver dnsResolver = config.httpClient().blockLocalAddresses() - ? new BlockLocalAddressesDnsResolver(SystemDefaultDnsResolver.INSTANCE) - : SystemDefaultDnsResolver.INSTANCE; + DnsResolver dnsResolver = config.httpClient().blockLocalAddresses() ? new BlockLocalAddressesDnsResolver(DNS_RESOLVER) + : DNS_RESOLVER; return PoolingHttpClientConnectionManagerBuilder.create() .setTlsSocketStrategy(Apache5SslUtils.toTlsSocketStrategy(sslFactory)) @@ -98,13 +109,18 @@ public class HttpClientFactory { } + private static boolean isLocalAddress(InetAddress address) { + return address.isSiteLocalAddress() || address.isAnyLocalAddress() || address.isLinkLocalAddress() || address.isLoopbackAddress() + || address.isMulticastAddress(); + } + private record BlockLocalAddressesDnsResolver(DnsResolver delegate) implements DnsResolver { @Override public InetAddress[] resolve(String host) throws UnknownHostException { InetAddress[] addresses = delegate.resolve(host); for (InetAddress addr : addresses) { if (isLocalAddress(addr)) { - throw new UnknownHostException("Access to address blocked: " + addr.getHostAddress()); + throw new UnknownHostException("Access to local address blocked: " + addr.getHostAddress()); } } return addresses; @@ -114,10 +130,36 @@ public class HttpClientFactory { public String resolveCanonicalHostname(String host) throws UnknownHostException { return delegate.resolveCanonicalHostname(host); } + } - private boolean isLocalAddress(InetAddress address) { - return address.isSiteLocalAddress() || address.isAnyLocalAddress() || address.isLinkLocalAddress() - || address.isLoopbackAddress() || address.isMulticastAddress(); + @RequiredArgsConstructor + private static class BlockLocalAddressesRedirectStrategy extends DefaultRedirectStrategy { + + private final DnsResolver delegate; + + @Override + public URI getLocationURI(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException { + URI redirectUri = super.getLocationURI(request, response, context); + + String host = redirectUri.getHost(); + if (host == null) { + throw new HttpException("Redirect URI does not have a host: " + redirectUri); + } + + InetAddress[] addresses; + try { + addresses = delegate.resolve(host); + } catch (UnknownHostException e) { + throw new HttpException("Unknown host: " + host); + } + + for (InetAddress addr : addresses) { + if (isLocalAddress(addr)) { + throw new HttpException("Access to local address blocked: " + addr.getHostAddress()); + } + } + + return redirectUri; } }