Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #5575 SEARCH method #5576

Merged
merged 10 commits into from Nov 11, 2020
261 changes: 139 additions & 122 deletions jetty-http/src/main/java/org/eclipse/jetty/http/HttpMethod.java
Expand Up @@ -26,162 +26,179 @@
import org.eclipse.jetty.util.Trie;

/**
*
* Known HTTP Methods
*/
public enum HttpMethod
{
GET,
POST,
HEAD,
PUT,
OPTIONS,
DELETE,
TRACE,
CONNECT,
MOVE,
PROXY,
PRI;

/**
* Optimized 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 An HttpMethod if a match or null if no easy match.
*/
public static HttpMethod lookAheadGet(byte[] bytes, final int position, int limit)
// From https://www.iana.org/assignments/http-methods/http-methods.xhtml
ACL("ACL", Type.IDEMPOTENT),
BASELINE_CONTROL("BASELINE-CONTROL", Type.IDEMPOTENT),
BIND("BIND", Type.IDEMPOTENT),
CHECKIN("CHECKIN", Type.IDEMPOTENT),
CHECKOUT("CHECKOUT", Type.IDEMPOTENT),
CONNECT("CONNECT", Type.NORMAL),
COPY("COPY", Type.IDEMPOTENT),
DELETE("DELETE", Type.IDEMPOTENT),
GET("GET", Type.SAFE),
HEAD("HEAD", Type.SAFE),
LABEL("LABEL", Type.IDEMPOTENT),
LINK("LINK", Type.IDEMPOTENT),
LOCK("LOCK", Type.NORMAL),
MERGE("MERGE", Type.IDEMPOTENT),
MKACTIVITY("MKACTIVITY", Type.IDEMPOTENT),
MKCALENDAR("MKCALENDAR", Type.IDEMPOTENT),
MKCOL("MKCOL", Type.IDEMPOTENT),
MKREDIRECTREF("MKREDIRECTREF", Type.IDEMPOTENT),
MKWORKSPACE("MKWORKSPACE", Type.IDEMPOTENT),
MOVE("MOVE", Type.IDEMPOTENT),
OPTIONS("OPTIONS", Type.SAFE),
ORDERPATCH("ORDERPATCH", Type.IDEMPOTENT),
PATCH("PATCH", Type.NORMAL),
POST("POST", Type.NORMAL),
PRI("PRI", Type.SAFE),
PROPFIND("PROPFIND", Type.SAFE),
PROPPATCH("PROPPATCH", Type.IDEMPOTENT),
PUT("PUT", Type.IDEMPOTENT),
REBIND("REBIND", Type.IDEMPOTENT),
REPORT("REPORT", Type.SAFE),
SEARCH("SEARCH", Type.SAFE),
TRACE("TRACE", Type.SAFE),
UNBIND("UNBIND", Type.IDEMPOTENT),
UNCHECKOUT("UNCHECKOUT", Type.IDEMPOTENT),
UNLINK("UNLINK", Type.IDEMPOTENT),
UNLOCK("UNLOCK", Type.IDEMPOTENT),
UPDATE("UPDATE", Type.IDEMPOTENT),
UPDATEREDIRECTREF("UPDATEREDIRECTREF", Type.IDEMPOTENT),
VERSION_CONTROL("VERSION-CONTROL", Type.IDEMPOTENT),

// Other methods
PROXY("PROXY", Type.NORMAL);

// The type of the method
private enum Type
{
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] == 'R' && bytes[position + 2] == 'O' && bytes[position + 3] == 'X' && length >= 6 && bytes[position + 4] == 'Y' && bytes[position + 5] == ' ')
return PROXY;
if (bytes[position + 1] == 'U' && bytes[position + 2] == 'T' && bytes[position + 3] == ' ')
return PUT;
if (bytes[position + 1] == 'R' && bytes[position + 2] == 'I' && bytes[position + 3] == ' ')
return PRI;
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] == 'P' && 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' && length >= 5 && bytes[position + 4] == ' ')
return MOVE;
break;

default:
break;
}
return null;
NORMAL,
IDEMPOTENT,
SAFE
}

private final String _method;
private final byte[] _bytes;
private final ByteBuffer _buffer;
private final Type _type;

/**
* Optimized lookup to find a method name and trailing space in a byte array.
*
* @param buffer buffer containing ISO-8859-1 characters, it is not modified.
* @return An HttpMethod if a match or null if no easy match.
*/
public static HttpMethod lookAheadGet(ByteBuffer buffer)
HttpMethod(String method, Type type)
{
if (buffer.hasArray())
return lookAheadGet(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.arrayOffset() + buffer.limit());

int l = buffer.remaining();
if (l >= 4)
{
HttpMethod m = CACHE.getBest(buffer, 0, l);
if (m != null)
{
int ml = m.asString().length();
if (l > ml && buffer.get(buffer.position() + ml) == ' ')
return m;
}
}
return null;
_method = method;
gregw marked this conversation as resolved.
Show resolved Hide resolved
_type = type;
_bytes = StringUtil.getBytes(_method);
_buffer = ByteBuffer.wrap(_bytes);
}

public static final Trie<HttpMethod> INSENSITIVE_CACHE = new ArrayTrie<>();
public byte[] getBytes()
{
return _bytes;
}

static
public boolean is(String s)
{
for (HttpMethod method : HttpMethod.values())
{
INSENSITIVE_CACHE.put(method.toString(), method);
}
return toString().equalsIgnoreCase(s);
}

public static final Trie<HttpMethod> CACHE = new ArrayTernaryTrie<>(false);
/**
* An HTTP method is safe if it doesn't alter the state of the server.
* In other words, a method is safe if it leads to a read-only operation.
* Several common HTTP methods are safe: GET , HEAD , or OPTIONS .
* All safe methods are also idempotent, but not all idempotent methods are safe
* @return if the method is safe.
*/
public boolean isSafe()
gregw marked this conversation as resolved.
Show resolved Hide resolved
{
return _type == Type.SAFE;
}

static
/**
* An idempotent HTTP method is an HTTP method that can be called many times without different outcomes.
* It would not matter if the method is called only once, or ten times over. The result should be the same.
* @return true if the method is idempotent.
*/
public boolean isIdempotent()
gregw marked this conversation as resolved.
Show resolved Hide resolved
{
for (HttpMethod method : HttpMethod.values())
{
CACHE.put(method.toString(), method);
}
return _type.ordinal() >= Type.IDEMPOTENT.ordinal();
}

private final ByteBuffer _buffer;
private final byte[] _bytes;
public ByteBuffer asBuffer()
{
return _buffer.asReadOnlyBuffer();
}

HttpMethod()
public String asString()
{
_bytes = StringUtil.getBytes(toString());
_buffer = ByteBuffer.wrap(_bytes);
return _method;
}

public byte[] getBytes()
public String toString()
{
return _bytes;
return _method;
}

public boolean is(String s)
public static final Trie<HttpMethod> INSENSITIVE_CACHE = new ArrayTrie<>(252);
public static final Trie<HttpMethod> CACHE = new ArrayTernaryTrie<>(false, 300);
gregw marked this conversation as resolved.
Show resolved Hide resolved
public static final Trie<HttpMethod> LOOK_AHEAD = new ArrayTernaryTrie<>(false, 330);
static
{
return toString().equalsIgnoreCase(s);
for (HttpMethod method : HttpMethod.values())
{
if (!INSENSITIVE_CACHE.put(method.asString(), method))
throw new IllegalStateException("INSENSITIVE_CACHE too small: " + method);

if (!CACHE.put(method.asString(), method))
throw new IllegalStateException("CACHE too small: " + method);

if (!LOOK_AHEAD.put(method.asString() + ' ', method))
throw new IllegalStateException("LOOK_AHEAD too small: " + method);
}
}

public ByteBuffer asBuffer()
/**
* Optimized 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 An HttpMethod if a match or null if no easy match.
*/
public static HttpMethod lookAheadGet(byte[] bytes, final int position, int limit)
{
return _buffer.asReadOnlyBuffer();
int len = limit - position;
if (limit > 3)
{
// Short cut for GET
if (bytes[position] == 'G' && bytes[position + 1] == 'E' && bytes[position + 2] == 'T' && bytes[position + 3] == ' ')
return GET;
// Otherwise lookup in the Trie
return LOOK_AHEAD.getBest(bytes, position, len);
}
return null;
}

public String asString()
/**
* Optimized lookup to find a method name and trailing space in a byte array.
*
* @param buffer buffer containing ISO-8859-1 characters, it is not modified.
* @return An HttpMethod if a match or null if no easy match.
* @deprecated Not used
gregw marked this conversation as resolved.
Show resolved Hide resolved
*/
@Deprecated
public static HttpMethod lookAheadGet(ByteBuffer buffer)
{
return toString();
return LOOK_AHEAD.getBest(buffer, 0, buffer.remaining());
gregw marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Converts the given String parameter to an HttpMethod
* Converts the given String parameter to an HttpMethod.
* The string may differ from the Enum name as a '-' in the method
* name is represented as a '_' in the Enum name.
*
* @param method the String to get the equivalent HttpMethod from
* @return the HttpMethod or null if the parameter method is unknown
Expand Down
Expand Up @@ -85,12 +85,20 @@ public static void parseAll(HttpParser parser, ByteBuffer buffer)
@Test
public void httpMethodTest()
{
assertNull(HttpMethod.lookAheadGet(BufferUtil.toBuffer("Wibble ")));
assertNull(HttpMethod.lookAheadGet(BufferUtil.toBuffer("GET")));
assertNull(HttpMethod.lookAheadGet(BufferUtil.toBuffer("MO")));

assertEquals(HttpMethod.GET, HttpMethod.lookAheadGet(BufferUtil.toBuffer("GET ")));
assertEquals(HttpMethod.MOVE, HttpMethod.lookAheadGet(BufferUtil.toBuffer("MOVE ")));
for (HttpMethod m : HttpMethod.values())
{
assertNull(HttpMethod.lookAheadGet(BufferUtil.toBuffer(m.asString().substring(0,2))));
assertNull(HttpMethod.lookAheadGet(BufferUtil.toBuffer(m.asString())));
assertNull(HttpMethod.lookAheadGet(BufferUtil.toBuffer(m.asString() + "FOO")));
assertEquals(m, HttpMethod.lookAheadGet(BufferUtil.toBuffer(m.asString() + " ")));
assertEquals(m, HttpMethod.lookAheadGet(BufferUtil.toBuffer(m.asString() + " /foo/bar")));

assertNull(HttpMethod.lookAheadGet(m.asString().substring(0,2).getBytes(), 0,2));
assertNull(HttpMethod.lookAheadGet(m.asString().getBytes(), 0, m.asString().length()));
assertNull(HttpMethod.lookAheadGet((m.asString() + "FOO").getBytes(), 0, m.asString().length() + 3));
assertEquals(m, HttpMethod.lookAheadGet(("\n" + m.asString() + " ").getBytes(), 1, m.asString().length() + 2));
assertEquals(m, HttpMethod.lookAheadGet(("\n" + m.asString() + " /foo").getBytes(), 1, m.asString().length() + 6));
}

ByteBuffer b = BufferUtil.allocateDirect(128);
BufferUtil.append(b, BufferUtil.toBuffer("GET"));
Expand Down
Expand Up @@ -368,6 +368,12 @@ public V getBest(ByteBuffer b, int offset, int len)
return getBest(0, b, offset, len);
}

@Override
public V getBest(byte[] b, int offset, int len)
{
return getBest(0, b, offset, len);
}

private V getBest(int t, byte[] b, int offset, int len)
{
int node = t;
Expand Down