diff --git a/jetty-client/pom.xml b/jetty-client/pom.xml index 37d6461b2ed..c7ef18c08dd 100644 --- a/jetty-client/pom.xml +++ b/jetty-client/pom.xml @@ -1,101 +1,104 @@ - - - org.eclipse.jetty - jetty-project - 7.2.0-SNAPSHOT - - 4.0.0 - jetty-client - Jetty :: Asynchronous HTTP Client - {$jetty.url} - - ${project.groupId}.client - - - - - org.apache.felix - maven-bundle-plugin - ${felix.bundle.version} - true - - - - manifest - - - - javax.net.*,* - - - - - - - - org.apache.maven.plugins - maven-jar-plugin - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - - - - org.codehaus.mojo - findbugs-maven-plugin - - org.eclipse.jetty.client.* - - - - - - - org.eclipse.jetty - jetty-http - ${project.version} - - - org.eclipse.jetty - jetty-server - ${project.version} - test - - - org.eclipse.jetty - jetty-security - ${project.version} - test - - - junit - junit - ${junit4-version} - test - - - org.eclipse.jetty - jetty-servlet - ${project.version} - test - - - - org.eclipse.jetty - jetty-websocket - ${project.version} - test - - + + + org.eclipse.jetty + jetty-project + 7.2.0-SNAPSHOT + + + 4.0.0 + jetty-client + Jetty :: Asynchronous HTTP Client + {$jetty.url} + + + ${project.groupId}.client + + + + + + org.apache.felix + maven-bundle-plugin + ${felix.bundle.version} + true + + + + manifest + + + + javax.net.*,* + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + org.codehaus.mojo + findbugs-maven-plugin + + org.eclipse.jetty.client.* + + + + + + + + org.eclipse.jetty + jetty-http + ${project.version} + + + org.eclipse.jetty + jetty-server + ${project.version} + test + + + org.eclipse.jetty + jetty-security + ${project.version} + test + + + org.eclipse.jetty + jetty-servlet + ${project.version} + test + + + org.eclipse.jetty + jetty-servlets + ${project.version} + test + + + org.eclipse.jetty + jetty-websocket + ${project.version} + test + + + junit + junit + ${junit4-version} + test + + diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java index 92443143559..05253c06123 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java @@ -36,7 +36,6 @@ import org.eclipse.jetty.io.ByteArrayBuffer; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.View; -import org.eclipse.jetty.io.nio.SelectChannelEndPoint; import org.eclipse.jetty.io.nio.SslSelectChannelEndPoint; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.thread.Timeout; @@ -209,7 +208,7 @@ public class HttpConnection implements Connection if (_exchange == null) continue; } - + long flushed = _generator.flushBuffer(); io += flushed; @@ -236,7 +235,7 @@ public class HttpConnection implements Connection } else _generator.complete(); - } + } else _generator.complete(); } @@ -254,7 +253,7 @@ public class HttpConnection implements Connection long filled = _parser.parseAvailable(); io += filled; } - + if (io > 0) no_progress = 0; else if (no_progress++ >= 2 && !_endp.isBlocking()) @@ -342,12 +341,12 @@ public class HttpConnection implements Connection HttpExchange exchange=_exchange; _exchange.disassociate(); _exchange = null; - + if (_status==HttpStatus.SWITCHING_PROTOCOLS_101) { Connection switched=exchange.onSwitchProtocol(_endp); if (switched!=null) - { + { // switched protocol! exchange = _pipeline; _pipeline = null; @@ -394,13 +393,13 @@ public class HttpConnection implements Connection { _exchange.disassociate(); } - + if (!_generator.isComplete() && _generator.getBytesBuffered()>0 && _endp instanceof AsyncEndPoint) - { + { ((AsyncEndPoint)_endp).setWritable(false); } } - + return this; } @@ -436,18 +435,27 @@ public class HttpConnection implements Connection _exchange.setStatus(HttpExchange.STATUS_SENDING_REQUEST); _generator.setVersion(_exchange.getVersion()); + String method=_exchange.getMethod(); String uri = _exchange.getURI(); - if (_destination.isProxied() && uri.startsWith("/")) + if (_destination.isProxied() && !HttpMethods.CONNECT.equals(method) && uri.startsWith("/")) { - // TODO suppress port 80 or 443 - uri = (_destination.isSecure()?HttpSchemes.HTTPS:HttpSchemes.HTTP) + "://" + _destination.getAddress().getHost() + ":" - + _destination.getAddress().getPort() + uri; + boolean secure = _destination.isSecure(); + String host = _destination.getAddress().getHost(); + int port = _destination.getAddress().getPort(); + StringBuilder absoluteURI = new StringBuilder(); + absoluteURI.append(secure ? HttpSchemes.HTTPS : HttpSchemes.HTTP); + absoluteURI.append("://"); + absoluteURI.append(host); + // Avoid adding default ports + if (!(secure && port == 443 || !secure && port == 80)) + absoluteURI.append(":").append(port); + absoluteURI.append(uri); + uri = absoluteURI.toString(); Authentication auth = _destination.getProxyAuthentication(); if (auth != null) auth.setCredentials(_exchange); } - String method=_exchange.getMethod(); _generator.setRequest(method, uri); _parser.setHeadResponse(HttpMethods.HEAD.equalsIgnoreCase(method)); @@ -594,10 +602,10 @@ public class HttpConnection implements Connection public void close() throws IOException { - //if there is a live, unfinished exchange, set its status to be + //if there is a live, unfinished exchange, set its status to be //excepted and wake up anyone waiting on waitForDone() - - if (_exchange != null && !_exchange.isDone()) + + if (_exchange != null && !_exchange.isDone()) { switch (_exchange.getStatus()) { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java index b8858280228..590179c5f3e 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java @@ -12,9 +12,9 @@ // ======================================================================== package org.eclipse.jetty.client; - import java.io.IOException; import java.lang.reflect.Constructor; +import java.net.ConnectException; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; @@ -25,9 +25,12 @@ import org.eclipse.jetty.client.security.Authentication; import org.eclipse.jetty.client.security.SecurityListener; import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpHeaders; +import org.eclipse.jetty.http.HttpMethods; +import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.PathMap; import org.eclipse.jetty.io.Buffer; import org.eclipse.jetty.io.ByteArrayBuffer; +import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.util.log.Log; /** @@ -307,7 +310,7 @@ public class HttpDestination } } - public void onNewConnection(HttpConnection connection) throws IOException + public void onNewConnection(final HttpConnection connection) throws IOException { HttpConnection q_connection = null; @@ -328,8 +331,20 @@ public class HttpDestination } else { - HttpExchange ex = _queue.removeFirst(); - send(connection, ex); + EndPoint endPoint = connection.getEndPoint(); + if (isProxied() && endPoint instanceof SelectConnector.ProxySelectChannelEndPoint) + { + SelectConnector.ProxySelectChannelEndPoint proxyEndPoint = (SelectConnector.ProxySelectChannelEndPoint)endPoint; + HttpExchange exchange = _queue.peekFirst(); + ConnectExchange connect = new ConnectExchange(getAddress(), proxyEndPoint, exchange); + connect.setAddress(getProxy()); + send(connection, connect); + } + else + { + HttpExchange exchange = _queue.removeFirst(); + send(connection, exchange); + } } } @@ -580,4 +595,41 @@ public class HttpDestination } } } + + private class ConnectExchange extends ContentExchange + { + private final SelectConnector.ProxySelectChannelEndPoint proxyEndPoint; + private final HttpExchange exchange; + + public ConnectExchange(Address serverAddress, SelectConnector.ProxySelectChannelEndPoint proxyEndPoint, HttpExchange exchange) + { + this.proxyEndPoint = proxyEndPoint; + this.exchange = exchange; + setMethod(HttpMethods.CONNECT); + String serverHostAndPort = serverAddress.toString(); + setURI(serverHostAndPort); + addRequestHeader(HttpHeaders.HOST, serverHostAndPort); + addRequestHeader(HttpHeaders.PROXY_CONNECTION, "keep-alive"); + addRequestHeader(HttpHeaders.USER_AGENT, "Jetty-Client"); + } + + @Override + protected void onResponseComplete() throws IOException + { + if (getResponseStatus() == HttpStatus.OK_200) + { + proxyEndPoint.upgrade(); + } + else + { + onConnectionFailed(new ConnectException(exchange.getAddress().toString())); + } + } + + @Override + protected void onConnectionFailed(Throwable x) + { + HttpDestination.this.onConnectionFailed(x); + } + } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/SelectConnector.java b/jetty-client/src/main/java/org/eclipse/jetty/client/SelectConnector.java index 24e714b8036..cb58420cafe 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/SelectConnector.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/SelectConnector.java @@ -16,20 +16,18 @@ package org.eclipse.jetty.client; import java.io.IOException; import java.net.SocketTimeoutException; import java.nio.channels.SelectionKey; -import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLSession; -import org.eclipse.jetty.http.HttpMethods; -import org.eclipse.jetty.http.HttpVersions; +import org.eclipse.jetty.http.HttpGenerator; +import org.eclipse.jetty.http.HttpParser; import org.eclipse.jetty.io.Buffer; import org.eclipse.jetty.io.Buffers; import org.eclipse.jetty.io.ConnectedEndPoint; import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ThreadLocalBuffers; import org.eclipse.jetty.io.nio.DirectNIOBuffer; import org.eclipse.jetty.io.nio.IndirectNIOBuffer; @@ -218,14 +216,14 @@ class SelectConnector extends AbstractLifeCycle implements HttpClient.Connector, { if (dest.isProxied()) { - String connect = HttpMethods.CONNECT+" "+dest.getAddress()+HttpVersions.HTTP_1_0+"\r\n\r\n"; - // TODO need to send this over channel unencrypted and setup endpoint to ignore the 200 OK response. - - throw new IllegalStateException("Not Implemented"); + SSLEngine engine=newSslEngine(); + ep = new ProxySelectChannelEndPoint(channel,selectSet,key,_sslBuffers,engine); + } + else + { + SSLEngine engine=newSslEngine(); + ep = new SslSelectChannelEndPoint(_sslBuffers,channel,selectSet,key,engine); } - - SSLEngine engine=newSslEngine(); - ep = new SslSelectChannelEndPoint(_sslBuffers,channel,selectSet,key,engine); } else { @@ -283,4 +281,204 @@ class SelectConnector extends AbstractLifeCycle implements HttpClient.Connector, } } } + + /** + * An endpoint that is able to "upgrade" from a normal endpoint to a SSL endpoint. + * Since {@link HttpParser} and {@link HttpGenerator} only depend on the {@link EndPoint} + * interface, this class overrides all methods of {@link EndPoint} to provide the right + * behavior depending on the fact that it has been upgraded or not. + */ + public static class ProxySelectChannelEndPoint extends SslSelectChannelEndPoint + { + private final SelectChannelEndPoint plainEndPoint; + private volatile boolean upgraded = false; + + public ProxySelectChannelEndPoint(SocketChannel channel, SelectorManager.SelectSet selectSet, SelectionKey key, Buffers sslBuffers, SSLEngine engine) throws IOException + { + super(sslBuffers, channel, selectSet, key, engine); + this.plainEndPoint = new SelectChannelEndPoint(channel, selectSet, key); + } + + public void upgrade() + { + upgraded = true; + } + + public void shutdownOutput() throws IOException + { + if (upgraded) + super.shutdownOutput(); + else + plainEndPoint.shutdownOutput(); + } + + public void close() throws IOException + { + if (upgraded) + super.close(); + else + plainEndPoint.close(); + } + + public int fill(Buffer buffer) throws IOException + { + if (upgraded) + return super.fill(buffer); + else + return plainEndPoint.fill(buffer); + } + + public int flush(Buffer buffer) throws IOException + { + if (upgraded) + return super.flush(buffer); + else + return plainEndPoint.flush(buffer); + } + + public int flush(Buffer header, Buffer buffer, Buffer trailer) throws IOException + { + if (upgraded) + return super.flush(header, buffer, trailer); + else + return plainEndPoint.flush(header, buffer, trailer); + } + + public String getLocalAddr() + { + if (upgraded) + return super.getLocalAddr(); + else + return plainEndPoint.getLocalAddr(); + } + + public String getLocalHost() + { + if (upgraded) + return super.getLocalHost(); + else + return plainEndPoint.getLocalHost(); + } + + public int getLocalPort() + { + if (upgraded) + return super.getLocalPort(); + else + return plainEndPoint.getLocalPort(); + } + + public String getRemoteAddr() + { + if (upgraded) + return super.getRemoteAddr(); + else + return plainEndPoint.getRemoteAddr(); + } + + public String getRemoteHost() + { + if (upgraded) + return super.getRemoteHost(); + else + return plainEndPoint.getRemoteHost(); + } + + public int getRemotePort() + { + if (upgraded) + return super.getRemotePort(); + else + return plainEndPoint.getRemotePort(); + } + + public boolean isBlocking() + { + if (upgraded) + return super.isBlocking(); + else + return plainEndPoint.isBlocking(); + } + + public boolean isBufferred() + { + if (upgraded) + return super.isBufferred(); + else + return plainEndPoint.isBufferred(); + } + + public boolean blockReadable(long millisecs) throws IOException + { + if (upgraded) + return super.blockReadable(millisecs); + else + return plainEndPoint.blockReadable(millisecs); + } + + public boolean blockWritable(long millisecs) throws IOException + { + if (upgraded) + return super.blockWritable(millisecs); + else + return plainEndPoint.blockWritable(millisecs); + } + + public boolean isOpen() + { + if (upgraded) + return super.isOpen(); + else + return plainEndPoint.isOpen(); + } + + public Object getTransport() + { + if (upgraded) + return super.getTransport(); + else + return plainEndPoint.getTransport(); + } + + public boolean isBufferingInput() + { + if (upgraded) + return super.isBufferingInput(); + else + return plainEndPoint.isBufferingInput(); + } + + public boolean isBufferingOutput() + { + if (upgraded) + return super.isBufferingOutput(); + else + return plainEndPoint.isBufferingOutput(); + } + + public void flush() throws IOException + { + if (upgraded) + super.flush(); + else + plainEndPoint.flush(); + + } + + public int getMaxIdleTime() + { + if (upgraded) + return super.getMaxIdleTime(); + else + return plainEndPoint.getMaxIdleTime(); + } + + public void setMaxIdleTime(int timeMs) throws IOException + { + if (upgraded) + super.setMaxIdleTime(timeMs); + else + plainEndPoint.setMaxIdleTime(timeMs); + } + } } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ProxyTunnellingTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ProxyTunnellingTest.java new file mode 100644 index 00000000000..f0c864789fa --- /dev/null +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ProxyTunnellingTest.java @@ -0,0 +1,287 @@ +package org.eclipse.jetty.client; + +import java.io.File; +import java.io.IOException; +import java.net.URLEncoder; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpHeaders; +import org.eclipse.jetty.http.HttpMethods; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.io.ByteArrayBuffer; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.server.handler.HandlerCollection; +import org.eclipse.jetty.server.handler.ProxyHandler; +import org.eclipse.jetty.server.nio.SelectChannelConnector; +import org.eclipse.jetty.server.ssl.SslSelectChannelConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.servlets.ProxyServlet; +import org.junit.After; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class ProxyTunnellingTest +{ + private Server server; + private Connector serverConnector; + private Server proxy; + private Connector proxyConnector; + + private void startSSLServer(Handler handler) throws Exception + { + SslSelectChannelConnector connector = new SslSelectChannelConnector(); + String keyStorePath = System.getProperty("basedir"); + assertNotNull(keyStorePath); + keyStorePath += File.separator + "src" + File.separator + "test" + File.separator + "resources" + File.separator + "keystore"; + connector.setKeystore(keyStorePath); + connector.setPassword("storepwd"); + connector.setKeyPassword("keypwd"); + startServer(connector, handler); + } + + private void startServer(Connector connector, Handler handler) throws Exception + { + server = new Server(); + serverConnector = connector; + server.addConnector(serverConnector); + server.setHandler(handler); + server.start(); + } + + private void startProxy() throws Exception + { + proxy = new Server(); + proxyConnector = new SelectChannelConnector(); + proxy.addConnector(proxyConnector); + ProxyHandler proxyHandler = new ProxyHandler(); + proxy.setHandler(proxyHandler); + HandlerCollection handlers = new HandlerCollection(); + proxyHandler.setHandler(handlers); + ServletContextHandler context = new ServletContextHandler(handlers, "/", ServletContextHandler.SESSIONS); + ServletHolder proxyServlet = new ServletHolder(ProxyServlet.class); + context.addServlet(proxyServlet, "/*"); + proxy.start(); + } + + @After + public void stop() throws Exception + { + stopProxy(); + stopServer(); + } + + private void stopServer() throws Exception + { + server.stop(); + server.join(); + } + + private void stopProxy() throws Exception + { + proxy.stop(); + proxy.join(); + } + + + @Test + public void testNoSSL() throws Exception + { + startServer(new SelectChannelConnector(), new ServerHandler()); + startProxy(); + + HttpClient httpClient = new HttpClient(); + httpClient.setProxy(new Address("localhost", proxyConnector.getLocalPort())); + httpClient.start(); + + try + { + ContentExchange exchange = new ContentExchange(true); + String body = "BODY"; + exchange.setURL("http://localhost:" + serverConnector.getLocalPort() + "/echo?body=" + URLEncoder.encode(body, "UTF-8")); + exchange.setMethod(HttpMethods.GET); + + httpClient.send(exchange); + assertEquals(HttpExchange.STATUS_COMPLETED, exchange.waitForDone()); + String content = exchange.getResponseContent(); + assertEquals(body, content); + } + finally + { + httpClient.stop(); + } + } + + @Test + public void testOneMessageSSL() throws Exception + { + startSSLServer(new ServerHandler()); + startProxy(); + + HttpClient httpClient = new HttpClient(); + httpClient.setProxy(new Address("localhost", proxyConnector.getLocalPort())); + httpClient.start(); + + try + { + ContentExchange exchange = new ContentExchange(true); + exchange.setMethod(HttpMethods.GET); + String body = "BODY"; + exchange.setURL("https://localhost:" + serverConnector.getLocalPort() + "/echo?body=" + URLEncoder.encode(body, "UTF-8")); + + httpClient.send(exchange); + assertEquals(HttpExchange.STATUS_COMPLETED, exchange.waitForDone()); + String content = exchange.getResponseContent(); + assertEquals(body, content); + } + finally + { + httpClient.stop(); + } + } + + @Test + public void testTwoMessagesSSL() throws Exception + { + startSSLServer(new ServerHandler()); + startProxy(); + + HttpClient httpClient = new HttpClient(); + httpClient.setProxy(new Address("localhost", proxyConnector.getLocalPort())); + httpClient.start(); + + try + { + ContentExchange exchange = new ContentExchange(true); + exchange.setMethod(HttpMethods.GET); + String body = "BODY"; + exchange.setURL("https://localhost:" + serverConnector.getLocalPort() + "/echo?body=" + URLEncoder.encode(body, "UTF-8")); + + httpClient.send(exchange); + assertEquals(HttpExchange.STATUS_COMPLETED, exchange.waitForDone()); + String content = exchange.getResponseContent(); + assertEquals(body, content); + + exchange = new ContentExchange(true); + exchange.setMethod(HttpMethods.POST); + exchange.setURL("https://localhost:" + serverConnector.getLocalPort() + "/echo"); + exchange.setRequestHeader(HttpHeaders.CONTENT_TYPE, MimeTypes.FORM_ENCODED); + content = "body=" + body; + exchange.setRequestHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(content.length())); + exchange.setRequestContent(new ByteArrayBuffer(content, "UTF-8")); + + httpClient.send(exchange); + assertEquals(HttpExchange.STATUS_COMPLETED, exchange.waitForDone()); + content = exchange.getResponseContent(); + assertEquals(body, content); + } + finally + { + httpClient.stop(); + } + } + + @Test + public void testProxyDown() throws Exception + { + startSSLServer(new ServerHandler()); + startProxy(); + int proxyPort = proxyConnector.getLocalPort(); + stopProxy(); + + HttpClient httpClient = new HttpClient(); + httpClient.setProxy(new Address("localhost", proxyPort)); + httpClient.start(); + + try + { + final CountDownLatch latch = new CountDownLatch(1); + ContentExchange exchange = new ContentExchange(true) + { + @Override + protected void onConnectionFailed(Throwable x) + { + latch.countDown(); + } + }; + exchange.setMethod(HttpMethods.GET); + String body = "BODY"; + exchange.setURL("https://localhost:" + serverConnector.getLocalPort() + "/echo?body=" + URLEncoder.encode(body, "UTF-8")); + + httpClient.send(exchange); + assertTrue(latch.await(1000, TimeUnit.MILLISECONDS)); + } + finally + { + httpClient.stop(); + } + } + + @Test + public void testServerDown() throws Exception + { + startSSLServer(new ServerHandler()); + int serverPort = serverConnector.getLocalPort(); + stopServer(); + startProxy(); + + HttpClient httpClient = new HttpClient(); + httpClient.setProxy(new Address("localhost", proxyConnector.getLocalPort())); + httpClient.start(); + + try + { + final CountDownLatch latch = new CountDownLatch(1); + ContentExchange exchange = new ContentExchange(true) + { + @Override + protected void onConnectionFailed(Throwable x) + { + latch.countDown(); + } + }; + exchange.setMethod(HttpMethods.GET); + String body = "BODY"; + exchange.setURL("https://localhost:" + serverPort + "/echo?body=" + URLEncoder.encode(body, "UTF-8")); + + httpClient.send(exchange); + assertTrue(latch.await(1000, TimeUnit.MILLISECONDS)); + } + finally + { + httpClient.stop(); + } + } + + private static class ServerHandler extends AbstractHandler + { + public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException + { + request.setHandled(true); + + String uri = httpRequest.getRequestURI(); + if ("/echo".equals(uri)) + { + String body = httpRequest.getParameter("body"); + ServletOutputStream output = httpResponse.getOutputStream(); + output.print(body); + } + else + { + throw new ServletException(); + } + } + } +}