// // ======================================================================== // 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.test; import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertThat; import java.io.IOException; import java.io.OutputStream; import java.net.Inet4Address; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import javax.net.ssl.SSLSocket; import javax.servlet.AsyncContext; import javax.servlet.ReadListener; import javax.servlet.ServletException; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpChannelState; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.LocalConnector.LocalEndPoint; import org.eclipse.jetty.server.NetworkConnector; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.hamcrest.Matchers; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @RunWith(Parameterized.class) public class HttpInputIntegrationTest { enum Mode { BLOCKING, ASYNC_DISPATCHED, ASYNC_OTHER_DISPATCHED, ASYNC_OTHER_WAIT } public final static String EOF = "__EOF__"; public final static String DELAY = "__DELAY__"; public final static String ABORT = "__ABORT__"; private static Server __server; private static HttpConfiguration __config; private static HttpConfiguration __sslConfig; private static SslContextFactory __sslContextFactory; @BeforeClass public static void beforeClass() throws Exception { __config = new HttpConfiguration(); __server = new Server(); LocalConnector local=new LocalConnector(__server,new HttpConnectionFactory(__config)); local.setIdleTimeout(4000); __server.addConnector(local); ServerConnector http = new ServerConnector(__server,new HttpConnectionFactory(__config),new HTTP2CServerConnectionFactory(__config)); http.setIdleTimeout(4000); __server.addConnector(http); // SSL Context Factory for HTTPS and HTTP/2 String jetty_distro = System.getProperty("jetty.distro","../../jetty-distribution/target/distribution"); __sslContextFactory = new SslContextFactory(); __sslContextFactory.setKeyStorePath(jetty_distro + "/../../../jetty-server/src/test/config/etc/keystore"); __sslContextFactory.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4"); __sslContextFactory.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g"); // HTTPS Configuration __sslConfig = new HttpConfiguration(__config); __sslConfig.addCustomizer(new SecureRequestCustomizer()); // HTTP/1 Connection Factory HttpConnectionFactory h1=new HttpConnectionFactory(__sslConfig); /* TODO // HTTP/2 Connection Factory HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(__sslConfig); NegotiatingServerConnectionFactory.checkProtocolNegotiationAvailable(); ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); alpn.setDefaultProtocol(h1.getProtocol()); */ // SSL Connection Factory SslConnectionFactory ssl = new SslConnectionFactory(__sslContextFactory,h1.getProtocol() /*TODO alpn.getProtocol()*/); // HTTP/2 Connector ServerConnector http2 = new ServerConnector(__server,ssl,/*TODO alpn,h2,*/ h1); http2.setIdleTimeout(4000); __server.addConnector(http2); ServletContextHandler context = new ServletContextHandler(__server,"/ctx"); ServletHolder holder = new ServletHolder(new TestServlet()); holder.setAsyncSupported(true); context.addServlet(holder,"/*"); __server.start(); } @AfterClass public static void afterClass() throws Exception { __server.stop(); } interface TestClient { /* ------------------------------------------------------------ */ /** * @param uri The URI to test, typically /ctx/test?mode=THE_MODE * @param delayMs the delay in MS to use. * @param delayInFrame If null, send the request with no delays, if FALSE then send with delays between frames, if TRUE send with delays within frames * @param contentLength The content length header to send. * @param content The content to send, with each string to be converted to a chunk or a frame * @return The response received in HTTP/1 format * @throws Exception */ String send(String uri,int delayMs, Boolean delayInFrame, int contentLength, List<String> content) throws Exception; } @Parameterized.Parameters public static Collection<Object[]> data() { List<Object[]> tests = new ArrayList<>(); // TODO other client types! // test with the following clients/protocols: // + Local // + HTTP/1 // + SSL + HTTP/1 // + HTTP/2 // + SSL + HTTP/2 // + FASTCGI for (Class<? extends TestClient> client : new Class[]{LocalClient.class,H1Client.class,H1SClient.class}) { // test async actions that are run: // + By a thread in a container callback // + By another thread while a container callback is active // + By another thread while no container callback is active for (Mode mode: Mode.values()) { // test servlet dispatch with: // + Delayed dispatch on // + Delayed dispatch off for (Boolean dispatch : new Boolean[]{false,true}) { // test send with // + No delays between frames // + Delays between frames // + Delays within frames! for (Boolean delayWithinFrame : new Boolean[]{null,false,true}) { // test content // + unknown length + EOF // + unknown length + content + EOF // + unknown length + content + content + EOF // + known length + EOF // + known length + content + EOF // + known length + content + content + EOF tests.add(new Object[]{tests.size(),client,mode,dispatch,delayWithinFrame,200,0,-1,new String[]{}}); tests.add(new Object[]{tests.size(),client,mode,dispatch,delayWithinFrame,200,8,-1,new String[]{"content0"}}); tests.add(new Object[]{tests.size(),client,mode,dispatch,delayWithinFrame,200,16,-1,new String[]{"content0","CONTENT1"}}); tests.add(new Object[]{tests.size(),client,mode,dispatch,delayWithinFrame,200,0,0,new String[]{}}); tests.add(new Object[]{tests.size(),client,mode,dispatch,delayWithinFrame,200,8,8,new String[]{"content0"}}); tests.add(new Object[]{tests.size(),client,mode,dispatch,delayWithinFrame,200,16,16,new String[]{"content0","CONTENT1"}}); } } } } return tests; } final int _id; final Class<? extends TestClient> _client; final Mode _mode; final Boolean _delay; final int _status; final int _read; final int _length; final List<String> _send; public HttpInputIntegrationTest(int id,Class<? extends TestClient> client, Mode mode,boolean dispatch,Boolean delay,int status,int read,int length,String... send) { _id=id; _client=client; _mode=mode; __config.setDelayDispatchUntilContent(dispatch); _delay=delay; _status=status; _read=read; _length=length; _send = Arrays.asList(send); } private static void runmode(Mode mode,final Request request, final Runnable test) { switch(mode) { case ASYNC_DISPATCHED: { test.run(); break; } case ASYNC_OTHER_DISPATCHED: { final CountDownLatch latch = new CountDownLatch(1); new Thread() { @Override public void run() { try { test.run(); } finally { latch.countDown(); } } }.start(); // prevent caller returning until other thread complete try { if (!latch.await(5,TimeUnit.SECONDS)) Assert.fail(); } catch(Exception e) { Assert.fail(); } break; } case ASYNC_OTHER_WAIT: { final CountDownLatch latch = new CountDownLatch(1); final HttpChannelState.State S=request.getHttpChannelState().getState(); new Thread() { @Override public void run() { try { if (!latch.await(5,TimeUnit.SECONDS)) Assert.fail(); // Spin until state change HttpChannelState.State s=request.getHttpChannelState().getState(); while(request.getHttpChannelState().getState()==S ) { Thread.yield(); s=request.getHttpChannelState().getState(); } test.run(); } catch (Exception e) { e.printStackTrace(); } } }.start(); // ensure other thread running before trying to return latch.countDown(); break; } default: throw new IllegalStateException(); } } @Test public void testOne() throws Exception { System.err.printf("[%d] TEST c=%s, m=%s, delayDispatch=%b delayInFrame=%s content-length:%d expect=%d read=%d content:%s%n",_id,_client.getSimpleName(),_mode,__config.isDelayDispatchUntilContent(),_delay,_length,_status,_read,_send); TestClient client=_client.newInstance(); String response = client.send("/ctx/test?mode="+_mode,50,_delay,_length,_send); int sum=0; for (String s:_send) for (char c : s.toCharArray()) sum+=c; assertThat(response,startsWith("HTTP")); assertThat(response,Matchers.containsString(" "+_status+" ")); assertThat(response,Matchers.containsString("read="+_read)); assertThat(response,Matchers.containsString("sum="+sum)); } @Test public void testStress() throws Exception { System.err.printf("[%d] STRESS c=%s, m=%s, delayDispatch=%b delayInFrame=%s content-length:%d expect=%d read=%d content:%s%n",_id,_client.getSimpleName(),_mode,__config.isDelayDispatchUntilContent(),_delay,_length,_status,_read,_send); int sum=0; for (String s:_send) for (char c : s.toCharArray()) sum+=c; final int summation=sum; final int threads=10; final int loops=10; final AtomicInteger count = new AtomicInteger(0); Thread[] t = new Thread[threads]; Runnable run = new Runnable() { @Override public void run() { try { TestClient client=_client.newInstance(); for (int j=0;j<loops;j++) { String response = client.send("/ctx/test?mode="+_mode,10,_delay,_length,_send); assertThat(response,startsWith("HTTP")); assertThat(response,Matchers.containsString(" "+_status+" ")); assertThat(response,Matchers.containsString("read="+_read)); assertThat(response,Matchers.containsString("sum="+summation)); count.incrementAndGet(); } } catch(Exception e) { e.printStackTrace(); } } }; for (int i=0;i<threads;i++) { t[i]=new Thread(run); t[i].start(); } for (int i=0;i<threads;i++) t[i].join(); assertThat(count.get(),Matchers.is(threads*loops)); } public static class TestServlet extends HttpServlet { String expected ="content0CONTENT1"; @Override protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { final Mode mode = Mode.valueOf(req.getParameter("mode")); resp.setContentType("text/plain"); if (mode==Mode.BLOCKING) { try { String content = IO.toString(req.getInputStream()); resp.setStatus(200); resp.setContentType("text/plain"); resp.getWriter().println("read="+content.length()); int sum = 0; for (char c:content.toCharArray()) sum+=c; resp.getWriter().println("sum="+sum); } catch(Exception e) { e.printStackTrace(); resp.setStatus(500); resp.getWriter().println("read="+e); resp.getWriter().println("sum=-1"); } } else { // we are asynchronous final AsyncContext context = req.startAsync(); context.setTimeout(10000); final ServletInputStream in = req.getInputStream(); final Request request = Request.getBaseRequest(req); final AtomicInteger read = new AtomicInteger(0); final AtomicInteger sum = new AtomicInteger(0); runmode(mode,request,new Runnable() { @Override public void run() { in.setReadListener(new ReadListener() { @Override public void onError(Throwable t) { t.printStackTrace(); try { resp.sendError(500); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e); } context.complete(); } @Override public void onDataAvailable() throws IOException { runmode(mode,request,new Runnable() { @Override public void run() { while(in.isReady() && !in.isFinished()) { try { int b = in.read(); if (b<0) return; sum.addAndGet(b); int i=read.getAndIncrement(); if (b!=expected.charAt(i)) { System.err.printf("XXX '%c'!='%c' at %d%n",expected.charAt(i),(char)b,i); System.err.println(" "+request.getHttpChannel()); System.err.println(" "+request.getHttpChannel().getHttpTransport()); } } catch (IOException e) { onError(e); } } } }); } @Override public void onAllDataRead() throws IOException { resp.setStatus(200); resp.setContentType("text/plain"); resp.getWriter().println("read="+read.get()); resp.getWriter().println("sum="+sum.get()); context.complete(); } }); } }); } } } public static class LocalClient implements TestClient { StringBuilder flushed = new StringBuilder(); @Override public String send(String uri,int delayMs, Boolean delayInFrame,int contentLength, List<String> content) throws Exception { LocalConnector connector = __server.getBean(LocalConnector.class); StringBuilder buffer = new StringBuilder(); buffer.append("GET ").append(uri).append(" HTTP/1.1\r\n"); buffer.append("Host: localhost\r\n"); buffer.append("Connection: close\r\n"); LocalEndPoint local = connector.executeRequest(""); flush(local,buffer,delayMs,delayInFrame,true); boolean chunked=contentLength<0; if (chunked) buffer.append("Transfer-Encoding: chunked\r\n"); else buffer.append("Content-Length: ").append(contentLength).append("\r\n"); if (contentLength>0) buffer.append("Content-Type: text/plain\r\n"); buffer.append("\r\n"); flush(local,buffer,delayMs,delayInFrame,false); for (String c : content) { if (chunked) { buffer.append(Integer.toHexString(c.length())).append("\r\n"); flush(local,buffer,delayMs,delayInFrame,true); } buffer.append(c.substring(0,1)); flush(local,buffer,delayMs,delayInFrame,true); buffer.append(c.substring(1)); if (chunked) buffer.append("\r\n"); flush(local,buffer,delayMs,delayInFrame,false); } if (chunked) { buffer.append("0"); flush(local,buffer,delayMs,delayInFrame,true); buffer.append("\r\n\r\n"); } flush(local,buffer); local.waitUntilClosed(); return local.takeOutputString(); } private void flush(LocalEndPoint local, StringBuilder buffer,int delayMs, Boolean delayInFrame, boolean inFrame) throws Exception { // Flush now if we should delay if (delayInFrame!=null && delayInFrame.equals(inFrame)) { flush(local,buffer); Thread.sleep(delayMs); } } private void flush(final LocalEndPoint local, StringBuilder buffer) throws Exception { final String flush=buffer.toString(); buffer.setLength(0); flushed.append(flush); local.addInputAndExecute(BufferUtil.toBuffer(flush)); } } public static class H1Client implements TestClient { NetworkConnector _connector; public H1Client() { for (Connector c:__server.getConnectors()) { if (c instanceof NetworkConnector && c.getDefaultConnectionFactory().getProtocol().equals(HttpVersion.HTTP_1_1.asString())) { _connector=(NetworkConnector)c; break; } } } @Override public String send(String uri, int delayMs, Boolean delayInFrame,int contentLength, List<String> content) throws Exception { int port=_connector.getLocalPort(); try (Socket client = newSocket("localhost", port)) { client.setSoTimeout(5000); client.setTcpNoDelay(true); client.setSoLinger(true,1); OutputStream out = client.getOutputStream(); StringBuilder buffer = new StringBuilder(); buffer.append("GET ").append(uri).append(" HTTP/1.1\r\n"); buffer.append("Host: localhost:").append(port).append("\r\n"); buffer.append("Connection: close\r\n"); flush(out,buffer,delayMs,delayInFrame,true); boolean chunked=contentLength<0; if (chunked) buffer.append("Transfer-Encoding: chunked\r\n"); else buffer.append("Content-Length: ").append(contentLength).append("\r\n"); if (contentLength>0) buffer.append("Content-Type: text/plain\r\n"); buffer.append("\r\n"); flush(out,buffer,delayMs,delayInFrame,false); for (String c : content) { if (chunked) { buffer.append(Integer.toHexString(c.length())).append("\r\n"); flush(out,buffer,delayMs,delayInFrame,true); } buffer.append(c.substring(0,1)); flush(out,buffer,delayMs,delayInFrame,true); buffer.append(c.substring(1)); flush(out,buffer,delayMs,delayInFrame,false); if (chunked) buffer.append("\r\n"); } if (chunked) { buffer.append("0"); flush(out,buffer,delayMs,delayInFrame,true); buffer.append("\r\n\r\n"); } flush(out,buffer); return IO.toString(client.getInputStream()); } } private void flush(OutputStream out, StringBuilder buffer, int delayMs, Boolean delayInFrame, boolean inFrame) throws Exception { // Flush now if we should delay if (delayInFrame!=null && delayInFrame.equals(inFrame)) { flush(out,buffer); Thread.sleep(delayMs); } } private void flush(OutputStream out, StringBuilder buffer) throws Exception { String flush=buffer.toString(); buffer.setLength(0); out.write(flush.getBytes(StandardCharsets.ISO_8859_1)); out.flush(); } public Socket newSocket(String host, int port) throws IOException { return new Socket(host, port); } } public static class H1SClient extends H1Client { public H1SClient() { for (Connector c:__server.getConnectors()) { if (c instanceof NetworkConnector && c.getDefaultConnectionFactory().getProtocol().equals("SSL")) { _connector=(NetworkConnector)c; break; } } } public Socket newSocket(String host, int port) throws IOException { SSLSocket socket = __sslContextFactory.newSslSocket(); socket.connect(new InetSocketAddress(Inet4Address.getByName(host),port)); return socket; } } }