jetty-9 converted parser to consume all

This commit is contained in:
Greg Wilkins 2012-05-15 16:03:07 +02:00
parent 68f8e073db
commit 1c319703b9
11 changed files with 978 additions and 558 deletions

View File

@ -14,6 +14,10 @@
package org.eclipse.jetty.http;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.eclipse.jetty.util.StringMap;
import org.eclipse.jetty.util.StringUtil;
@ -114,7 +118,72 @@ public enum HttpHeader
if (header!=UNKNOWN)
CACHE.put(header.toString(),header);
}
/* ------------------------------------------------------------ */
private final static HttpHeader[] __hashed= new HttpHeader[4096];
private final static int __maxHashed;
static
{
// This hash function has been picked to have no collisions for
// the known header values. This allows a very quick lookup.
int max=0;
Map<Integer,HttpHeader> hashes=new HashMap<>();
for (HttpHeader header : HttpHeader.values())
{
String s=header.asString();
max=Math.max(max,s.length());
int h=0;
for (char c:s.toCharArray())
h = 31*h + ((c>='a')?(c-'a'+'A'):c);
int hash=h%__hashed.length;
if (hash<0)hash=-hash;
if (hashes.containsKey(hash))
{
// This should not happen with known headers.
System.err.println("Duplicate hash "+header+" "+hashes.get(hash));
System.exit(1);
}
hashes.put(hash,header);
__hashed[hash]=header;
}
__maxHashed=max;
}
public static HttpHeader lookAheadGet(byte[] bytes, int position, int limit)
{
int h=0;
byte b=0;
limit=Math.min(position+__maxHashed,limit);
for (int i=position;i<limit;i++)
{
b=bytes[i];
if (b==':'||b==' ')
break;
h= 31*h+ ((b>='a')?(b-'a'+'A'):b);
}
if (b!=':'&&b!=' ')
return null;
int hash=h%__hashed.length;
if (hash<0)hash=-hash;
HttpHeader header=__hashed[hash];
if (header!=null)
{
String s=header.asString();
for (int i=s.length();i-->0;)
{
b=bytes[position+i];
char c=s.charAt(i);
if (c!=b && Character.toUpperCase(c)!=(b>='a'?(b-'a'+'A'):b))
return null;
}
}
return header;
}
private final String _string;
private final byte[] _bytes;
private final byte[] _bytesColonSpace;

View File

@ -63,6 +63,12 @@ public enum HttpHeaderValue
return _buffer.asReadOnlyBuffer();
}
/* ------------------------------------------------------------ */
public String asString()
{
return _string;
}
/* ------------------------------------------------------------ */
@Override
public String toString()

View File

@ -34,6 +34,81 @@ public enum HttpMethod
CONNECT,
MOVE;
/* ------------------------------------------------------------ */
/**
* Optimised lookup to find a method name and trailing space in a byte array.
* @param bytes Array containing ISO-8859-1 characters
* @param position The first valid index
* @param limit The first non valid index
* @return A HttpMethod if a match or null if no easy match.
*/
public static HttpMethod lookAheadGet(byte[] bytes, int position, int limit)
{
int length=limit-position;
if (length<4)
return null;
switch(bytes[position])
{
case 'G':
if (bytes[position+1]=='E' && bytes[position+2]=='T' && bytes[position+3]==' ')
return GET;
break;
case 'P':
if (bytes[position+1]=='O' && bytes[position+2]=='S' && bytes[position+3]=='T' && length>=5 && bytes[position+4]==' ')
return POST;
if (bytes[position+1]=='U' && bytes[position+2]=='T' && bytes[position+3]==' ')
return PUT;
break;
case 'H':
if (bytes[position+1]=='E' && bytes[position+2]=='A' && bytes[position+3]=='D' && length>=5 && bytes[position+4]==' ')
return HEAD;
break;
case 'O':
if (bytes[position+1]=='O' && bytes[position+2]=='T' && bytes[position+3]=='I' && length>=8 &&
bytes[position+4]=='O' && bytes[position+5]=='N' && bytes[position+6]=='S' && bytes[position+7]==' ' )
return OPTIONS;
break;
case 'D':
if (bytes[position+1]=='E' && bytes[position+2]=='L' && bytes[position+3]=='E' && length>=7 &&
bytes[position+4]=='T' && bytes[position+5]=='E' && bytes[position+6]==' ' )
return DELETE;
break;
case 'T':
if (bytes[position+1]=='R' && bytes[position+2]=='A' && bytes[position+3]=='C' && length>=6 &&
bytes[position+4]=='E' && bytes[position+5]==' ' )
return TRACE;
break;
case 'C':
if (bytes[position+1]=='O' && bytes[position+2]=='N' && bytes[position+3]=='N' && length>=8 &&
bytes[position+4]=='E' && bytes[position+5]=='C' && bytes[position+6]=='T' && bytes[position+7]==' ' )
return CONNECT;
break;
case 'M':
if (bytes[position+1]=='O' && bytes[position+2]=='V' && bytes[position+3]=='E' && bytes[position+4]==' ')
return MOVE;
break;
default:
break;
}
return null;
}
/* ------------------------------------------------------------ */
/**
* Optimised lookup to find a method name and trailing space in a byte array.
* @param bytes Array containing ISO-8859-1 characters
* @param position The first valid index
* @param limit The first non valid index
* @return A HttpMethod if a match or null if no easy match.
*/
public static HttpMethod lookAheadGet(ByteBuffer buffer)
{
if (buffer.hasArray())
return lookAheadGet(buffer.array(),buffer.arrayOffset()+buffer.position(),buffer.arrayOffset()+buffer.limit());
return null;
}
/* ------------------------------------------------------------ */
public final static StringMap<HttpMethod> CACHE= new StringMap<HttpMethod>(true);
static

File diff suppressed because it is too large Load Diff

View File

@ -24,7 +24,8 @@ public enum HttpVersion
{
HTTP_0_9("HTTP/0.9",9),
HTTP_1_0("HTTP/1.0",10),
HTTP_1_1("HTTP/1.1",11);
HTTP_1_1("HTTP/1.1",11),
HTTP_2_0("HTTP/2.0",20);
/* ------------------------------------------------------------ */
public final static StringMap<HttpVersion> CACHE= new StringMap<HttpVersion>(true);
@ -34,6 +35,64 @@ public enum HttpVersion
CACHE.put(version.toString(),version);
}
/* ------------------------------------------------------------ */
/**
* Optimised lookup to find a Http Version and whitespace in a byte array.
* @param bytes Array containing ISO-8859-1 characters
* @param position The first valid index
* @param limit The first non valid index
* @return A HttpMethod if a match or null if no easy match.
*/
public static HttpVersion lookAheadGet(byte[] bytes, int position, int limit)
{
int length=limit-position;
if (length<9)
return null;
if (bytes[position+4]=='/' && bytes[position+6]=='.' && Character.isWhitespace((char)bytes[position+8]) &&
((bytes[position]=='H' && bytes[position+1]=='T' && bytes[position+2]=='T' && bytes[position+3]=='P') ||
(bytes[position]=='h' && bytes[position+1]=='t' && bytes[position+2]=='t' && bytes[position+3]=='p')))
{
switch(bytes[position+5])
{
case '1':
switch(bytes[position+7])
{
case '0':
return HTTP_1_0;
case '1':
return HTTP_1_1;
}
break;
case '2':
switch(bytes[position+7])
{
case '0':
return HTTP_2_0;
}
break;
}
}
return null;
}
/* ------------------------------------------------------------ */
/**
* Optimised lookup to find a HTTP Version and trailing white space in a byte array.
* @param bytes Array containing ISO-8859-1 characters
* @param position The first valid index
* @param limit The first non valid index
* @return A HttpVersion if a match or null if no easy match.
*/
public static HttpVersion lookAheadGet(ByteBuffer buffer)
{
if (buffer.hasArray())
return lookAheadGet(buffer.array(),buffer.arrayOffset()+buffer.position(),buffer.arrayOffset()+buffer.limit());
return null;
}
private final String _string;
private final byte[] _bytes;
private final ByteBuffer _buffer;
@ -72,6 +131,12 @@ public enum HttpVersion
return _string.equalsIgnoreCase(s);
}
/* ------------------------------------------------------------ */
public String asString()
{
return _string;
}
/* ------------------------------------------------------------ */
@Override
public String toString()

View File

@ -23,7 +23,6 @@ import static org.junit.matchers.JUnitMatchers.containsString;
import static org.junit.matchers.JUnitMatchers.either;
import java.io.IOException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
@ -81,7 +80,7 @@ public class HttpGeneratorServerTest
}
@Override
public boolean startResponse(String version, int status, String reason) throws IOException
public boolean startResponse(HttpVersion version, int status, String reason) throws IOException
{
_version=version;
_status=status;
@ -273,7 +272,7 @@ public class HttpGeneratorServerTest
private int _status;
private final List<String> _val=new ArrayList<>();
private String _version;
private HttpVersion _version;
private final TR[] tr =
{
@ -309,15 +308,15 @@ public class HttpGeneratorServerTest
for (int c=0;c<(v==11?connect.length:(connect.length-1));c++)
{
String t="v="+v+",chunks="+chunks+",connect="+connect[c]+",tr="+r+"="+tr[r];
System.err.println("\n==========================================");
System.err.println(t);
// System.err.println("\n==========================================");
// System.err.println(t);
gen.reset();
tr[r].getHttpFields().clear();
String response=tr[r].build(v,gen,"OK\r\nTest",connect[c],null,chunks);
System.err.println("---\n"+t+"\n"+response+(gen.isPersistent()?"...":"==="));
// System.err.println("---\n"+t+"\n"+response+(gen.isPersistent()?"...":"==="));
if (v==9)
{

View File

@ -120,12 +120,13 @@ public class HttpParserTest
"GET / HTTP/1.0\015\012" +
"Host: localhost\015\012" +
"Header1: value1\015\012" +
"Header2 : value 2a \015\012" +
"Header 2 : value 2a \015\012" +
" value 2b \015\012" +
"Header3: \015\012" +
"Header4 \015\012" +
" value4\015\012" +
"Server5: notServer\015\012" +
"Server5 : notServer\015\012" +
"Host Header: notHost\015\012" +
"\015\012");
Handler handler = new Handler();
HttpParser parser= new HttpParser((HttpParser.RequestHandler)handler);
@ -138,7 +139,7 @@ public class HttpParserTest
assertEquals("localhost", val[0]);
assertEquals("Header1", hdr[1]);
assertEquals("value1", val[1]);
assertEquals("Header2", hdr[2]);
assertEquals("Header 2", hdr[2]);
assertEquals("value 2a value 2b", val[2]);
assertEquals("Header3", hdr[3]);
assertEquals(null, val[3]);
@ -146,23 +147,25 @@ public class HttpParserTest
assertEquals("value4", val[4]);
assertEquals("Server5", hdr[5]);
assertEquals("notServer", val[5]);
assertEquals(5, h);
assertEquals("Host Header", hdr[6]);
assertEquals("notHost", val[6]);
assertEquals(6, h);
}
@Test
public void testSplitHeaderParse() throws Exception
{
ByteBuffer buffer= BufferUtil.toBuffer(
"XXXXGET / HTTP/1.0\015\012" +
"Host: localhost\015\012" +
"Header1: value1\015\012" +
"Header2 : value 2a \015\012" +
" value 2b \015\012" +
"Header3: \015\012" +
"Header4 \015\012" +
" value4\015\012" +
"Server5: notServer\015\012" +
"\015\012ZZZZ");
"XXXXSPLIT / HTTP/1.0\015\012" +
"Host: localhost\015\012" +
"Header1: value1\015\012" +
"Header2 : value 2a \015\012" +
" value 2b \015\012" +
"Header3: \015\012" +
"Header4 \015\012" +
" value4\015\012" +
"Server5: notServer\015\012" +
"\015\012ZZZZ");
buffer.position(2);
buffer.limit(buffer.capacity()-2);
buffer=buffer.slice();
@ -177,11 +180,15 @@ public class HttpParserTest
if (!parser.parseNext(buffer))
{
// consumed all
assertEquals(0,buffer.remaining());
// parse the rest
buffer.limit(buffer.capacity()-2);
parser.parseNext(buffer);
}
assertEquals("GET", f0);
assertEquals("SPLIT", f0);
assertEquals("/", f1);
assertEquals("HTTP/1.0", f2);
assertEquals("Host", hdr[0]);
@ -478,7 +485,7 @@ public class HttpParserTest
}
@Override
public boolean startRequest(String method, String uri, String version)
public boolean startRequest(HttpMethod httpMethod, String method, String uri, HttpVersion version)
{
request=true;
h= -1;
@ -486,7 +493,7 @@ public class HttpParserTest
val= new String[9];
f0= method;
f1= uri;
f2= version;
f2= version==null?null:version.asString();
fields=new HttpFields();
messageCompleted = false;
@ -530,10 +537,10 @@ public class HttpParserTest
}
@Override
public boolean startResponse(String version, int status, String reason)
public boolean startResponse(HttpVersion version, int status, String reason)
{
request=false;
f0 = version.toString();
f0 = version.asString();
f1 = Integer.toString(status);
f2 = reason==null?null:reason.toString();

View File

@ -297,6 +297,8 @@ public abstract class HttpChannel
_responseFields.clear();
_response.recycle();
_uri.clear();
if (_out!=null)
_out.reset();
synchronized (_inputQ)
{
_inputEOF=false;
@ -437,8 +439,13 @@ public abstract class HttpChannel
_response.setStatus(status,reason);
if (close)
_responseFields.add(HttpHeader.CONNECTION,HttpHeaderValue.CLOSE);
ByteBuffer buffer=BufferUtil.toBuffer(content,StringUtil.__UTF8_CHARSET);
_response.setContentLength(buffer.remaining());
ByteBuffer buffer=null;
if (content!=null)
{
buffer=BufferUtil.toBuffer(content,StringUtil.__UTF8_CHARSET);
_response.setContentLength(buffer.remaining());
}
HttpGenerator.ResponseInfo info = _handler.commit();
commit(info,buffer);
@ -555,7 +562,7 @@ public abstract class HttpChannel
private class RequestHandler implements EventHandler
{
@Override
public boolean startRequest(String method, String uri, String version) throws IOException
public boolean startRequest(HttpMethod httpMethod,String method, String uri, HttpVersion version) throws IOException
{
_host = false;
_expect = false;
@ -564,30 +571,18 @@ public abstract class HttpChannel
if(_request.getTimeStamp()==0)
_request.setTimeStamp(System.currentTimeMillis());
HttpMethod m = HttpMethod.CACHE.get(method);
_request.setMethod(m,method);
_request.setMethod(httpMethod,method);
try
{
if (m==HttpMethod.CONNECT)
if (httpMethod==HttpMethod.CONNECT)
_uri.parseConnect(uri);
else
_uri.parse(uri);
_request.setUri(_uri);
if (version==null)
{
_request.setHttpVersion(HttpVersion.HTTP_0_9);
_version=HttpVersion.HTTP_0_9;
}
else
{
_version= HttpVersion.CACHE.get(version);
if (_version==null)
throw new HttpException(HttpStatus.BAD_REQUEST_400,null);
_request.setHttpVersion(_version);
}
_version=version==null?HttpVersion.HTTP_0_9:version;
_request.setHttpVersion(_version);
}
catch (Exception e)
{
@ -713,7 +708,10 @@ public abstract class HttpChannel
@Override
public boolean content(ByteBuffer ref) throws IOException
{
_inputQ.add(ref);
synchronized (_inputQ.lock())
{
_inputQ.add(ref);
}
return true;
}
@ -826,9 +824,7 @@ public abstract class HttpChannel
public abstract HttpConnector getHttpConnector();
protected abstract void blockForContent() throws IOException;
protected abstract void contentConsumed();
protected abstract int write(ByteBuffer content) throws IOException;
protected abstract void commit(ResponseInfo info, ByteBuffer content) throws IOException;

View File

@ -200,8 +200,15 @@ public class HttpConnection extends AbstractAsyncConnection
?_bufferPool.acquire(_connector.getRequestBufferSize(),false)
:_bufferPool.acquire(_connector.getRequestHeaderSize(),false);
int filled=getEndPoint().fill(_requestBuffer);
LOG.debug("{} filled {}",this,filled);
// Only fill if buffer is fully consumed
if (BufferUtil.isEmpty(_requestBuffer))
{
// TODO this is still dangerous as a suspended request might have references
// to the buffer still for unconsumed input. We need a callback to say
// all content is consumed and we can read again.
int filled=getEndPoint().fill(_requestBuffer);
LOG.debug("{} filled {}",this,filled);
}
// If we parse to an event, call the connection
if (BufferUtil.hasContent(_requestBuffer) && _parser.parseNext(_requestBuffer))
@ -220,6 +227,9 @@ public class HttpConnection extends AbstractAsyncConnection
getEndPoint().setCheckForIdle(true);
}
}
else if (BufferUtil.hasContent(_requestBuffer))
throw new IllegalStateException("parser should consume all content OR return true");
}
catch (HttpException e)
@ -565,6 +575,7 @@ public class HttpConnection extends AbstractAsyncConnection
protected void blockForContent() throws IOException
{
// While progress and the connection has not changed
boolean parsed_event=false;
while (getEndPoint().isOpen())
{
try
@ -578,8 +589,13 @@ public class HttpConnection extends AbstractAsyncConnection
if (_requestBuffer==null)
_requestBuffer=_bufferPool.acquire(_connector.getRequestBufferSize(),false);
int filled=getEndPoint().fill(_requestBuffer);
LOG.debug("{} filled {}",this,filled);
// If we parse to an event, return
if (BufferUtil.hasContent(_requestBuffer) && _parser.parseNext(_requestBuffer))
while (BufferUtil.hasContent(_requestBuffer) && _parser.inContentState())
parsed_event|=_parser.parseNext(_requestBuffer);
if (parsed_event)
return;
}
catch (InterruptedException e)
@ -603,13 +619,6 @@ public class HttpConnection extends AbstractAsyncConnection
}
}
@Override
protected void contentConsumed()
{
// TODO Auto-generated method stub
}
@Override
protected void commit(ResponseInfo info, ByteBuffer content) throws IOException
{

View File

@ -71,7 +71,6 @@ public class StringMap<O> extends AbstractMap<String,O>
s1=o1.toString();
String s2=(String)o2;
int n1 = s1==null?b1.remaining():s1.length();
int n2 = s2.length();
int min = Math.min(n1, n2);
@ -79,7 +78,6 @@ public class StringMap<O> extends AbstractMap<String,O>
char c1 = s1==null?(char)b1.get(b1.position()+i):s1.charAt(i);
char c2 = s2.charAt(i);
if (c1 != c2) {
if (ignoreCase)
{
c1 = Character.toUpperCase(c1);
@ -98,11 +96,8 @@ public class StringMap<O> extends AbstractMap<String,O>
}
}
return n1 - n2;
}
});
}
/* ------------------------------------------------------------ */
@ -138,17 +133,11 @@ public class StringMap<O> extends AbstractMap<String,O>
}
/* ------------------------------------------------------------ */
public O get(ByteBuffer buffer, int position, int length)
public O get(ByteBuffer buffer)
{
ByteBuffer ro=buffer.asReadOnlyBuffer();
ro.limit(ro.capacity());
ro.position(position);
ro.limit(position+length);
return _map.get(ro);
return _map.get(buffer);
}
/* ------------------------------------------------------------ */
@Override
public O remove(Object key)
@ -156,7 +145,6 @@ public class StringMap<O> extends AbstractMap<String,O>
return _map.remove(key);
}
/* ------------------------------------------------------------ */
public O remove(String key)
{

View File

@ -88,7 +88,11 @@ public class StringUtil
*/
public static String normalizeCharset(ByteBuffer b,int position,int length)
{
String n=CHARSETS.get(b,position,length);
ByteBuffer ro=b.asReadOnlyBuffer();
ro.limit(ro.capacity());
ro.position(position);
ro.limit(position+length);
String n=CHARSETS.get(ro);
if (n!=null)
return n;
ByteBuffer slice = b.slice();