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/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/tests/integration/webclient/AuthHttpProxyTest.java b/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/tests/integration/webclient/AuthHttpProxyTest.java
new file mode 100644
index 00000000000..0af5e09ea7c
--- /dev/null
+++ b/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/tests/integration/webclient/AuthHttpProxyTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.tests.integration.webclient;
+
+import io.helidon.common.http.Http;
+import io.helidon.nima.http2.webclient.Http2Client;
+import io.helidon.nima.testing.junit5.webserver.ServerTest;
+import io.helidon.nima.testing.junit5.webserver.SetUpRoute;
+import io.helidon.nima.webclient.api.HttpClient;
+import io.helidon.nima.webclient.api.HttpClientResponse;
+import io.helidon.nima.webclient.api.Proxy;
+import io.helidon.nima.webclient.api.Proxy.ProxyType;
+import io.helidon.nima.webclient.http1.Http1Client;
+import io.helidon.nima.webserver.WebServer;
+import io.helidon.nima.webserver.http.HttpRouting;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static io.helidon.common.http.Http.Method.GET;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.fail;
+
+@ServerTest
+class AuthHttpProxyTest {
+
+ private static final String PROXY_HOST = "localhost";
+ private static final String USER = "user";
+ private static final String PASSWORD = "password";
+ private int proxyPort;
+ private HttpProxy httpProxy;
+
+ private final HttpClient> clientHttp1;
+ private final HttpClient> clientHttp2;
+
+ AuthHttpProxyTest(WebServer server) {
+ String uri = "http://localhost:" + server.port();
+ this.clientHttp1 = Http1Client.builder()
+ .baseUri(uri)
+ .proxy(Proxy.noProxy())
+ .build();
+ this.clientHttp2 = Http2Client.builder()
+ .baseUri(uri)
+ .proxy(Proxy.noProxy())
+ .build();
+ }
+
+ @SetUpRoute
+ static void routing(HttpRouting.Builder router) {
+ router.route(GET, "/get", Routes::get);
+ }
+
+ @BeforeEach
+ void before() {
+ httpProxy = new HttpProxy(0, USER, PASSWORD);
+ httpProxy.start();
+ proxyPort = httpProxy.connectedPort();
+ assertThat(httpProxy.counter(), is(0));
+ }
+
+ @AfterEach
+ void after() {
+ httpProxy.stop();
+ }
+
+ @Test
+ void testUserPasswordCorrect1() {
+ Proxy proxy = Proxy.builder().type(ProxyType.HTTP).host(PROXY_HOST)
+ .username(USER).password(PASSWORD.toCharArray()).port(proxyPort).build();
+ successVerify(proxy, clientHttp1);
+ }
+
+ @Test
+ void testUserPasswordCorrect2() {
+ Proxy proxy = Proxy.builder().type(ProxyType.HTTP).host(PROXY_HOST)
+ .username(USER).password(PASSWORD.toCharArray()).port(proxyPort).build();
+ successVerify(proxy, clientHttp2);
+ }
+
+ @Test
+ void testUserPasswordNotCorrect1() {
+ Proxy proxy = Proxy.builder().type(ProxyType.HTTP).host(PROXY_HOST)
+ .username(USER).password("wrong".toCharArray()).port(proxyPort).build();
+ failVerify(proxy, clientHttp1);
+ }
+
+ @Test
+ void testUserPasswordNotCorrect2() {
+ Proxy proxy = Proxy.builder().type(ProxyType.HTTP).host(PROXY_HOST)
+ .username(USER).password("wrong".toCharArray()).port(proxyPort).build();
+ failVerify(proxy, clientHttp2);
+ }
+
+ private void successVerify(Proxy proxy, HttpClient> client) {
+ try (HttpClientResponse response = client.get("/get").proxy(proxy).request()) {
+ assertThat(response.status(), is(Http.Status.OK_200));
+ String entity = response.entity().as(String.class);
+ assertThat(entity, is("Hello"));
+ }
+ assertThat(httpProxy.counter(), is(1));
+ }
+
+ private void failVerify(Proxy proxy, HttpClient> client) {
+ try (HttpClientResponse response = client.get("/get").proxy(proxy).request()) {
+ fail("Expected exception");
+ } catch (IllegalStateException e) {
+ assertThat(e.getMessage(), is("Proxy sent wrong HTTP response code: 401 Unauthorized"));
+ }
+ assertThat(httpProxy.counter(), is(1));
+ }
+
+ private static class Routes {
+ private static String get() {
+ return "Hello";
+ }
+ }
+}
diff --git a/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/tests/integration/webclient/AuthHttpsProxyTest.java b/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/tests/integration/webclient/AuthHttpsProxyTest.java
new file mode 100644
index 00000000000..094ccbfae7d
--- /dev/null
+++ b/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/tests/integration/webclient/AuthHttpsProxyTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.tests.integration.webclient;
+
+import io.helidon.common.configurable.Resource;
+import io.helidon.common.http.Http;
+import io.helidon.common.pki.Keys;
+import io.helidon.nima.common.tls.Tls;
+import io.helidon.nima.http2.webclient.Http2Client;
+import io.helidon.nima.testing.junit5.webserver.ServerTest;
+import io.helidon.nima.testing.junit5.webserver.SetUpRoute;
+import io.helidon.nima.testing.junit5.webserver.SetUpServer;
+import io.helidon.nima.webclient.api.HttpClient;
+import io.helidon.nima.webclient.api.HttpClientResponse;
+import io.helidon.nima.webclient.api.Proxy;
+import io.helidon.nima.webclient.api.Proxy.ProxyType;
+import io.helidon.nima.webclient.http1.Http1Client;
+import io.helidon.nima.webserver.WebServer;
+import io.helidon.nima.webserver.WebServerConfig.Builder;
+import io.helidon.nima.webserver.http.HttpRouting;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static io.helidon.common.http.Http.Method.GET;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.fail;
+
+@ServerTest
+class AuthHttpsProxyTest {
+
+ private static final String PROXY_HOST = "localhost";
+ private static final String USER = "user";
+ private static final String PASSWORD = "password";
+ private int proxyPort;
+ private HttpProxy httpProxy;
+
+ private final HttpClient> clientHttp1;
+ private final HttpClient> clientHttp2;
+
+ AuthHttpsProxyTest(WebServer server) {
+ int port = server.port();
+
+ Tls tls = Tls.builder()
+ .trustAll(true)
+ .endpointIdentificationAlgorithm(Tls.ENDPOINT_IDENTIFICATION_NONE)
+ .build();
+
+ this.clientHttp1 = Http1Client.builder()
+ .baseUri("https://localhost:" + port)
+ .tls(tls)
+ .proxy(Proxy.noProxy())
+ .build();
+ this.clientHttp2 = Http2Client.builder()
+ .baseUri("https://localhost:" + port)
+ .tls(tls)
+ .proxy(Proxy.noProxy())
+ .build();
+ }
+
+ @SetUpRoute
+ static void routing(HttpRouting.Builder router) {
+ router.route(GET, "/get", Routes::get);
+ }
+
+ @SetUpServer
+ static void server(Builder builder) {
+ Keys privateKeyConfig = Keys.builder()
+ .keystore(keystore -> keystore
+ .keystore(Resource.create("server.p12"))
+ .passphrase("password"))
+ .build();
+
+ Tls tls = Tls.builder()
+ .privateKey(privateKeyConfig.privateKey().get())
+ .privateKeyCertChain(privateKeyConfig.certChain())
+ .trustAll(true)
+ .endpointIdentificationAlgorithm(Tls.ENDPOINT_IDENTIFICATION_NONE)
+ .build();
+
+ builder.tls(tls);
+ }
+
+ @BeforeEach
+ void before() {
+ httpProxy = new HttpProxy(0, USER, PASSWORD);
+ httpProxy.start();
+ proxyPort = httpProxy.connectedPort();
+ assertThat(httpProxy.counter(), is(0));
+ }
+
+ @AfterEach
+ void after() {
+ httpProxy.stop();
+ }
+
+ @Test
+ void testUserPasswordCorrect1() {
+ Proxy proxy = Proxy.builder().type(ProxyType.HTTP).host(PROXY_HOST)
+ .username(USER).password(PASSWORD.toCharArray()).port(proxyPort).build();
+ successVerify(proxy, clientHttp1);
+ }
+
+ @Test
+ void testUserPasswordCorrect2() {
+ Proxy proxy = Proxy.builder().type(ProxyType.HTTP).host(PROXY_HOST)
+ .username(USER).password(PASSWORD.toCharArray()).port(proxyPort).build();
+ successVerify(proxy, clientHttp2);
+ }
+
+ @Test
+ void testUserPasswordNotCorrect1() {
+ Proxy proxy = Proxy.builder().type(ProxyType.HTTP).host(PROXY_HOST)
+ .username(USER).password("wrong".toCharArray()).port(proxyPort).build();
+ failVerify(proxy, clientHttp1);
+ }
+
+ @Test
+ void testUserPasswordNotCorrect2() {
+ Proxy proxy = Proxy.builder().type(ProxyType.HTTP).host(PROXY_HOST)
+ .username(USER).password("wrong".toCharArray()).port(proxyPort).build();
+ failVerify(proxy, clientHttp2);
+ }
+
+ private void failVerify(Proxy proxy, HttpClient> client) {
+ try (HttpClientResponse response = client.get("/get").proxy(proxy).request()) {
+ fail("Expected exception");
+ } catch (IllegalStateException e) {
+ assertThat(e.getMessage(), is("Proxy sent wrong HTTP response code: 401 Unauthorized"));
+ }
+ assertThat(httpProxy.counter(), is(1));
+ }
+
+ private void successVerify(Proxy proxy, HttpClient> client) {
+ try (HttpClientResponse response = client.get("/get").proxy(proxy).request()) {
+ assertThat(response.status(), is(Http.Status.OK_200));
+ String entity = response.entity().as(String.class);
+ assertThat(entity, is("Hello"));
+ }
+ }
+
+ private static class Routes {
+ private static String get() {
+ return "Hello";
+ }
+ }
+}
diff --git a/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/tests/integration/webclient/HttpProxy.java b/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/tests/integration/webclient/HttpProxy.java
index ec199690bdf..232a6a7dbe3 100644
--- a/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/tests/integration/webclient/HttpProxy.java
+++ b/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/tests/integration/webclient/HttpProxy.java
@@ -24,11 +24,22 @@
import java.net.Socket;
import java.util.Arrays;
import java.util.Base64;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
+/**
+ * HttpProxy implementation. It has to handle two sockets per each connection to it:
+ * 1. The socket that starts the connection to the HTTP Proxy, known as origin.
+ * 2. The socket that will connect from the HTTP Proxy to the desired remote host, known as remote.
+ *
+ * An HTTP Proxy has to primarily pass data from both sides, origin to remote and remote to origin.
+ * Before doing this, it has to handle a first request from the origin to know where is the remote host.
+ *
+ * An instance of HttpProxy can not be re-used after stopping it.
+ */
class HttpProxy {
private static final System.Logger LOGGER = System.getLogger(HttpProxy.class.getName());
@@ -53,16 +64,21 @@ class HttpProxy {
this(port, null, null);
}
- boolean start() {
+ void start() {
+ CountDownLatch ready = new CountDownLatch(1);
executor.submit(() -> {
try (ServerSocket server = new ServerSocket(port)) {
this.connectedPort = server.getLocalPort();
LOGGER.log(Level.INFO, "Listening connections in port: " + connectedPort);
while (!stop) {
+ // Origin is the socket that starts the connection
Socket origin = server.accept();
LOGGER.log(Level.DEBUG, "Open: " + origin);
counter.incrementAndGet();
+ ready.countDown();
origin.setSoTimeout(TIMEOUT);
+ // Remote is the socket that will connect to the desired host, for example www.google.com
+ // It is not connected yet because we need to wait for the HTTP CONNECT to know where.
Socket remote = new Socket();
remote.setSoTimeout(TIMEOUT);
MiddleCommunicator remoteToOrigin = new MiddleCommunicator(executor, remote, origin, null);
@@ -82,9 +98,10 @@ boolean start() {
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(connectedPort), 10000);
responding = true;
- } catch (IOException e) {}
+ // Wait for counter is set to 0
+ ready.await(5, TimeUnit.SECONDS);
+ } catch (IOException | InterruptedException e) {}
}
- return responding;
}
int counter() {
@@ -108,11 +125,23 @@ int connectedPort() {
return connectedPort;
}
+ /**
+ * This is the core of the HTTP Proxy. One HTTP Proxy must run 2 instances of this, as it will be explained here.
+ *
+ * Its goal is to forward what arrives. There are two workflows:
+ * 1. Listen from origin to forward it to remote.
+ * 2. Listen from remote to forward it to origin.
+ *
+ * One instance of MiddleCommunicator can only handle 1 workflow, then you need 2 instances of it because
+ * it is needed to send data to remote, and also read data from it.
+ *
+ * The workflow number 1 (originToRemote) also requires one additional step. It has to handle
+ * an HTTP CONNECT request before start forwarding. This is needed to know where is the remote and to authenticate.
+ */
private class MiddleCommunicator {
private static final System.Logger LOGGER = System.getLogger(MiddleCommunicator.class.getName());
private static final int BUFFER_SIZE = 1024 * 1024;
- private static final String HOST = "HOST: ";
private final ExecutorService executor;
private final Socket readerSocket;
private final Socket writerSocket;
@@ -125,6 +154,7 @@ private MiddleCommunicator(ExecutorService executor, Socket readerSocket, Socket
this.readerSocket = readerSocket;
this.writerSocket = writerSocket;
this.originToRemote = callback != null;
+ // Both are the same thing with different name. The only purpose of this is to understand better stack traces.
this.reader = originToRemote ? new OriginToRemoteReader() : new RemoteToOriginReader();
this.callback = callback;
}
@@ -156,46 +186,41 @@ public void run() {
byte[] buffer = new byte[BUFFER_SIZE];
Exception exception = null;
try {
+ boolean handleFirstRequest = true;
int read;
- OriginInfo originInfo = null;
while ((read = readerSocket.getInputStream().read(buffer)) != -1) {
- final int readB = read;
- LOGGER.log(Level.DEBUG, readerSocket + " read " + readB + " bytes");
- LOGGER.log(Level.DEBUG, new String(buffer, 0, readB));
- if (originToRemote) {
- if (originInfo == null) {
- originInfo = getOriginInfo(buffer, read);
- LOGGER.log(Level.DEBUG, "Incoming request: " + originInfo);
- if (originInfo.respondOrigin()) {
- if (authenticate(originInfo)) {
- // Respond origin
- String response = "HTTP/1.1 200 Connection established\r\n\r\n";
- writerSocket.connect(new InetSocketAddress(originInfo.host, originInfo.port));
- LOGGER.log(Level.DEBUG, "Open: " + writerSocket);
- readerSocket.getOutputStream()
- .write(response.getBytes());
- // Start listening from origin
- callback.start();
- readerSocket.getOutputStream().flush();
- } else {
- LOGGER.log(Level.WARNING, "Invalid " + originInfo.user + ":" + originInfo.password);
- originInfo = null;
- String response = "HTTP/1.1 401 Unauthorized\r\n\r\n";
- readerSocket.getOutputStream().write(response.getBytes());
- readerSocket.getOutputStream().flush();
- readerSocket.close();
- }
- }
+ final int readb = read;
+ LOGGER.log(Level.DEBUG, () -> readerSocket + " read " + readb + " bytes\n" + new String(buffer, 0, readb));
+ // Handling workflow number 1
+ if (originToRemote && handleFirstRequest) {
+ handleFirstRequest = false;
+ // It is expected the first request is HTTP CONNECT
+ OriginInfo originInfo = getOriginInfo(buffer, readb);
+ LOGGER.log(Level.DEBUG, "Incoming request: " + originInfo);
+ if (authenticate(originInfo)) {
+ // Respond origin
+ String response = "HTTP/1.1 200 Connection established\r\n\r\n";
+ writerSocket.connect(new InetSocketAddress(originInfo.host, originInfo.port));
+ LOGGER.log(Level.DEBUG, "Open: " + writerSocket);
+ readerSocket.getOutputStream()
+ .write(response.getBytes());
+ // Now we know where to connect, so we can connect the socket to the remote.
+ callback.start();
+ readerSocket.getOutputStream().flush();
} else {
- writerSocket.getOutputStream().write(buffer, 0, read);
- writerSocket.getOutputStream().flush();
+ LOGGER.log(Level.WARNING, "Invalid " + originInfo.user + ":" + originInfo.password);
+ originInfo = null;
+ String response = "HTTP/1.1 401 Unauthorized\r\n\r\n";
+ readerSocket.getOutputStream().write(response.getBytes());
+ readerSocket.getOutputStream().flush();
+ readerSocket.close();
}
} else {
- writerSocket.getOutputStream().write(buffer, 0, read);
+ writerSocket.getOutputStream().write(buffer, 0, readb);
writerSocket.getOutputStream().flush();
}
}
- } catch (IOException e) {
+ } catch (Exception e) {
exception = e;
// LOGGER.log(Level.SEVERE, e.getMessage(), e);
} finally {
@@ -222,16 +247,14 @@ private OriginInfo getOriginInfo(byte[] buffer, int read) throws MalformedURLExc
for (String line : lines) {
if (line.startsWith(OriginInfo.CONNECT)) {
request.parseFirstLine(line);
- } else if (line.toUpperCase().startsWith(HOST)) {
- request.parseHost(line);
- } else if (line.toUpperCase().startsWith(OriginInfo.AUTHORIZATION)) {
+ } else if (line.startsWith(OriginInfo.AUTHORIZATION)) {
request.parseAuthorization(line);
}
}
return request;
}
- // Make it easy to understand stacktraces
+ // Make it easy to understand stack traces
private class OriginToRemoteReader extends Reader {
@Override
public void run() {
@@ -248,7 +271,7 @@ public void run() {
private static class OriginInfo {
private static final String CONNECT = "CONNECT";
- private static final String AUTHORIZATION = "AUTHORIZATION:";
+ private static final String AUTHORIZATION = "Proxy-Authorization:";
private String host;
private int port = 80;
private String protocol;
@@ -261,19 +284,14 @@ private void parseFirstLine(String line) {
String[] parts = line.split(" ");
this.method = parts[0].trim();
this.protocol = parts[2].trim();
- }
-
- // Host: host:port
- private void parseHost(String line) {
- line = line.substring(HOST.length()).trim();
- String[] hostPort = line.split(":");
+ String[] hostPort = parts[1].split(":");
this.host = hostPort[0];
if (hostPort.length > 1) {
this.port = Integer.parseInt(hostPort[1]);
}
}
- // Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
+ // Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
private void parseAuthorization(String line) {
String[] parts = line.split(" ");
String base64 = parts[2];
@@ -282,10 +300,6 @@ private void parseAuthorization(String line) {
password = userPass[1];
}
- private boolean respondOrigin() {
- return CONNECT.equals(method);
- }
-
@Override
public String toString() {
return "OriginInfo [host=" + host + ", port=" + port + ", protocol=" + protocol + ", method=" + method
diff --git a/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/tests/integration/webclient/HttpProxyTest.java b/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/tests/integration/webclient/HttpProxyTest.java
index 237825cc736..915c1d64708 100644
--- a/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/tests/integration/webclient/HttpProxyTest.java
+++ b/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/tests/integration/webclient/HttpProxyTest.java
@@ -16,26 +16,30 @@
package io.helidon.nima.tests.integration.webclient;
-import static io.helidon.common.http.Http.Method.GET;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import io.helidon.common.http.Http;
+import io.helidon.nima.http2.webclient.Http2Client;
import io.helidon.nima.testing.junit5.webserver.ServerTest;
import io.helidon.nima.testing.junit5.webserver.SetUpRoute;
+import io.helidon.nima.webclient.api.HttpClient;
+import io.helidon.nima.webclient.api.HttpClientResponse;
import io.helidon.nima.webclient.api.Proxy;
import io.helidon.nima.webclient.api.Proxy.ProxyType;
import io.helidon.nima.webclient.http1.Http1Client;
-import io.helidon.nima.webclient.http1.Http1ClientResponse;
+import io.helidon.nima.webserver.WebServer;
import io.helidon.nima.webserver.http.HttpRouting;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import static io.helidon.common.http.Http.Method.GET;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
@ServerTest
class HttpProxyTest {
@@ -43,82 +47,157 @@ class HttpProxyTest {
private int proxyPort;
private HttpProxy httpProxy;
- private final Http1Client client;
-
+ private final HttpClient> clientHttp1;
+ private final HttpClient> clientHttp2;
+
+ HttpProxyTest(WebServer server) {
+ String uri = "http://localhost:" + server.port();
+ this.clientHttp1 = Http1Client.builder()
+ .baseUri(uri)
+ .proxy(Proxy.noProxy())
+ .build();
+ this.clientHttp2 = Http2Client.builder()
+ .baseUri(uri)
+ .proxy(Proxy.noProxy())
+ .build();
+ }
+
@SetUpRoute
static void routing(HttpRouting.Builder router) {
router.route(GET, "/get", Routes::get);
}
@BeforeEach
- public void before() {
+ void before() {
httpProxy = new HttpProxy(0);
httpProxy.start();
proxyPort = httpProxy.connectedPort();
+ assertThat(httpProxy.counter(), is(0));
}
@AfterEach
- public void after() {
+ void after() {
httpProxy.stop();
}
- HttpProxyTest(Http1Client client) {
- this.client = client;
+ @Test
+ void testDefaultIsSystem1() {
+ ProxySelector original = ProxySelector.getDefault();
+ try {
+ ProxySelector.setDefault(ProxySelector.of(new InetSocketAddress(PROXY_HOST, proxyPort)));
+ Proxy proxy = Proxy.builder().build();
+ assertEquals(ProxyType.SYSTEM, proxy.type());
+ successVerify(proxy, clientHttp1);
+ } finally {
+ ProxySelector.setDefault(original);
+ }
}
@Test
- void testNoProxy() {
- noProxyChecks();
+ void testDefaultIsSystem2() {
+ ProxySelector original = ProxySelector.getDefault();
+ try {
+ ProxySelector.setDefault(ProxySelector.of(new InetSocketAddress(PROXY_HOST, proxyPort)));
+ Proxy proxy = Proxy.builder().build();
+ assertEquals(ProxyType.SYSTEM, proxy.type());
+ successVerify(proxy, clientHttp2);
+ } finally {
+ ProxySelector.setDefault(original);
+ }
}
@Test
- void testNoProxyTypeDefaultsToNone() {
- noProxyChecks();
+ void testNoProxy1() {
+ noProxyChecks(clientHttp1);
}
@Test
- void testNoHosts() {
- Proxy proxy = Proxy.builder().host(PROXY_HOST).port(proxyPort).addNoProxy(PROXY_HOST).build();
- try (Http1ClientResponse response = client.get("/get").proxy(proxy).request()) {
- assertThat(response.status(), is(Http.Status.OK_200));
- String entity = response.entity().as(String.class);
- assertThat(entity, is("Hello"));
- }
- assertThat(httpProxy.counter(), is(0));
+ void testNoProxy2() {
+ noProxyChecks(clientHttp2);
+ }
+
+ @Test
+ void testNoHosts1() {
+ noHosts(clientHttp1);
}
@Test
- void testNoProxyTypeButHasHost() {
+ void testNoHosts2() {
+ noHosts(clientHttp2);
+ }
+
+ @Test
+ void testNoProxyTypeButHasHost1() {
Proxy proxy = Proxy.builder().host(PROXY_HOST).port(proxyPort).build();
- successVerify(proxy);
+ successVerify(proxy, clientHttp1);
}
@Test
- void testProxyNoneTypeButHasHost() {
+ void testNoProxyTypeButHasHost2() {
+ Proxy proxy = Proxy.builder().host(PROXY_HOST).port(proxyPort).build();
+ successVerify(proxy, clientHttp2);
+ }
+
+ @Test
+ void testProxyNoneTypeButHasHost1() {
Proxy proxy = Proxy.builder().type(ProxyType.NONE).host(PROXY_HOST).port(proxyPort).build();
- successVerify(proxy);
+ successVerify(proxy, clientHttp1);
}
@Test
- void testSimpleProxy() {
+ void testProxyNoneTypeButHasHost2() {
+ Proxy proxy = Proxy.builder().type(ProxyType.NONE).host(PROXY_HOST).port(proxyPort).build();
+ successVerify(proxy, clientHttp2);
+ }
+
+ @Test
+ void testSimpleProxy1() {
+ Proxy proxy = Proxy.builder().type(ProxyType.HTTP).host(PROXY_HOST).port(proxyPort).build();
+ successVerify(proxy, clientHttp1);
+ }
+
+ @Test
+ void testSimpleProxy2() {
Proxy proxy = Proxy.builder().type(ProxyType.HTTP).host(PROXY_HOST).port(proxyPort).build();
- successVerify(proxy);
+ successVerify(proxy, clientHttp2);
}
@Test
- void testSystemProxy() {
+ void testSystemProxy1() {
ProxySelector original = ProxySelector.getDefault();
try {
ProxySelector.setDefault(ProxySelector.of(new InetSocketAddress(PROXY_HOST, proxyPort)));
Proxy proxy = Proxy.create();
- successVerify(proxy);
+ successVerify(proxy, clientHttp1);
} finally {
ProxySelector.setDefault(original);
}
}
- private void successVerify(Proxy proxy) {
- try (Http1ClientResponse response = client.get("/get").proxy(proxy).request()) {
+ @Test
+ void testSystemProxy2() {
+ ProxySelector original = ProxySelector.getDefault();
+ try {
+ ProxySelector.setDefault(ProxySelector.of(new InetSocketAddress(PROXY_HOST, proxyPort)));
+ Proxy proxy = Proxy.create();
+ successVerify(proxy, clientHttp2);
+ } finally {
+ ProxySelector.setDefault(original);
+ }
+ }
+
+ private void noHosts(HttpClient> client) {
+ Proxy proxy = Proxy.builder().host(PROXY_HOST).port(proxyPort).addNoProxy(PROXY_HOST).build();
+ try (HttpClientResponse response = client.get("/get").proxy(proxy).request()) {
+ assertThat(response.status(), is(Http.Status.OK_200));
+ String entity = response.entity().as(String.class);
+ assertThat(entity, is("Hello"));
+ }
+ assertThat(httpProxy.counter(), is(0));
+ }
+
+ private void successVerify(Proxy proxy, HttpClient> client) {
+ try (HttpClientResponse response = client.get("/get").proxy(proxy).request()) {
assertThat(response.status(), is(Http.Status.OK_200));
String entity = response.entity().as(String.class);
assertThat(entity, is("Hello"));
@@ -126,8 +205,8 @@ private void successVerify(Proxy proxy) {
assertThat(httpProxy.counter(), is(1));
}
- private void noProxyChecks() {
- try (Http1ClientResponse response = client.get("/get").request()) {
+ private void noProxyChecks(HttpClient> client) {
+ try (HttpClientResponse response = client.get("/get").request()) {
assertThat(response.status(), is(Http.Status.OK_200));
String entity = response.entity().as(String.class);
assertThat(entity, is("Hello"));
diff --git a/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/tests/integration/webclient/HttpsProxyTest.java b/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/tests/integration/webclient/HttpsProxyTest.java
index 7f3752772fc..29ff2b733f2 100644
--- a/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/tests/integration/webclient/HttpsProxyTest.java
+++ b/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/tests/integration/webclient/HttpsProxyTest.java
@@ -24,13 +24,15 @@
import io.helidon.common.http.Http;
import io.helidon.common.pki.Keys;
import io.helidon.nima.common.tls.Tls;
+import io.helidon.nima.http2.webclient.Http2Client;
import io.helidon.nima.testing.junit5.webserver.ServerTest;
import io.helidon.nima.testing.junit5.webserver.SetUpRoute;
import io.helidon.nima.testing.junit5.webserver.SetUpServer;
+import io.helidon.nima.webclient.api.HttpClient;
+import io.helidon.nima.webclient.api.HttpClientResponse;
import io.helidon.nima.webclient.api.Proxy;
import io.helidon.nima.webclient.api.Proxy.ProxyType;
import io.helidon.nima.webclient.http1.Http1Client;
-import io.helidon.nima.webclient.http1.Http1ClientResponse;
import io.helidon.nima.webserver.WebServer;
import io.helidon.nima.webserver.WebServerConfig.Builder;
import io.helidon.nima.webserver.http.HttpRouting;
@@ -50,7 +52,8 @@ class HttpsProxyTest {
private static int PROXY_PORT;
private static HttpProxy httpProxy;
- private final Http1Client client;
+ private final HttpClient> clientHttp1;
+ private final HttpClient> clientHttp2;
HttpsProxyTest(WebServer server) {
int port = server.port();
@@ -60,21 +63,27 @@ class HttpsProxyTest {
.endpointIdentificationAlgorithm(Tls.ENDPOINT_IDENTIFICATION_NONE)
.build();
- client = Http1Client.builder()
+ this.clientHttp1 = Http1Client.builder()
.baseUri("https://localhost:" + port)
.tls(tls)
+ .proxy(Proxy.noProxy())
+ .build();
+ this.clientHttp2 = Http2Client.builder()
+ .baseUri("https://localhost:" + port)
+ .tls(tls)
+ .proxy(Proxy.noProxy())
.build();
}
@BeforeAll
- public static void beforeAll() throws IOException {
+ static void beforeAll() throws IOException {
httpProxy = new HttpProxy(0);
httpProxy.start();
PROXY_PORT = httpProxy.connectedPort();
}
@AfterAll
- public static void afterAll() {
+ static void afterAll() {
httpProxy.stop();
}
@@ -102,55 +111,85 @@ static void routing(HttpRouting.Builder router) {
}
@Test
- void testNoProxy() {
- noProxyChecks();
+ void testNoProxy1() {
+ noProxyChecks(clientHttp1);
}
@Test
- void testNoProxyTypeDefaultsToNone() {
- noProxyChecks();
+ void testNoProxy2() {
+ noProxyChecks(clientHttp2);
+ }
+
+ @Test
+ void testNoProxyTypeButHasHost1() {
+ Proxy proxy = Proxy.builder().host(PROXY_HOST).port(PROXY_PORT).build();
+ successVerify(proxy, clientHttp1);
}
@Test
- void testNoProxyTypeButHasHost() {
+ void testNoProxyTypeButHasHost2() {
Proxy proxy = Proxy.builder().host(PROXY_HOST).port(PROXY_PORT).build();
- successVerify(proxy);
+ successVerify(proxy, clientHttp2);
}
@Test
- void testProxyNoneTypeButHasHost() {
+ void testProxyNoneTypeButHasHost1() {
Proxy proxy = Proxy.builder().type(ProxyType.NONE).host(PROXY_HOST).port(PROXY_PORT).build();
- successVerify(proxy);
+ successVerify(proxy, clientHttp1);
}
@Test
- void testSimpleProxy() {
+ void testProxyNoneTypeButHasHost2() {
+ Proxy proxy = Proxy.builder().type(ProxyType.NONE).host(PROXY_HOST).port(PROXY_PORT).build();
+ successVerify(proxy, clientHttp2);
+ }
+
+ @Test
+ void testSimpleProxy1() {
Proxy proxy = Proxy.builder().type(ProxyType.HTTP).host(PROXY_HOST).port(PROXY_PORT).build();
- successVerify(proxy);
+ successVerify(proxy, clientHttp1);
+ }
+
+ @Test
+ void testSimpleProxy2() {
+ Proxy proxy = Proxy.builder().type(ProxyType.HTTP).host(PROXY_HOST).port(PROXY_PORT).build();
+ successVerify(proxy, clientHttp2);
+ }
+
+ @Test
+ void testSystemProxy1() {
+ ProxySelector original = ProxySelector.getDefault();
+ try {
+ ProxySelector.setDefault(ProxySelector.of(new InetSocketAddress(PROXY_HOST, PROXY_PORT)));
+ Proxy proxy = Proxy.create();
+ successVerify(proxy, clientHttp1);
+ } finally {
+ ProxySelector.setDefault(original);
+ }
}
@Test
- void testSystemProxy() {
+ void testSystemProxy2() {
ProxySelector original = ProxySelector.getDefault();
try {
ProxySelector.setDefault(ProxySelector.of(new InetSocketAddress(PROXY_HOST, PROXY_PORT)));
Proxy proxy = Proxy.create();
- successVerify(proxy);
+ successVerify(proxy, clientHttp2);
} finally {
ProxySelector.setDefault(original);
}
}
- private void successVerify(Proxy proxy) {
- try (Http1ClientResponse response = client.get("/get").proxy(proxy).request()) {
+ private void successVerify(Proxy proxy, HttpClient> client) {
+ try (HttpClientResponse response = client.get("/get").proxy(proxy).request()) {
assertThat(response.status(), is(Http.Status.OK_200));
String entity = response.entity().as(String.class);
assertThat(entity, is("Hello"));
}
}
- private void noProxyChecks() {
- try (Http1ClientResponse response = client.get("/get").request()) {
+ private void noProxyChecks(HttpClient> client) {
+ try (HttpClientResponse response = client.get("/get").request()) {
assertThat(response.status(), is(Http.Status.OK_200));
String entity = response.entity().as(String.class);
assertThat(entity, is("Hello"));
diff --git a/nima/webclient/api/src/main/java/io/helidon/nima/webclient/api/Proxy.java b/nima/webclient/api/src/main/java/io/helidon/nima/webclient/api/Proxy.java
index 4b328b4036e..44b089b4952 100644
--- a/nima/webclient/api/src/main/java/io/helidon/nima/webclient/api/Proxy.java
+++ b/nima/webclient/api/src/main/java/io/helidon/nima/webclient/api/Proxy.java
@@ -24,7 +24,9 @@
import java.net.ProxySelector;
import java.net.Socket;
import java.net.URI;
+import java.nio.charset.StandardCharsets;
import java.util.Arrays;
+import java.util.Base64;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
@@ -39,6 +41,8 @@
import io.helidon.common.config.Config;
import io.helidon.common.configurable.LruCache;
import io.helidon.common.http.Http;
+import io.helidon.common.http.Http.Header;
+import io.helidon.common.http.Http.HeaderValue;
import io.helidon.common.media.type.MediaTypes;
import io.helidon.common.socket.SocketOptions;
import io.helidon.config.metadata.Configured;
@@ -51,13 +55,13 @@
public class Proxy {
private static final System.Logger LOGGER = System.getLogger(Proxy.class.getName());
private static final Tls NO_TLS = Tls.builder().enabled(false).build();
+ private static final HeaderValue PROXY_CONNECTION =
+ Header.create(Http.Header.create("Proxy-Connection"), "keep-alive");
/**
* No proxy instance.
*/
private static final Proxy NO_PROXY = new Proxy(builder().type(ProxyType.NONE));
- private static final Proxy SYSTEM_PROXY = new Proxy(builder().type(ProxyType.SYSTEM));
-
private static final Pattern PORT_PATTERN = Pattern.compile(".*:(\\d+)");
private static final Pattern IP_V4 = Pattern.compile("^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\."
+ "(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}$");
@@ -81,6 +85,8 @@ public class Proxy {
private final Function noProxy;
private final Optional username;
private final Optional password;
+ private final ProxySelector systemProxySelector;
+ private final Optional proxyAuthHeader;
private Proxy(Proxy.Builder builder) {
this.host = builder.host();
@@ -96,8 +102,20 @@ private Proxy(Proxy.Builder builder) {
if (type == ProxyType.SYSTEM) {
this.noProxy = inetSocketAddress -> true;
+ this.systemProxySelector = ProxySelector.getDefault();
} else {
this.noProxy = prepareNoProxy(builder.noProxyHosts());
+ this.systemProxySelector = null;
+ }
+
+ if (username.isPresent()) {
+ char[] pass = password.orElse(new char[0]);
+ // Making the password char[] to String looks not correct, but it is done in the same way in HttpBasicAuthProvider
+ String b64 = Base64.getEncoder().encodeToString((username.get() + ":" + new String(pass))
+ .getBytes(StandardCharsets.UTF_8));
+ this.proxyAuthHeader = Optional.of(Header.create(Header.PROXY_AUTHORIZATION, "Basic " + b64));
+ } else {
+ this.proxyAuthHeader = Optional.empty();
}
}
@@ -145,7 +163,8 @@ public static Proxy create(Config config) {
* @return a proxy instance configured based on this system settings
*/
public static Proxy create() {
- return SYSTEM_PROXY;
+ // we must create a new instance, as the system proxy may be reset
+ return builder().type(ProxyType.SYSTEM).build();
}
static Function prepareNoProxy(Set noProxyHosts) {
@@ -344,6 +363,7 @@ public boolean equals(Object o) {
Proxy proxy = (Proxy) o;
return port == proxy.port
&& type == proxy.type
+ && Objects.equals(systemProxySelector, proxy.systemProxySelector)
&& Objects.equals(host, proxy.host)
&& Objects.equals(noProxy, proxy.noProxy)
&& Objects.equals(username, proxy.username)
@@ -412,7 +432,8 @@ private static Optional isIpV6HostRegExp(String host) {
private static Socket connectToProxy(WebClient webClient,
InetSocketAddress proxyAddress,
- InetSocketAddress targetAddress) {
+ InetSocketAddress targetAddress,
+ Proxy proxy) {
WebClientConfig clientConfig = webClient.prototype();
TcpClientConnection connection = TcpClientConnection.create(webClient,
@@ -429,14 +450,20 @@ private static Socket connectToProxy(WebClient webClient,
})
.connect();
- HttpClientResponse response = webClient.method(Http.Method.CONNECT)
+ HttpClientRequest request = webClient.method(Http.Method.CONNECT)
.followRedirects(false) // do not follow redirects for proxy connect itself
.connection(connection)
.uri("http://" + proxyAddress.getHostName() + ":" + proxyAddress.getPort())
.protocolId("http/1.1") // MUST be 1.1, if not available, proxy connection will fail
.header(Http.Header.HOST, targetAddress.getHostName() + ":" + targetAddress.getPort())
- .accept(MediaTypes.WILDCARD)
- .request(); // we cannot close the response, as that would close the connection
+ .accept(MediaTypes.WILDCARD);
+ if (clientConfig.keepAlive()) {
+ request.header(Http.HeaderValues.CONNECTION_KEEP_ALIVE)
+ .header(PROXY_CONNECTION);
+ }
+ proxy.proxyAuthHeader.ifPresent(request::header);
+ // we cannot close the response, as that would close the connection
+ HttpClientResponse response = request.request();
if (response.status().family() != Http.Status.Family.SUCCESSFUL) {
response.close();
throw new IllegalStateException("Proxy sent wrong HTTP response code: " + response.status());
@@ -482,7 +509,10 @@ Socket connect(WebClient webClient,
SocketOptions socketOptions,
boolean tls) {
String scheme = tls ? "https" : "http";
- List proxies = ProxySelector.getDefault()
+ if (proxy.systemProxySelector == null) {
+ return NONE.connect(webClient, proxy, targetAddress, socketOptions, tls);
+ }
+ List proxies = proxy.systemProxySelector
.select(URI.create(scheme + "://" + targetAddress.getHostName() + ":" + targetAddress.getPort()));
if (proxies.isEmpty()) {
return NONE.connect(webClient, proxy, targetAddress, socketOptions, tls);
@@ -511,7 +541,8 @@ Socket connect(WebClient webClient,
return proxy.address(targetAddress)
.map(proxyAddress -> connectToProxy(webClient,
proxyAddress,
- targetAddress))
+ targetAddress,
+ proxy))
.orElseGet(() -> NONE.connect(webClient, proxy, targetAddress, socketOptions, tls));
}
};
@@ -531,7 +562,7 @@ public static class Builder implements io.helidon.common.Builder
private final Set noProxyHosts = new HashSet<>();
// Defaults to system
- private ProxyType type;
+ private ProxyType type = ProxyType.SYSTEM;
private String host;
private int port = 80;
private String username;
@@ -542,11 +573,6 @@ private Builder() {
@Override
public Proxy build() {
- if ((null == host) || (host.isEmpty())) {
- return NO_PROXY;
- } else if (type == ProxyType.SYSTEM) {
- return SYSTEM_PROXY;
- }
return new Proxy(this);
}
@@ -715,5 +741,4 @@ Optional password() {
return Optional.ofNullable(password);
}
}
-
}
diff --git a/nima/webclient/http1/src/main/java/io/helidon/nima/webclient/http1/Http1CallChainBase.java b/nima/webclient/http1/src/main/java/io/helidon/nima/webclient/http1/Http1CallChainBase.java
index 404ad08c768..5d1fd8e7726 100644
--- a/nima/webclient/http1/src/main/java/io/helidon/nima/webclient/http1/Http1CallChainBase.java
+++ b/nima/webclient/http1/src/main/java/io/helidon/nima/webclient/http1/Http1CallChainBase.java
@@ -31,6 +31,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.common.socket.HelidonSocket;
@@ -117,15 +119,20 @@ abstract WebClientServiceResponse doProceed(ClientConnection connection,
BufferData writeBuffer);
void prologue(BufferData nonEntityData, WebClientServiceRequest request, ClientUri uri) {
- // TODO When proxy is implemented, change default value of Http1ClientConfig.relativeUris to false
- // and below conditional statement to:
- // proxy == Proxy.create() || proxy.noProxyPredicate().apply(finalUri) || clientConfig.relativeUris
- String schemeHostPort = clientConfig.relativeUris() ? "" : uri.scheme() + "://" + uri.host() + ":" + uri.port();
- nonEntityData.writeAscii(request.method().text()
- + " "
- + schemeHostPort
- + uri.pathWithQueryAndFragment()
- + " 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()
+ + " HTTP/1.1\r\n");
+ }
}
ClientResponseHeaders readHeaders(DataReader reader) {
diff --git a/nima/webclient/http1/src/main/java/io/helidon/nima/webclient/http1/Http1StatusParser.java b/nima/webclient/http1/src/main/java/io/helidon/nima/webclient/http1/Http1StatusParser.java
index 2f16de7baea..3b3c636235f 100644
--- a/nima/webclient/http1/src/main/java/io/helidon/nima/webclient/http1/Http1StatusParser.java
+++ b/nima/webclient/http1/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/http1/src/test/java/io/helidon/nima/webclient/http1/Http1StatusParserTest.java b/nima/webclient/http1/src/test/java/io/helidon/nima/webclient/http1/Http1StatusParserTest.java
new file mode 100644
index 00000000000..50e66c4cfa8
--- /dev/null
+++ b/nima/webclient/http1/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));
+ }
+}
\ No newline at end of file