From b3b533b303540b2e3d03a73473ff2145335baa37 Mon Sep 17 00:00:00 2001 From: Jorge Bescos Gascon Date: Wed, 9 Aug 2023 11:11:41 +0200 Subject: [PATCH] HTTP 1 parser, CONNECT host:port fix, HTTP2 support tests, Authentication and keep alive headers * HTTP 1 parser, CONNECT host:port fix, HTTP2 support tests --------- Signed-off-by: Jorge Bescos Gascon --- .../integration/webclient/webclient/pom.xml | 4 + .../webclient/AuthHttpProxyTest.java | 133 ++++++++++++++ .../webclient/AuthHttpsProxyTest.java | 163 ++++++++++++++++++ .../integration/webclient/HttpProxy.java | 118 +++++++------ .../integration/webclient/HttpProxyTest.java | 149 ++++++++++++---- .../integration/webclient/HttpsProxyTest.java | 81 ++++++--- .../io/helidon/nima/webclient/api/Proxy.java | 57 ++++-- .../webclient/http1/Http1CallChainBase.java | 25 ++- .../webclient/http1/Http1StatusParser.java | 14 +- .../http1/Http1StatusParserTest.java | 49 ++++++ 10 files changed, 653 insertions(+), 140 deletions(-) create mode 100644 nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/tests/integration/webclient/AuthHttpProxyTest.java create mode 100644 nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/tests/integration/webclient/AuthHttpsProxyTest.java create mode 100644 nima/webclient/http1/src/test/java/io/helidon/nima/webclient/http1/Http1StatusParserTest.java 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