diff --git a/jetty-spdy/spdy-http-client-transport/pom.xml b/jetty-spdy/spdy-http-client-transport/pom.xml new file mode 100644 index 00000000000..7c14d20eab1 --- /dev/null +++ b/jetty-spdy/spdy-http-client-transport/pom.xml @@ -0,0 +1,101 @@ + + + + org.eclipse.jetty.spdy + spdy-parent + 9.1.0-SNAPSHOT + + + 4.0.0 + spdy-http-client-transport + Jetty :: SPDY :: HTTP Client Transport + + + ${project.groupId}.client.http + + + + + + maven-dependency-plugin + + + copy + generate-resources + + copy + + + + + org.mortbay.jetty.npn + npn-boot + ${npn.version} + jar + false + ${project.build.directory}/npn + + + + + + + + maven-surefire-plugin + + -Xbootclasspath/p:${project.build.directory}/npn/npn-boot-${npn.version}.jar + + + + org.apache.felix + maven-bundle-plugin + true + + + + manifest + + + + org.eclipse.jetty.spdy.client.http;version="9.1" + !org.eclipse.jetty.npn,org.eclipse.jetty.*;version="[9.0,10.0)",* + + + + + + + + + + + org.eclipse.jetty + jetty-client + ${project.version} + + + org.eclipse.jetty.spdy + spdy-client + ${project.version} + + + org.eclipse.jetty.spdy + spdy-http-common + ${project.version} + + + + org.eclipse.jetty + jetty-server + ${project.version} + test + + + org.eclipse.jetty.spdy + spdy-http-server + ${project.version} + test + + + + diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpChannelOverSPDY.java b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpChannelOverSPDY.java similarity index 97% rename from jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpChannelOverSPDY.java rename to jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpChannelOverSPDY.java index 96a56058e20..992f2a57616 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpChannelOverSPDY.java +++ b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpChannelOverSPDY.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.client.spdy; +package org.eclipse.jetty.spdy.client.http; import org.eclipse.jetty.client.HttpChannel; import org.eclipse.jetty.client.HttpDestination; diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpClientTransportOverSPDY.java b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpClientTransportOverSPDY.java similarity index 98% rename from jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpClientTransportOverSPDY.java rename to jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpClientTransportOverSPDY.java index c76849673fb..1d5d1190893 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpClientTransportOverSPDY.java +++ b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpClientTransportOverSPDY.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.client.spdy; +package org.eclipse.jetty.spdy.client.http; import java.net.SocketAddress; diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpConnectionOverSPDY.java b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpConnectionOverSPDY.java similarity index 97% rename from jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpConnectionOverSPDY.java rename to jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpConnectionOverSPDY.java index fe325a9f13a..ffe025146bf 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpConnectionOverSPDY.java +++ b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpConnectionOverSPDY.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.client.spdy; +package org.eclipse.jetty.spdy.client.http; import org.eclipse.jetty.client.HttpChannel; import org.eclipse.jetty.client.HttpClient; diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpDestinationOverSPDY.java b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpDestinationOverSPDY.java similarity index 70% rename from jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpDestinationOverSPDY.java rename to jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpDestinationOverSPDY.java index 0c505b74f40..24b2acf4b76 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpDestinationOverSPDY.java +++ b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpDestinationOverSPDY.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.client.spdy; +package org.eclipse.jetty.spdy.client.http; import java.util.concurrent.atomic.AtomicReference; @@ -30,7 +30,7 @@ import org.eclipse.jetty.util.Promise; public class HttpDestinationOverSPDY extends HttpDestination implements Promise { private final AtomicReference connect = new AtomicReference<>(ConnectState.DISCONNECTED); - private volatile HttpConnectionOverSPDY connection; + private HttpConnectionOverSPDY connection; public HttpDestinationOverSPDY(HttpClient client, String scheme, String host, int port) { @@ -48,7 +48,7 @@ public class HttpDestinationOverSPDY extends HttpDestination implements Promise< case DISCONNECTED: { if (!connect.compareAndSet(current, ConnectState.CONNECTING)) - continue; + break; newConnection(this); return; } @@ -59,10 +59,9 @@ public class HttpDestinationOverSPDY extends HttpDestination implements Promise< } case CONNECTED: { - HttpConnectionOverSPDY connection = this.connection; - if (connection != null) - process(connection, false); - break; + if (process(connection, false)) + break; + return; } default: { @@ -75,14 +74,14 @@ public class HttpDestinationOverSPDY extends HttpDestination implements Promise< @Override public void succeeded(Connection result) { + HttpConnectionOverSPDY connection = this.connection = (HttpConnectionOverSPDY)result; if (connect.compareAndSet(ConnectState.CONNECTING, ConnectState.CONNECTED)) { - HttpConnectionOverSPDY connection = this.connection = (HttpConnectionOverSPDY)result; process(connection, true); } else { - result.close(); + connection.close(); failed(new IllegalStateException()); } } @@ -93,39 +92,40 @@ public class HttpDestinationOverSPDY extends HttpDestination implements Promise< connect.set(ConnectState.DISCONNECTED); } - private void process(final HttpConnectionOverSPDY connection, boolean dispatch) + private boolean process(final HttpConnectionOverSPDY connection, boolean dispatch) { HttpClient client = getHttpClient(); final HttpExchange exchange = getHttpExchanges().poll(); LOG.debug("Processing exchange {} on connection {}", exchange, connection); - if (exchange != null) + if (exchange == null) + return false; + + final Request request = exchange.getRequest(); + Throwable cause = request.getAbortCause(); + if (cause != null) { - final Request request = exchange.getRequest(); - Throwable cause = request.getAbortCause(); - if (cause != null) + LOG.debug("Abort before processing {}: {}", exchange, cause); + abort(exchange, cause); + } + else + { + if (dispatch) { - abort(exchange, cause); - LOG.debug("Aborted before processing {}: {}", exchange, cause); + client.getExecutor().execute(new Runnable() + { + @Override + public void run() + { + connection.send(exchange); + } + }); } else { - if (dispatch) - { - client.getExecutor().execute(new Runnable() - { - @Override - public void run() - { - connection.send(exchange); - } - }); - } - else - { - connection.send(exchange); - } + connection.send(exchange); } } + return true; } private enum ConnectState diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpReceiverOverSPDY.java b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpReceiverOverSPDY.java similarity index 56% rename from jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpReceiverOverSPDY.java rename to jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpReceiverOverSPDY.java index 8cc69fdafbe..a870b5e7d7c 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpReceiverOverSPDY.java +++ b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpReceiverOverSPDY.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.client.spdy; +package org.eclipse.jetty.spdy.client.http; import org.eclipse.jetty.client.HttpExchange; import org.eclipse.jetty.client.HttpReceiver; @@ -32,6 +32,7 @@ import org.eclipse.jetty.spdy.api.RstInfo; import org.eclipse.jetty.spdy.api.Stream; import org.eclipse.jetty.spdy.api.StreamFrameListener; import org.eclipse.jetty.spdy.api.StreamStatus; +import org.eclipse.jetty.spdy.http.HTTPSPDYHeader; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Fields; @@ -55,37 +56,52 @@ public class HttpReceiverOverSPDY extends HttpReceiver implements StreamFrameLis if (exchange == null) return; - HttpResponse response = exchange.getResponse(); - - Fields fields = replyInfo.getHeaders(); - // TODO: use HTTPSPDYHeader enum - HttpVersion version = HttpVersion.fromString(fields.get(":version").value()); - response.version(version); - Integer status = fields.get(":status").valueAsInt(); - response.status(status); - response.reason(HttpStatus.getMessage(status)); - - onResponseBegin(exchange); - - for (Fields.Field field : fields) + try { - // TODO: handle multiple values properly - // TODO: skip special headers - HttpField httpField = new HttpField(field.name(), field.value()); - onResponseHeader(exchange, httpField); + HttpResponse response = exchange.getResponse(); + + Fields fields = replyInfo.getHeaders(); + short spdy = stream.getSession().getVersion(); + HttpVersion version = HttpVersion.fromString(fields.get(HTTPSPDYHeader.VERSION.name(spdy)).value()); + response.version(version); + String[] status = fields.get(HTTPSPDYHeader.STATUS.name(spdy)).value().split(" ", 2); + + Integer code = Integer.parseInt(status[0]); + response.status(code); + String reason = status.length < 2 ? HttpStatus.getMessage(code) : status[1]; + response.reason(reason); + + if (responseBegin(exchange)) + { + for (Fields.Field field : fields) + { + String name = field.name(); + if (HTTPSPDYHeader.from(spdy, name) != null) + continue; + // TODO: handle multiple values properly + HttpField httpField = new HttpField(name, field.value()); + responseHeader(exchange, httpField); + } + + if (responseHeaders(exchange)) + { + if (replyInfo.isClose()) + { + responseSuccess(exchange); + } + } + } } - - onResponseHeaders(exchange); - - if (replyInfo.isClose()) + catch (Exception x) { - onResponseSuccess(exchange); + responseFailure(x); } } @Override public StreamFrameListener onPush(Stream stream, PushInfo pushInfo) { + // SPDY push not supported getHttpChannel().getSession().rst(new RstInfo(stream.getId(), StreamStatus.REFUSED_STREAM), new Callback.Adapter()); return null; } @@ -103,14 +119,24 @@ public class HttpReceiverOverSPDY extends HttpReceiver implements StreamFrameLis if (exchange == null) return; - int length = dataInfo.length(); - // TODO: avoid data copy here - onResponseContent(exchange, dataInfo.asByteBuffer(false)); - dataInfo.consume(length); - - if (dataInfo.isClose()) + try { - onResponseSuccess(exchange); + int length = dataInfo.length(); + // TODO: avoid data copy here + boolean process = responseContent(exchange, dataInfo.asByteBuffer(false)); + dataInfo.consume(length); + + if (process) + { + if (dataInfo.isClose()) + { + responseSuccess(exchange); + } + } + } + catch (Exception x) + { + responseFailure(x); } } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpSenderOverSPDY.java b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpSenderOverSPDY.java similarity index 60% rename from jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpSenderOverSPDY.java rename to jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpSenderOverSPDY.java index 5ff7ca2dec0..34ac5a5da04 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpSenderOverSPDY.java +++ b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpSenderOverSPDY.java @@ -16,17 +16,18 @@ // ======================================================================== // -package org.eclipse.jetty.client.spdy; +package org.eclipse.jetty.spdy.client.http; import org.eclipse.jetty.client.HttpContent; import org.eclipse.jetty.client.HttpExchange; import org.eclipse.jetty.client.HttpSender; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.http.HttpField; -import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.spdy.api.ByteBufferDataInfo; import org.eclipse.jetty.spdy.api.Stream; import org.eclipse.jetty.spdy.api.SynInfo; +import org.eclipse.jetty.spdy.http.HTTPSPDYHeader; +import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Fields; import org.eclipse.jetty.util.Promise; @@ -46,14 +47,33 @@ public class HttpSenderOverSPDY extends HttpSender } @Override - protected void sendHeaders(HttpExchange exchange, final HttpContent content) + protected void sendHeaders(HttpExchange exchange, final HttpContent content, final Callback callback) { final Request request = exchange.getRequest(); + short spdyVersion = getHttpChannel().getSession().getVersion(); Fields fields = new Fields(); - HttpFields headers = request.getHeaders(); - for (HttpField header : headers) - fields.add(header.getName(), header.getValue()); + HttpField hostHeader = null; + for (HttpField header : request.getHeaders()) + { + String name = header.getName(); + // The host header needs a special treatment + if (HTTPSPDYHeader.from(spdyVersion, name) != HTTPSPDYHeader.HOST) + fields.add(name, header.getValue()); + else + hostHeader = header; + } + + // Add special SPDY headers + fields.put(HTTPSPDYHeader.METHOD.name(spdyVersion), request.getMethod().asString()); + String path = request.getPath(); + String query = request.getQuery(); + if (query != null) + path += "?" + query; + fields.put(HTTPSPDYHeader.URI.name(spdyVersion), path); + fields.put(HTTPSPDYHeader.VERSION.name(spdyVersion), request.getVersion().asString()); + if (hostHeader != null) + fields.put(HTTPSPDYHeader.HOST.name(spdyVersion), hostHeader.getValue()); SynInfo synInfo = new SynInfo(fields, !content.hasContent()); getHttpChannel().getSession().syn(synInfo, getHttpChannel().getHttpReceiver(), new Promise() @@ -63,23 +83,29 @@ public class HttpSenderOverSPDY extends HttpSender { if (content.hasContent()) HttpSenderOverSPDY.this.stream = stream; - content.succeeded(); + callback.succeeded(); } @Override public void failed(Throwable failure) { - content.failed(failure); + callback.failed(failure); } }); } @Override - protected void sendContent(HttpExchange exchange, HttpContent content) + protected void sendContent(HttpExchange exchange, HttpContent content, Callback callback) { - assert stream != null; - ByteBufferDataInfo dataInfo = new ByteBufferDataInfo(content.getByteBuffer(), content.isLast()); - stream.data(dataInfo, content); + if (content.isConsumed()) + { + callback.succeeded(); + } + else + { + ByteBufferDataInfo dataInfo = new ByteBufferDataInfo(content.getByteBuffer(), content.isLast()); + stream.data(dataInfo, callback); + } } @Override diff --git a/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/AbstractHttpClientServerTest.java b/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/AbstractHttpClientServerTest.java new file mode 100644 index 00000000000..644067fcff7 --- /dev/null +++ b/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/AbstractHttpClientServerTest.java @@ -0,0 +1,82 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 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.spdy.client.http; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.NetworkConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.spdy.api.SPDY; +import org.eclipse.jetty.spdy.client.SPDYClient; +import org.eclipse.jetty.spdy.server.http.HTTPSPDYServerConnectionFactory; +import org.eclipse.jetty.spdy.server.http.PushStrategy; +import org.eclipse.jetty.toolchain.test.TestTracker; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.After; +import org.junit.Rule; + +public abstract class AbstractHttpClientServerTest +{ + @Rule + public final TestTracker tracker = new TestTracker(); + + protected Server server; + protected NetworkConnector connector; + protected SPDYClient.Factory factory; + protected HttpClient client; + protected String scheme = HttpScheme.HTTP.asString(); + + public void start(Handler handler) throws Exception + { + server = new Server(); + + short version = SPDY.V3; + + HTTPSPDYServerConnectionFactory spdyConnectionFactory = new HTTPSPDYServerConnectionFactory(version, new HttpConfiguration(), new PushStrategy.None()); + connector = new ServerConnector(server, spdyConnectionFactory); + + server.addConnector(connector); + server.setHandler(handler); + server.start(); + + QueuedThreadPool executor = new QueuedThreadPool(); + executor.setName(executor.getName() + "-client"); + + factory = new SPDYClient.Factory(executor); + factory.start(); + + client = new HttpClient(new HttpClientTransportOverSPDY(factory.newSPDYClient(version)), null); + client.setExecutor(executor); + client.start(); + } + + @After + public void dispose() throws Exception + { + if (client != null) + client.stop(); + if (factory != null) + factory.stop(); + if (server != null) + server.stop(); + } +} diff --git a/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/EmptyServerHandler.java b/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/EmptyServerHandler.java new file mode 100644 index 00000000000..dfa5b95ad8b --- /dev/null +++ b/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/EmptyServerHandler.java @@ -0,0 +1,36 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 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.spdy.client.http; + +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; + +public class EmptyServerHandler extends AbstractHandler +{ + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + baseRequest.setHandled(true); + } +} diff --git a/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/HttpClientTest.java b/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/HttpClientTest.java new file mode 100644 index 00000000000..76e29eeab0b --- /dev/null +++ b/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/HttpClientTest.java @@ -0,0 +1,422 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 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.spdy.client.http; + +import java.io.IOException; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.zip.GZIPOutputStream; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.util.BytesContentProvider; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.toolchain.test.annotation.Slow; +import org.junit.Assert; +import org.junit.Test; + +public class HttpClientTest extends AbstractHttpClientServerTest +{ + @Test + public void test_GET_ResponseWithoutContent() throws Exception + { + start(new EmptyServerHandler()); + + Response response = client.GET(scheme + "://localhost:" + connector.getLocalPort()); + + Assert.assertNotNull(response); + Assert.assertEquals(200, response.getStatus()); + } + + @Test + public void test_GET_ResponseWithContent() throws Exception + { + final byte[] data = new byte[]{0, 1, 2, 3, 4, 5, 6, 7}; + start(new AbstractHandler() + { + @Override + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + response.getOutputStream().write(data); + baseRequest.setHandled(true); + } + }); + + ContentResponse response = client.GET(scheme + "://localhost:" + connector.getLocalPort()); + + Assert.assertNotNull(response); + Assert.assertEquals(200, response.getStatus()); + byte[] content = response.getContent(); + Assert.assertArrayEquals(data, content); + } + + @Test + public void test_GET_WithParameters_ResponseWithContent() throws Exception + { + final String paramName1 = "a"; + final String paramName2 = "b"; + start(new AbstractHandler() + { + @Override + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + response.setCharacterEncoding("UTF-8"); + ServletOutputStream output = response.getOutputStream(); + String paramValue1 = request.getParameter(paramName1); + output.write(paramValue1.getBytes("UTF-8")); + String paramValue2 = request.getParameter(paramName2); + Assert.assertEquals("", paramValue2); + output.write("empty".getBytes("UTF-8")); + baseRequest.setHandled(true); + } + }); + + String value1 = "\u20AC"; + String paramValue1 = URLEncoder.encode(value1, "UTF-8"); + String query = paramName1 + "=" + paramValue1 + "&" + paramName2; + ContentResponse response = client.GET(scheme + "://localhost:" + connector.getLocalPort() + "/?" + query); + + Assert.assertNotNull(response); + Assert.assertEquals(200, response.getStatus()); + String content = new String(response.getContent(), "UTF-8"); + Assert.assertEquals(value1 + "empty", content); + } + + @Test + public void test_GET_WithParametersMultiValued_ResponseWithContent() throws Exception + { + final String paramName1 = "a"; + final String paramName2 = "b"; + start(new AbstractHandler() + { + @Override + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + response.setCharacterEncoding("UTF-8"); + ServletOutputStream output = response.getOutputStream(); + String[] paramValues1 = request.getParameterValues(paramName1); + for (String paramValue : paramValues1) + output.write(paramValue.getBytes("UTF-8")); + String paramValue2 = request.getParameter(paramName2); + output.write(paramValue2.getBytes("UTF-8")); + baseRequest.setHandled(true); + } + }); + + String value11 = "\u20AC"; + String value12 = "\u20AA"; + String value2 = "&"; + String paramValue11 = URLEncoder.encode(value11, "UTF-8"); + String paramValue12 = URLEncoder.encode(value12, "UTF-8"); + String paramValue2 = URLEncoder.encode(value2, "UTF-8"); + String query = paramName1 + "=" + paramValue11 + "&" + paramName1 + "=" + paramValue12 + "&" + paramName2 + "=" + paramValue2; + ContentResponse response = client.GET(scheme + "://localhost:" + connector.getLocalPort() + "/?" + query); + + Assert.assertNotNull(response); + Assert.assertEquals(200, response.getStatus()); + String content = new String(response.getContent(), "UTF-8"); + Assert.assertEquals(value11 + value12 + value2, content); + } + + @Test + public void test_POST_WithParameters() throws Exception + { + final String paramName = "a"; + final String paramValue = "\u20AC"; + start(new AbstractHandler() + { + @Override + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + baseRequest.setHandled(true); + String value = request.getParameter(paramName); + if (paramValue.equals(value)) + { + response.setCharacterEncoding("UTF-8"); + response.setContentType("text/plain"); + response.getOutputStream().print(value); + } + } + }); + + ContentResponse response = client.POST(scheme + "://localhost:" + connector.getLocalPort()) + .param(paramName, paramValue) + .timeout(5, TimeUnit.SECONDS) + .send(); + + Assert.assertNotNull(response); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(paramValue, new String(response.getContent(), "UTF-8")); + } + + @Test + public void test_PUT_WithParameters() throws Exception + { + final String paramName = "a"; + final String paramValue = "\u20AC"; + start(new AbstractHandler() + { + @Override + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + baseRequest.setHandled(true); + String value = request.getParameter(paramName); + if (paramValue.equals(value)) + { + response.setCharacterEncoding("UTF-8"); + response.setContentType("text/plain"); + response.getOutputStream().print(value); + } + } + }); + + URI uri = URI.create(scheme + "://localhost:" + connector.getLocalPort() + "/path?" + paramName + "=" + paramValue); + ContentResponse response = client.newRequest(uri) + .method(HttpMethod.PUT) + .timeout(5, TimeUnit.SECONDS) + .send(); + + Assert.assertNotNull(response); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(paramValue, new String(response.getContent(), "UTF-8")); + } + + @Test + public void test_POST_WithParameters_WithContent() throws Exception + { + final byte[] content = {0, 1, 2, 3}; + final String paramName = "a"; + final String paramValue = "\u20AC"; + start(new AbstractHandler() + { + @Override + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + baseRequest.setHandled(true); + String value = request.getParameter(paramName); + if (paramValue.equals(value)) + { + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/octet-stream"); + response.getOutputStream().write(content); + } + } + }); + + ContentResponse response = client.POST(scheme + "://localhost:" + connector.getLocalPort() + "/?b=1") + .param(paramName, paramValue) + .content(new BytesContentProvider(content)) + .timeout(555, TimeUnit.SECONDS) + .send(); + + Assert.assertNotNull(response); + Assert.assertEquals(200, response.getStatus()); + Assert.assertArrayEquals(content, response.getContent()); + } + + @Test + public void test_POST_WithContent_NotifiesRequestContentListener() throws Exception + { + final byte[] content = {0, 1, 2, 3}; + start(new EmptyServerHandler()); + + ContentResponse response = client.POST(scheme + "://localhost:" + connector.getLocalPort()) + .onRequestContent(new Request.ContentListener() + { + @Override + public void onContent(Request request, ByteBuffer buffer) + { + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + if (!Arrays.equals(content, bytes)) + request.abort(new Exception()); + } + }) + .content(new BytesContentProvider(content)) + .timeout(5, TimeUnit.SECONDS) + .send(); + + Assert.assertNotNull(response); + Assert.assertEquals(200, response.getStatus()); + } + + @Test + public void test_POST_WithContent_TracksProgress() throws Exception + { + start(new EmptyServerHandler()); + + final AtomicInteger progress = new AtomicInteger(); + ContentResponse response = client.POST(scheme + "://localhost:" + connector.getLocalPort()) + .onRequestContent(new Request.ContentListener() + { + @Override + public void onContent(Request request, ByteBuffer buffer) + { + byte[] bytes = new byte[buffer.remaining()]; + Assert.assertEquals(1, bytes.length); + buffer.get(bytes); + Assert.assertEquals(bytes[0], progress.getAndIncrement()); + } + }) + .content(new BytesContentProvider(new byte[]{0}, new byte[]{1}, new byte[]{2}, new byte[]{3}, new byte[]{4})) + .timeout(5, TimeUnit.SECONDS) + .send(); + + Assert.assertNotNull(response); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(5, progress.get()); + } + + @Test + public void test_GZIP_ContentEncoding() throws Exception + { + final byte[] data = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + start(new AbstractHandler() + { + @Override + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + baseRequest.setHandled(true); + response.setHeader("Content-Encoding", "gzip"); + GZIPOutputStream gzipOutput = new GZIPOutputStream(response.getOutputStream()); + gzipOutput.write(data); + gzipOutput.finish(); + } + }); + + ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .timeout(5, TimeUnit.SECONDS) + .send(); + + Assert.assertEquals(200, response.getStatus()); + Assert.assertArrayEquals(data, response.getContent()); + } + + @Slow + @Test + public void test_Request_IdleTimeout() throws Exception + { + final long idleTimeout = 1000; + start(new AbstractHandler() + { + @Override + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + try + { + baseRequest.setHandled(true); + TimeUnit.MILLISECONDS.sleep(2 * idleTimeout); + } + catch (InterruptedException x) + { + throw new ServletException(x); + } + } + }); + + final String host = "localhost"; + final int port = connector.getLocalPort(); + try + { + client.newRequest(host, port) + .scheme(scheme) + .idleTimeout(idleTimeout, TimeUnit.MILLISECONDS) + .timeout(3 * idleTimeout, TimeUnit.MILLISECONDS) + .send(); + Assert.fail(); + } + catch (ExecutionException expected) + { + Assert.assertTrue(expected.getCause() instanceof TimeoutException); + } + + // Make another request without specifying the idle timeout, should not fail + ContentResponse response = client.newRequest(host, port) + .scheme(scheme) + .timeout(3 * idleTimeout, TimeUnit.MILLISECONDS) + .send(); + + Assert.assertNotNull(response); + Assert.assertEquals(200, response.getStatus()); + } + + @Test + public void testSendToIPv6Address() throws Exception + { + start(new EmptyServerHandler()); + + ContentResponse response = client.newRequest("[::1]", connector.getLocalPort()) + .scheme(scheme) + .timeout(5, TimeUnit.SECONDS) + .send(); + + Assert.assertNotNull(response); + Assert.assertEquals(200, response.getStatus()); + } + + @Test + public void test_HEAD_With_ResponseContentLength() throws Exception + { + final int length = 1024; + start(new AbstractHandler() + { + @Override + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + baseRequest.setHandled(true); + response.getOutputStream().write(new byte[length]); + } + }); + + // HEAD requests receive a Content-Length header, but do not + // receive the content so they must handle this case properly + ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .method(HttpMethod.HEAD) + .timeout(5, TimeUnit.SECONDS) + .send(); + + Assert.assertNotNull(response); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(0, response.getContent().length); + + // Perform a normal GET request to be sure the content is now read + response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .timeout(5, TimeUnit.SECONDS) + .send(); + + Assert.assertNotNull(response); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(length, response.getContent().length); + } +} diff --git a/jetty-spdy/spdy-http-client-transport/src/test/resources/jetty-logging.properties b/jetty-spdy/spdy-http-client-transport/src/test/resources/jetty-logging.properties new file mode 100644 index 00000000000..8163013d571 --- /dev/null +++ b/jetty-spdy/spdy-http-client-transport/src/test/resources/jetty-logging.properties @@ -0,0 +1,4 @@ +org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog +#org.eclipse.jetty.LEVEL=DEBUG +#org.eclipse.jetty.client.LEVEL=DEBUG +#org.eclipse.jetty.spdy.LEVEL=DEBUG diff --git a/jetty-spdy/spdy-http-client-transport/src/test/resources/keystore.jks b/jetty-spdy/spdy-http-client-transport/src/test/resources/keystore.jks new file mode 100644 index 00000000000..428ba54776e Binary files /dev/null and b/jetty-spdy/spdy-http-client-transport/src/test/resources/keystore.jks differ diff --git a/jetty-spdy/spdy-http-client-transport/src/test/resources/truststore.jks b/jetty-spdy/spdy-http-client-transport/src/test/resources/truststore.jks new file mode 100644 index 00000000000..839cb8c3515 Binary files /dev/null and b/jetty-spdy/spdy-http-client-transport/src/test/resources/truststore.jks differ