diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java index 2ab329f951d..12f74a50bbf 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java @@ -298,6 +298,12 @@ public class HttpParser _complianceHandler=(ComplianceHandler)(handler instanceof ComplianceHandler?handler:null); } + /* ------------------------------------------------------------------------------- */ + public HttpHandler getHandler() + { + return _handler; + } + /* ------------------------------------------------------------------------------- */ /** Check RFC compliance violation * @param compliance The compliance level violated diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpTester.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpTester.java index becf1c94585..7da2f9bd010 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpTester.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpTester.java @@ -20,13 +20,44 @@ package org.eclipse.jetty.http; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; +import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.StringUtil; + +/** + * A HTTP Testing helper class. + * + * Example usage: + *
+ *        try(Socket socket = new Socket("www.google.com",80))
+ *        {
+ *          HttpTester.Request request = HttpTester.newRequest();
+ *          request.setMethod("POST");
+ *          request.setURI("/search");
+ *          request.setVersion(HttpVersion.HTTP_1_0);
+ *          request.put(HttpHeader.HOST,"www.google.com");
+ *          request.put("Content-Type","application/x-www-form-urlencoded");
+ *          request.setContent("q=jetty%20server");
+ *          ByteBuffer output = request.generate();
+ *          
+ *          socket.getOutputStream().write(output.array(),output.arrayOffset()+output.position(),output.remaining());
+ *          HttpTester.Input input = HttpTester.from(socket.getInputStream());
+ *          HttpTester.Response response = HttpTester.parseResponse(input);
+ *          System.err.printf("%s %s %s%n",response.getVersion(),response.getStatus(),response.getReason());
+ *          for (HttpField field:response)
+ *              System.err.printf("%s: %s%n",field.getName(),field.getValue());
+ *          System.err.printf("%n%s%n",response.getContent());
+ *       }
+ * 
+ */ public class HttpTester { private HttpTester() @@ -35,7 +66,11 @@ public class HttpTester public static Request newRequest() { - return new Request(); + Request r=new Request(); + r.setMethod(HttpMethod.GET.asString()); + r.setURI("/"); + r.setVersion(HttpVersion.HTTP_1_1); + return r; } public static Request parseRequest(String request) @@ -70,12 +105,121 @@ public class HttpTester return r; } + public abstract static class Input + { + boolean _eof=false; + HttpParser _parser; + ByteBuffer _buffer = BufferUtil.allocate(8192); + + public ByteBuffer getBuffer() + { + return _buffer; + } + + public void setHttpParser(HttpParser parser) + { + _parser=parser; + } + + public HttpParser getHttpParser() + { + return _parser; + } + + public HttpParser takeHttpParser() + { + HttpParser p=_parser; + _parser=null; + return p; + } + + public boolean isEOF() + { + return BufferUtil.isEmpty(_buffer) && _eof; + } + + public abstract int fillBuffer() throws IOException; + + } + + public static Input from(final InputStream in) + { + return new Input() + { + @Override + public int fillBuffer() throws IOException + { + BufferUtil.compact(_buffer); + int len=in.read(_buffer.array(),_buffer.arrayOffset()+_buffer.limit(),BufferUtil.space(_buffer)); + if (len<0) + _eof=true; + else + _buffer.limit(_buffer.limit()+len); + return len; + } + }; + } + + public static Input from(final ReadableByteChannel in) + { + return new Input() + { + @Override + public int fillBuffer() throws IOException + { + BufferUtil.compact(_buffer); + int pos=BufferUtil.flipToFill(_buffer); + int len=in.read(_buffer); + if (len<0) + _eof=true; + BufferUtil.flipToFlush(_buffer,pos); + return len; + } + }; + } + + public static Response parseResponse(Input in) throws IOException + { + Response r; + HttpParser parser=in.takeHttpParser(); + if (parser==null) + { + r=new Response(); + parser =new HttpParser(r); + } + else + r=(Response)parser.getHandler(); + + ByteBuffer buffer = in.getBuffer(); + + int len=0; + while(len>=0) + { + if (BufferUtil.hasContent(buffer)) + if (parser.parseNext(buffer)) + break; + if (in.fillBuffer()<=0) + break; + } + + if (r.isComplete()) + return r; + in.setHttpParser(parser); + return null; + } + public abstract static class Message extends HttpFields implements HttpParser.HttpHandler { + boolean _complete=false; ByteArrayOutputStream _content; HttpVersion _version=HttpVersion.HTTP_1_0; + public boolean isComplete() + { + return _complete; + } + public HttpVersion getVersion() { return _version; @@ -129,6 +273,27 @@ public class HttpTester throw new RuntimeException(e); } } + + public byte[] getContentBytes() + { + if (_content==null) + return null; + return _content.toByteArray(); + } + + public String getContent() + { + if (_content==null) + return null; + byte[] bytes=_content.toByteArray(); + + String content_type=get(HttpHeader.CONTENT_TYPE); + String encoding=MimeTypes.getCharsetFromContentType(content_type); + Charset charset=encoding==null?StandardCharsets.UTF_8:Charset.forName(encoding); + + return new String(bytes,charset); + } + @Override public void parsedHeader(HttpField field) { @@ -138,6 +303,7 @@ public class HttpTester @Override public boolean messageComplete() { + _complete=true; return true; } @@ -323,26 +489,6 @@ public class HttpTester return _reason; } - public byte[] getContentBytes() - { - if (_content==null) - return null; - return _content.toByteArray(); - } - - public String getContent() - { - if (_content==null) - return null; - byte[] bytes=_content.toByteArray(); - - String content_type=get(HttpHeader.CONTENT_TYPE); - String encoding=MimeTypes.getCharsetFromContentType(content_type); - Charset charset=encoding==null?StandardCharsets.UTF_8:Charset.forName(encoding); - - return new String(bytes,charset); - } - @Override public MetaData.Response getInfo() { diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpTesterTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpTesterTest.java new file mode 100644 index 00000000000..a7feda20bb8 --- /dev/null +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpTesterTest.java @@ -0,0 +1,328 @@ +// +// ======================================================================== +// Copyright (c) 1995-2016 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.http; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.*; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import org.junit.Ignore; +import org.junit.Test; + +public class HttpTesterTest +{ + + @Test + @Ignore + public void testExampleUsage() throws Exception + { + try(Socket socket = new Socket("www.google.com",80)) + { + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("POST"); + request.setURI("/search"); + request.setVersion(HttpVersion.HTTP_1_0); + request.put(HttpHeader.HOST,"www.google.com"); + request.put("Content-Type","application/x-www-form-urlencoded"); + request.setContent("q=jetty%20server"); + ByteBuffer output = request.generate(); + + socket.getOutputStream().write(output.array(),output.arrayOffset()+output.position(),output.remaining()); + HttpTester.Input input = HttpTester.from(socket.getInputStream()); + HttpTester.Response response = HttpTester.parseResponse(input); + System.err.printf("%s %s %s%n",response.getVersion(),response.getStatus(),response.getReason()); + for (HttpField field:response) + System.err.printf("%s: %s%n",field.getName(),field.getValue()); + System.err.printf("%n%s%n",response.getContent()); + } + + } + + + @Test + public void testGetRequestBuffer10() + { + HttpTester.Request request =HttpTester.parseRequest( + "GET /uri HTTP/1.0\r\n"+ + "Host: localhost\r\n"+ + "Connection: keep-alive\r\n"+ + "\r\n"+ + "GET /some/other/request /HTTP/1.0\r\n"+ + "Host: localhost\r\n"+ + "\r\n" + ); + assertThat(request.getMethod(),is("GET")); + assertThat(request.getUri(),is("/uri")); + assertThat(request.getVersion(),is(HttpVersion.HTTP_1_0)); + assertThat(request.get(HttpHeader.HOST),is("localhost")); + assertThat(request.getContent(),is("")); + } + + @Test + public void testGetRequestBuffer11() + { + HttpTester.Request request =HttpTester.parseRequest( + "GET /uri HTTP/1.1\r\n"+ + "Host: localhost\r\n"+ + "\r\n"+ + "GET /some/other/request /HTTP/1.1\r\n"+ + "Host: localhost\r\n"+ + "\r\n" + ); + assertThat(request.getMethod(),is("GET")); + assertThat(request.getUri(),is("/uri")); + assertThat(request.getVersion(),is(HttpVersion.HTTP_1_1)); + assertThat(request.get(HttpHeader.HOST),is("localhost")); + assertThat(request.getContent(),is("")); + } + + + + @Test + public void testPostRequestBuffer10() + { + HttpTester.Request request =HttpTester.parseRequest( + "POST /uri HTTP/1.0\r\n"+ + "Host: localhost\r\n"+ + "Connection: keep-alive\r\n"+ + "Content-Length: 16\r\n"+ + "\r\n"+ + "0123456789ABCDEF"+ + "\r\n"+ + "GET /some/other/request /HTTP/1.0\r\n"+ + "Host: localhost\r\n"+ + "\r\n" + ); + assertThat(request.getMethod(),is("POST")); + assertThat(request.getUri(),is("/uri")); + assertThat(request.getVersion(),is(HttpVersion.HTTP_1_0)); + assertThat(request.get(HttpHeader.HOST),is("localhost")); + assertThat(request.getContent(),is("0123456789ABCDEF")); + + } + + @Test + public void testPostRequestBuffer11() + { + HttpTester.Request request =HttpTester.parseRequest( + "POST /uri HTTP/1.1\r\n"+ + "Host: localhost\r\n"+ + "Transfer-Encoding: chunked\r\n"+ + "\r\n"+ + "A\r\n"+ + "0123456789\r\n"+ + "6\r\n"+ + "ABCDEF\r\n"+ + "0\r\n"+ + "\r\n"+ + "GET /some/other/request /HTTP/1.1\r\n"+ + "Host: localhost\r\n"+ + "\r\n" + ); + assertThat(request.getMethod(),is("POST")); + assertThat(request.getUri(),is("/uri")); + assertThat(request.getVersion(),is(HttpVersion.HTTP_1_1)); + assertThat(request.get(HttpHeader.HOST),is("localhost")); + assertThat(request.getContent(),is("0123456789ABCDEF")); + + } + + + @Test + public void testResponseEOFBuffer() + { + HttpTester.Response response =HttpTester.parseResponse( + "HTTP/1.1 200 OK\r\n"+ + "Header: value\r\n"+ + "Connection: close\r\n"+ + "\r\n"+ + "0123456789ABCDEF" + ); + + assertThat(response.getVersion(),is(HttpVersion.HTTP_1_1)); + assertThat(response.getStatus(),is(200)); + assertThat(response.getReason(),is("OK")); + assertThat(response.get("Header"),is("value")); + assertThat(response.getContent(),is("0123456789ABCDEF")); + } + + @Test + public void testResponseLengthBuffer() + { + HttpTester.Response response =HttpTester.parseResponse( + "HTTP/1.1 200 OK\r\n"+ + "Header: value\r\n"+ + "Content-Length: 16\r\n"+ + "\r\n"+ + "0123456789ABCDEF"+ + "HTTP/1.1 200 OK\r\n"+ + "\r\n" + ); + + assertThat(response.getVersion(),is(HttpVersion.HTTP_1_1)); + assertThat(response.getStatus(),is(200)); + assertThat(response.getReason(),is("OK")); + assertThat(response.get("Header"),is("value")); + assertThat(response.getContent(),is("0123456789ABCDEF")); + } + + @Test + public void testResponseChunkedBuffer() + { + HttpTester.Response response =HttpTester.parseResponse( + "HTTP/1.1 200 OK\r\n"+ + "Header: value\r\n"+ + "Transfer-Encoding: chunked\r\n"+ + "\r\n"+ + "A\r\n"+ + "0123456789\r\n"+ + "6\r\n"+ + "ABCDEF\r\n"+ + "0\r\n"+ + "\r\n"+ + "HTTP/1.1 200 OK\r\n"+ + "\r\n" + ); + + assertThat(response.getVersion(),is(HttpVersion.HTTP_1_1)); + assertThat(response.getStatus(),is(200)); + assertThat(response.getReason(),is("OK")); + assertThat(response.get("Header"),is("value")); + assertThat(response.getContent(),is("0123456789ABCDEF")); + } + + @Test + public void testResponsesInput() throws Exception + { + ByteArrayInputStream stream = new ByteArrayInputStream(( + "HTTP/1.1 200 OK\r\n"+ + "Header: value\r\n"+ + "Transfer-Encoding: chunked\r\n"+ + "\r\n"+ + "A\r\n"+ + "0123456789\r\n"+ + "6\r\n"+ + "ABCDEF\r\n"+ + "0\r\n"+ + "\r\n"+ + "HTTP/1.1 400 OK\r\n"+ + "Next: response\r\n"+ + "Content-Length: 16\r\n"+ + "\r\n"+ + "0123456789ABCDEF").getBytes(StandardCharsets.ISO_8859_1) + ); + + HttpTester.Input in = HttpTester.from(stream); + + HttpTester.Response response = HttpTester.parseResponse(in); + + assertThat(response.getVersion(),is(HttpVersion.HTTP_1_1)); + assertThat(response.getStatus(),is(200)); + assertThat(response.getReason(),is("OK")); + assertThat(response.get("Header"),is("value")); + assertThat(response.getContent(),is("0123456789ABCDEF")); + + response = HttpTester.parseResponse(in); + assertThat(response.getVersion(),is(HttpVersion.HTTP_1_1)); + assertThat(response.getStatus(),is(400)); + assertThat(response.getReason(),is("OK")); + assertThat(response.get("Next"),is("response")); + assertThat(response.getContent(),is("0123456789ABCDEF")); + } + + @Test + public void testResponsesSplitInput() throws Exception + { + PipedOutputStream src = new PipedOutputStream(); + PipedInputStream stream = new PipedInputStream(src) + { + @Override + public synchronized int read(byte[] b, int off, int len) throws IOException + { + if (available()==0) + return 0; + return super.read(b,off,len); + } + }; + + HttpTester.Input in = HttpTester.from(stream); + + src.write(( + "HTTP/1.1 200 OK\r\n"+ + "Header: value\r\n"+ + "Transfer-Encoding: chunked\r\n"+ + "\r\n"+ + "A\r\n"+ + "0123456789\r\n"+ + "6\r\n"+ + "ABC" + ).getBytes(StandardCharsets.ISO_8859_1) + ); + + HttpTester.Response response = HttpTester.parseResponse(in); + assertThat(response,nullValue()); + src.write(( + "DEF\r\n"+ + "0\r\n"+ + "\r\n"+ + "HTTP/1.1 400 OK\r\n"+ + "Next: response\r\n"+ + "Content-Length: 16\r\n"+ + "\r\n"+ + "0123456789" + ).getBytes(StandardCharsets.ISO_8859_1) + ); + + + response = HttpTester.parseResponse(in); + assertThat(response.getVersion(),is(HttpVersion.HTTP_1_1)); + assertThat(response.getStatus(),is(200)); + assertThat(response.getReason(),is("OK")); + assertThat(response.get("Header"),is("value")); + assertThat(response.getContent(),is("0123456789ABCDEF")); + + response = HttpTester.parseResponse(in); + assertThat(response,nullValue()); + + src.write(( + "ABCDEF" + ).getBytes(StandardCharsets.ISO_8859_1) + ); + + response = HttpTester.parseResponse(in); + assertThat(response.getVersion(),is(HttpVersion.HTTP_1_1)); + assertThat(response.getStatus(),is(400)); + assertThat(response.getReason(),is("OK")); + assertThat(response.get("Next"),is("response")); + assertThat(response.getContent(),is("0123456789ABCDEF")); + } + + + + + +}