diff --git a/core/src/main/java/com/linecorp/armeria/client/HttpClientDelegate.java b/core/src/main/java/com/linecorp/armeria/client/HttpClientDelegate.java index f3d60aea270..24e25d36934 100644 --- a/core/src/main/java/com/linecorp/armeria/client/HttpClientDelegate.java +++ b/core/src/main/java/com/linecorp/armeria/client/HttpClientDelegate.java @@ -15,6 +15,7 @@ */ package com.linecorp.armeria.client; +import static com.linecorp.armeria.internal.common.util.IpAddrUtil.isCreatedWithIpAddressOnly; import static java.util.Objects.requireNonNull; import java.net.InetAddress; @@ -88,24 +89,35 @@ public HttpResponse execute(ClientRequestContext ctx, HttpRequest req) throws Ex } final SessionProtocol protocol = ctx.sessionProtocol(); - final ProxyConfig proxyConfig; + + final Endpoint endpointWithPort = endpoint.withDefaultPort(ctx.sessionProtocol()); + final EventLoop eventLoop = ctx.eventLoop().withoutContext(); + // TODO(ikhoon) Use ctx.exchangeType() to create an optimized HttpResponse for non-streaming response. + final DecodedHttpResponse res = new DecodedHttpResponse(eventLoop); + updateCancellationTask(ctx, req, res); + try { - proxyConfig = getProxyConfig(protocol, endpoint); + resolveProxyConfig(protocol, endpoint, ctx, (proxyConfig, thrown) -> { + if (thrown != null) { + earlyFailedResponse(thrown, ctx, res); + } else { + execute0(ctx, endpointWithPort, req, res, proxyConfig); + } + }); } catch (Throwable t) { return earlyFailedResponse(t, ctx); } + return res; + } + private void execute0(ClientRequestContext ctx, Endpoint endpointWithPort, HttpRequest req, + DecodedHttpResponse res, ProxyConfig proxyConfig) { final Throwable cancellationCause = ctx.cancellationCause(); if (cancellationCause != null) { - return earlyFailedResponse(cancellationCause, ctx); + earlyFailedResponse(cancellationCause, ctx, res); + return; } - final Endpoint endpointWithPort = endpoint.withDefaultPort(ctx.sessionProtocol()); - final EventLoop eventLoop = ctx.eventLoop().withoutContext(); - // TODO(ikhoon) Use ctx.exchangeType() to create an optimized HttpResponse for non-streaming response. - final DecodedHttpResponse res = new DecodedHttpResponse(eventLoop); - updateCancellationTask(ctx, req, res); - final ClientConnectionTimingsBuilder timingsBuilder = ClientConnectionTimings.builder(); if (endpointWithPort.hasIpAddr() || @@ -125,8 +137,6 @@ public HttpResponse execute(ClientRequestContext ctx, HttpRequest req) throws Ex } }); } - - return res; } private static void updateCancellationTask(ClientRequestContext ctx, HttpRequest req, @@ -215,24 +225,52 @@ private void acquireConnectionAndExecute0(ClientRequestContext ctx, Endpoint end } } - private ProxyConfig getProxyConfig(SessionProtocol protocol, Endpoint endpoint) { - final ProxyConfig proxyConfig = factory.proxyConfigSelector().select(protocol, endpoint); - requireNonNull(proxyConfig, "proxyConfig"); + private void resolveProxyConfig(SessionProtocol protocol, Endpoint endpoint, ClientRequestContext ctx, + BiConsumer<@Nullable ProxyConfig, @Nullable Throwable> onComplete) { + final ProxyConfig unresolvedProxyConfig = factory.proxyConfigSelector().select(protocol, endpoint); + requireNonNull(unresolvedProxyConfig, "unresolvedProxyConfig"); + final ProxyConfig proxyConfig = maybeSetHAProxySourceAddress(unresolvedProxyConfig); - // special behavior for haproxy when sourceAddress is null - if (proxyConfig.proxyType() == ProxyType.HAPROXY && - ((HAProxyConfig) proxyConfig).sourceAddress() == null) { - final InetSocketAddress proxyAddress = proxyConfig.proxyAddress(); + final InetSocketAddress proxyAddress = proxyConfig.proxyAddress(); + final boolean needsDnsResolution = proxyAddress != null && !isCreatedWithIpAddressOnly(proxyAddress); + if (needsDnsResolution) { assert proxyAddress != null; + final Future resolveFuture = addressResolverGroup + .getResolver(ctx.eventLoop().withoutContext()) + .resolve(createUnresolvedAddressForRefreshing(proxyAddress)); - // use proxy information in context if available - final ServiceRequestContext serviceCtx = ServiceRequestContext.currentOrNull(); - if (serviceCtx != null) { - final ProxiedAddresses proxiedAddresses = serviceCtx.proxiedAddresses(); - return ProxyConfig.haproxy(proxyAddress, proxiedAddresses.sourceAddress()); - } + resolveFuture.addListener(future -> { + if (future.isSuccess()) { + final InetSocketAddress resolvedAddress = (InetSocketAddress) future.getNow(); + final ProxyConfig newProxyConfig = proxyConfig.withProxyAddress(resolvedAddress); + onComplete.accept(newProxyConfig, null); + } else { + final Throwable cause = future.cause(); + onComplete.accept(null, cause); + } + }); + } else { + onComplete.accept(proxyConfig, null); } + } + private static ProxyConfig maybeSetHAProxySourceAddress(ProxyConfig proxyConfig) { + if (proxyConfig.proxyType() != ProxyType.HAPROXY) { + return proxyConfig; + } + if (((HAProxyConfig) proxyConfig).sourceAddress() != null) { + return proxyConfig; + } + + final ServiceRequestContext sctx = ServiceRequestContext.currentOrNull(); + final ProxiedAddresses serviceProxiedAddresses = sctx == null ? null : sctx.proxiedAddresses(); + if (serviceProxiedAddresses != null) { + // A special behavior for haproxy when sourceAddress is null. + // Use proxy information in the service context if available. + final InetSocketAddress proxyAddress = proxyConfig.proxyAddress(); + assert proxyAddress != null; + return ProxyConfig.haproxy(proxyAddress, serviceProxiedAddresses.sourceAddress()); + } return proxyConfig; } @@ -253,6 +291,15 @@ private static HttpResponse earlyFailedResponse(Throwable t, ClientRequestContex return HttpResponse.ofFailure(cause); } + private static HttpResponse earlyFailedResponse(Throwable t, + ClientRequestContext ctx, + DecodedHttpResponse response) { + final UnprocessedRequestException cause = UnprocessedRequestException.of(t); + ctx.cancel(cause); + response.close(cause); + return response; + } + private static void doExecute(PooledChannel pooledChannel, ClientRequestContext ctx, HttpRequest req, DecodedHttpResponse res) { final Channel channel = pooledChannel.get(); @@ -260,4 +307,8 @@ private static void doExecute(PooledChannel pooledChannel, ClientRequestContext res.init(session.inboundTrafficController()); session.invoke(pooledChannel, ctx, req, res); } + + private static InetSocketAddress createUnresolvedAddressForRefreshing(InetSocketAddress previousAddress) { + return InetSocketAddress.createUnresolved(previousAddress.getHostString(), previousAddress.getPort()); + } } diff --git a/core/src/main/java/com/linecorp/armeria/client/proxy/ConnectProxyConfig.java b/core/src/main/java/com/linecorp/armeria/client/proxy/ConnectProxyConfig.java index 085609d2d41..7cb08d5dda2 100644 --- a/core/src/main/java/com/linecorp/armeria/client/proxy/ConnectProxyConfig.java +++ b/core/src/main/java/com/linecorp/armeria/client/proxy/ConnectProxyConfig.java @@ -90,6 +90,12 @@ public ProxyType proxyType() { return ProxyType.CONNECT; } + @Override + public ProxyConfig withProxyAddress(InetSocketAddress newProxyAddress) { + return new ConnectProxyConfig(newProxyAddress, this.username, + this.password, this.headers, this.useTls); + } + @Override public boolean equals(@Nullable Object o) { if (this == o) { diff --git a/core/src/main/java/com/linecorp/armeria/client/proxy/DirectProxyConfig.java b/core/src/main/java/com/linecorp/armeria/client/proxy/DirectProxyConfig.java index 3a08c219e24..7eb33e83ee0 100644 --- a/core/src/main/java/com/linecorp/armeria/client/proxy/DirectProxyConfig.java +++ b/core/src/main/java/com/linecorp/armeria/client/proxy/DirectProxyConfig.java @@ -40,6 +40,12 @@ public InetSocketAddress proxyAddress() { return null; } + @Override + public ProxyConfig withProxyAddress(InetSocketAddress newProxyAddress) { + throw new UnsupportedOperationException( + "A proxy address can't be set to DirectProxyConfig."); + } + @Override public String toString() { return "DirectProxyConfig{proxyType=DIRECT}"; diff --git a/core/src/main/java/com/linecorp/armeria/client/proxy/HAProxyConfig.java b/core/src/main/java/com/linecorp/armeria/client/proxy/HAProxyConfig.java index f300e524f80..79b7bccbab3 100644 --- a/core/src/main/java/com/linecorp/armeria/client/proxy/HAProxyConfig.java +++ b/core/src/main/java/com/linecorp/armeria/client/proxy/HAProxyConfig.java @@ -17,10 +17,14 @@ package com.linecorp.armeria.client.proxy; import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; import java.net.InetSocketAddress; import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.google.common.base.MoreObjects; import com.linecorp.armeria.common.annotation.Nullable; @@ -31,6 +35,8 @@ */ public final class HAProxyConfig extends ProxyConfig { + private static final Logger logger = LoggerFactory.getLogger(HAProxyConfig.class); + private final InetSocketAddress proxyAddress; @Nullable @@ -42,8 +48,11 @@ public final class HAProxyConfig extends ProxyConfig { } HAProxyConfig(InetSocketAddress proxyAddress, InetSocketAddress sourceAddress) { - checkArgument(sourceAddress.getAddress().getClass() == proxyAddress.getAddress().getClass(), - "sourceAddress and proxyAddress should be the same type"); + // If proxyAddress is unresolved, getAddress() will return null. + if (proxyAddress.getAddress() != null) { + checkArgument(sourceAddress.getAddress().getClass() == proxyAddress.getAddress().getClass(), + "sourceAddress and proxyAddress should be the same type"); + } this.proxyAddress = proxyAddress; this.sourceAddress = sourceAddress; } @@ -67,6 +76,13 @@ public InetSocketAddress sourceAddress() { return sourceAddress; } + @Override + public ProxyConfig withProxyAddress(InetSocketAddress newProxyAddress) { + requireNonNull(newProxyAddress, "newProxyAddress"); + return this.sourceAddress == null ? new HAProxyConfig(newProxyAddress) + : new HAProxyConfig(newProxyAddress, this.sourceAddress); + } + @Override public boolean equals(@Nullable Object o) { if (this == o) { diff --git a/core/src/main/java/com/linecorp/armeria/client/proxy/ProxyConfig.java b/core/src/main/java/com/linecorp/armeria/client/proxy/ProxyConfig.java index 736124b8c91..e26c2590b01 100644 --- a/core/src/main/java/com/linecorp/armeria/client/proxy/ProxyConfig.java +++ b/core/src/main/java/com/linecorp/armeria/client/proxy/ProxyConfig.java @@ -40,7 +40,6 @@ public abstract class ProxyConfig { */ public static Socks4ProxyConfig socks4(InetSocketAddress proxyAddress) { requireNonNull(proxyAddress, "proxyAddress"); - checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved"); return new Socks4ProxyConfig(proxyAddress, null); } @@ -52,7 +51,6 @@ public static Socks4ProxyConfig socks4(InetSocketAddress proxyAddress) { */ public static Socks4ProxyConfig socks4(InetSocketAddress proxyAddress, String username) { requireNonNull(proxyAddress, "proxyAddress"); - checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved"); return new Socks4ProxyConfig(proxyAddress, requireNonNull(username, "username")); } @@ -63,7 +61,6 @@ public static Socks4ProxyConfig socks4(InetSocketAddress proxyAddress, String us */ public static Socks5ProxyConfig socks5(InetSocketAddress proxyAddress) { requireNonNull(proxyAddress, "proxyAddress"); - checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved"); return new Socks5ProxyConfig(proxyAddress, null, null); } @@ -77,7 +74,6 @@ public static Socks5ProxyConfig socks5(InetSocketAddress proxyAddress) { public static Socks5ProxyConfig socks5( InetSocketAddress proxyAddress, String username, String password) { requireNonNull(proxyAddress, "proxyAddress"); - checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved"); return new Socks5ProxyConfig(proxyAddress, requireNonNull(username, "username"), requireNonNull(password, "password")); } @@ -89,7 +85,6 @@ public static Socks5ProxyConfig socks5( */ public static ConnectProxyConfig connect(InetSocketAddress proxyAddress) { requireNonNull(proxyAddress, "proxyAddress"); - checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved"); return new ConnectProxyConfig(proxyAddress, null, null, HttpHeaders.of(), false); } @@ -101,7 +96,6 @@ public static ConnectProxyConfig connect(InetSocketAddress proxyAddress) { */ public static ConnectProxyConfig connect(InetSocketAddress proxyAddress, boolean useTls) { requireNonNull(proxyAddress, "proxyAddress"); - checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved"); return new ConnectProxyConfig(proxyAddress, null, null, HttpHeaders.of(), useTls); } @@ -129,7 +123,6 @@ public static ConnectProxyConfig connect( public static ConnectProxyConfig connect( InetSocketAddress proxyAddress, HttpHeaders headers, boolean useTls) { requireNonNull(proxyAddress, "proxyAddress"); - checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved"); return new ConnectProxyConfig(proxyAddress, null, null, headers, useTls); } @@ -146,7 +139,6 @@ public static ConnectProxyConfig connect( public static ConnectProxyConfig connect(InetSocketAddress proxyAddress, String username, String password, HttpHeaders headers, boolean useTls) { requireNonNull(proxyAddress, "proxyAddress"); - checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved"); requireNonNull(username, "username"); requireNonNull(password, "password"); requireNonNull(headers, "headers"); @@ -162,7 +154,6 @@ public static ConnectProxyConfig connect(InetSocketAddress proxyAddress, String public static HAProxyConfig haproxy( InetSocketAddress proxyAddress, InetSocketAddress sourceAddress) { requireNonNull(proxyAddress, "proxyAddress"); - checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved"); requireNonNull(sourceAddress, "sourceAddress"); checkArgument(!sourceAddress.isUnresolved(), "sourceAddress must be resolved"); return new HAProxyConfig(proxyAddress, sourceAddress); @@ -176,7 +167,6 @@ public static HAProxyConfig haproxy( */ public static ProxyConfig haproxy(InetSocketAddress proxyAddress) { requireNonNull(proxyAddress, "proxyAddress"); - checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved"); return new HAProxyConfig(proxyAddress); } @@ -201,6 +191,12 @@ public static ProxyConfig direct() { @Nullable public abstract InetSocketAddress proxyAddress(); + /** + * Returns a new proxy address instance that respects DNS TTL. + * @param newProxyAddress the inet socket address + */ + public abstract ProxyConfig withProxyAddress(InetSocketAddress newProxyAddress); + @Nullable static String maskPassword(@Nullable String username, @Nullable String password) { return username != null ? "****" : null; diff --git a/core/src/main/java/com/linecorp/armeria/client/proxy/Socks4ProxyConfig.java b/core/src/main/java/com/linecorp/armeria/client/proxy/Socks4ProxyConfig.java index 49adc940602..d8f76ef6fa5 100644 --- a/core/src/main/java/com/linecorp/armeria/client/proxy/Socks4ProxyConfig.java +++ b/core/src/main/java/com/linecorp/armeria/client/proxy/Socks4ProxyConfig.java @@ -56,6 +56,11 @@ public ProxyType proxyType() { return ProxyType.SOCKS4; } + @Override + public ProxyConfig withProxyAddress(InetSocketAddress newProxyAddress) { + return new Socks4ProxyConfig(newProxyAddress, this.username); + } + @Override public boolean equals(@Nullable Object o) { if (this == o) { diff --git a/core/src/main/java/com/linecorp/armeria/client/proxy/Socks5ProxyConfig.java b/core/src/main/java/com/linecorp/armeria/client/proxy/Socks5ProxyConfig.java index 8e541b8c041..b100ff90439 100644 --- a/core/src/main/java/com/linecorp/armeria/client/proxy/Socks5ProxyConfig.java +++ b/core/src/main/java/com/linecorp/armeria/client/proxy/Socks5ProxyConfig.java @@ -69,6 +69,11 @@ public ProxyType proxyType() { return ProxyType.SOCKS5; } + @Override + public ProxyConfig withProxyAddress(InetSocketAddress newProxyAddress) { + return new Socks5ProxyConfig(newProxyAddress, this.username, this.password); + } + @Override public boolean equals(@Nullable Object o) { if (this == o) { diff --git a/core/src/main/java/com/linecorp/armeria/internal/common/util/IpAddrUtil.java b/core/src/main/java/com/linecorp/armeria/internal/common/util/IpAddrUtil.java index 3fd74fdfe5c..b9b5a063d4b 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/common/util/IpAddrUtil.java +++ b/core/src/main/java/com/linecorp/armeria/internal/common/util/IpAddrUtil.java @@ -15,6 +15,9 @@ */ package com.linecorp.armeria.internal.common.util; +import java.net.InetAddress; +import java.net.InetSocketAddress; + import com.linecorp.armeria.common.annotation.Nullable; import io.netty.util.NetUtil; @@ -36,5 +39,15 @@ public static String normalize(@Nullable String ipAddr) { return NetUtil.bytesToIpAddress(array); } + public static boolean isCreatedWithIpAddressOnly(InetSocketAddress socketAddress) { + if (socketAddress.isUnresolved()) { + return false; + } + + final InetAddress inetAddress = socketAddress.getAddress(); + // If hostname and host address are the same, it was created with an IP address + return socketAddress.getHostString().equals(inetAddress.getHostAddress()); + } + private IpAddrUtil() {} } diff --git a/core/src/test/java/com/linecorp/armeria/client/DNSResolverFacadeUtils.java b/core/src/test/java/com/linecorp/armeria/client/DNSResolverFacadeUtils.java new file mode 100644 index 00000000000..c0a0eaf2d0e --- /dev/null +++ b/core/src/test/java/com/linecorp/armeria/client/DNSResolverFacadeUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright 2024 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * 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 com.linecorp.armeria.client; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +import java.net.InetSocketAddress; +import java.util.function.Function; +import java.util.stream.Stream; + +import com.linecorp.armeria.client.endpoint.dns.TestDnsServer; +import com.linecorp.armeria.client.retry.Backoff; +import com.linecorp.armeria.common.prometheus.PrometheusMeterRegistries; + +import io.netty.channel.EventLoopGroup; +import io.netty.resolver.AddressResolverGroup; +import io.netty.resolver.ResolvedAddressTypes; +import io.netty.resolver.dns.DnsServerAddressStreamProvider; +import io.netty.resolver.dns.DnsServerAddresses; + +public final class DNSResolverFacadeUtils { + + private DNSResolverFacadeUtils() { } + + public static Function> getAddressResolverGroupForTest( + TestDnsServer dnsServer) { + return eventLoopGroup -> { + final DnsResolverGroupBuilder builder = builder(dnsServer); + builder.autoRefreshBackoff(Backoff.fixed(0L)); + return builder.build(eventLoopGroup); + }; + } + + private static DnsResolverGroupBuilder builder(TestDnsServer... servers) { + return builder(true, servers); + } + + private static DnsResolverGroupBuilder builder(boolean withCacheOption, TestDnsServer... servers) { + final DnsServerAddressStreamProvider dnsServerAddressStreamProvider = + hostname -> DnsServerAddresses.sequential( + Stream.of(servers).map(TestDnsServer::addr).collect(toImmutableList())).stream(); + final DnsResolverGroupBuilder builder = new DnsResolverGroupBuilder() + .serverAddressStreamProvider(dnsServerAddressStreamProvider) + .meterRegistry(PrometheusMeterRegistries.newRegistry()) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY) + .traceEnabled(false); + if (withCacheOption) { + builder.dnsCache(DnsCache.builder().build()); + } + return builder; + } +} diff --git a/core/src/test/java/com/linecorp/armeria/client/proxy/ProxyClientIntegrationTest.java b/core/src/test/java/com/linecorp/armeria/client/proxy/ProxyClientIntegrationTest.java index 932c15197a1..097d5746900 100644 --- a/core/src/test/java/com/linecorp/armeria/client/proxy/ProxyClientIntegrationTest.java +++ b/core/src/test/java/com/linecorp/armeria/client/proxy/ProxyClientIntegrationTest.java @@ -15,6 +15,9 @@ */ package com.linecorp.armeria.client.proxy; +import static com.linecorp.armeria.client.endpoint.dns.TestDnsServer.newAddressRecord; +import static io.netty.handler.codec.dns.DnsRecordType.A; +import static io.netty.handler.codec.dns.DnsSection.ANSWER; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; @@ -51,12 +54,15 @@ import org.junit.jupiter.params.provider.MethodSource; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.linecorp.armeria.client.ClientFactory; +import com.linecorp.armeria.client.DNSResolverFacadeUtils; import com.linecorp.armeria.client.Endpoint; import com.linecorp.armeria.client.SessionProtocolNegotiationException; import com.linecorp.armeria.client.UnprocessedRequestException; import com.linecorp.armeria.client.WebClient; +import com.linecorp.armeria.client.endpoint.dns.TestDnsServer; import com.linecorp.armeria.client.logging.LoggingClient; import com.linecorp.armeria.common.AggregatedHttpResponse; import com.linecorp.armeria.common.Flags; @@ -75,6 +81,8 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.dns.DefaultDnsQuestion; +import io.netty.handler.codec.dns.DefaultDnsResponse; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.EmptyHttpHeaders; @@ -95,6 +103,7 @@ import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.traffic.ChannelTrafficShapingHandler; +import io.netty.resolver.dns.DnsErrorCauseException; import io.netty.util.ReferenceCountUtil; class ProxyClientIntegrationTest { @@ -396,7 +405,7 @@ public void connectFailed(SessionProtocol protocol, Endpoint endpoint, assertThatThrownBy(responseFuture::join).isInstanceOf(CompletionException.class) .hasCauseInstanceOf(UnprocessedRequestException.class) .hasRootCauseInstanceOf(NullPointerException.class) - .hasRootCauseMessage("proxyConfig"); + .hasRootCauseMessage("unresolvedProxyConfig"); } } @@ -790,6 +799,66 @@ public void connectFailed(SessionProtocol protocol, Endpoint endpoint, } } + @Test + void testProxyConfigShouldBeResolved() throws Exception { + try (TestDnsServer server = new TestDnsServer(ImmutableMap.of( + new DefaultDnsQuestion("a.com.", A), + new DefaultDnsResponse(0).addRecord(ANSWER, newAddressRecord("a.com.", "127.0.0.1")))) + ) { + final InetSocketAddress proxySocketAddress = new InetSocketAddress( + "a.com", httpProxyServer.address().getPort()); + try (ClientFactory clientFactory = + ClientFactory.builder() + .addressResolverGroupFactory( + DNSResolverFacadeUtils.getAddressResolverGroupForTest(server)) + .proxyConfig(ProxyConfig.connect(proxySocketAddress)) + .useHttp2Preface(true) + .build()) { + + final WebClient webClient = WebClient.builder(SessionProtocol.H1C, backendServer.httpEndpoint()) + .factory(clientFactory) + .decorator(LoggingClient.newDecorator()) + .build(); + final CompletableFuture responseFuture = + webClient.get(PROXY_PATH).aggregate(); + final AggregatedHttpResponse response = responseFuture.join(); + assertThat(response.status()).isEqualTo(HttpStatus.OK); + assertThat(response.contentUtf8()).isEqualTo(SUCCESS_RESPONSE); + assertThat(numSuccessfulProxyRequests).isEqualTo(1); + } + } + } + + @Test + void testProxyConfigShouldBeFailedToResolved() throws Exception { + try (TestDnsServer server = new TestDnsServer(ImmutableMap.of( + new DefaultDnsQuestion("b.com.", A), + new DefaultDnsResponse(0).addRecord(ANSWER, newAddressRecord("b.com.", "127.0.0.1")))) + ) { + final InetSocketAddress proxySocketAddress = new InetSocketAddress( + "a.com", httpProxyServer.address().getPort()); + try (ClientFactory clientFactory = + ClientFactory.builder() + .addressResolverGroupFactory( + DNSResolverFacadeUtils.getAddressResolverGroupForTest(server)) + .proxyConfig(ProxyConfig.connect(proxySocketAddress)) + .useHttp2Preface(true) + .build()) { + + final WebClient webClient = WebClient.builder(SessionProtocol.H1C, backendServer.httpEndpoint()) + .factory(clientFactory) + .decorator(LoggingClient.newDecorator()) + .build(); + final CompletableFuture responseFuture = + webClient.get(PROXY_PATH).aggregate(); + assertThatThrownBy(responseFuture::join).isInstanceOf(CompletionException.class) + .hasCauseInstanceOf(UnprocessedRequestException.class) + .hasRootCauseInstanceOf(DnsErrorCauseException.class) + .hasRootCauseMessage("Query failed with NXDOMAIN"); + } + } + } + private static final class SleepHandler extends ChannelInboundHandlerAdapter { @Override diff --git a/core/src/test/java/com/linecorp/armeria/client/proxy/ProxyConfigTest.java b/core/src/test/java/com/linecorp/armeria/client/proxy/ProxyConfigTest.java index 1c0b8c68bc7..295cf96c6b3 100644 --- a/core/src/test/java/com/linecorp/armeria/client/proxy/ProxyConfigTest.java +++ b/core/src/test/java/com/linecorp/armeria/client/proxy/ProxyConfigTest.java @@ -48,13 +48,19 @@ void testEqualityAndHashCode(ProxyConfig config) { void testUnresolvedProxyAddress() { final InetSocketAddress unresolved = InetSocketAddress.createUnresolved("unresolved", 0); final InetSocketAddress resolved = new InetSocketAddress("127.0.0.1", 80); - assertThatThrownBy(() -> ProxyConfig.socks4(unresolved)).isInstanceOf(IllegalArgumentException.class); - assertThatThrownBy(() -> ProxyConfig.socks5(unresolved)).isInstanceOf(IllegalArgumentException.class); - assertThatThrownBy(() -> ProxyConfig.connect(unresolved)).isInstanceOf(IllegalArgumentException.class); - assertThatThrownBy(() -> ProxyConfig.haproxy(unresolved, resolved)) - .isInstanceOf(IllegalArgumentException.class); - assertThatThrownBy(() -> ProxyConfig.haproxy(resolved, unresolved)) - .isInstanceOf(IllegalArgumentException.class); + + assertThat(ProxyConfig.socks4(unresolved)).isInstanceOf(Socks4ProxyConfig.class); + assertThat(ProxyConfig.socks5(unresolved)).isInstanceOf(Socks5ProxyConfig.class); + assertThat(ProxyConfig.connect(unresolved)).isInstanceOf(ConnectProxyConfig.class); + + // for the HAProxy. + assertThat(ProxyConfig.haproxy(unresolved, resolved)).isInstanceOf(HAProxyConfig.class); + assertThat(ProxyConfig.haproxy(unresolved)).isInstanceOf(HAProxyConfig.class); + assertThat(ProxyConfig.haproxy(resolved)).isInstanceOf(HAProxyConfig.class); + assertThatThrownBy( + () -> ProxyConfig.haproxy(resolved, unresolved)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy( + () -> ProxyConfig.haproxy(unresolved, unresolved)).isInstanceOf(IllegalArgumentException.class); } @Test diff --git a/core/src/test/java/com/linecorp/armeria/internal/common/util/IpAddrUtilTest.java b/core/src/test/java/com/linecorp/armeria/internal/common/util/IpAddrUtilTest.java new file mode 100644 index 00000000000..8f670ab67e1 --- /dev/null +++ b/core/src/test/java/com/linecorp/armeria/internal/common/util/IpAddrUtilTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2024 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * 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 com.linecorp.armeria.internal.common.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; + +import org.junit.jupiter.api.Test; + +class IpAddrUtilTest { + + @Test + void testIsCreatedWithIpAddressOnly() throws UnknownHostException { + InetSocketAddress inetSocketAddress = new InetSocketAddress("foo.com", 8080); + assertThat(IpAddrUtil.isCreatedWithIpAddressOnly(inetSocketAddress)).isFalse(); + + inetSocketAddress = InetSocketAddress.createUnresolved("foo.com", 8080); + assertThat(IpAddrUtil.isCreatedWithIpAddressOnly(inetSocketAddress)).isFalse(); + + inetSocketAddress = new InetSocketAddress("1.2.3.4", 8080); + assertThat(IpAddrUtil.isCreatedWithIpAddressOnly(inetSocketAddress)).isTrue(); + + InetAddress inetAddress = InetAddress.getByName("1.2.3.4"); + inetSocketAddress = new InetSocketAddress(inetAddress, 8080); + assertThat(IpAddrUtil.isCreatedWithIpAddressOnly(inetSocketAddress)).isTrue(); + + inetAddress = InetAddress.getByName("0.0.0.0"); + inetSocketAddress = new InetSocketAddress(inetAddress, 8080); + assertThat(IpAddrUtil.isCreatedWithIpAddressOnly(inetSocketAddress)).isTrue(); + + inetAddress = InetAddress.getByName("::"); + inetSocketAddress = new InetSocketAddress(inetAddress, 8080); + assertThat(IpAddrUtil.isCreatedWithIpAddressOnly(inetSocketAddress)).isTrue(); + + inetAddress = InetAddress.getByAddress("foo.com", new byte[] { 1, 2, 3, 4 }); + inetSocketAddress = new InetSocketAddress(inetAddress, 8080); + assertThat(IpAddrUtil.isCreatedWithIpAddressOnly(inetSocketAddress)).isFalse(); + + inetAddress = InetAddress.getByName("foo.com"); + inetSocketAddress = new InetSocketAddress(inetAddress, 8080); + assertThat(IpAddrUtil.isCreatedWithIpAddressOnly(inetSocketAddress)).isFalse(); + } +}