diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/ProxyConfigBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/ProxyConfigBuilder.java index dece611c71..643d6ab9f3 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/ProxyConfigBuilder.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/ProxyConfigBuilder.java @@ -26,8 +26,19 @@ */ public final class ProxyConfigBuilder<A> { + private static final Consumer<HttpHeaders> NOOP_HEADERS_CONSUMER = new Consumer<HttpHeaders>() { + @Override + public void accept(final HttpHeaders headers) { + } + + @Override + public String toString() { + return "NOOP"; + } + }; + private final A address; - private Consumer<HttpHeaders> connectRequestHeadersInitializer = __ -> { }; + private Consumer<HttpHeaders> connectRequestHeadersInitializer = NOOP_HEADERS_CONSUMER; /** * Creates a new instance. @@ -82,5 +93,36 @@ public A address() { public Consumer<HttpHeaders> connectRequestHeadersInitializer() { return connectRequestHeadersInitializer; } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DefaultProxyConfig)) { + return false; + } + + final DefaultProxyConfig<?> that = (DefaultProxyConfig<?>) o; + if (!address.equals(that.address)) { + return false; + } + return connectRequestHeadersInitializer.equals(that.connectRequestHeadersInitializer); + } + + @Override + public int hashCode() { + int result = address.hashCode(); + result = 31 * result + connectRequestHeadersInitializer.hashCode(); + return result; + } + + @Override + public String toString() { + return getClass().getSimpleName() + + "{address=" + address + + ", connectRequestHeadersInitializer=" + connectRequestHeadersInitializer + + '}'; + } } } diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultSingleAddressHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultSingleAddressHttpClientBuilder.java index 4ba17a788f..d577542643 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultSingleAddressHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultSingleAddressHttpClientBuilder.java @@ -109,7 +109,7 @@ final class DefaultSingleAddressHttpClientBuilder<U, R> implements SingleAddress @Nullable private final U address; @Nullable - private ProxyConfig<U> proxyConfig; + private U proxyAddress; private final HttpClientConfig config; final HttpExecutionContextBuilder executionContextBuilder; private final ClientStrategyInfluencerChainBuilder strategyComputation; @@ -148,7 +148,7 @@ final class DefaultSingleAddressHttpClientBuilder<U, R> implements SingleAddress private DefaultSingleAddressHttpClientBuilder(@Nullable final U address, final DefaultSingleAddressHttpClientBuilder<U, R> from) { this.address = address; - proxyConfig = from.proxyConfig; + proxyAddress = from.proxyAddress; config = new HttpClientConfig(from.config); executionContextBuilder = new HttpExecutionContextBuilder(from.executionContextBuilder); strategyComputation = from.strategyComputation.copy(); @@ -192,7 +192,7 @@ private static final class HttpClientBuildContext<U, R> { U address() { assert builder.address != null : "Attempted to buildStreaming with an unknown address"; - return builder.proxyConfig != null ? builder.proxyConfig.address() : builder.address; + return builder.proxyAddress != null ? builder.proxyAddress : builder.address; } HttpClientConfig httpConfig() { @@ -243,10 +243,10 @@ public HttpExecutionStrategy executionStrategy() { ctx.builder.strategyComputation.buildForConnectionFactory(); if (roConfig.hasProxy() && sslContext != null) { - assert roConfig.connectAddress() != null; + assert roConfig.proxyConfig() != null; @SuppressWarnings("deprecation") final ConnectionFactoryFilter<R, FilterableStreamingHttpConnection> proxy = - new ProxyConnectConnectionFactoryFilter<>(roConfig.connectAddress(), connectionFactoryStrategy); + new ProxyConnectConnectionFactoryFilter<>(roConfig.proxyConfig().address()); assert !proxy.requiredOffloads().hasOffloads(); connectionFactoryFilter = appendConnectionFilter(proxy, connectionFactoryFilter); } @@ -388,9 +388,8 @@ private static StreamingHttpRequestResponseFactory defaultReqRespFactory(ReadOnl private static <U, R> String targetAddress(final HttpClientBuildContext<U, R> ctx) { assert ctx.builder.address != null; - return ctx.builder.proxyConfig == null ? - ctx.builder.address.toString() : - ctx.builder.address + " (via " + ctx.builder.proxyConfig.address() + ")"; + return ctx.builder.proxyAddress == null ? + ctx.builder.address.toString() : ctx.builder.address + " (via " + ctx.builder.proxyAddress + ")"; } private static ContextAwareStreamingHttpClientFilterFactory appendFilter( @@ -449,8 +448,8 @@ private AbsoluteAddressHttpRequesterFilter proxyAbsoluteAddressFilterFactory() { @Override public DefaultSingleAddressHttpClientBuilder<U, R> proxyConfig(final ProxyConfig<U> proxyConfig) { - this.proxyConfig = requireNonNull(proxyConfig); - config.proxy(proxyConfig, hostToCharSequenceFunction.apply(address)); + this.proxyAddress = requireNonNull(proxyConfig.address()); + config.proxyConfig(hostToCharSequenceFunction.apply(address), proxyConfig); return this; } diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpClientConfig.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpClientConfig.java index b2a5eb07ef..3cbade6d47 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpClientConfig.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpClientConfig.java @@ -1,5 +1,5 @@ /* - * Copyright © 2018-2020 Apple Inc. and the ServiceTalk project authors + * Copyright © 2018-2023 Apple Inc. and the ServiceTalk project authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,14 @@ package io.servicetalk.http.netty; import io.servicetalk.http.api.Http2Settings; +import io.servicetalk.http.api.HttpHeaders; import io.servicetalk.http.api.ProxyConfig; import io.servicetalk.tcp.netty.internal.TcpClientConfig; import io.servicetalk.transport.api.ClientSslConfig; import io.servicetalk.transport.api.DelegatingClientSslConfig; import java.util.List; +import java.util.function.Consumer; import javax.annotation.Nullable; import static io.netty.handler.codec.http2.Http2CodecUtil.SETTINGS_ENABLE_PUSH; @@ -35,9 +37,7 @@ final class HttpClientConfig { private final TcpClientConfig tcpConfig; private final HttpConfig protocolConfigs; @Nullable - private ProxyConfig<?> proxyConfig; - @Nullable - private CharSequence connectAddress; + private ProxyConfig<String> proxyConfig; @Nullable private String fallbackPeerHost; private int fallbackPeerPort = -1; @@ -66,7 +66,6 @@ final class HttpClientConfig { tcpConfig = new TcpClientConfig(from.tcpConfig()); protocolConfigs = new HttpConfig(from.protocolConfigs()); proxyConfig = from.proxyConfig; - connectAddress = from.connectAddress; fallbackPeerHost = from.fallbackPeerHost; fallbackPeerPort = from.fallbackPeerPort; inferPeerHost = from.inferPeerHost; @@ -83,18 +82,15 @@ HttpConfig protocolConfigs() { } @Nullable - ProxyConfig<?> proxyConfig() { + ProxyConfig<String> proxyConfig() { return proxyConfig; } - @Nullable - CharSequence connectAddress() { - return connectAddress; - } - - void proxy(final ProxyConfig<?> proxyConfig, final CharSequence connectAddress) { - this.proxyConfig = requireNonNull(proxyConfig); - this.connectAddress = requireNonNull(connectAddress); + void proxyConfig(final CharSequence connectAddress, final ProxyConfig<?> proxyConfig) { + // Original ProxyConfig.address() is used only by DefaultSingleAddressHttpClientBuilder. For the actual + // ProxyConnectLBHttpConnectionFactory, we need only "connectAddress". To simplify internal state, we override + // ProxyConfig.address() with "connectAddress" and delegate all other methods to original ProxyConfig. + this.proxyConfig = new DelegatingProxyConfig(connectAddress.toString(), proxyConfig); } void fallbackPeerHost(@Nullable String fallbackPeerHost) { @@ -179,4 +175,56 @@ private static String filterSniName(@Nullable String peerHost) { // Literal IPv4 and IPv6 addresses are not permitted in "HostName". return peerHost == null || isValidIpV4Address(peerHost) || isValidIpV6Address(peerHost) ? null : peerHost; } + + private static final class DelegatingProxyConfig implements ProxyConfig<String> { + + private final String address; + private final ProxyConfig<?> delegate; + + DelegatingProxyConfig(final String address, final ProxyConfig<?> delegate) { + this.address = requireNonNull(address); + this.delegate = requireNonNull(delegate); + } + + @Override + public String address() { + return address; + } + + @Override + public Consumer<HttpHeaders> connectRequestHeadersInitializer() { + return delegate.connectRequestHeadersInitializer(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DelegatingProxyConfig)) { + return false; + } + + final DelegatingProxyConfig that = (DelegatingProxyConfig) o; + if (!address.equals(that.address)) { + return false; + } + return delegate.equals(that.delegate); + } + + @Override + public int hashCode() { + int result = address.hashCode(); + result = 31 * result + delegate.hashCode(); + return result; + } + + @Override + public String toString() { + return getClass().getSimpleName() + + "{address='" + address + '\'' + + ", delegate=" + delegate + + '}'; + } + } } diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ProxyConnectChannelSingle.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ProxyConnectChannelSingle.java index a8f79a7b3d..ec9f0415d9 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ProxyConnectChannelSingle.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ProxyConnectChannelSingle.java @@ -60,26 +60,23 @@ final class ProxyConnectChannelSingle extends ChannelInitSingle<Channel> { private final ConnectionObserver observer; private final HttpHeadersFactory headersFactory; - private final String connectAddress; - private final ProxyConfig<?> proxyConfig; + private final ProxyConfig<String> proxyConfig; ProxyConnectChannelSingle(final Channel channel, final ChannelInitializer channelInitializer, final ConnectionObserver observer, final HttpHeadersFactory headersFactory, - final String connectAddress, - final ProxyConfig<?> proxyConfig) { + final ProxyConfig<String> proxyConfig) { super(channel, channelInitializer); this.observer = observer; this.headersFactory = headersFactory; - this.connectAddress = connectAddress; this.proxyConfig = proxyConfig; assert !channel.config().isAutoRead(); } @Override protected ChannelHandler newChannelHandler(final Subscriber<? super Channel> subscriber) { - return new ProxyConnectHandler(observer, headersFactory, connectAddress, proxyConfig, subscriber); + return new ProxyConnectHandler(observer, headersFactory, proxyConfig, subscriber); } private static final class ProxyConnectHandler extends ChannelDuplexHandler { @@ -88,8 +85,7 @@ private static final class ProxyConnectHandler extends ChannelDuplexHandler { private final ConnectionObserver observer; private final HttpHeadersFactory headersFactory; - private final String connectAddress; - private final ProxyConfig<?> proxyConfig; + private final ProxyConfig<String> proxyConfig; @Nullable private Subscriber<? super Channel> subscriber; @Nullable @@ -99,12 +95,10 @@ private static final class ProxyConnectHandler extends ChannelDuplexHandler { private ProxyConnectHandler(final ConnectionObserver observer, final HttpHeadersFactory headersFactory, - final String connectAddress, - final ProxyConfig<?> proxyConfig, + final ProxyConfig<String> proxyConfig, final Subscriber<? super Channel> subscriber) { this.observer = observer; this.headersFactory = headersFactory; - this.connectAddress = connectAddress; this.proxyConfig = proxyConfig; this.subscriber = subscriber; } @@ -123,8 +117,8 @@ public void channelActive(final ChannelHandlerContext ctx) { } private void sendConnectRequest(final ChannelHandlerContext ctx) { - final HttpRequestMetaData request = newRequestMetaData(HTTP_1_1, CONNECT, connectAddress, - headersFactory.newHeaders()).addHeader(HOST, connectAddress); + final HttpRequestMetaData request = newRequestMetaData(HTTP_1_1, CONNECT, proxyConfig.address(), + headersFactory.newHeaders()).addHeader(HOST, proxyConfig.address()); proxyConfig.connectRequestHeadersInitializer().accept(request.headers()); connectObserver = observer.onProxyConnect(request); ctx.writeAndFlush(request).addListener(f -> { @@ -147,7 +141,7 @@ public void channelRead(final ChannelHandlerContext ctx, final Object msg) { } response = (HttpResponseMetaData) msg; if (response.status().statusClass() != SUCCESSFUL_2XX) { - failSubscriber(ctx, unsuccessfulResponse(ctx.channel(), response, connectAddress)); + failSubscriber(ctx, unsuccessfulResponse(ctx.channel(), response, proxyConfig.address())); } // We do not complete subscriber here because we need to wait for the HttpResponseDecoder state machine // to complete. Completion will be signalled by InboundDataEndEvent. Any other messages before that are @@ -201,7 +195,7 @@ public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt connectObserver.proxyConnectComplete(response); ctx.pipeline().remove(this); final Channel channel = ctx.channel(); - LOGGER.debug("{} Received successful response from proxy on CONNECT {}", channel, connectAddress); + LOGGER.debug("{} Received successful response from proxy on CONNECT {}", channel, proxyConfig.address()); final Subscriber<? super Channel> subscriberCopy = subscriber; subscriber = null; subscriberCopy.onSuccess(channel); diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ProxyConnectConnectionFactoryFilter.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ProxyConnectConnectionFactoryFilter.java index badce43788..b7d463f8b4 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ProxyConnectConnectionFactoryFilter.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ProxyConnectConnectionFactoryFilter.java @@ -25,7 +25,6 @@ import io.servicetalk.http.api.HttpContextKeys; import io.servicetalk.http.api.HttpExecutionStrategies; import io.servicetalk.http.api.HttpExecutionStrategy; -import io.servicetalk.transport.api.ExecutionStrategy; import io.servicetalk.transport.api.TransportObserver; import org.slf4j.Logger; @@ -57,8 +56,8 @@ final class ProxyConnectConnectionFactoryFilter<ResolvedAddress, C extends Filte private final String connectAddress; - ProxyConnectConnectionFactoryFilter(final CharSequence connectAddress, final ExecutionStrategy connectStrategy) { - this.connectAddress = connectAddress.toString(); + ProxyConnectConnectionFactoryFilter(final String connectAddress) { + this.connectAddress = connectAddress; } @Override diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ProxyConnectLBHttpConnectionFactory.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ProxyConnectLBHttpConnectionFactory.java index 3a5e979e70..909c7362a4 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ProxyConnectLBHttpConnectionFactory.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ProxyConnectLBHttpConnectionFactory.java @@ -66,7 +66,6 @@ */ final class ProxyConnectLBHttpConnectionFactory<ResolvedAddress> extends AbstractLBHttpConnectionFactory<ResolvedAddress> { - private final String connectAddress; ProxyConnectLBHttpConnectionFactory( final ReadOnlyHttpClientConfig config, final HttpExecutionContext executionContext, @@ -80,8 +79,6 @@ final class ProxyConnectLBHttpConnectionFactory<ResolvedAddress> assert config.hasProxy() : "Unexpected hasProxy flag"; assert config.tcpConfig().sslContext() != null : "Proxy CONNECT works only for TLS connections"; assert config.proxyConfig() != null : "ProxyConfig is required"; - assert config.connectAddress() != null : "Address (authority) for CONNECT request is required"; - this.connectAddress = config.connectAddress().toString(); } @Override @@ -103,7 +100,7 @@ private Single<? extends FilterableStreamingHttpConnection> createConnection( new TcpClientChannelInitializer(config.tcpConfig(), observer, executionContext, true) .andThen(new HttpClientChannelInitializer( getByteBufAllocator(executionContext.bufferAllocator()), h1Config, closeHandler)), - observer, h1Config.headersFactory(), connectAddress, config.proxyConfig()) + observer, h1Config.headersFactory(), config.proxyConfig()) .flatMap(ProxyConnectLBHttpConnectionFactory::handshake) .flatMap(protocol -> finishConnectionInitialization(protocol, channel, closeHandler, observer)) .onErrorMap(cause -> handleException(cause, channel)); diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ReadOnlyHttpClientConfig.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ReadOnlyHttpClientConfig.java index e65496c0c4..5495ac66ad 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ReadOnlyHttpClientConfig.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ReadOnlyHttpClientConfig.java @@ -1,5 +1,5 @@ /* - * Copyright © 2018-2019 Apple Inc. and the ServiceTalk project authors + * Copyright © 2018-2019, 2021, 2023 Apple Inc. and the ServiceTalk project authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,9 +27,7 @@ final class ReadOnlyHttpClientConfig { @Nullable private final H2ProtocolConfig h2Config; @Nullable - private final ProxyConfig<?> proxyConfig; - @Nullable - private final CharSequence connectAddress; + private final ProxyConfig<String> proxyConfig; private final boolean allowDropTrailers; ReadOnlyHttpClientConfig(final HttpClientConfig from) { @@ -38,7 +36,6 @@ final class ReadOnlyHttpClientConfig { h1Config = configs.h1Config(); h2Config = configs.h2Config(); proxyConfig = from.proxyConfig(); - connectAddress = from.connectAddress(); allowDropTrailers = configs.allowDropTrailersReadFromTransport(); } @@ -65,16 +62,11 @@ boolean isH2PriorKnowledge() { } @Nullable - ProxyConfig<?> proxyConfig() { + ProxyConfig<String> proxyConfig() { return proxyConfig; } - @Nullable - CharSequence connectAddress() { - return connectAddress; - } - boolean hasProxy() { - return connectAddress != null; + return proxyConfig != null; } }