// // ======================================================================== // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.jetty.http2.client.http; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketTimeoutException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpDestination; import org.eclipse.jetty.client.HttpProxy; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http2.ErrorCode; import org.eclipse.jetty.http2.HTTP2Session; import org.eclipse.jetty.http2.api.Session; import org.eclipse.jetty.http2.api.Stream; import org.eclipse.jetty.http2.api.server.ServerSessionListener; import org.eclipse.jetty.http2.client.HTTP2Client; import org.eclipse.jetty.http2.frames.DataFrame; import org.eclipse.jetty.http2.frames.GoAwayFrame; import org.eclipse.jetty.http2.frames.HeadersFrame; import org.eclipse.jetty.http2.frames.ResetFrame; import org.eclipse.jetty.http2.frames.SettingsFrame; import org.eclipse.jetty.http2.generator.Generator; import org.eclipse.jetty.http2.parser.ServerParser; import org.eclipse.jetty.http2.server.RawHTTP2ServerConnectionFactory; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.MappedByteBufferPool; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; public class HttpClientTransportOverHTTP2Test extends AbstractTest { @Test public void testPropertiesAreForwarded() throws Exception { HTTP2Client http2Client = new HTTP2Client(); HttpClient httpClient = new HttpClient(new HttpClientTransportOverHTTP2(http2Client), null); Executor executor = new QueuedThreadPool(); httpClient.setExecutor(executor); httpClient.setConnectTimeout(13); httpClient.setIdleTimeout(17); httpClient.start(); Assert.assertTrue(http2Client.isStarted()); Assert.assertSame(httpClient.getExecutor(), http2Client.getExecutor()); Assert.assertSame(httpClient.getScheduler(), http2Client.getScheduler()); Assert.assertSame(httpClient.getByteBufferPool(), http2Client.getByteBufferPool()); Assert.assertEquals(httpClient.getConnectTimeout(), http2Client.getConnectTimeout()); Assert.assertEquals(httpClient.getIdleTimeout(), http2Client.getIdleTimeout()); httpClient.stop(); Assert.assertTrue(http2Client.isStopped()); } @Test public void testRequestAbortSendsResetFrame() throws Exception { CountDownLatch resetLatch = new CountDownLatch(1); start(new ServerSessionListener.Adapter() { @Override public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) { return new Stream.Listener.Adapter() { @Override public void onReset(Stream stream, ResetFrame frame) { resetLatch.countDown(); } }; } }); try { client.newRequest("localhost", connector.getLocalPort()) .onRequestCommit(request -> request.abort(new Exception("explicitly_aborted_by_test"))) .send(); Assert.fail(); } catch (ExecutionException x) { Assert.assertTrue(resetLatch.await(5, TimeUnit.SECONDS)); } } @Test public void testResponseAbortSendsResetFrame() throws Exception { CountDownLatch resetLatch = new CountDownLatch(1); start(new ServerSessionListener.Adapter() { @Override public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) { MetaData.Response metaData = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, new HttpFields()); stream.headers(new HeadersFrame(stream.getId(), metaData, null, false), new Callback() { @Override public void succeeded() { ByteBuffer data = ByteBuffer.allocate(1024); stream.data(new DataFrame(stream.getId(), data, false), NOOP); } }); return new Stream.Listener.Adapter() { @Override public void onReset(Stream stream, ResetFrame frame) { resetLatch.countDown(); } }; } }); try { client.newRequest("localhost", connector.getLocalPort()) .onResponseContent((response, buffer) -> response.abort(new Exception("explicitly_aborted_by_test"))) .send(); Assert.fail(); } catch (ExecutionException x) { Assert.assertTrue(resetLatch.await(5, TimeUnit.SECONDS)); } } @Test public void testRequestHasHTTP2Version() throws Exception { start(new AbstractHandler() { @Override public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { baseRequest.setHandled(true); HttpVersion version = HttpVersion.fromString(request.getProtocol()); response.setStatus(version == HttpVersion.HTTP_2 ? HttpStatus.OK_200 : HttpStatus.INTERNAL_SERVER_ERROR_500); } }); ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) .onRequestBegin(request -> { if (request.getVersion() != HttpVersion.HTTP_2) request.abort(new Exception("Not a HTTP/2 request")); }) .send(); Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); } @Test public void testLastStreamId() throws Exception { prepareServer(new RawHTTP2ServerConnectionFactory(new HttpConfiguration(), new ServerSessionListener.Adapter() { @Override public Map<Integer, Integer> onPreface(Session session) { Map<Integer, Integer> settings = new HashMap<>(); settings.put(SettingsFrame.MAX_CONCURRENT_STREAMS, 1); return settings; } @Override public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) { MetaData.Request request = (MetaData.Request)frame.getMetaData(); if (HttpMethod.HEAD.is(request.getMethod())) { stream.getSession().close(ErrorCode.REFUSED_STREAM_ERROR.code, null, Callback.NOOP); } else { MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, new HttpFields()); stream.headers(new HeadersFrame(stream.getId(), response, null, true), Callback.NOOP); } return null; } })); server.start(); CountDownLatch latch = new CountDownLatch(2); AtomicInteger lastStream = new AtomicInteger(); AtomicReference<Stream> streamRef = new AtomicReference<>(); CountDownLatch streamLatch = new CountDownLatch(1); client = new HttpClient(new HttpClientTransportOverHTTP2(new HTTP2Client()) { @Override protected HttpConnectionOverHTTP2 newHttpConnection(HttpDestination destination, Session session) { return new HttpConnectionOverHTTP2(destination, session) { @Override protected HttpChannelOverHTTP2 newHttpChannel(boolean push) { return new HttpChannelOverHTTP2(getHttpDestination(), this, getSession(), push) { @Override public void setStream(Stream stream) { super.setStream(stream); streamRef.set(stream); streamLatch.countDown(); } }; } }; } @Override protected void onClose(HttpConnectionOverHTTP2 connection, GoAwayFrame frame) { super.onClose(connection, frame); lastStream.set(frame.getLastStreamId()); latch.countDown(); } }, null); QueuedThreadPool clientExecutor = new QueuedThreadPool(); clientExecutor.setName("client"); client.setExecutor(clientExecutor); client.start(); // Prime the connection to allow client and server prefaces to be exchanged. ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) .path("/zero") .timeout(5, TimeUnit.SECONDS) .send(); Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); org.eclipse.jetty.client.api.Request request = client.newRequest("localhost", connector.getLocalPort()) .method(HttpMethod.HEAD) .path("/one"); request.send(result -> { if (result.isFailed()) latch.countDown(); }); Assert.assertTrue(streamLatch.await(5, TimeUnit.SECONDS)); Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); Stream stream = streamRef.get(); Assert.assertNotNull(stream); Assert.assertEquals(lastStream.get(), stream.getId()); } @Test public void testAbsoluteFormTarget() throws Exception { String path = "/path"; String query = "a=b"; start(new AbstractHandler() { @Override public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { baseRequest.setHandled(true); Assert.assertEquals(path, request.getRequestURI()); Assert.assertEquals(query, request.getQueryString()); } }); ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) .path("http://localhost:" + connector.getLocalPort() + path + "?" + query) .timeout(5, TimeUnit.SECONDS) .send(); Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); } @Test public void testRequestViaForwardHttpProxy() throws Exception { String path = "/path"; String query = "a=b"; start(new AbstractHandler() { @Override public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { baseRequest.setHandled(true); Assert.assertEquals(path, request.getRequestURI()); Assert.assertEquals(query, request.getQueryString()); } }); int proxyPort = connector.getLocalPort(); client.getProxyConfiguration().getProxies().add(new HttpProxy("localhost", proxyPort)); int serverPort = proxyPort + 1; // Any port will do, just not the same as the proxy. ContentResponse response = client.newRequest("localhost", serverPort) .path(path + "?" + query) .timeout(5, TimeUnit.SECONDS) .send(); Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); } @Test public void testConnectionIdleTimeoutSendsResetFrame() throws Exception { long idleTimeout = 1000; CountDownLatch resetLatch = new CountDownLatch(1); start(new ServerSessionListener.Adapter() { @Override public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) { return new Stream.Listener.Adapter() { @Override public void onReset(Stream stream, ResetFrame frame) { resetLatch.countDown(); } }; } }); client.stop(); client.setIdleTimeout(idleTimeout); client.start(); try { client.newRequest("localhost", connector.getLocalPort()) // Make sure the connection idle times out, not the stream. .idleTimeout(2 * idleTimeout, TimeUnit.MILLISECONDS) .send(); Assert.fail(); } catch (ExecutionException e) { // Expected. } Assert.assertTrue(resetLatch.await(5, TimeUnit.SECONDS)); } @Test public void testRequestIdleTimeoutSendsResetFrame() throws Exception { CountDownLatch resetLatch = new CountDownLatch(1); start(new ServerSessionListener.Adapter() { @Override public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) { return new Stream.Listener.Adapter() { @Override public void onReset(Stream stream, ResetFrame frame) { resetLatch.countDown(); } }; } }); try { long idleTimeout = 1000; client.newRequest("localhost", connector.getLocalPort()) .idleTimeout(idleTimeout, TimeUnit.MILLISECONDS) .send(); Assert.fail(); } catch (ExecutionException e) { // Expected. } Assert.assertTrue(resetLatch.await(5, TimeUnit.SECONDS)); } @Test public void testClientStopsServerDoesNotCloseClientCloses() throws Exception { try (ServerSocket server = new ServerSocket(0)) { List<Session> sessions = new ArrayList<>(); HTTP2Client h2Client = new HTTP2Client(); HttpClient client = new HttpClient(new HttpClientTransportOverHTTP2(h2Client) { @Override protected HttpConnectionOverHTTP2 newHttpConnection(HttpDestination destination, Session session) { sessions.add(session); return super.newHttpConnection(destination, session); } }, null); QueuedThreadPool clientExecutor = new QueuedThreadPool(); clientExecutor.setName("client"); client.setExecutor(clientExecutor); client.start(); CountDownLatch resultLatch = new CountDownLatch(1); client.newRequest("localhost", server.getLocalPort()) .send(result -> { if (result.getResponse().getStatus() == HttpStatus.OK_200) resultLatch.countDown(); }); ByteBufferPool byteBufferPool = new MappedByteBufferPool(); ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool); Generator generator = new Generator(byteBufferPool); try (Socket socket = server.accept()) { socket.setSoTimeout(1000); OutputStream output = socket.getOutputStream(); InputStream input = socket.getInputStream(); ServerParser parser = new ServerParser(byteBufferPool, new ServerParser.Listener.Adapter() { @Override public void onHeaders(HeadersFrame request) { // Server's preface. generator.control(lease, new SettingsFrame(new HashMap<>(), false)); // Reply to client's SETTINGS. generator.control(lease, new SettingsFrame(new HashMap<>(), true)); // Response. MetaData.Response metaData = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, new HttpFields()); HeadersFrame response = new HeadersFrame(request.getStreamId(), metaData, null, true); generator.control(lease, response); try { // Write the frames. for (ByteBuffer buffer : lease.getByteBuffers()) output.write(BufferUtil.toArray(buffer)); } catch (Throwable x) { x.printStackTrace(); } } }, 4096, 8192); byte[] bytes = new byte[1024]; while (true) { try { int read = input.read(bytes); if (read < 0) Assert.fail(); parser.parse(ByteBuffer.wrap(bytes, 0, read)); } catch (SocketTimeoutException x) { break; } } Assert.assertTrue(resultLatch.await(5, TimeUnit.SECONDS)); // The client will send a GO_AWAY, but the server will not close. client.stop(); // Give some time to process the stop/close operations. Thread.sleep(1000); Assert.assertTrue(h2Client.getBeans(Session.class).isEmpty()); for (Session session : sessions) { Assert.assertTrue(session.isClosed()); Assert.assertTrue(((HTTP2Session)session).isDisconnected()); } } } } @Ignore @Test public void testExternalServer() throws Exception { HTTP2Client http2Client = new HTTP2Client(); SslContextFactory sslContextFactory = new SslContextFactory(); HttpClient httpClient = new HttpClient(new HttpClientTransportOverHTTP2(http2Client), sslContextFactory); Executor executor = new QueuedThreadPool(); httpClient.setExecutor(executor); httpClient.start(); // ContentResponse response = httpClient.GET("https://http2.akamai.com/"); ContentResponse response = httpClient.GET("https://webtide.com/"); Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); httpClient.stop(); } }