From 938fade2e083eabe2fdb4a6816061911cc24c388 Mon Sep 17 00:00:00 2001 From: Jorge Bescos Gascon Date: Tue, 11 Jul 2023 13:59:33 +0200 Subject: [PATCH 1/3] CONNECT host:port is wrong Signed-off-by: Jorge Bescos Gascon --- .../io/helidon/common/http/HeaderEnum.java | 1 + .../java/io/helidon/common/http/Http.java | 7 ++++++ .../http1/Http1ClientConnection.java | 13 +++++++++- .../webclient/http1/HttpCallChainBase.java | 25 ++++++++++++------- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/common/http/src/main/java/io/helidon/common/http/HeaderEnum.java b/common/http/src/main/java/io/helidon/common/http/HeaderEnum.java index fdbd289be23..503ff4d0c50 100644 --- a/common/http/src/main/java/io/helidon/common/http/HeaderEnum.java +++ b/common/http/src/main/java/io/helidon/common/http/HeaderEnum.java @@ -77,6 +77,7 @@ enum HeaderEnum implements Http.HeaderName { LINK("Link"), LOCATION("Location"), PRAGMA("Pragma"), + PROXY_CONNECTION("Proxy-Connection"), PUBLIC_KEY_PINS("Public-Key-Pins"), RETRY_AFTER("Retry-After"), SERVER("Server"), diff --git a/common/http/src/main/java/io/helidon/common/http/Http.java b/common/http/src/main/java/io/helidon/common/http/Http.java index ee931aef813..b0a2450ba11 100644 --- a/common/http/src/main/java/io/helidon/common/http/Http.java +++ b/common/http/src/main/java/io/helidon/common/http/Http.java @@ -1332,6 +1332,13 @@ public static final class Header { * Implementation-specific fields that may have various effects anywhere along the request-response chain. */ public static final HeaderName PRAGMA = HeaderEnum.PRAGMA; + /** + * The {@code Proxy-Connection} header name. + * Implemented as a misunderstanding of the HTTP specifications. Common because of mistakes in + * implementations of early HTTP versions. Has exactly the same functionality as standard + * Connection field. Must not be used with HTTP/2. + */ + public static final HeaderName PROXY_CONNECTION = HeaderEnum.PROXY_CONNECTION; /** * The {@code Public-Key-Pins} header name. * HTTP Public Key Pinning, announces hash of website's authentic TLS certificate. diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientConnection.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientConnection.java index 885b1961be8..3361cb44dbe 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientConnection.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientConnection.java @@ -157,6 +157,14 @@ private int connectToProxy(InetSocketAddress remoteAddress, InetSocketAddress pr uriHelper.scheme("http"); uriHelper.host(proxyAddress.getHostName()); uriHelper.port(proxyAddress.getPort()); + /* + * Example: + * CONNECT www.youtube.com:443 HTTP/1.1 + * User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:108.0) Gecko/20100101 Firefox/108.0 + * Proxy-Connection: keep-alive + * Connection: keep-alive + * Host: www.youtube.com:443 + */ Http1ClientConfig clientConfig = Http1ClientConfig.builder().mediaContext(MediaContext.create()) .socketOptions(options).dnsResolver(connectionKey.dnsResolver()) .dnsAddressLookup(connectionKey.dnsAddressLookup()).defaultHeaders(WritableHeaders.create()).build(); @@ -164,7 +172,10 @@ private int connectToProxy(InetSocketAddress remoteAddress, InetSocketAddress pr Method.CONNECT, uriHelper, UriQueryWriteable.create(), Collections.emptyMap()); httpClient.connection(proxyConnection); httpClient.header(Header.HOST, hostPort); - httpClient.header(Header.ACCEPT, "*/*"); + if (keepAlive) { + httpClient.header(Header.CONNECTION, "keep-alive"); + httpClient.header(Header.PROXY_CONNECTION, "keep-alive"); + } Http1ClientResponse response = httpClient.request(); // Re-use socket diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/HttpCallChainBase.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/HttpCallChainBase.java index be6d3b5d2bc..d46ed70ead9 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/HttpCallChainBase.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/HttpCallChainBase.java @@ -24,6 +24,8 @@ import io.helidon.common.http.ClientResponseHeaders; import io.helidon.common.http.Headers; import io.helidon.common.http.Http; +import io.helidon.common.http.Http.Header; +import io.helidon.common.http.Http.Method; import io.helidon.common.http.Http1HeadersParser; import io.helidon.common.http.WritableHeaders; import io.helidon.nima.common.tls.Tls; @@ -89,15 +91,20 @@ abstract WebClientServiceResponse doProceed(ClientConnection connection, BufferData writeBuffer); void prologue(BufferData nonEntityData, WebClientServiceRequest request, UriHelper uri) { - // TODO When proxy is implemented, change default value of Http1ClientConfig.relativeUris to false - // and below conditional statement to: - // proxy == Proxy.noProxy() || proxy.noProxyPredicate().apply(finalUri) || clientConfig.relativeUris - String schemeHostPort = clientConfig.relativeUris() ? "" : uri.scheme() + "://" + uri.host() + ":" + uri.port(); - nonEntityData.writeAscii(request.method().text() - + " " - + schemeHostPort - + uri.pathWithQueryAndFragment(request.query(), request.fragment()) - + " HTTP/1.1\r\n"); + if (request.method() == Method.CONNECT) { + // When CONNECT, the first line contains the remote host:port, in the same way as the HOST header. + nonEntityData.writeAscii(request.method().text() + + " " + + request.headers().get(Header.HOST).value() + + " HTTP/1.1\r\n"); + } else { + String schemeHostPort = clientConfig.relativeUris() ? "" : uri.scheme() + "://" + uri.host() + ":" + uri.port(); + nonEntityData.writeAscii(request.method().text() + + " " + + schemeHostPort + + uri.pathWithQueryAndFragment(request.query(), request.fragment()) + + " HTTP/1.1\r\n"); + } } ClientResponseHeaders readHeaders(DataReader reader) { From 79bef69eec25a1976332041263baed4a3cc07568 Mon Sep 17 00:00:00 2001 From: Jorge Bescos Gascon Date: Wed, 12 Jul 2023 07:36:53 +0200 Subject: [PATCH 2/3] Allow HTTP 1.0 responses Signed-off-by: Jorge Bescos Gascon --- .../http1/Http1ClientConnection.java | 4 +- .../webclient/http1/Http1StatusParser.java | 14 +++--- .../http1/Http1StatusParserTest.java | 49 +++++++++++++++++++ 3 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/Http1StatusParserTest.java diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientConnection.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientConnection.java index 3361cb44dbe..29eed702611 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientConnection.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientConnection.java @@ -180,7 +180,7 @@ private int connectToProxy(InetSocketAddress remoteAddress, InetSocketAddress pr // Re-use socket this.socket = proxyConnection.socket; - // Note that Http1StatusParser fails parsing HTTP/1.0 and some proxies will return that. + return response.status().code(); } @@ -194,6 +194,7 @@ Http1ClientConnection connect() { socket = new Socket(); socket.setSoTimeout((int) options.readTimeout().toMillis()); options.configureSocket(socket); + channelId = createChannelId(socket); InetSocketAddress remoteAddress = inetSocketAddress(); strategy = ConnectionStrategy.get(this, remoteAddress); strategy.connect(this, remoteAddress); @@ -207,7 +208,6 @@ Http1ClientConnection connect() { socket.getLocalAddress(), Thread.currentThread().getName())); } - this.channelId = createChannelId(socket); this.reader = new DataReader(helidonSocket); this.writer = new DataWriter() { @Override diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1StatusParser.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1StatusParser.java index 2f16de7baea..3b3c636235f 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1StatusParser.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1StatusParser.java @@ -24,14 +24,14 @@ import io.helidon.common.http.Http; /** - * Parser of HTTP/1.1 response status. + * Parser of HTTP/1.0 or HTTP/1.1 response status. */ public final class Http1StatusParser { private Http1StatusParser() { } /** - * Read the status line from HTTP/1.1 response. + * Read the status line from HTTP/1.0 or HTTP/1.1 response. * * @param reader data reader to obtain bytes from * @param maxLength maximal number of bytes that can be processed before end of line is reached @@ -73,15 +73,15 @@ public static Http.Status readStatus(DataReader reader, int maxLength) { reader.skip(1); // space newLine -= space; newLine--; - if (!protocolVersion.equals("1.1")) { - throw new IllegalStateException("HTTP response did not contain correct status line. Version is not 1.1: \n" + if (!protocolVersion.equals("1.0") && !protocolVersion.equals("1.1")) { + throw new IllegalStateException("HTTP response did not contain correct status line. Version is not 1.0 or 1.1: \n" + BufferData.create(protocolVersion.getBytes(StandardCharsets.US_ASCII)) .debugDataHex()); } - // HTTP/1.1 200 OK + // HTTP/1.0 or HTTP/1.1 200 OK space = reader.findOrNewLine(Bytes.SPACE_BYTE, newLine); if (space == newLine) { - throw new IllegalStateException("HTTP Response did not contain HTTP status line. Line: HTTP/1.1\n" + throw new IllegalStateException("HTTP Response did not contain HTTP status line. Line: HTTP/1.0 or HTTP/1.1\n" + reader.readBuffer(newLine).debugDataHex()); } String code = reader.readAsciiString(space); @@ -94,7 +94,7 @@ public static Http.Status readStatus(DataReader reader, int maxLength) { try { return Http.Status.create(Integer.parseInt(code), phrase); } catch (NumberFormatException e) { - throw new IllegalStateException("HTTP Response did not contain HTTP status line. Line HTTP/1.1 \n" + throw new IllegalStateException("HTTP Response did not contain HTTP status line. Line HTTP/1.0 or HTTP/1.1 \n" + BufferData.create(code.getBytes(StandardCharsets.US_ASCII)) + "\n" + BufferData.create(phrase.getBytes(StandardCharsets.US_ASCII))); } diff --git a/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/Http1StatusParserTest.java b/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/Http1StatusParserTest.java new file mode 100644 index 00000000000..56cad753bb3 --- /dev/null +++ b/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/Http1StatusParserTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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 io.helidon.nima.webclient.http1; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import io.helidon.common.buffers.DataReader; +import io.helidon.common.http.Http.Status; + +import org.junit.jupiter.api.Test; + +class Http1StatusParserTest { + + @Test + public void http10() { + String response = "HTTP/1.0 200 Connection established\r\n"; + Status status = Http1StatusParser.readStatus(new DataReader(() -> response.getBytes()), 256); + assertEquals(200, status.code()); + } + + @Test + public void http11() { + String response = "HTTP/1.1 200 Connection established\r\n"; + Status status = Http1StatusParser.readStatus(new DataReader(() -> response.getBytes()), 256); + assertEquals(200, status.code()); + } + + @Test + public void wrong() { + String response = "HTTP/1.2 200 Connection established\r\n"; + assertThrows(IllegalStateException.class, + () -> Http1StatusParser.readStatus(new DataReader(() -> response.getBytes()), 256)); + } +} From e9738f7690f84be89e1d02288eb8ab3b1f370556 Mon Sep 17 00:00:00 2001 From: Jorge Bescos Gascon Date: Thu, 13 Jul 2023 11:17:01 +0200 Subject: [PATCH 3/3] Refactoring for usage in http2 Signed-off-by: Jorge Bescos Gascon --- .../http2/webclient/ClientRequestImpl.java | 9 +- .../http2/webclient/ClientResponseImpl.java | 6 + .../nima/http2/webclient/ConnectionKey.java | 3 +- .../webclient/Http2ClientConnection.java | 3 +- .../webserver/DirectClientConnection.java | 7 + .../integration/webclient/webclient/pom.xml | 4 + .../webclient/etc/spotbugs/exclude.xml | 2 +- .../nima/webclient/ClientConnection.java | 8 + .../nima/webclient/ClientResponse.java | 8 + .../nima/webclient/ConnectionStrategy.java | 314 ++++++++++++++++++ .../java/io/helidon/nima/webclient/Proxy.java | 5 + .../webclient/http1/ClientRequestImpl.java | 1 + .../webclient/http1/ClientResponseImpl.java | 5 + .../nima/webclient/http1/ConnectionCache.java | 13 +- .../http1/Http1ClientConnection.java | 246 +------------- .../nima/webclient/HttpClientTest.java | 1 + .../http1/ClientRequestImplTest.java | 6 + 17 files changed, 399 insertions(+), 242 deletions(-) create mode 100644 nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ConnectionStrategy.java diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientRequestImpl.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientRequestImpl.java index e0642b37fed..b86fa7d86ec 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientRequestImpl.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientRequestImpl.java @@ -22,6 +22,7 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.function.Consumer; @@ -68,6 +69,7 @@ class ClientRequestImpl implements Http2ClientRequest { private int requestPrefetch = 0; private int maxRedirects; private ClientConnection explicitConnection; + private Proxy proxy; private Duration flowControlTimeout = Duration.ofMillis(100); private Duration timeout = Duration.ofSeconds(10); private UriFragment fragment = UriFragment.empty(); @@ -337,7 +339,8 @@ private Http2ClientStream newStream(UriHelper uri) { priorKnowledge, tls, client.dnsResolver(), - client.dnsAddressLookup()); + client.dnsAddressLookup(), + proxy); // this statement locks all threads - must not do anything complicated (just create a new instance) return CHANNEL_CACHE.computeIfAbsent(connectionKey, @@ -375,6 +378,8 @@ public void accept(HeaderValue httpHeader) { @Override public Http2ClientRequest proxy(Proxy proxy) { - throw new UnsupportedOperationException("Proxy is not supported in HTTP2"); + this.proxy = Objects.requireNonNull(proxy); + return this; } + } diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientResponseImpl.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientResponseImpl.java index f798963d750..6bc9b196255 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientResponseImpl.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientResponseImpl.java @@ -23,6 +23,7 @@ import io.helidon.common.http.Http; import io.helidon.nima.http.media.ReadableEntity; import io.helidon.nima.http2.Http2Headers; +import io.helidon.nima.webclient.ClientConnection; import io.helidon.nima.webclient.UriHelper; class ClientResponseImpl implements Http2ClientResponse { @@ -58,6 +59,11 @@ public URI lastEndpointUri() { return lastEndpointUri.toUri(); } + @Override + public ClientConnection connection() { + throw new UnsupportedOperationException("Not supported"); + } + @Override public void close() { if (stream != null) { diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ConnectionKey.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ConnectionKey.java index 9edcb1d4758..efe8543a66a 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ConnectionKey.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ConnectionKey.java @@ -19,8 +19,9 @@ import io.helidon.common.http.Http; import io.helidon.nima.common.tls.Tls; import io.helidon.nima.webclient.DnsAddressLookup; +import io.helidon.nima.webclient.Proxy; import io.helidon.nima.webclient.spi.DnsResolver; record ConnectionKey(Http.Method method, String scheme, String host, int port, boolean priorKnowledge, Tls tls, - DnsResolver dnsResolver, DnsAddressLookup dnsAddressLookup) { + DnsResolver dnsResolver, DnsAddressLookup dnsAddressLookup, Proxy proxy) { } diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java index 6ba8c32c75a..a4ba249b1ba 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java @@ -64,6 +64,7 @@ import io.helidon.nima.http2.Http2Settings; import io.helidon.nima.http2.Http2WindowUpdate; import io.helidon.nima.http2.WindowSize; +import io.helidon.nima.webclient.Proxy; import io.helidon.nima.webclient.spi.DnsResolver; import static java.lang.System.Logger.Level.DEBUG; @@ -312,7 +313,7 @@ private void handle() { private void doConnect() throws IOException { boolean useTls = "https".equals(connectionKey.scheme()) && connectionKey.tls() != null; - + Proxy proxy = connectionKey.proxy(); SSLSocket sslSocket = useTls ? connectionKey.tls().createSocket("h2") : null; socket = sslSocket == null ? new Socket() : sslSocket; diff --git a/nima/testing/junit5/webserver/src/main/java/io/helidon/nima/testing/junit5/webserver/DirectClientConnection.java b/nima/testing/junit5/webserver/src/main/java/io/helidon/nima/testing/junit5/webserver/DirectClientConnection.java index ea82b39b9d3..dc99c0de3c6 100644 --- a/nima/testing/junit5/webserver/src/main/java/io/helidon/nima/testing/junit5/webserver/DirectClientConnection.java +++ b/nima/testing/junit5/webserver/src/main/java/io/helidon/nima/testing/junit5/webserver/DirectClientConnection.java @@ -16,6 +16,7 @@ package io.helidon.nima.testing.junit5.webserver; +import java.net.Socket; import java.time.Duration; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; @@ -88,6 +89,11 @@ public void readTimeout(Duration readTimeout) { //NOOP } + @Override + public Socket socket() { + throw new UnsupportedOperationException("Socket does not exist in direct connection"); + } + private DataWriter writer(ArrayBlockingQueue queue) { return new DataWriter() { @Override @@ -153,4 +159,5 @@ private void startServer() { } }); } + } diff --git a/nima/tests/integration/webclient/webclient/pom.xml b/nima/tests/integration/webclient/webclient/pom.xml index f1fbce70400..570b0a31792 100644 --- a/nima/tests/integration/webclient/webclient/pom.xml +++ b/nima/tests/integration/webclient/webclient/pom.xml @@ -32,6 +32,10 @@ io.helidon.nima.webclient helidon-nima-webclient + + io.helidon.nima.http2 + helidon-nima-http2-webclient + io.helidon.nima.webserver helidon-nima-webserver diff --git a/nima/webclient/webclient/etc/spotbugs/exclude.xml b/nima/webclient/webclient/etc/spotbugs/exclude.xml index 51d89dbeec8..2d699018120 100644 --- a/nima/webclient/webclient/etc/spotbugs/exclude.xml +++ b/nima/webclient/webclient/etc/spotbugs/exclude.xml @@ -26,7 +26,7 @@ - + diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ClientConnection.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ClientConnection.java index b6b5c728734..4c83d070233 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ClientConnection.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ClientConnection.java @@ -16,6 +16,7 @@ package io.helidon.nima.webclient; +import java.net.Socket; import java.time.Duration; import io.helidon.common.buffers.DataReader; @@ -63,4 +64,11 @@ public interface ClientConnection { * @param readTimeout connection read timeout */ void readTimeout(Duration readTimeout); + + /** + * Socket of the connection. + * + * @return socket of the connection + */ + Socket socket(); } diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ClientResponse.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ClientResponse.java index eed7d39d2d0..72e65818c43 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ClientResponse.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ClientResponse.java @@ -100,4 +100,12 @@ default > void source(GenericType sourceType, T source) { */ @Override void close(); + + /** + * Return the current response connection. + * This is necessary for proxies because they need to re-use the same connection. + * + * @return the connection of the request + */ + ClientConnection connection(); } diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ConnectionStrategy.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ConnectionStrategy.java new file mode 100644 index 00000000000..5b14b0ecc23 --- /dev/null +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ConnectionStrategy.java @@ -0,0 +1,314 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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 io.helidon.nima.webclient; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.HexFormat; +import java.util.Optional; + +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; + +import io.helidon.common.http.Http.Header; +import io.helidon.common.http.Http.Method; +import io.helidon.common.http.Http.Status; +import io.helidon.common.socket.HelidonSocket; +import io.helidon.common.socket.PlainSocket; +import io.helidon.common.socket.SocketOptions; +import io.helidon.common.socket.TlsSocket; +import io.helidon.nima.common.tls.Tls; +import io.helidon.nima.webclient.Proxy.ProxyType; +import io.helidon.nima.webclient.http1.Http1Client; +import io.helidon.nima.webclient.http1.Http1ClientRequest; +import io.helidon.nima.webclient.http1.Http1ClientResponse; +import io.helidon.nima.webclient.spi.DnsResolver; + +import static java.lang.System.Logger.Level.DEBUG; +import static java.lang.System.Logger.Level.TRACE; + +/** + * Deals with the complexity of creating a connection to a server, with different HTTP protocols, TLS and + * proxy. + * + */ +public final class ConnectionStrategy { + + private static final System.Logger LOGGER = System.getLogger(ConnectionStrategy.class.getName()); + private final String host; + private final int port; + private final Proxy proxy; + private final Tls tls; + private final SocketOptions socketOptions; + private final DnsResolver dnsResolver; + private final DnsAddressLookup dnsAddressLookup; + private final boolean keepAlive; + private String channelId; + private Socket socket; + private HelidonSocket helidonSocket; + + private ConnectionStrategy(String host, int port, Proxy proxy, Tls tls, SocketOptions socketOptions, + DnsResolver dnsResolver, DnsAddressLookup dnsAddressLookup, boolean keepAlive) { + this.host = host; + this.port = port; + this.proxy = proxy; + this.tls = tls; + this.socketOptions = socketOptions; + this.dnsResolver = dnsResolver; + this.dnsAddressLookup = dnsAddressLookup; + this.keepAlive = keepAlive; + } + + private void connect() { + ConnectionType strategy; + try { + socket = new Socket(); + channelId = createChannelId(socket); + socket.setSoTimeout((int) socketOptions.readTimeout().toMillis()); + socketOptions.configureSocket(socket); + InetSocketAddress remoteAddress = inetSocketAddress(); + strategy = ConnectionType.get(proxy, tls, remoteAddress); + strategy.connect(this, remoteAddress); + } catch (IOException e) { + throw new UncheckedIOException("Could not connect to " + host + ":" + port, e); + } + + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, String.format("[%s] client connected %s %s", + channelId, + socket.getLocalAddress(), + Thread.currentThread().getName())); + } + } + + private InetSocketAddress inetSocketAddress() { + if (dnsResolver.useDefaultJavaResolver()) { + return new InetSocketAddress(host, port); + } else { + InetAddress address = dnsResolver.resolveAddress(host, dnsAddressLookup); + return new InetSocketAddress(address, port); + } + } + + private int connectToProxy(InetSocketAddress remoteAddress, InetSocketAddress proxyAddress) { + String hostPort = remoteAddress.getHostName() + ":" + remoteAddress.getPort(); + UriHelper uriHelper = UriHelper.create(); + uriHelper.scheme("http"); + uriHelper.host(proxyAddress.getHostName()); + uriHelper.port(proxyAddress.getPort()); + /* + * Example: + * CONNECT www.youtube.com:443 HTTP/1.1 + * User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:108.0) Gecko/20100101 Firefox/108.0 + * Proxy-Connection: keep-alive + * Connection: keep-alive + * Host: www.youtube.com:443 + */ + Http1Client client = Http1Client.builder().baseUri(uriHelper.toUri()) + .channelOptions(socketOptions).dnsResolver(dnsResolver) + .dnsAddressLookup(dnsAddressLookup).build(); + Http1ClientRequest request = client.method(Method.CONNECT) + // This avoids caching the connection + .proxy(Proxy.TUNNELING) + .header(Header.HOST, hostPort); + if (keepAlive) { + request = request.header(Header.CONNECTION, "keep-alive") + .header(Header.PROXY_CONNECTION, "keep-alive"); + } + Http1ClientResponse response = request.request(); + + // Re-use socket + this.socket = response.connection().socket(); + + return response.status().code(); + } + + private void debugTls(SSLSocket sslSocket) { + SSLSession sslSession = sslSocket.getSession(); + if (sslSession == null) { + LOGGER.log(TRACE, "No SSL session"); + return; + } + + String msg = "[client " + channelId + "] TLS negotiated:\n" + + "Application protocol: " + sslSocket.getApplicationProtocol() + "\n" + + "Handshake application protocol: " + sslSocket.getHandshakeApplicationProtocol() + "\n" + + "Protocol: " + sslSession.getProtocol() + "\n" + + "Cipher Suite: " + sslSession.getCipherSuite() + "\n" + + "Peer host: " + sslSession.getPeerHost() + "\n" + + "Peer port: " + sslSession.getPeerPort() + "\n" + + "Application buffer size: " + sslSession.getApplicationBufferSize() + "\n" + + "Packet buffer size: " + sslSession.getPacketBufferSize() + "\n" + + "Local principal: " + sslSession.getLocalPrincipal() + "\n"; + + try { + msg += "Peer principal: " + sslSession.getPeerPrincipal() + "\n"; + msg += "Peer certs: " + certsToString(sslSession.getPeerCertificates()) + "\n"; + } catch (SSLPeerUnverifiedException e) { + msg += "Peer not verified"; + } + + LOGGER.log(TRACE, msg); + } + + private String createChannelId(Socket socket) { + return "0x" + HexFormat.of().toHexDigits(System.identityHashCode(socket)); + } + + private String certsToString(Certificate[] peerCertificates) { + String[] certs = new String[peerCertificates.length]; + + for (int i = 0; i < peerCertificates.length; i++) { + Certificate peerCertificate = peerCertificates[i]; + if (peerCertificate instanceof X509Certificate x509) { + certs[i] = "type=" + peerCertificate.getType() + + ";key=" + peerCertificate.getPublicKey().getAlgorithm() + + "(" + peerCertificate.getPublicKey().getFormat() + ")" + + ";x509=V" + x509.getVersion() + + ";from=" + x509.getNotBefore() + + ";to=" + x509.getNotAfter() + + ";serial=" + x509.getSerialNumber().toString(16); + } else { + certs[i] = "type=" + peerCertificate.getType() + ";key=" + peerCertificate.getPublicKey(); + } + + } + + return String.join(", ", certs); + } + + /** + * Obtains a ConnectionValues having the data initialized. + * + * @param host uri address host + * @param port uri address port + * @param proxy Proxy server to use for outgoing requests + * @param tls TLS to be used in connection + * @param socketOptions the socket options + * @param dnsResolver DNS resolver to be used + * @param dnsAddressLookup DNS address lookup strategy + * @param keepAlive specifies whether the connection will be opened + * @return the ConnectionValues having the data initialized + */ + public static ConnectionValues connect(String host, int port, Proxy proxy, Tls tls, SocketOptions socketOptions, + DnsResolver dnsResolver, DnsAddressLookup dnsAddressLookup, boolean keepAlive) { + // FIXME Some args are Optional + ConnectionStrategy srategy = new ConnectionStrategy(host, port, proxy, tls, socketOptions, + dnsResolver, dnsAddressLookup, keepAlive); + srategy.connect(); + return new ConnectionValues(srategy.channelId, srategy.socket, srategy.helidonSocket); + } + + /** + * Connection values containing the scoket and related information. + * + * @param channelId unique identifier of socket + * @param socket the connected socket + * @param helidonSocket the connected helidonSocket + */ + public record ConnectionValues(String channelId, Socket socket, HelidonSocket helidonSocket) { } + + private enum ConnectionType { + PLAIN { + @Override + protected void connect(ConnectionStrategy connection, InetSocketAddress remoteAddress) throws IOException { + connection.socket.connect(remoteAddress, (int) connection.socketOptions.connectTimeout().toMillis()); + connection.helidonSocket = PlainSocket.client(connection.socket, connection.channelId); + } + }, + HTTPS { + @Override + protected void connect(ConnectionStrategy connection, InetSocketAddress remoteAddress) throws IOException { + // FIXME should it change for http/2.0? + SSLSocket sslSocket = connection.tls.createSocket("http/1.1"); + connection.socket = sslSocket; + connection.socket.connect(remoteAddress, (int) connection.socketOptions.connectTimeout().toMillis()); + sslSocket.startHandshake(); + connection.helidonSocket = TlsSocket.client(sslSocket, connection.channelId); + if (LOGGER.isLoggable(TRACE)) { + connection.debugTls(sslSocket); + } + } + }, + PROXY_PLAIN { + @Override + protected void connect(ConnectionStrategy connection, InetSocketAddress remoteAddress) throws IOException { + UriHelper uri = UriHelper.create(); + uri.scheme("http"); + uri.host(remoteAddress.getHostName()); + uri.port(remoteAddress.getPort()); + InetSocketAddress proxyAddress = connection.proxy.address(uri).get(); + int responseCode = connection.connectToProxy(remoteAddress, proxyAddress); + if (responseCode != Status.OK_200.code()) { + throw new IllegalStateException("Proxy sent wrong HTTP response code: " + responseCode); + } + connection.helidonSocket = PlainSocket.client(connection.socket, connection.channelId); + } + }, + PROXY_HTTPS { + @Override + protected void connect(ConnectionStrategy connection, InetSocketAddress remoteAddress) throws IOException { + UriHelper uri = UriHelper.create(); + uri.scheme("http"); + uri.host(remoteAddress.getHostName()); + uri.port(remoteAddress.getPort()); + InetSocketAddress proxyAddress = connection.proxy.address(uri).get(); + int responseCode = connection.connectToProxy(remoteAddress, proxyAddress); + if (responseCode != Status.OK_200.code()) { + throw new IllegalStateException("Proxy sent wrong HTTP response code: " + responseCode); + } + SSLSocket sslSocket = connection.tls.createSocket("http/1.1", connection.socket, remoteAddress); + connection.socket = sslSocket; + sslSocket.startHandshake(); + connection.helidonSocket = TlsSocket.client(sslSocket, connection.channelId); + if (LOGGER.isLoggable(TRACE)) { + connection.debugTls(sslSocket); + } + } + }; + + protected abstract void connect(ConnectionStrategy connection, InetSocketAddress remoteAddress) throws IOException; + + static ConnectionType get(Proxy proxy, Tls tls, InetSocketAddress remoteAddress) { + boolean useProxy = false; + if (proxy != null && proxy.type() != ProxyType.NONE) { + UriHelper uri = UriHelper.create(); + uri.scheme("http"); + uri.host(remoteAddress.getHostName()); + uri.port(remoteAddress.getPort()); + if (proxy.type() == ProxyType.SYSTEM) { + Optional optional = proxy.address(uri); + useProxy = optional.isPresent(); + } else if (!proxy.isNoHosts(uri)) { + useProxy = true; + } + } + if (useProxy) { + return tls != null ? ConnectionType.PROXY_HTTPS : ConnectionType.PROXY_PLAIN; + } else { + return tls != null ? ConnectionType.HTTPS : ConnectionType.PLAIN; + } + } + } + +} diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/Proxy.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/Proxy.java index 9df20ee4e62..1cc13efa134 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/Proxy.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/Proxy.java @@ -42,6 +42,11 @@ public class Proxy { private static final System.Logger LOGGER = System.getLogger(Proxy.class.getName()); + /** + * For internal usage when there is a connection to the proxy server. + * Specifies that the connection must not be cached. + */ + public static final Proxy TUNNELING = new Proxy(builder().type(ProxyType.NONE)); /** * No proxy instance. */ diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientRequestImpl.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientRequestImpl.java index db061b95928..9b16fa8792b 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientRequestImpl.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientRequestImpl.java @@ -450,4 +450,5 @@ private void rejectHeadWithEntity() { throw new IllegalArgumentException("Payload in method '" + Http.Method.HEAD + "' has no defined semantics"); } } + } diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientResponseImpl.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientResponseImpl.java index c25ec9a4cd8..8676031bfaa 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientResponseImpl.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientResponseImpl.java @@ -146,6 +146,11 @@ public void close() { } } + @Override + public ClientConnection connection() { + return connection; + } + @Override @SuppressWarnings("unchecked") public > void source(GenericType sourceType, T source) { diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ConnectionCache.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ConnectionCache.java index a687d96db26..65f94be3ef4 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ConnectionCache.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ConnectionCache.java @@ -48,12 +48,17 @@ static ClientConnection connection(Http1ClientConfig clientConfig, UriHelper uri, ClientRequestHeaders headers, boolean defaultKeepAlive) { - boolean keepAlive = handleKeepAlive(defaultKeepAlive, headers); Tls effectiveTls = HTTPS.equals(uri.scheme()) ? tls : null; - if (keepAlive) { - return keepAliveConnection(clientConfig, effectiveTls, uri, proxy); - } else { + if (proxy == Proxy.TUNNELING) { + // This connection could be keep alive, but we don't want to cache it return oneOffConnection(clientConfig, effectiveTls, uri, proxy); + } else { + boolean keepAlive = handleKeepAlive(defaultKeepAlive, headers); + if (keepAlive) { + return keepAliveConnection(clientConfig, effectiveTls, uri, proxy); + } else { + return oneOffConnection(clientConfig, effectiveTls, uri, proxy); + } } } diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientConnection.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientConnection.java index 29eed702611..17fa1ffee41 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientConnection.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientConnection.java @@ -18,44 +18,22 @@ import java.io.IOException; import java.io.UncheckedIOException; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketException; -import java.security.cert.Certificate; -import java.security.cert.X509Certificate; import java.time.Duration; -import java.util.Collections; -import java.util.HexFormat; -import java.util.Optional; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocket; - import io.helidon.common.buffers.BufferData; import io.helidon.common.buffers.DataReader; import io.helidon.common.buffers.DataWriter; -import io.helidon.common.http.Http.Header; -import io.helidon.common.http.Http.Method; -import io.helidon.common.http.Http.Status; -import io.helidon.common.http.WritableHeaders; import io.helidon.common.socket.HelidonSocket; -import io.helidon.common.socket.PlainSocket; import io.helidon.common.socket.SocketOptions; -import io.helidon.common.socket.TlsSocket; -import io.helidon.common.uri.UriQueryWriteable; -import io.helidon.nima.http.media.MediaContext; import io.helidon.nima.webclient.ClientConnection; -import io.helidon.nima.webclient.Proxy; -import io.helidon.nima.webclient.Proxy.ProxyType; -import io.helidon.nima.webclient.UriHelper; -import io.helidon.nima.webclient.spi.DnsResolver; +import io.helidon.nima.webclient.ConnectionStrategy; +import io.helidon.nima.webclient.ConnectionStrategy.ConnectionValues; import static java.lang.System.Logger.Level.DEBUG; -import static java.lang.System.Logger.Level.TRACE; class Http1ClientConnection implements ClientConnection { private static final System.Logger LOGGER = System.getLogger(Http1ClientConnection.class.getName()); @@ -114,6 +92,11 @@ public String channelId() { return channelId; } + @Override + public Socket socket() { + return socket; + } + @Override public void readTimeout(Duration readTimeout) { if (!isConnected()) { @@ -130,84 +113,14 @@ boolean isConnected() { return socket != null && socket.isConnected(); } - private InetSocketAddress inetSocketAddress() { - DnsResolver dnsResolver = connectionKey.dnsResolver(); - if (dnsResolver.useDefaultJavaResolver()) { - return new InetSocketAddress(connectionKey.host(), connectionKey.port()); - } else { - InetAddress address = dnsResolver.resolveAddress(connectionKey.host(), connectionKey.dnsAddressLookup()); - return new InetSocketAddress(address, connectionKey.port()); - } - } - - private Http1ClientConnection proxyConnection(InetSocketAddress proxyAddress) { - return new Http1ClientConnection(options, new ConnectionKey("http", - proxyAddress.getHostName(), - proxyAddress.getPort(), - null, - connectionKey.dnsResolver(), - connectionKey.dnsAddressLookup(), - null)).connect(); - } - - private int connectToProxy(InetSocketAddress remoteAddress, InetSocketAddress proxyAddress) { - String hostPort = remoteAddress.getHostName() + ":" + remoteAddress.getPort(); - Http1ClientConnection proxyConnection = proxyConnection(proxyAddress); - UriHelper uriHelper = UriHelper.create(); - uriHelper.scheme("http"); - uriHelper.host(proxyAddress.getHostName()); - uriHelper.port(proxyAddress.getPort()); - /* - * Example: - * CONNECT www.youtube.com:443 HTTP/1.1 - * User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:108.0) Gecko/20100101 Firefox/108.0 - * Proxy-Connection: keep-alive - * Connection: keep-alive - * Host: www.youtube.com:443 - */ - Http1ClientConfig clientConfig = Http1ClientConfig.builder().mediaContext(MediaContext.create()) - .socketOptions(options).dnsResolver(connectionKey.dnsResolver()) - .dnsAddressLookup(connectionKey.dnsAddressLookup()).defaultHeaders(WritableHeaders.create()).build(); - ClientRequestImpl httpClient = new ClientRequestImpl(clientConfig, - Method.CONNECT, uriHelper, UriQueryWriteable.create(), Collections.emptyMap()); - httpClient.connection(proxyConnection); - httpClient.header(Header.HOST, hostPort); - if (keepAlive) { - httpClient.header(Header.CONNECTION, "keep-alive"); - httpClient.header(Header.PROXY_CONNECTION, "keep-alive"); - } - Http1ClientResponse response = httpClient.request(); - - // Re-use socket - this.socket = proxyConnection.socket; - - return response.status().code(); - } - - private String createChannelId(Socket socket) { - return "0x" + HexFormat.of().toHexDigits(System.identityHashCode(socket)); - } - Http1ClientConnection connect() { - ConnectionStrategy strategy; - try { - socket = new Socket(); - socket.setSoTimeout((int) options.readTimeout().toMillis()); - options.configureSocket(socket); - channelId = createChannelId(socket); - InetSocketAddress remoteAddress = inetSocketAddress(); - strategy = ConnectionStrategy.get(this, remoteAddress); - strategy.connect(this, remoteAddress); - } catch (IOException e) { - throw new UncheckedIOException("Could not connect to " + connectionKey.host() + ":" + connectionKey.port(), e); - } + ConnectionValues connection = ConnectionStrategy.connect(connectionKey.host(), connectionKey.port(), + connectionKey.proxy(), connectionKey.tls(), options, connectionKey.dnsResolver(), + connectionKey.dnsAddressLookup(), keepAlive); + this.socket = connection.socket(); + this.channelId = connection.channelId(); + this.helidonSocket = connection.helidonSocket(); - if (LOGGER.isLoggable(DEBUG)) { - LOGGER.log(DEBUG, String.format("[%s] client connected %s %s", - channelId, - socket.getLocalAddress(), - Thread.currentThread().getName())); - } this.reader = new DataReader(helidonSocket); this.writer = new DataWriter() { @Override @@ -259,137 +172,4 @@ void finishRequest() { // Close if unable to add to queue close(); } - - private void debugTls(SSLSocket sslSocket) { - SSLSession sslSession = sslSocket.getSession(); - if (sslSession == null) { - LOGGER.log(TRACE, "No SSL session"); - return; - } - - String msg = "[client " + channelId + "] TLS negotiated:\n" - + "Application protocol: " + sslSocket.getApplicationProtocol() + "\n" - + "Handshake application protocol: " + sslSocket.getHandshakeApplicationProtocol() + "\n" - + "Protocol: " + sslSession.getProtocol() + "\n" - + "Cipher Suite: " + sslSession.getCipherSuite() + "\n" - + "Peer host: " + sslSession.getPeerHost() + "\n" - + "Peer port: " + sslSession.getPeerPort() + "\n" - + "Application buffer size: " + sslSession.getApplicationBufferSize() + "\n" - + "Packet buffer size: " + sslSession.getPacketBufferSize() + "\n" - + "Local principal: " + sslSession.getLocalPrincipal() + "\n"; - - try { - msg += "Peer principal: " + sslSession.getPeerPrincipal() + "\n"; - msg += "Peer certs: " + certsToString(sslSession.getPeerCertificates()) + "\n"; - } catch (SSLPeerUnverifiedException e) { - msg += "Peer not verified"; - } - - LOGGER.log(TRACE, msg); - } - - private String certsToString(Certificate[] peerCertificates) { - String[] certs = new String[peerCertificates.length]; - - for (int i = 0; i < peerCertificates.length; i++) { - Certificate peerCertificate = peerCertificates[i]; - if (peerCertificate instanceof X509Certificate x509) { - certs[i] = "type=" + peerCertificate.getType() - + ";key=" + peerCertificate.getPublicKey().getAlgorithm() - + "(" + peerCertificate.getPublicKey().getFormat() + ")" - + ";x509=V" + x509.getVersion() - + ";from=" + x509.getNotBefore() - + ";to=" + x509.getNotAfter() - + ";serial=" + x509.getSerialNumber().toString(16); - } else { - certs[i] = "type=" + peerCertificate.getType() + ";key=" + peerCertificate.getPublicKey(); - } - - } - - return String.join(", ", certs); - } - - private enum ConnectionStrategy { - PLAIN { - @Override - protected void connect(Http1ClientConnection connection, InetSocketAddress remoteAddress) throws IOException { - connection.socket.connect(remoteAddress, (int) connection.options.connectTimeout().toMillis()); - connection.helidonSocket = PlainSocket.client(connection.socket, connection.channelId); - } - }, - HTTPS { - @Override - protected void connect(Http1ClientConnection connection, InetSocketAddress remoteAddress) throws IOException { - SSLSocket sslSocket = connection.connectionKey.tls().createSocket("http/1.1"); - connection.socket = sslSocket; - connection.socket.connect(remoteAddress, (int) connection.options.connectTimeout().toMillis()); - sslSocket.startHandshake(); - connection.helidonSocket = TlsSocket.client(sslSocket, connection.channelId); - if (LOGGER.isLoggable(TRACE)) { - connection.debugTls(sslSocket); - } - } - }, - PROXY_PLAIN { - @Override - protected void connect(Http1ClientConnection connection, InetSocketAddress remoteAddress) throws IOException { - UriHelper uri = UriHelper.create(); - uri.scheme("http"); - uri.host(remoteAddress.getHostName()); - uri.port(remoteAddress.getPort()); - InetSocketAddress proxyAddress = connection.connectionKey.proxy().address(uri).get(); - int responseCode = connection.connectToProxy(remoteAddress, proxyAddress); - if (responseCode != Status.OK_200.code()) { - throw new IllegalStateException("Proxy sent wrong HTTP response code: " + responseCode); - } - connection.helidonSocket = PlainSocket.client(connection.socket, connection.channelId); - } - }, - PROXY_HTTPS { - @Override - protected void connect(Http1ClientConnection connection, InetSocketAddress remoteAddress) throws IOException { - UriHelper uri = UriHelper.create(); - uri.scheme("http"); - uri.host(remoteAddress.getHostName()); - uri.port(remoteAddress.getPort()); - InetSocketAddress proxyAddress = connection.connectionKey.proxy().address(uri).get(); - int responseCode = connection.connectToProxy(remoteAddress, proxyAddress); - if (responseCode != Status.OK_200.code()) { - throw new IllegalStateException("Proxy sent wrong HTTP response code: " + responseCode); - } - SSLSocket sslSocket = connection.connectionKey.tls().createSocket("http/1.1", connection.socket, remoteAddress); - connection.socket = sslSocket; - sslSocket.startHandshake(); - connection.helidonSocket = TlsSocket.client(sslSocket, connection.channelId); - if (LOGGER.isLoggable(TRACE)) { - connection.debugTls(sslSocket); - } - } - }; - - protected abstract void connect(Http1ClientConnection connection, InetSocketAddress remoteAddress) throws IOException; - - static ConnectionStrategy get(Http1ClientConnection connection, InetSocketAddress remoteAddress) { - Proxy proxy = connection.connectionKey.proxy(); - boolean useProxy = false; - if (proxy != null && proxy.type() != ProxyType.NONE) { - UriHelper uri = UriHelper.create(); - uri.scheme("http"); - uri.host(remoteAddress.getHostName()); - uri.port(remoteAddress.getPort()); - if (proxy.type() == ProxyType.SYSTEM) { - Optional optional = proxy.address(uri); - useProxy = optional.isPresent(); - } else if (!proxy.isNoHosts(uri)) { - useProxy = true; - } - } - if (useProxy) { - return connection.connectionKey.tls() != null ? ConnectionStrategy.PROXY_HTTPS : ConnectionStrategy.PROXY_PLAIN; - } else { - return connection.connectionKey.tls() != null ? ConnectionStrategy.HTTPS : ConnectionStrategy.PLAIN; - } - } - } } diff --git a/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/HttpClientTest.java b/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/HttpClientTest.java index 6f125733787..22f72f7e38f 100644 --- a/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/HttpClientTest.java +++ b/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/HttpClientTest.java @@ -198,5 +198,6 @@ public FakeHttpClientRequest property(String propertyName, String propertyValue) public FakeHttpClientRequest keepAlive(boolean keepAlive) { return this; } + } } diff --git a/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java b/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java index 5d70023729b..cf386cfc051 100644 --- a/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java +++ b/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java @@ -17,6 +17,7 @@ package io.helidon.nima.webclient.http1; import java.io.OutputStream; +import java.net.Socket; import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -528,6 +529,11 @@ public void readTimeout(Duration readTimeout) { //NOOP } + @Override + public Socket socket() { + return null; + } + // This will be used for testing the element of Prologue String getPrologue() { return prologue;