Merge pull request #2736 from eclipse/jetty-9.4.x-2679-h2spec_compliance

Jetty 9.4.x 2679 h2spec compliance
This commit is contained in:
Simone Bordet 2018-07-21 17:26:14 +02:00 committed by GitHub
commit af880f119a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1834 additions and 474 deletions

View File

@ -164,7 +164,8 @@ public class HttpURI
_host=host;
_port=port;
parse(State.PATH,pathQuery,0,pathQuery.length());
if (pathQuery!=null)
parse(State.PATH,pathQuery,0,pathQuery.length());
}

View File

@ -163,17 +163,26 @@ public class MetaData implements Iterable<HttpField>
public Request(String method, HttpScheme scheme, HostPortHttpField hostPort, String uri, HttpVersion version, HttpFields fields)
{
this(method, new HttpURI(scheme == null ? null : scheme.asString(), hostPort.getHost(), hostPort.getPort(), uri), version, fields);
this(method, new HttpURI(scheme == null ? null : scheme.asString(),
hostPort==null?null:hostPort.getHost(),
hostPort==null?-1:hostPort.getPort(),
uri), version, fields);
}
public Request(String method, HttpScheme scheme, HostPortHttpField hostPort, String uri, HttpVersion version, HttpFields fields, long contentLength)
{
this(method, new HttpURI(scheme == null ? null : scheme.asString(), hostPort.getHost(), hostPort.getPort(), uri), version, fields, contentLength);
this(method, new HttpURI(scheme==null?null:scheme.asString(),
hostPort==null?null:hostPort.getHost(),
hostPort==null?-1:hostPort.getPort(),
uri), version, fields, contentLength);
}
public Request(String method, String scheme, HostPortHttpField hostPort, String uri, HttpVersion version, HttpFields fields, long contentLength)
{
this(method, new HttpURI(scheme, hostPort.getHost(), hostPort.getPort(), uri), version, fields, contentLength);
this(method, new HttpURI(scheme,
hostPort==null?null:hostPort.getHost(),
hostPort==null?-1:hostPort.getPort(),
uri), version, fields, contentLength);
}
public Request(Request request)

View File

@ -34,6 +34,7 @@ import org.eclipse.jetty.alpn.client.ALPNClientConnectionFactory;
import org.eclipse.jetty.http2.BufferingFlowControlStrategy;
import org.eclipse.jetty.http2.FlowControlStrategy;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.ClientConnectionFactory;
@ -130,6 +131,7 @@ public class HTTP2Client extends ContainerLifeCycle
private List<String> protocols = Arrays.asList("h2", "h2-17", "h2-16", "h2-15", "h2-14");
private int initialSessionRecvWindow = 16 * 1024 * 1024;
private int initialStreamRecvWindow = 8 * 1024 * 1024;
private int maxFrameLength = Frame.DEFAULT_MAX_LENGTH;
private int maxConcurrentPushedStreams = 32;
private int maxSettingsKeys = SettingsFrame.DEFAULT_MAX_KEYS;
private FlowControlStrategy.Factory flowControlStrategyFactory = () -> new BufferingFlowControlStrategy(0.5F);
@ -337,6 +339,17 @@ public class HTTP2Client extends ContainerLifeCycle
this.initialStreamRecvWindow = initialStreamRecvWindow;
}
@ManagedAttribute("The max frame length in bytes")
public int getMaxFrameLength()
{
return maxFrameLength;
}
public void setMaxFrameLength(int maxFrameLength)
{
this.maxFrameLength = maxFrameLength;
}
@ManagedAttribute("The max number of concurrent pushed streams")
public int getMaxConcurrentPushedStreams()
{

View File

@ -68,6 +68,7 @@ public class HTTP2ClientConnectionFactory implements ClientConnectionFactory
session.setMaxRemoteStreams(client.getMaxConcurrentPushedStreams());
Parser parser = new Parser(byteBufferPool, session, 4096, 8192);
parser.setMaxFrameLength(client.getMaxFrameLength());
parser.setMaxSettingsKeys(client.getMaxSettingsKeys());
HTTP2ClientConnection connection = new HTTP2ClientConnection(client, byteBufferPool, executor, endPoint,
@ -113,6 +114,10 @@ public class HTTP2ClientConnectionFactory implements ClientConnectionFactory
settings.computeIfAbsent(SettingsFrame.INITIAL_WINDOW_SIZE, k -> client.getInitialStreamRecvWindow());
settings.computeIfAbsent(SettingsFrame.MAX_CONCURRENT_STREAMS, k -> client.getMaxConcurrentPushedStreams());
Integer maxFrameLength = settings.get(SettingsFrame.MAX_FRAME_SIZE);
if (maxFrameLength != null)
getParser().setMaxFrameLength(maxFrameLength);
PrefaceFrame prefaceFrame = new PrefaceFrame();
SettingsFrame settingsFrame = new SettingsFrame(settings, false);

View File

@ -78,6 +78,7 @@ public class HTTP2ClientSession extends HTTP2Session
if (LOG.isDebugEnabled())
LOG.debug("Received {}", frame);
// HEADERS can be received for normal and pushed responses.
int streamId = frame.getStreamId();
IStream stream = getStream(streamId);
if (stream != null)
@ -96,7 +97,23 @@ public class HTTP2ClientSession extends HTTP2Session
else
{
if (LOG.isDebugEnabled())
LOG.debug("Ignoring {}, stream #{} not found", frame, streamId);
LOG.debug("Stream #{} not found", streamId);
if (isClientStream(streamId))
{
// Normal stream.
// Headers or trailers arriving after
// the stream has been reset are ignored.
if (!isLocalStreamClosed(streamId))
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_headers_frame");
}
else
{
// Pushed stream.
// Headers or trailers arriving after
// the stream has been reset are ignored.
if (!isRemoteStreamClosed(streamId))
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_headers_frame");
}
}
}
@ -117,9 +134,12 @@ public class HTTP2ClientSession extends HTTP2Session
else
{
IStream pushStream = createRemoteStream(pushStreamId);
pushStream.process(frame, Callback.NOOP);
Stream.Listener listener = notifyPush(stream, pushStream, frame);
pushStream.setListener(listener);
if (pushStream != null)
{
pushStream.process(frame, Callback.NOOP);
Stream.Listener listener = notifyPush(stream, pushStream, frame);
pushStream.setListener(listener);
}
}
}

View File

@ -734,7 +734,21 @@ public abstract class FlowControlStrategyTest
public void testClientExceedingSessionWindow() throws Exception
{
// On server, we don't consume the data.
start(new ServerSessionListener.Adapter());
start(new ServerSessionListener.Adapter()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
return new Stream.Listener.Adapter()
{
@Override
public void onData(Stream stream, DataFrame frame, Callback callback)
{
// Do not succeed the callback.
}
};
}
});
final CountDownLatch closeLatch = new CountDownLatch(1);
Session session = newClient(new Session.Listener.Adapter()
@ -805,6 +819,19 @@ public abstract class FlowControlStrategyTest
((ISession)session).updateRecvWindow(FlowControlStrategy.DEFAULT_WINDOW_SIZE);
return super.onPreface(session);
}
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
return new Stream.Listener.Adapter()
{
@Override
public void onData(Stream stream, DataFrame frame, Callback callback)
{
// Do not succeed the callback.
}
};
}
});
final CountDownLatch closeLatch = new CountDownLatch(1);

View File

@ -46,12 +46,7 @@ import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.GoAwayFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.PingFrame;
import org.eclipse.jetty.http2.frames.PriorityFrame;
import org.eclipse.jetty.http2.frames.PushPromiseFrame;
import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
import org.eclipse.jetty.http2.parser.ServerParser;
import org.eclipse.jetty.http2.server.RawHTTP2ServerConnectionFactory;
import org.eclipse.jetty.server.Connector;
@ -752,7 +747,7 @@ public class HTTP2Test extends AbstractTest
@Override
protected ServerParser newServerParser(Connector connector, ServerParser.Listener listener)
{
return super.newServerParser(connector, new ServerParserListenerWrapper(listener)
return super.newServerParser(connector, new ServerParser.Listener.Wrapper(listener)
{
@Override
public void onGoAway(GoAwayFrame frame)
@ -806,80 +801,4 @@ public class HTTP2Test extends AbstractTest
throw new RuntimeException();
}
}
private static class ServerParserListenerWrapper implements ServerParser.Listener
{
private final ServerParser.Listener listener;
private ServerParserListenerWrapper(ServerParser.Listener listener)
{
this.listener = listener;
}
@Override
public void onPreface()
{
listener.onPreface();
}
@Override
public void onData(DataFrame frame)
{
listener.onData(frame);
}
@Override
public void onHeaders(HeadersFrame frame)
{
listener.onHeaders(frame);
}
@Override
public void onPriority(PriorityFrame frame)
{
listener.onPriority(frame);
}
@Override
public void onReset(ResetFrame frame)
{
listener.onReset(frame);
}
@Override
public void onSettings(SettingsFrame frame)
{
listener.onSettings(frame);
}
@Override
public void onPushPromise(PushPromiseFrame frame)
{
listener.onPushPromise(frame);
}
@Override
public void onPing(PingFrame frame)
{
listener.onPing(frame);
}
@Override
public void onGoAway(GoAwayFrame frame)
{
listener.onGoAway(frame);
}
@Override
public void onWindowUpdate(WindowUpdateFrame frame)
{
listener.onWindowUpdate(frame);
}
@Override
public void onConnectionFailure(int error, String reason)
{
listener.onConnectionFailure(error, reason);
}
}
}

View File

@ -26,13 +26,13 @@ import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
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.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
@ -42,11 +42,13 @@ import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.FuturePromise;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.StringUtil;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Test;
@ -106,7 +108,7 @@ public class TrailersTest extends AbstractTest
start(new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
{
Request jettyRequest = (Request)request;
// No trailers yet.
@ -238,7 +240,7 @@ public class TrailersTest extends AbstractTest
start(new EmptyHttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
{
Request jettyRequest = (Request)request;
Response jettyResponse = jettyRequest.getResponse();
@ -287,4 +289,63 @@ public class TrailersTest extends AbstractTest
Assert.assertTrue(trailers.isEndStream());
Assert.assertThat(trailers.getMetaData().getFields().get(trailerName), Matchers.equalTo(trailerValue));
}
@Test
public void testRequestTrailerInvalidHpack() throws Exception
{
CountDownLatch serverLatch = new CountDownLatch(1);
start(new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
{
try
{
// Read the content to read the trailers
ServletInputStream input = request.getInputStream();
while (true)
{
int read = input.read();
if (read < 0)
break;
}
}
catch (IOException x)
{
serverLatch.countDown();
throw x;
}
}
});
CountDownLatch clientLatch = new CountDownLatch(1);
Session session = newClient(new Session.Listener.Adapter());
MetaData.Request request = newRequest("POST", new HttpFields());
HeadersFrame requestFrame = new HeadersFrame(request, null, false);
FuturePromise<Stream> promise = new FuturePromise<>();
session.newStream(requestFrame, promise, new Stream.Listener.Adapter()
{
@Override
public void onReset(Stream stream, ResetFrame frame)
{
clientLatch.countDown();
}
});
Stream stream = promise.get(5, TimeUnit.SECONDS);
ByteBuffer data = ByteBuffer.wrap(StringUtil.getUtf8Bytes("hello"));
Callback.Completable completable = new Callback.Completable();
stream.data(new DataFrame(stream.getId(), data, false), completable);
completable.thenRun(() ->
{
// Invalid trailer: cannot contain pseudo headers.
HttpFields trailerFields = new HttpFields();
trailerFields.put(HttpHeader.C_METHOD, "GET");
MetaData trailer = new MetaData(HttpVersion.HTTP_2, trailerFields);
HeadersFrame trailerFrame = new HeadersFrame(stream.getId(), trailer, null, true);
stream.headers(trailerFrame, Callback.NOOP);
});
Assert.assertTrue(serverLatch.await(5, TimeUnit.SECONDS));
Assert.assertTrue(clientLatch.await(5, TimeUnit.SECONDS));
}
}

View File

@ -19,6 +19,7 @@
package org.eclipse.jetty.http2;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
@ -96,6 +97,19 @@ public enum ErrorCode
return Codes.codes.get(error);
}
public static String toString(int error, String dft)
{
ErrorCode errorCode = from(error);
String result;
if (errorCode != null)
result = errorCode.name().toLowerCase(Locale.ENGLISH);
else if (dft == null)
result = String.valueOf(error);
else
result = dft;
return result;
}
private static class Codes
{
private static final Map<Integer, ErrorCode> codes = new HashMap<>();

View File

@ -27,14 +27,6 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.GoAwayFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.PingFrame;
import org.eclipse.jetty.http2.frames.PriorityFrame;
import org.eclipse.jetty.http2.frames.PushPromiseFrame;
import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
import org.eclipse.jetty.http2.parser.Parser;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.ByteBufferPool;
@ -220,6 +212,7 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
private final Callback fillableCallback = new FillableCallback();
private NetworkBuffer buffer;
private boolean shutdown;
private boolean failed;
private void setInputBuffer(ByteBuffer byteBuffer)
{
@ -237,7 +230,7 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
if (task != null)
return task;
if (isFillInterested() || shutdown)
if (isFillInterested() || shutdown || failed)
return null;
if (buffer == null)
@ -248,11 +241,22 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
if (parse)
{
buffer.retain();
while (buffer.hasRemaining())
parser.parse(buffer.buffer);
boolean released = buffer.tryRelease();
boolean released;
try
{
while (buffer.hasRemaining())
{
parser.parse(buffer.buffer);
if (failed)
return null;
}
}
finally
{
released = buffer.tryRelease();
if (failed && released)
releaseNetworkBuffer();
}
task = pollTask();
if (LOG.isDebugEnabled())
@ -345,13 +349,11 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
}
}
private class ParserListener implements Parser.Listener
private class ParserListener extends Parser.Listener.Wrapper
{
private final Parser.Listener listener;
private ParserListener(Parser.Listener listener)
{
this.listener = listener;
super(listener);
}
@Override
@ -363,58 +365,11 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
session.onData(frame, callback);
}
@Override
public void onHeaders(HeadersFrame frame)
{
listener.onHeaders(frame);
}
@Override
public void onPriority(PriorityFrame frame)
{
listener.onPriority(frame);
}
@Override
public void onReset(ResetFrame frame)
{
listener.onReset(frame);
}
@Override
public void onSettings(SettingsFrame frame)
{
listener.onSettings(frame);
}
@Override
public void onPushPromise(PushPromiseFrame frame)
{
listener.onPushPromise(frame);
}
@Override
public void onPing(PingFrame frame)
{
listener.onPing(frame);
}
@Override
public void onGoAway(GoAwayFrame frame)
{
listener.onGoAway(frame);
}
@Override
public void onWindowUpdate(WindowUpdateFrame frame)
{
listener.onWindowUpdate(frame);
}
@Override
public void onConnectionFailure(int error, String reason)
{
listener.onConnectionFailure(error, reason);
producer.failed = true;
super.onConnectionFailure(error, reason);
}
}

View File

@ -37,6 +37,7 @@ import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.DisconnectFrame;
import org.eclipse.jetty.http2.frames.FailureFrame;
import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.FrameType;
import org.eclipse.jetty.http2.frames.GoAwayFrame;
@ -72,8 +73,8 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
private static final Logger LOG = Log.getLogger(HTTP2Session.class);
private final ConcurrentMap<Integer, IStream> streams = new ConcurrentHashMap<>();
private final AtomicInteger streamIds = new AtomicInteger();
private final AtomicInteger lastStreamId = new AtomicInteger();
private final AtomicInteger localStreamIds = new AtomicInteger();
private final AtomicInteger lastRemoteStreamId = new AtomicInteger();
private final AtomicInteger localStreamCount = new AtomicInteger();
private final AtomicBiInteger remoteStreamCount = new AtomicBiInteger();
private final AtomicInteger sendWindow = new AtomicInteger();
@ -105,7 +106,8 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
this.flusher = new HTTP2Flusher(this);
this.maxLocalStreams = -1;
this.maxRemoteStreams = -1;
this.streamIds.set(initialStreamId);
this.localStreamIds.set(initialStreamId);
this.lastRemoteStreamId.set(isClientStream(initialStreamId) ? 0 : -1);
this.streamIdleTimeout = endPoint.getIdleTimeout();
this.sendWindow.set(FlowControlStrategy.DEFAULT_WINDOW_SIZE);
this.recvWindow.set(FlowControlStrategy.DEFAULT_WINDOW_SIZE);
@ -229,35 +231,46 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
LOG.debug("Received {}", frame);
int streamId = frame.getStreamId();
final IStream stream = getStream(streamId);
IStream stream = getStream(streamId);
// SPEC: the session window must be updated even if the stream is null.
// The flow control length includes the padding bytes.
final int flowControlLength = frame.remaining() + frame.padding();
int flowControlLength = frame.remaining() + frame.padding();
flowControl.onDataReceived(this, stream, flowControlLength);
if (stream != null)
{
if (getRecvWindow() < 0)
{
close(ErrorCode.FLOW_CONTROL_ERROR.code, "session_window_exceeded", callback);
}
onConnectionFailure(ErrorCode.FLOW_CONTROL_ERROR.code, "session_window_exceeded", callback);
else
{
stream.process(frame, new DataCallback(callback, stream, flowControlLength));
}
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("Ignoring {}, stream #{} not found", frame, streamId);
LOG.debug("Stream #{} not found", streamId);
// We must enlarge the session flow control window,
// otherwise other requests will be stalled.
flowControl.onDataConsumed(this, null, flowControlLength);
callback.succeeded();
boolean local = (streamId & 1) == (localStreamIds.get() & 1);
boolean closed = local ? isLocalStreamClosed(streamId) : isRemoteStreamClosed(streamId);
if (closed)
reset(new ResetFrame(streamId, ErrorCode.STREAM_CLOSED_ERROR.code), callback);
else
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_data_frame", callback);
}
}
protected boolean isLocalStreamClosed(int streamId)
{
return streamId <= localStreamIds.get();
}
protected boolean isRemoteStreamClosed(int streamId)
{
return streamId <= getLastRemoteStreamId();
}
@Override
public abstract void onHeaders(HeadersFrame frame);
@ -274,11 +287,19 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
if (LOG.isDebugEnabled())
LOG.debug("Received {}", frame);
IStream stream = getStream(frame.getStreamId());
int streamId = frame.getStreamId();
IStream stream = getStream(streamId);
if (stream != null)
stream.process(frame, new ResetCallback());
{
stream.process(frame, new OnResetCallback());
}
else
notifyReset(this, frame);
{
if (isRemoteStreamClosed(streamId))
notifyReset(this, frame);
else
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_rst_stream_frame");
}
}
@Override
@ -306,54 +327,42 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
case SettingsFrame.HEADER_TABLE_SIZE:
{
if (LOG.isDebugEnabled())
LOG.debug("Update HPACK header table size to {} for {}", value, this);
LOG.debug("Updating HPACK header table size to {} for {}", value, this);
generator.setHeaderTableSize(value);
break;
}
case SettingsFrame.ENABLE_PUSH:
{
// SPEC: check the value is sane.
if (value != 0 && value != 1)
{
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "invalid_settings_enable_push");
return;
}
pushEnabled = value == 1;
if (LOG.isDebugEnabled())
LOG.debug("{} push for {}", pushEnabled ? "Enable" : "Disable", this);
LOG.debug("{} push for {}", pushEnabled ? "Enabling" : "Disabling", this);
pushEnabled = value == 1;
break;
}
case SettingsFrame.MAX_CONCURRENT_STREAMS:
{
maxLocalStreams = value;
if (LOG.isDebugEnabled())
LOG.debug("Update max local concurrent streams to {} for {}", maxLocalStreams, this);
LOG.debug("Updating max local concurrent streams to {} for {}", maxLocalStreams, this);
maxLocalStreams = value;
break;
}
case SettingsFrame.INITIAL_WINDOW_SIZE:
{
if (LOG.isDebugEnabled())
LOG.debug("Update initial window size to {} for {}", value, this);
LOG.debug("Updating initial window size to {} for {}", value, this);
flowControl.updateInitialStreamWindow(this, value, false);
break;
}
case SettingsFrame.MAX_FRAME_SIZE:
{
if (LOG.isDebugEnabled())
LOG.debug("Update max frame size to {} for {}", value, this);
// SPEC: check the max frame size is sane.
if (value < Frame.DEFAULT_MAX_LENGTH || value > Frame.MAX_MAX_LENGTH)
{
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "invalid_settings_max_frame_size");
return;
}
LOG.debug("Updating max frame size to {} for {}", value, this);
generator.setMaxFrameSize(value);
break;
}
case SettingsFrame.MAX_HEADER_LIST_SIZE:
{
if (LOG.isDebugEnabled())
LOG.debug("Update max header list size to {} for {}", value, this);
LOG.debug("Updating max header list size to {} for {}", value, this);
generator.setMaxHeaderListSize(value);
break;
}
@ -448,25 +457,86 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
LOG.debug("Received {}", frame);
int streamId = frame.getStreamId();
int windowDelta = frame.getWindowDelta();
if (streamId > 0)
{
IStream stream = getStream(streamId);
if (stream != null)
if (windowDelta == 0)
{
stream.process(frame, Callback.NOOP);
onWindowUpdate(stream, frame);
reset(new ResetFrame(streamId, ErrorCode.PROTOCOL_ERROR.code), Callback.NOOP);
}
else
{
IStream stream = getStream(streamId);
if (stream != null)
{
int streamSendWindow = stream.updateSendWindow(0);
if (sumOverflows(streamSendWindow, windowDelta))
{
reset(new ResetFrame(streamId, ErrorCode.FLOW_CONTROL_ERROR.code), Callback.NOOP);
}
else
{
stream.process(frame, Callback.NOOP);
onWindowUpdate(stream, frame);
}
}
else
{
if (!isRemoteStreamClosed(streamId))
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_window_update_frame");
}
}
}
else
{
onWindowUpdate(null, frame);
if (windowDelta == 0)
{
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "invalid_window_update_frame");
}
else
{
int sessionSendWindow = updateSendWindow(0);
if (sumOverflows(sessionSendWindow, windowDelta))
onConnectionFailure(ErrorCode.FLOW_CONTROL_ERROR.code, "invalid_flow_control_window");
else
onWindowUpdate(null, frame);
}
}
}
@Override
public void onStreamFailure(int streamId, int error, String reason)
{
Callback callback = new ResetCallback(streamId, error, Callback.NOOP);
IStream stream = getStream(streamId);
if (stream != null)
stream.process(new FailureFrame(error, reason), callback);
else
callback.succeeded();
}
private boolean sumOverflows(int a, int b)
{
try
{
Math.addExact(a, b);
return false;
}
catch (ArithmeticException x)
{
return true;
}
}
@Override
public void onConnectionFailure(int error, String reason)
{
notifyFailure(this, new IOException(String.format("%d/%s", error, reason)), new CloseCallback(error, reason));
onConnectionFailure(error, reason, Callback.NOOP);
}
protected void onConnectionFailure(int error, String reason, Callback callback)
{
notifyFailure(this, new IOException(String.format("%d/%s", error, reason)), new CloseCallback(error, reason, callback));
}
@Override
@ -482,7 +552,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
int streamId = frame.getStreamId();
if (streamId <= 0)
{
streamId = streamIds.getAndAdd(2);
streamId = localStreamIds.getAndAdd(2);
PriorityFrame priority = frame.getPriority();
priority = priority == null ? null : new PriorityFrame(streamId, priority.getParentStreamId(),
priority.getWeight(), priority.isExclusive());
@ -511,7 +581,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
IStream stream = streams.get(streamId);
if (stream == null)
{
streamId = streamIds.getAndAdd(2);
streamId = localStreamIds.getAndAdd(2);
frame = new PriorityFrame(streamId, frame.getParentStreamId(),
frame.getWeight(), frame.isExclusive());
}
@ -529,7 +599,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
boolean queued;
synchronized (this)
{
int streamId = streamIds.getAndAdd(2);
int streamId = localStreamIds.getAndAdd(2);
frame = new PushPromiseFrame(frame.getStreamId(), streamId, frame.getMetaData());
IStream pushStream = createLocalStream(streamId);
@ -629,7 +699,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
reason = reason.substring(0, Math.min(reason.length(), 32));
payload = reason.getBytes(StandardCharsets.UTF_8);
}
return new GoAwayFrame(closeState, lastStreamId.get(), error, payload);
return new GoAwayFrame(closeState, getLastRemoteStreamId(), error, payload);
}
@Override
@ -736,7 +806,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
// SPEC: duplicate stream is treated as connection error.
if (streams.putIfAbsent(streamId, stream) == null)
{
updateLastStreamId(streamId);
updateLastRemoteStreamId(streamId);
stream.setIdleTimeout(getStreamIdleTimeout());
flowControl.onStreamCreated(stream);
if (LOG.isDebugEnabled())
@ -745,7 +815,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
}
else
{
close(ErrorCode.PROTOCOL_ERROR.code, "duplicate_stream", Callback.NOOP);
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "duplicate_stream");
return null;
}
}
@ -1014,9 +1084,14 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
return !endPoint.isOpen();
}
private void updateLastStreamId(int streamId)
protected int getLastRemoteStreamId()
{
Atomics.updateMax(lastStreamId, streamId);
return lastRemoteStreamId.get();
}
private void updateLastRemoteStreamId(int streamId)
{
Atomics.updateMax(lastRemoteStreamId, streamId);
}
protected Stream.Listener notifyNewStream(Stream stream, HeadersFrame frame)
@ -1120,6 +1195,12 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
}
}
protected static boolean isClientStream(int streamId)
{
// Client-initiated stream ids are odd.
return (streamId & 1) == 1;
}
@Override
public void dump(Appendable out, String indent) throws IOException
{
@ -1448,7 +1529,37 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
}
}
private class ResetCallback implements Callback
private class ResetCallback extends Callback.Nested
{
private final int streamId;
private final int error;
private ResetCallback(int streamId, int error, Callback callback)
{
super(callback);
this.streamId = streamId;
this.error = error;
}
@Override
public void succeeded()
{
complete();
}
@Override
public void failed(Throwable x)
{
complete();
}
private void complete()
{
reset(new ResetFrame(streamId, error), getCallback());
}
}
private class OnResetCallback implements Callback
{
@Override
public void succeeded()
@ -1474,13 +1585,14 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
}
}
private class CloseCallback implements Callback
private class CloseCallback extends Callback.Nested
{
private final int error;
private final String reason;
private CloseCallback(int error, String reason)
private CloseCallback(int error, String reason, Callback callback)
{
super(callback);
this.error = error;
this.reason = reason;
}
@ -1497,15 +1609,9 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
complete();
}
@Override
public InvocationType getInvocationType()
{
return InvocationType.NON_BLOCKING;
}
private void complete()
{
close(error, reason, Callback.NOOP);
close(error, reason, getCallback());
}
}

View File

@ -28,8 +28,12 @@ import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.FailureFrame;
import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.PushPromiseFrame;
@ -58,9 +62,10 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
private final ISession session;
private final int streamId;
private final boolean local;
private volatile Listener listener;
private volatile boolean localReset;
private volatile boolean remoteReset;
private boolean localReset;
private Listener listener;
private boolean remoteReset;
private long dataLength;
public HTTP2Stream(Scheduler scheduler, ISession session, int streamId, boolean local)
{
@ -68,6 +73,7 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
this.session = session;
this.streamId = streamId;
this.local = local;
this.dataLength = Long.MIN_VALUE;
}
@Override
@ -255,6 +261,11 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
onWindowUpdate((WindowUpdateFrame)frame, callback);
break;
}
case FAILURE:
{
onFailure((FailureFrame)frame, callback);
break;
}
default:
{
throw new UnsupportedOperationException();
@ -266,6 +277,15 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
{
if (updateClose(frame.isEndStream(), CloseState.Event.RECEIVED))
session.removeStream(this);
MetaData metaData = frame.getMetaData();
if (metaData.isRequest() || metaData.isResponse())
{
HttpFields fields = metaData.getFields();
long length = -1;
if (fields != null)
length = fields.getLongField(HttpHeader.CONTENT_LENGTH.asString());
dataLength = length >= 0 ? length : Long.MIN_VALUE;
}
callback.succeeded();
}
@ -295,8 +315,20 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
return;
}
if (dataLength != Long.MIN_VALUE)
{
dataLength -= frame.remaining();
if (frame.isEndStream() && dataLength != 0)
{
reset(new ResetFrame(streamId, ErrorCode.PROTOCOL_ERROR.code), Callback.NOOP);
callback.failed(new IOException("invalid_data_length"));
return;
}
}
if (updateClose(frame.isEndStream(), CloseState.Event.RECEIVED))
session.removeStream(this);
notifyData(this, frame, callback);
}
@ -321,6 +353,11 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
callback.succeeded();
}
private void onFailure(FailureFrame frame, Callback callback)
{
notifyFailure(this, frame, callback);
}
@Override
public boolean updateClose(boolean update, CloseState.Event event)
{
@ -498,31 +535,43 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
private void notifyData(Stream stream, DataFrame frame, Callback callback)
{
final Listener listener = this.listener;
if (listener == null)
return;
try
Listener listener = this.listener;
if (listener != null)
{
listener.onData(stream, frame, callback);
try
{
listener.onData(stream, frame, callback);
}
catch (Throwable x)
{
LOG.info("Failure while notifying listener " + listener, x);
callback.failed(x);
}
}
catch (Throwable x)
else
{
LOG.info("Failure while notifying listener " + listener, x);
callback.succeeded();
}
}
private void notifyReset(Stream stream, ResetFrame frame, Callback callback)
{
final Listener listener = this.listener;
if (listener == null)
return;
try
Listener listener = this.listener;
if (listener != null)
{
listener.onReset(stream, frame, callback);
try
{
listener.onReset(stream, frame, callback);
}
catch (Throwable x)
{
LOG.info("Failure while notifying listener " + listener, x);
callback.failed(x);
}
}
catch (Throwable x)
else
{
LOG.info("Failure while notifying listener " + listener, x);
callback.succeeded();
}
}
@ -542,6 +591,27 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
}
}
private void notifyFailure(Stream stream, FailureFrame frame, Callback callback)
{
Listener listener = this.listener;
if (listener != null)
{
try
{
listener.onFailure(stream, frame.getError(), frame.getReason(), callback);
}
catch (Throwable x)
{
LOG.info("Failure while notifying listener " + listener, x);
callback.failed(x);
}
}
else
{
callback.succeeded();
}
}
@Override
public String dump()
{

View File

@ -214,6 +214,11 @@ public interface Stream
return true;
}
public default void onFailure(Stream stream, int error, String reason, Callback callback)
{
callback.succeeded();
}
/**
* <p>Empty implementation of {@link Listener}</p>
*/

View File

@ -0,0 +1,42 @@
//
// ========================================================================
// Copyright (c) 1995-2018 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.http2.frames;
public class FailureFrame extends Frame
{
private final int error;
private final String reason;
public FailureFrame(int error, String reason)
{
super(FrameType.FAILURE);
this.error = error;
this.reason = reason;
}
public int getError()
{
return error;
}
public String getReason()
{
return reason;
}
}

View File

@ -35,7 +35,8 @@ public enum FrameType
CONTINUATION(9),
// Synthetic frames only needed by the implementation.
PREFACE(10),
DISCONNECT(11);
DISCONNECT(11),
FAILURE(12);
public static FrameType from(int type)
{

View File

@ -76,11 +76,10 @@ public class GoAwayFrame extends Frame
@Override
public String toString()
{
ErrorCode errorCode = ErrorCode.from(error);
return String.format("%s,%d/%s/%s/%s",
super.toString(),
lastStreamId,
errorCode != null ? errorCode.toString() : String.valueOf(error),
ErrorCode.toString(error, null),
tryConvertPayload(),
closeState);
}

View File

@ -83,6 +83,7 @@ public class HeadersFrame extends Frame
@Override
public String toString()
{
return String.format("%s#%d{end=%b}", super.toString(), streamId, endStream);
return String.format("%s#%d{end=%b}%s", super.toString(), streamId, endStream,
priority == null ? "" : String.format("+%s", priority));
}
}

View File

@ -18,8 +18,6 @@
package org.eclipse.jetty.http2.frames;
import java.util.Locale;
import org.eclipse.jetty.http2.ErrorCode;
public class ResetFrame extends Frame
@ -49,8 +47,6 @@ public class ResetFrame extends Frame
@Override
public String toString()
{
ErrorCode errorCode = ErrorCode.from(error);
String reason = errorCode == null ? "error=" + error : errorCode.name().toLowerCase(Locale.ENGLISH);
return String.format("%s#%d{%s}", super.toString(), streamId, reason);
return String.format("%s#%d{%s}", super.toString(), streamId, ErrorCode.toString(error, null));
}
}

View File

@ -45,7 +45,7 @@ public class GoAwayGenerator extends FrameGenerator
public int generateGoAway(ByteBufferPool.Lease lease, int lastStreamId, int error, byte[] payload)
{
if (lastStreamId < 0)
throw new IllegalArgumentException("Invalid last stream id: " + lastStreamId);
lastStreamId = 0;
// The last streamId + the error code.
int fixedLength = 4 + 4;

View File

@ -222,4 +222,21 @@ public abstract class BodyParser
LOG.info("Failure while notifying listener " + listener, x);
}
}
protected void streamFailure(int streamId, int error, String reason)
{
notifyStreamFailure(streamId, error, reason);
}
private void notifyStreamFailure(int streamId, int error, String reason)
{
try
{
listener.onStreamFailure(streamId, error, reason);
}
catch (Throwable x)
{
LOG.info("Failure while notifying listener " + listener, x);
}
}
}

View File

@ -81,7 +81,7 @@ public class ContinuationBodyParser extends BodyParser
headerBlockFragments.storeFragment(buffer, length, last);
reset();
if (last)
onHeaders();
return onHeaders();
return true;
}
}
@ -94,12 +94,17 @@ public class ContinuationBodyParser extends BodyParser
return false;
}
private void onHeaders()
private boolean onHeaders()
{
ByteBuffer headerBlock = headerBlockFragments.complete();
MetaData metaData = headerBlockParser.parse(headerBlock, headerBlock.remaining());
if (metaData == HeaderBlockParser.SESSION_FAILURE)
return false;
if (metaData == null || metaData == HeaderBlockParser.STREAM_FAILURE)
return true;
HeadersFrame frame = new HeadersFrame(getStreamId(), metaData, headerBlockFragments.getPriorityFrame(), headerBlockFragments.isEndStream());
notifyHeaders(frame);
return true;
}
private void reset()

View File

@ -20,23 +20,46 @@ package org.eclipse.jetty.http2.parser;
import java.nio.ByteBuffer;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.ErrorCode;
import org.eclipse.jetty.http2.hpack.HpackDecoder;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
public class HeaderBlockParser
{
public static final MetaData STREAM_FAILURE = new MetaData(HttpVersion.HTTP_2, null);
public static final MetaData SESSION_FAILURE = new MetaData(HttpVersion.HTTP_2, null);
private static final Logger LOG = Log.getLogger(HeaderBlockParser.class);
private final HeaderParser headerParser;
private final ByteBufferPool byteBufferPool;
private final HpackDecoder hpackDecoder;
private final BodyParser notifier;
private ByteBuffer blockBuffer;
public HeaderBlockParser(ByteBufferPool byteBufferPool, HpackDecoder hpackDecoder)
public HeaderBlockParser(HeaderParser headerParser, ByteBufferPool byteBufferPool, HpackDecoder hpackDecoder, BodyParser notifier)
{
this.headerParser = headerParser;
this.byteBufferPool = byteBufferPool;
this.hpackDecoder = hpackDecoder;
this.notifier = notifier;
}
/**
* Parses @{code blockLength} HPACK bytes from the given {@code buffer}.
*
* @param buffer the buffer to parse
* @param blockLength the length of the HPACK block
* @return null, if the buffer contains less than {@code blockLength} bytes;
* {@link #STREAM_FAILURE} if parsing the HPACK block produced a stream failure;
* {@link #SESSION_FAILURE} if parsing the HPACK block produced a session failure;
* a valid MetaData object if the parsing was successful.
*/
public MetaData parse(ByteBuffer buffer, int blockLength)
{
// We must wait for the all the bytes of the header block to arrive.
@ -72,17 +95,41 @@ public class HeaderBlockParser
toDecode = buffer;
}
MetaData result = hpackDecoder.decode(toDecode);
buffer.limit(limit);
if (blockBuffer != null)
try
{
byteBufferPool.release(blockBuffer);
blockBuffer = null;
return hpackDecoder.decode(toDecode);
}
catch (HpackException.StreamException x)
{
if (LOG.isDebugEnabled())
LOG.debug(x);
notifier.streamFailure(headerParser.getStreamId(), ErrorCode.PROTOCOL_ERROR.code, "invalid_hpack_block");
return STREAM_FAILURE;
}
catch (HpackException.CompressionException x)
{
if (LOG.isDebugEnabled())
LOG.debug(x);
notifier.connectionFailure(buffer, ErrorCode.COMPRESSION_ERROR.code, "invalid_hpack_block");
return SESSION_FAILURE;
}
catch (HpackException.SessionException x)
{
if (LOG.isDebugEnabled())
LOG.debug(x);
notifier.connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_hpack_block");
return SESSION_FAILURE;
}
finally
{
buffer.limit(limit);
return result;
if (blockBuffer != null)
{
byteBufferPool.release(blockBuffer);
blockBuffer = null;
}
}
}
}
}

View File

@ -21,6 +21,7 @@ package org.eclipse.jetty.http2.parser;
import java.nio.ByteBuffer;
import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.FrameType;
/**
* <p>The parser for the frame header of HTTP/2 frames.</p>
@ -144,6 +145,12 @@ public class HeaderParser
return streamId;
}
@Override
public String toString()
{
return String.format("[%s|%d|%d|%d]", FrameType.from(getFrameType()), getLength(), flags, getStreamId());
}
private enum State
{
LENGTH, TYPE, FLAGS, STREAM_ID, STREAM_ID_BYTES

View File

@ -92,17 +92,11 @@ public class HeadersBodyParser extends BodyParser
length = getBodyLength();
if (isPadding())
{
state = State.PADDING_LENGTH;
}
else if (hasFlag(Flags.PRIORITY))
{
state = State.EXCLUSIVE;
}
else
{
state = State.HEADERS;
}
break;
}
case PADDING_LENGTH:
@ -162,6 +156,9 @@ public class HeadersBodyParser extends BodyParser
}
case WEIGHT:
{
// SPEC: stream cannot depend on itself.
if (getStreamId() == parentStreamId)
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_priority_frame");
weight = (buffer.get() & 0xFF) + 1;
--length;
state = State.HEADERS;
@ -173,13 +170,16 @@ public class HeadersBodyParser extends BodyParser
if (hasFlag(Flags.END_HEADERS))
{
MetaData metaData = headerBlockParser.parse(buffer, length);
if (metaData == HeaderBlockParser.SESSION_FAILURE)
return false;
if (metaData != null)
{
if (LOG.isDebugEnabled())
LOG.debug("Parsed {} frame hpack from {}", FrameType.HEADERS, buffer);
state = State.PADDING;
loop = paddingLength == 0;
onHeaders(parentStreamId, weight, exclusive, metaData);
if (metaData != HeaderBlockParser.STREAM_FAILURE)
onHeaders(parentStreamId, weight, exclusive, metaData);
}
}
else

View File

@ -24,6 +24,7 @@ import java.util.function.UnaryOperator;
import org.eclipse.jetty.http2.ErrorCode;
import org.eclipse.jetty.http2.Flags;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.FrameType;
import org.eclipse.jetty.http2.frames.GoAwayFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
@ -35,7 +36,6 @@ import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
import org.eclipse.jetty.http2.hpack.HpackDecoder;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@ -52,6 +52,8 @@ public class Parser
private final HeaderParser headerParser;
private final HeaderBlockParser headerBlockParser;
private final BodyParser[] bodyParsers;
private final UnknownBodyParser unknownBodyParser;
private int maxFrameLength;
private int maxSettingsKeys = SettingsFrame.DEFAULT_MAX_KEYS;
private boolean continuation;
private State state = State.HEADER;
@ -60,7 +62,9 @@ public class Parser
{
this.listener = listener;
this.headerParser = new HeaderParser();
this.headerBlockParser = new HeaderBlockParser(byteBufferPool, new HpackDecoder(maxDynamicTableSize, maxHeaderSize));
this.unknownBodyParser = new UnknownBodyParser(headerParser, listener);
this.headerBlockParser = new HeaderBlockParser(headerParser, byteBufferPool, new HpackDecoder(maxDynamicTableSize, maxHeaderSize), unknownBodyParser);
this.maxFrameLength = Frame.DEFAULT_MAX_LENGTH;
this.bodyParsers = new BodyParser[FrameType.values().length];
}
@ -128,8 +132,7 @@ public class Parser
{
if (LOG.isDebugEnabled())
LOG.debug(x);
BufferUtil.clear(buffer);
notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "parser_error");
connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR, "parser_error");
}
}
@ -138,31 +141,27 @@ public class Parser
if (!headerParser.parse(buffer))
return false;
FrameType frameType = FrameType.from(getFrameType());
if (LOG.isDebugEnabled())
LOG.debug("Parsed {} frame header from {}", frameType, buffer);
LOG.debug("Parsed {} frame header from {}", headerParser, buffer);
if (headerParser.getLength() > getMaxFrameLength())
return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR, "invalid_frame_length");
FrameType frameType = FrameType.from(getFrameType());
if (continuation)
{
// SPEC: CONTINUATION frames must be consecutive.
if (frameType != FrameType.CONTINUATION)
{
// SPEC: CONTINUATION frames must be consecutive.
BufferUtil.clear(buffer);
notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "continuation_frame_expected");
return false;
}
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR, "expected_continuation_frame");
if (headerParser.hasFlag(Flags.END_HEADERS))
{
continuation = false;
}
}
else
{
if (frameType == FrameType.HEADERS &&
!headerParser.hasFlag(Flags.END_HEADERS))
{
continuation = true;
}
if (frameType == FrameType.HEADERS)
continuation = !headerParser.hasFlag(Flags.END_HEADERS);
else if (frameType == FrameType.CONTINUATION)
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR, "unexpected_continuation_frame");
}
state = State.BODY;
return true;
@ -173,9 +172,13 @@ public class Parser
int type = getFrameType();
if (type < 0 || type >= bodyParsers.length)
{
BufferUtil.clear(buffer);
notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unknown_frame_type_" + type);
// Unknown frame types must be ignored.
if (LOG.isDebugEnabled())
LOG.debug("Ignoring unknown frame type {}", Integer.toHexString(type));
if (!unknownBodyParser.parse(buffer))
return false;
reset();
return true;
}
BodyParser bodyParser = bodyParsers[type];
@ -194,6 +197,11 @@ public class Parser
return true;
}
private boolean connectionFailure(ByteBuffer buffer, ErrorCode error, String reason)
{
return unknownBodyParser.connectionFailure(buffer, error.code, reason);
}
protected int getFrameType()
{
return headerParser.getFrameType();
@ -204,6 +212,16 @@ public class Parser
return headerParser.hasFlag(bit);
}
public int getMaxFrameLength()
{
return maxFrameLength;
}
public void setMaxFrameLength(int maxFrameLength)
{
this.maxFrameLength = maxFrameLength;
}
public int getMaxSettingsKeys()
{
return maxSettingsKeys;
@ -246,6 +264,8 @@ public class Parser
public void onWindowUpdate(WindowUpdateFrame frame);
public void onStreamFailure(int streamId, int error, String reason);
public void onConnectionFailure(int error, String reason);
public static class Adapter implements Listener
@ -295,12 +315,98 @@ public class Parser
{
}
@Override
public void onStreamFailure(int streamId, int error, String reason)
{
}
@Override
public void onConnectionFailure(int error, String reason)
{
LOG.warn("Connection failure: {}/{}", error, reason);
}
}
public static class Wrapper implements Listener
{
private final Parser.Listener listener;
public Wrapper(Parser.Listener listener)
{
this.listener = listener;
}
public Listener getParserListener()
{
return listener;
}
@Override
public void onData(DataFrame frame)
{
listener.onData(frame);
}
@Override
public void onHeaders(HeadersFrame frame)
{
listener.onHeaders(frame);
}
@Override
public void onPriority(PriorityFrame frame)
{
listener.onPriority(frame);
}
@Override
public void onReset(ResetFrame frame)
{
listener.onReset(frame);
}
@Override
public void onSettings(SettingsFrame frame)
{
listener.onSettings(frame);
}
@Override
public void onPushPromise(PushPromiseFrame frame)
{
listener.onPushPromise(frame);
}
@Override
public void onPing(PingFrame frame)
{
listener.onPing(frame);
}
@Override
public void onGoAway(GoAwayFrame frame)
{
listener.onGoAway(frame);
}
@Override
public void onWindowUpdate(WindowUpdateFrame frame)
{
listener.onWindowUpdate(frame);
}
@Override
public void onStreamFailure(int streamId, int error, String reason)
{
listener.onStreamFailure(streamId, error, reason);
}
@Override
public void onConnectionFailure(int error, String reason)
{
listener.onConnectionFailure(error, reason);
}
}
}
private enum State

View File

@ -102,7 +102,6 @@ public class PriorityBodyParser extends BodyParser
// SPEC: stream cannot depend on itself.
if (getStreamId() == parentStreamId)
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_priority_frame");
int weight = (buffer.get() & 0xFF) + 1;
return onPriority(parentStreamId, weight, exclusive);
}

View File

@ -125,11 +125,14 @@ public class PushPromiseBodyParser extends BodyParser
case HEADERS:
{
MetaData metaData = headerBlockParser.parse(buffer, length);
if (metaData == HeaderBlockParser.SESSION_FAILURE)
return false;
if (metaData != null)
{
state = State.PADDING;
loop = paddingLength == 0;
onPushPromise(streamId, metaData);
if (metaData != HeaderBlockParser.STREAM_FAILURE)
onPushPromise(streamId, metaData);
}
break;
}

View File

@ -158,6 +158,26 @@ public class ServerParser extends Parser
{
}
}
public static class Wrapper extends Parser.Listener.Wrapper implements Listener
{
public Wrapper(ServerParser.Listener listener)
{
super(listener);
}
@Override
public ServerParser.Listener getParserListener()
{
return (Listener)super.getParserListener();
}
@Override
public void onPreface()
{
getParserListener().onPreface();
}
}
}
private enum State

View File

@ -25,6 +25,7 @@ import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.http2.ErrorCode;
import org.eclipse.jetty.http2.Flags;
import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@ -71,7 +72,7 @@ public class SettingsBodyParser extends BodyParser
@Override
protected void emptyBody(ByteBuffer buffer)
{
onSettings(new HashMap<>());
onSettings(buffer, new HashMap<>());
}
@Override
@ -135,7 +136,7 @@ public class SettingsBodyParser extends BodyParser
state = State.SETTING_ID;
length -= 4;
if (length == 0)
return onSettings(settings);
return onSettings(buffer, settings);
}
else
{
@ -161,7 +162,7 @@ public class SettingsBodyParser extends BodyParser
return false;
state = State.SETTING_ID;
if (length == 0)
return onSettings(settings);
return onSettings(buffer, settings);
}
break;
}
@ -183,8 +184,21 @@ public class SettingsBodyParser extends BodyParser
return true;
}
protected boolean onSettings(Map<Integer, Integer> settings)
protected boolean onSettings(ByteBuffer buffer, Map<Integer, Integer> settings)
{
Integer enablePush = settings.get(SettingsFrame.ENABLE_PUSH);
if (enablePush != null && enablePush != 0 && enablePush != 1)
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_settings_enable_push");
Integer initialWindowSize = settings.get(SettingsFrame.INITIAL_WINDOW_SIZE);
// Values greater than Integer.MAX_VALUE will overflow to negative.
if (initialWindowSize != null && initialWindowSize < 0)
return connectionFailure(buffer, ErrorCode.FLOW_CONTROL_ERROR.code, "invalid_settings_initial_window_size");
Integer maxFrameLength = settings.get(SettingsFrame.MAX_FRAME_SIZE);
if (maxFrameLength != null && (maxFrameLength < Frame.DEFAULT_MAX_LENGTH || maxFrameLength > Frame.MAX_MAX_LENGTH))
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_settings_max_frame_size");
SettingsFrame frame = new SettingsFrame(settings, hasFlag(Flags.ACK));
reset();
notifySettings(frame);
@ -210,7 +224,7 @@ public class SettingsBodyParser extends BodyParser
}
@Override
protected boolean onSettings(Map<Integer, Integer> settings)
protected boolean onSettings(ByteBuffer buffer, Map<Integer, Integer> settings)
{
frameRef.set(new SettingsFrame(settings, false));
return true;

View File

@ -0,0 +1,54 @@
//
// ========================================================================
// Copyright (c) 1995-2018 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.http2.parser;
import java.nio.ByteBuffer;
public class UnknownBodyParser extends BodyParser
{
private int cursor;
public UnknownBodyParser(HeaderParser headerParser, Parser.Listener listener)
{
super(headerParser, listener);
}
@Override
public boolean parse(ByteBuffer buffer)
{
int length = cursor == 0 ? getBodyLength() : cursor;
cursor = consume(buffer, length);
return cursor == 0;
}
private int consume(ByteBuffer buffer, int length)
{
int remaining = buffer.remaining();
if (remaining >= length)
{
buffer.position(buffer.position() + length);
return 0;
}
else
{
buffer.position(buffer.limit());
return length - remaining;
}
}
}

View File

@ -0,0 +1,66 @@
//
// ========================================================================
// Copyright (c) 1995-2018 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.http2.frames;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http2.ErrorCode;
import org.eclipse.jetty.http2.parser.Parser;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.MappedByteBufferPool;
import org.junit.Assert;
import org.junit.Test;
public class MaxFrameSizeParseTest
{
private final ByteBufferPool byteBufferPool = new MappedByteBufferPool();
@Test
public void testMaxFrameSize()
{
int maxFrameLength = Frame.DEFAULT_MAX_LENGTH + 16;
AtomicInteger failure = new AtomicInteger();
Parser parser = new Parser(byteBufferPool, new Parser.Listener.Adapter()
{
@Override
public void onConnectionFailure(int error, String reason)
{
failure.set(error);
}
}, 4096, 8192);
parser.setMaxFrameLength(maxFrameLength);
parser.init(UnaryOperator.identity());
// Iterate a few times to be sure the parser is properly reset.
for (int i = 0; i < 2; ++i)
{
byte[] bytes = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0};
ByteBuffer buffer = ByteBuffer.wrap(bytes);
buffer.putInt(0, maxFrameLength + 1);
buffer.position(1);
while (buffer.hasRemaining())
parser.parse(buffer);
}
Assert.assertEquals(ErrorCode.FRAME_SIZE_ERROR.code, failure.get());
}
}

View File

@ -0,0 +1,72 @@
//
// ========================================================================
// Copyright (c) 1995-2018 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.http2.frames;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http2.parser.Parser;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.MappedByteBufferPool;
import org.junit.Assert;
import org.junit.Test;
public class UnknownParseTest
{
private final ByteBufferPool byteBufferPool = new MappedByteBufferPool();
@Test
public void testParse()
{
testParse(Function.identity());
}
@Test
public void testParseOneByteAtATime()
{
testParse(buffer -> ByteBuffer.wrap(new byte[]{buffer.get()}));
}
private void testParse(Function<ByteBuffer, ByteBuffer> fn)
{
AtomicBoolean failure = new AtomicBoolean();
Parser parser = new Parser(byteBufferPool, new Parser.Listener.Adapter()
{
@Override
public void onConnectionFailure(int error, String reason)
{
failure.set(true);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
// Iterate a few times to be sure the parser is properly reset.
for (int i = 0; i < 2; ++i)
{
byte[] bytes = new byte[]{0, 0, 4, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0};
ByteBuffer buffer = ByteBuffer.wrap(bytes);
while (buffer.hasRemaining())
parser.parse(fn.apply(buffer));
}
Assert.assertFalse(failure.get());
}
}

View File

@ -21,10 +21,8 @@ package org.eclipse.jetty.http2.hpack;
import java.nio.ByteBuffer;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.hpack.HpackContext.Entry;
import org.eclipse.jetty.util.TypeUtil;
@ -66,14 +64,16 @@ public class HpackDecoder
_localMaxDynamicTableSize=localMaxdynamciTableSize;
}
public MetaData decode(ByteBuffer buffer)
public MetaData decode(ByteBuffer buffer) throws HpackException.SessionException, HpackException.StreamException
{
if (LOG.isDebugEnabled())
LOG.debug(String.format("CtxTbl[%x] decoding %d octets",_context.hashCode(),buffer.remaining()));
// If the buffer is big, don't even think about decoding it
if (buffer.remaining()>_builder.getMaxSize())
throw new BadMessageException(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431,"Header frame size "+buffer.remaining()+">"+_builder.getMaxSize());
throw new HpackException.SessionException("431 Request Header Fields too large");
boolean emitted = false;
while(buffer.hasRemaining())
{
@ -92,14 +92,14 @@ public class HpackDecoder
int index = NBitInteger.decode(buffer,7);
Entry entry=_context.get(index);
if (entry==null)
{
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Unknown index "+index);
}
else if (entry.isStatic())
throw new HpackException.SessionException("Unknown index %d",index);
if (entry.isStatic())
{
if (LOG.isDebugEnabled())
LOG.debug("decode IdxStatic {}",entry);
// emit field
emitted = true;
_builder.emit(entry.getHttpField());
// TODO copy and add to reference set if there is room
@ -110,6 +110,7 @@ public class HpackDecoder
if (LOG.isDebugEnabled())
LOG.debug("decode Idx {}",entry);
// emit
emitted = true;
_builder.emit(entry.getHttpField());
}
}
@ -134,6 +135,8 @@ public class HpackDecoder
LOG.debug("decode resize="+size);
if (size>_localMaxDynamicTableSize)
throw new IllegalArgumentException();
if (emitted)
throw new HpackException.CompressionException("Dynamic table resize after fields");
_context.resize(size);
continue;
@ -178,7 +181,8 @@ public class HpackDecoder
char c=name.charAt(i);
if (c>='A'&&c<='Z')
{
throw new BadMessageException(400,"Uppercase header name");
_builder.streamException("Uppercase header name %s",name);
break;
}
}
header=HttpHeader.CACHE.get(name);
@ -240,6 +244,7 @@ public class HpackDecoder
}
// emit the field
emitted = true;
_builder.emit(field);
// if indexed add to dynamic table

View File

@ -21,8 +21,11 @@ package org.eclipse.jetty.http2.hpack;
import java.nio.ByteBuffer;
import java.util.EnumSet;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
@ -31,6 +34,9 @@ import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.http2.hpack.HpackContext.Entry;
import org.eclipse.jetty.http2.hpack.HpackContext.StaticEntry;
import org.eclipse.jetty.util.ArrayTrie;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.Trie;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@ -38,17 +44,13 @@ import org.eclipse.jetty.util.log.Logger;
public class HpackEncoder
{
public static final Logger LOG = Log.getLogger(HpackEncoder.class);
private final static HttpField[] __status= new HttpField[599];
final static EnumSet<HttpHeader> __DO_NOT_HUFFMAN =
EnumSet.of(
HttpHeader.AUTHORIZATION,
HttpHeader.CONTENT_MD5,
HttpHeader.PROXY_AUTHENTICATE,
HttpHeader.PROXY_AUTHORIZATION);
final static EnumSet<HttpHeader> __DO_NOT_INDEX =
EnumSet.of(
// HttpHeader.C_PATH, // TODO more data needed
@ -69,18 +71,21 @@ public class HpackEncoder
HttpHeader.LAST_MODIFIED,
HttpHeader.SET_COOKIE,
HttpHeader.SET_COOKIE2);
final static EnumSet<HttpHeader> __NEVER_INDEX =
EnumSet.of(
HttpHeader.AUTHORIZATION,
HttpHeader.SET_COOKIE,
HttpHeader.SET_COOKIE2);
private static final PreEncodedHttpField CONNECTION_TE = new PreEncodedHttpField(HttpHeader.CONNECTION, "te");
private static final PreEncodedHttpField TE_TRAILERS = new PreEncodedHttpField(HttpHeader.TE, "trailers");
private static final Trie<Boolean> specialHopHeaders = new ArrayTrie<>(6);
static
{
for (HttpStatus.Code code : HttpStatus.Code.values())
__status[code.getCode()]=new PreEncodedHttpField(HttpHeader.C_STATUS,Integer.toString(code.getCode()));
specialHopHeaders.put("close", true);
specialHopHeaders.put("te", true);
}
private final HpackContext _context;
@ -174,9 +179,30 @@ public class HpackEncoder
encode(buffer,status);
}
// Add all the other fields
for (HttpField field : metadata)
encode(buffer,field);
// Add all non-connection fields.
HttpFields fields = metadata.getFields();
if (fields != null)
{
Set<String> hopHeaders = fields.getCSV(HttpHeader.CONNECTION, false).stream()
.filter(v -> specialHopHeaders.get(v) == Boolean.TRUE)
.map(StringUtil::asciiToLowerCase)
.collect(Collectors.toSet());
for (HttpField field : fields)
{
if (field.getHeader() == HttpHeader.CONNECTION)
continue;
if (!hopHeaders.isEmpty() && hopHeaders.contains(StringUtil.asciiToLowerCase(field.getName())))
continue;
if (field.getHeader() == HttpHeader.TE)
{
if (!field.contains("trailers"))
continue;
encode(buffer, CONNECTION_TE);
encode(buffer, TE_TRAILERS);
}
encode(buffer,field);
}
}
// Check size
if (_maxHeaderListSize>0 && _headerListSize>_maxHeaderListSize)
@ -305,7 +331,7 @@ public class HpackEncoder
encoding="Lit"+
((name==null)?"HuffN":("IdxN"+(name.isStatic()?"S":"")+(1+NBitInteger.octectsNeeded(4,_context.index(name)))))+
(huffman?"HuffV":"LitV")+
(indexed?"Idx":(never_index?"!!Idx":"!Idx"));
(never_index?"!!Idx":"!Idx");
}
else if (field_size>=_context.getMaxDynamicTableSize() || header==HttpHeader.CONTENT_LENGTH && field.getValue().length()>2)
{

View File

@ -0,0 +1,65 @@
//
// ========================================================================
// Copyright (c) 1995-2018 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.http2.hpack;
@SuppressWarnings("serial")
public abstract class HpackException extends Exception
{
HpackException(String messageFormat, Object... args)
{
super(String.format(messageFormat, args));
}
/**
* A Stream HPACK exception.
* <p>Stream exceptions are not fatal to the connection and the
* hpack state is complete and able to continue handling other
* decoding/encoding for the session.
* </p>
*/
public static class StreamException extends HpackException
{
StreamException(String messageFormat, Object... args)
{
super(messageFormat,args);
}
}
/**
* A Session HPACK Exception.
* <p>Session exceptions are fatal for the stream and the HPACK
* state is unable to decode/encode further. </p>
*/
public static class SessionException extends HpackException
{
SessionException(String messageFormat, Object... args)
{
super(messageFormat,args);
}
}
public static class CompressionException extends SessionException
{
public CompressionException(String messageFormat, Object... args)
{
super(messageFormat,args);
}
}
}

View File

@ -287,6 +287,7 @@ public class Huffman
};
static final int[][] LCCODES = new int[CODES.length][];
static final char EOS = 256;
// Huffman decode tree stored in a flattened char array for good
// locality of reference.
@ -344,24 +345,25 @@ public class Huffman
}
}
public static String decode(ByteBuffer buffer)
public static String decode(ByteBuffer buffer) throws HpackException.CompressionException
{
return decode(buffer,buffer.remaining());
}
public static String decode(ByteBuffer buffer,int length)
public static String decode(ByteBuffer buffer,int length) throws HpackException.CompressionException
{
StringBuilder out = new StringBuilder(length*2);
int node = 0;
int current = 0;
int bits = 0;
byte[] array = buffer.array();
int position=buffer.position();
int start=buffer.arrayOffset()+position;
int end=start+length;
buffer.position(position+length);
for (int i=start; i<end; i++)
{
int b = array[i]&0xFF;
@ -373,6 +375,9 @@ public class Huffman
node = tree[node*256+c];
if (rowbits[node]!=0)
{
if(rowsym[node] == EOS)
throw new HpackException.CompressionException("EOS in content");
// terminal node
out.append(rowsym[node]);
bits -= rowbits[node];
@ -389,18 +394,30 @@ public class Huffman
while (bits > 0)
{
int c = (current << (8 - bits)) & 0xFF;
int lastNode = node;
node = tree[node*256+c];
if (rowbits[node]==0 || rowbits[node] > bits)
if (rowbits[node]==0 || rowbits[node] > bits)
{
int requiredPadding = 0;
for(int i=0; i<bits; i++)
requiredPadding = (requiredPadding << 1) | 1;
if((c>>(8-bits)) != requiredPadding)
throw new HpackException.CompressionException("Incorrect padding");
node = lastNode;
break;
if (rowbits[node]==0)
throw new IllegalStateException();
}
out.append(rowsym[node]);
bits -= rowbits[node];
node = 0;
}
if(node != 0)
throw new HpackException.CompressionException("Bad termination");
return out.toString();
}

View File

@ -20,27 +20,29 @@
package org.eclipse.jetty.http2.hpack;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HostPortHttpField;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.hpack.HpackException.SessionException;
public class MetaDataBuilder
{
private final int _maxSize;
private int _size;
private int _status;
private int _status=-1;
private String _method;
private HttpScheme _scheme;
private HostPortHttpField _authority;
private String _path;
private long _contentLength=Long.MIN_VALUE;
private HttpFields _fields = new HttpFields(10);
private HpackException.StreamException _streamException;
private boolean _request;
private boolean _response;
/**
* @param maxHeadersSize The maximum size of the headers, expressed as total name and value characters.
@ -66,7 +68,7 @@ public class MetaDataBuilder
return _size;
}
public void emit(HttpField field)
public void emit(HttpField field) throws HpackException.SessionException
{
HttpHeader header = field.getHeader();
String name = field.getName();
@ -74,7 +76,7 @@ public class MetaDataBuilder
int field_size = name.length() + (value == null ? 0 : value.length());
_size+=field_size+32;
if (_size>_maxSize)
throw new BadMessageException(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431,"Header size "+_size+">"+_maxSize);
throw new HpackException.SessionException("Header Size %d > %d",_size,_maxSize);
if (field instanceof StaticTableHttpField)
{
@ -82,15 +84,21 @@ public class MetaDataBuilder
switch(header)
{
case C_STATUS:
_status=(Integer)staticField.getStaticValue();
if(checkHeader(header, _status))
_status = (Integer)staticField.getStaticValue();
_response = true;
break;
case C_METHOD:
_method=value;
if(checkPseudoHeader(header, _method))
_method = value;
_request = true;
break;
case C_SCHEME:
_scheme = (HttpScheme)staticField.getStaticValue();
if(checkPseudoHeader(header, _scheme))
_scheme = (HttpScheme)staticField.getStaticValue();
_request = true;
break;
default:
@ -102,23 +110,32 @@ public class MetaDataBuilder
switch(header)
{
case C_STATUS:
_status=field.getIntValue();
if(checkHeader(header, _status))
_status = field.getIntValue();
_response = true;
break;
case C_METHOD:
_method=value;
if(checkPseudoHeader(header, _method))
_method = value;
_request = true;
break;
case C_SCHEME:
if (value != null)
if(checkPseudoHeader(header, _scheme) && value != null)
_scheme = HttpScheme.CACHE.get(value);
_request = true;
break;
case C_AUTHORITY:
if (field instanceof HostPortHttpField)
_authority = (HostPortHttpField)field;
else if (value != null)
_authority = new AuthorityHttpField(value);
if(checkPseudoHeader(header, _authority))
{
if (field instanceof HostPortHttpField)
_authority = (HostPortHttpField)field;
else if (value != null)
_authority = new AuthorityHttpField(value);
}
_request = true;
break;
case HOST:
@ -134,54 +151,129 @@ public class MetaDataBuilder
break;
case C_PATH:
_path = value;
if(checkPseudoHeader(header, _path))
{
if (value!=null && value.length()>0)
_path = value;
else
streamException("No Path");
}
_request = true;
break;
case CONTENT_LENGTH:
_contentLength = field.getLongValue();
_fields.add(field);
break;
case TE:
if ("trailers".equalsIgnoreCase(value))
_fields.add(field);
else
streamException("Unsupported TE value '%s'", value);
break;
case CONNECTION:
if ("TE".equalsIgnoreCase(value))
_fields.add(field);
else
streamException("Connection specific field '%s'", header);
break;
default:
if (name.charAt(0)!=':')
default:
if (name.charAt(0)==':')
streamException("Unknown pseudo header '%s'", name);
else
_fields.add(field);
break;
}
}
else
{
if (name.charAt(0)!=':')
if (name.charAt(0)==':')
streamException("Unknown pseudo header '%s'",name);
else
_fields.add(field);
}
}
public MetaData build()
void streamException(String messageFormat, Object... args)
{
HpackException.StreamException stream = new HpackException.StreamException(messageFormat, args);
if (_streamException==null)
_streamException = stream;
else
_streamException.addSuppressed(stream);
}
private boolean checkHeader(HttpHeader header, int value)
{
if (_fields.size()>0)
{
streamException("Pseudo header %s after fields", header.asString());
return false;
}
if (value==-1)
return true;
streamException("Duplicate pseudo header %s", header.asString());
return false;
}
private boolean checkPseudoHeader(HttpHeader header, Object value)
{
if (_fields.size()>0)
{
streamException("Pseudo header %s after fields", header.asString());
return false;
}
if (value==null)
return true;
streamException("Duplicate pseudo header %s", header.asString());
return false;
}
public MetaData build() throws HpackException.StreamException
{
if (_streamException!=null)
{
_streamException.addSuppressed(new Throwable());
throw _streamException;
}
if (_request && _response)
throw new HpackException.StreamException("Request and Response headers");
HttpFields fields = _fields;
try
{
HttpFields fields = _fields;
_fields = new HttpFields(Math.max(10,fields.size()+5));
if (_method!=null)
if (_request)
{
if (_method==null)
throw new HpackException.StreamException("No Method");
if (_scheme==null)
throw new HpackException.StreamException("No Scheme");
if (_path==null)
throw new HpackException.StreamException("No Path");
return new MetaData.Request(_method,_scheme,_authority,_path,HttpVersion.HTTP_2,fields,_contentLength);
if (_status!=0)
}
if (_response)
return new MetaData.Response(HttpVersion.HTTP_2,_status,fields,_contentLength);
if (_path!=null)
fields.put(HttpHeader.C_PATH,_path);
if (_authority!=null)
fields.put(HttpHeader.HOST,_authority.getValue());
return new MetaData(HttpVersion.HTTP_2,fields,_contentLength);
}
finally
{
_status=0;
_fields = new HttpFields(Math.max(10,fields.size()+5));
_request=false;
_response=false;
_status=-1;
_method=null;
_scheme=null;
_authority=null;
_path=null;
_size=0;
_contentLength=Long.MIN_VALUE;
_contentLength=Long.MIN_VALUE;
}
}
@ -189,13 +281,14 @@ public class MetaDataBuilder
* Check that the max size will not be exceeded.
* @param length the length
* @param huffman the huffman name
* @throws SessionException in case of size errors
*/
public void checkSize(int length, boolean huffman)
public void checkSize(int length, boolean huffman) throws SessionException
{
// Apply a huffman fudge factor
if (huffman)
length=(length*4)/3;
if ((_size+length)>_maxSize)
throw new BadMessageException(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431,"Header size "+(_size+length)+">"+_maxSize);
throw new HpackException.SessionException("Header too large %d > %d", _size+length, _maxSize);
}
}

View File

@ -411,7 +411,7 @@ public class HpackContextTest
}
@Test
public void testStaticHuffmanValues()
public void testStaticHuffmanValues() throws Exception
{
HpackContext ctx = new HpackContext(4096);
for (int i=2;i<=14;i++)

View File

@ -22,22 +22,17 @@ package org.eclipse.jetty.http2.hpack;
import java.nio.ByteBuffer;
import java.util.Iterator;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.http2.hpack.HpackException.CompressionException;
import org.eclipse.jetty.http2.hpack.HpackException.StreamException;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.log.Log;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Test;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@ -47,7 +42,7 @@ import static org.junit.Assert.assertTrue;
public class HpackDecoderTest
{
@Test
public void testDecodeD_3()
public void testDecodeD_3() throws Exception
{
HpackDecoder decoder = new HpackDecoder(4096,8192);
@ -95,7 +90,7 @@ public class HpackDecoderTest
}
@Test
public void testDecodeD_4()
public void testDecodeD_4() throws Exception
{
HpackDecoder decoder = new HpackDecoder(4096,8192);
@ -128,7 +123,7 @@ public class HpackDecoderTest
}
@Test
public void testDecodeWithArrayOffset()
public void testDecodeWithArrayOffset() throws Exception
{
String value = "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==";
@ -152,7 +147,7 @@ public class HpackDecoderTest
}
@Test
public void testDecodeHuffmanWithArrayOffset()
public void testDecodeHuffmanWithArrayOffset() throws Exception
{
HpackDecoder decoder = new HpackDecoder(4096,8192);
@ -172,7 +167,7 @@ public class HpackDecoderTest
}
@Test
public void testNghttpx()
public void testNghttpx() throws Exception
{
// Response encoded by nghttpx
String encoded="886196C361Be940b6a65B6850400B8A00571972e080a62D1Bf5f87497cA589D34d1f9a0f0d0234327690Aa69D29aFcA954D3A5358980Ae112e0f7c880aE152A9A74a6bF3";
@ -194,30 +189,59 @@ public class HpackDecoderTest
@Test
public void testResize() throws Exception
{
String encoded = "3f6166871e33A13a47497f205f8841E92b043d492d49";
String encoded = "203f136687A0E41d139d090760881c6490B2Cd39Ba7f";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
HpackDecoder decoder = new HpackDecoder(4096, 8192);
MetaData metaData = decoder.decode(buffer);
assertThat(metaData.getFields().get(HttpHeader.HOST),is("aHostName"));
assertThat(metaData.getFields().get(HttpHeader.CONTENT_TYPE),is("some/content"));
assertThat(decoder.getHpackContext().getDynamicTableSize(),is(0));
assertThat(metaData.getFields().get(HttpHeader.HOST),is( "localhost0"));
assertThat(metaData.getFields().get(HttpHeader.COOKIE),is("abcdefghij"));
assertThat(decoder.getHpackContext().getMaxDynamicTableSize(),is(50));
assertThat(decoder.getHpackContext().size(),is(1));
}
@Test
public void testBadResize() throws Exception
{
/*
4. Dynamic Table Management
4.2. Maximum Table Size
× 1: Sends a dynamic table size update at the end of header block
-> The endpoint MUST treat this as a decoding error.
Expected: GOAWAY Frame (Error Code: COMPRESSION_ERROR)
Connection closed
*/
String encoded = "203f136687A0E41d139d090760881c6490B2Cd39Ba7f20";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
HpackDecoder decoder = new HpackDecoder(4096, 8192);
try
{
decoder.decode(buffer);
Assert.fail();
}
catch(CompressionException e)
{
Assert.assertThat(e.getMessage(),Matchers.containsString("Dynamic table resize after fields"));
}
}
@Test
public void testTooBigToIndex()
public void testTooBigToIndex() throws Exception
{
String encoded = "44FfEc02Df3990A190A0D4Ee5b3d2940Ec98Aa4a62D127D29e273a0aA20dEcAa190a503b262d8a2671D4A2672a927aA874988a2471D05510750c951139EdA2452a3a548cAa1aA90bE4B228342864A9E0D450A5474a92992a1aA513395448E3A0Aa17B96cFe3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f14E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F353F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F54f";
String encoded = "3f610f17FfEc02Df3990A190A0D4Ee5b3d2940Ec98Aa4a62D127D29e273a0aA20dEcAa190a503b262d8a2671D4A2672a927aA874988a2471D05510750c951139EdA2452a3a548cAa1aA90bE4B228342864A9E0D450A5474a92992a1aA513395448E3A0Aa17B96cFe3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f14E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F353F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F54f";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
HpackDecoder decoder = new HpackDecoder(128,8192);
MetaData metaData = decoder.decode(buffer);
assertThat(decoder.getHpackContext().getDynamicTableSize(),is(0));
assertThat(metaData.getFields().get(HttpHeader.C_PATH),Matchers.startsWith("This is a very large field"));
assertThat(metaData.getFields().get("host"),Matchers.startsWith("This is a very large field"));
}
@Test
public void testUnknownIndex()
public void testUnknownIndex() throws Exception
{
String encoded = "BE";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
@ -228,11 +252,334 @@ public class HpackDecoderTest
decoder.decode(buffer);
Assert.fail();
}
catch (BadMessageException e)
catch (HpackException.SessionException e)
{
assertThat(e.getCode(),equalTo(HttpStatus.BAD_REQUEST_400));
assertThat(e.getReason(),Matchers.startsWith("Unknown index"));
assertThat(e.getMessage(),Matchers.startsWith("Unknown index"));
}
}
/* 8.1.2.1. Pseudo-Header Fields */
@Test()
public void test8_1_2_1_PsuedoHeaderFields() throws Exception
{
// 1:Sends a HEADERS frame that contains a unknown pseudo-header field
MetaDataBuilder mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(":unknown","value"));
try
{
mdb.build();
Assert.fail();
}
catch(StreamException ex)
{
Assert.assertThat(ex.getMessage(),Matchers.containsString("Unknown pseudo header"));
}
// 2: Sends a HEADERS frame that contains the pseudo-header field defined for response
mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.C_SCHEME,"http"));
mdb.emit(new HttpField(HttpHeader.C_METHOD,"GET"));
mdb.emit(new HttpField(HttpHeader.C_PATH,"/path"));
mdb.emit(new HttpField(HttpHeader.C_STATUS,"100"));
try
{
mdb.build();
Assert.fail();
}
catch(StreamException ex)
{
Assert.assertThat(ex.getMessage(),Matchers.containsString("Request and Response headers"));
}
// 3: Sends a HEADERS frame that contains a pseudo-header field as trailers
// 4: Sends a HEADERS frame that contains a pseudo-header field that appears in a header block after a regular header field
mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.C_SCHEME,"http"));
mdb.emit(new HttpField(HttpHeader.C_METHOD,"GET"));
mdb.emit(new HttpField(HttpHeader.C_PATH,"/path"));
mdb.emit(new HttpField("Accept","No Compromise"));
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY,"localhost"));
try
{
mdb.build();
Assert.fail();
}
catch(StreamException ex)
{
Assert.assertThat(ex.getMessage(),Matchers.containsString("Pseudo header :authority after fields"));
}
}
@Test()
public void test8_1_2_2_ConnectionSpecificHeaderFields() throws Exception
{
MetaDataBuilder mdb;
// 1: Sends a HEADERS frame that contains the connection-specific header field
mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.CONNECTION,"value"));
try
{
mdb.build();
Assert.fail();
}
catch(StreamException ex)
{
Assert.assertThat(ex.getMessage(),Matchers.containsString("Connection specific field 'Connection'"));
}
// 2: Sends a HEADERS frame that contains the TE header field with any value other than "trailers"
mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.TE,"not_trailers"));
try
{
mdb.build();
Assert.fail();
}
catch(StreamException ex)
{
Assert.assertThat(ex.getMessage(),Matchers.containsString("Unsupported TE value 'not_trailers'"));
}
mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.CONNECTION,"TE"));
mdb.emit(new HttpField(HttpHeader.TE,"trailers"));
Assert.assertNotNull(mdb.build());
}
@Test()
public void test8_1_2_3_RequestPseudoHeaderFields() throws Exception
{
MetaDataBuilder mdb;
mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.C_METHOD,"GET"));
mdb.emit(new HttpField(HttpHeader.C_SCHEME,"http"));
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY,"localhost:8080"));
mdb.emit(new HttpField(HttpHeader.C_PATH,"/"));
Assert.assertThat(mdb.build(),Matchers.instanceOf(MetaData.Request.class));
// 1: Sends a HEADERS frame with empty ":path" pseudo-header field
mdb = new MetaDataBuilder(4096);
mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.C_METHOD,"GET"));
mdb.emit(new HttpField(HttpHeader.C_SCHEME,"http"));
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY,"localhost:8080"));
mdb.emit(new HttpField(HttpHeader.C_PATH,""));
try
{
mdb.build();
Assert.fail();
}
catch(StreamException ex)
{
Assert.assertThat(ex.getMessage(),Matchers.containsString("No Path"));
}
// 2: Sends a HEADERS frame that omits ":method" pseudo-header field
mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.C_SCHEME,"http"));
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY,"localhost:8080"));
mdb.emit(new HttpField(HttpHeader.C_PATH,"/"));
try
{
mdb.build();
Assert.fail();
}
catch(StreamException ex)
{
Assert.assertThat(ex.getMessage(),Matchers.containsString("No Method"));
}
// 3: Sends a HEADERS frame that omits ":scheme" pseudo-header field
mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.C_METHOD,"GET"));
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY,"localhost:8080"));
mdb.emit(new HttpField(HttpHeader.C_PATH,"/"));
try
{
mdb.build();
Assert.fail();
}
catch(StreamException ex)
{
Assert.assertThat(ex.getMessage(),Matchers.containsString("No Scheme"));
}
// 4: Sends a HEADERS frame that omits ":path" pseudo-header field
mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.C_METHOD,"GET"));
mdb.emit(new HttpField(HttpHeader.C_SCHEME,"http"));
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY,"localhost:8080"));
try
{
mdb.build();
Assert.fail();
}
catch(StreamException ex)
{
Assert.assertThat(ex.getMessage(),Matchers.containsString("No Path"));
}
// 5: Sends a HEADERS frame with duplicated ":method" pseudo-header field
mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.C_METHOD,"GET"));
mdb.emit(new HttpField(HttpHeader.C_METHOD,"GET"));
mdb.emit(new HttpField(HttpHeader.C_SCHEME,"http"));
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY,"localhost:8080"));
mdb.emit(new HttpField(HttpHeader.C_PATH,"/"));
try
{
mdb.build();
Assert.fail();
}
catch(StreamException ex)
{
Assert.assertThat(ex.getMessage(),Matchers.containsString("Duplicate"));
}
// 6: Sends a HEADERS frame with duplicated ":scheme" pseudo-header field
mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.C_METHOD,"GET"));
mdb.emit(new HttpField(HttpHeader.C_SCHEME,"http"));
mdb.emit(new HttpField(HttpHeader.C_SCHEME,"http"));
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY,"localhost:8080"));
mdb.emit(new HttpField(HttpHeader.C_PATH,"/"));
try
{
mdb.build();
Assert.fail();
}
catch(StreamException ex)
{
Assert.assertThat(ex.getMessage(),Matchers.containsString("Duplicate"));
}
}
@Test()
public void testHuffmanEncodedStandard() throws Exception
{
HpackDecoder decoder = new HpackDecoder(4096, 8192);
String encoded = "82868441" + "83" + "49509F";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
MetaData.Request request = (MetaData.Request)decoder.decode(buffer);
assertEquals("GET", request.getMethod());
assertEquals(HttpScheme.HTTP.asString(), request.getURI().getScheme());
assertEquals("/", request.getURI().getPath());
assertEquals("test", request.getURI().getHost());
assertFalse(request.iterator().hasNext());
}
/* 5.2.1: Sends a Huffman-encoded string literal representation with padding longer than 7 bits */
@Test()
public void testHuffmanEncodedExtraPadding() throws Exception
{
HpackDecoder decoder = new HpackDecoder(4096, 8192);
String encoded = "82868441" + "84" + "49509FFF";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
try
{
decoder.decode(buffer);
Assert.fail();
}
catch (CompressionException ex)
{
Assert.assertThat(ex.getMessage(), Matchers.containsString("Bad termination"));
}
}
/* 5.2.2: Sends a Huffman-encoded string literal representation padded by zero */
@Test()
public void testHuffmanEncodedZeroPadding() throws Exception
{
HpackDecoder decoder = new HpackDecoder(4096, 8192);
String encoded = "82868441" + "83" + "495090";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
try
{
decoder.decode(buffer);
Assert.fail();
}
catch (CompressionException ex)
{
Assert.assertThat(ex.getMessage(), Matchers.containsString("Incorrect padding"));
}
}
/* 5.2.3: Sends a Huffman-encoded string literal representation containing the EOS symbol */
@Test()
public void testHuffmanEncodedWithEOS() throws Exception
{
HpackDecoder decoder = new HpackDecoder(4096, 8192);
String encoded = "82868441" + "87" + "497FFFFFFF427F";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
try
{
decoder.decode(buffer);
Assert.fail();
}
catch (CompressionException ex)
{
Assert.assertThat(ex.getMessage(), Matchers.containsString("EOS in content"));
}
}
@Test()
public void testHuffmanEncodedOneIncompleteOctet() throws Exception
{
HpackDecoder decoder = new HpackDecoder(4096, 8192);
String encoded = "82868441" + "81" + "FE";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
try
{
decoder.decode(buffer);
Assert.fail();
}
catch (CompressionException ex)
{
Assert.assertThat(ex.getMessage(), Matchers.containsString("Bad termination"));
}
}
@Test()
public void testHuffmanEncodedTwoIncompleteOctet() throws Exception
{
HpackDecoder decoder = new HpackDecoder(4096, 8192);
String encoded = "82868441" + "82" + "FFFE";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
try
{
decoder.decode(buffer);
Assert.fail();
}
catch (CompressionException ex)
{
Assert.assertThat(ex.getMessage(), Matchers.containsString("Bad termination"));
}
}
}

View File

@ -249,4 +249,28 @@ public class HpackEncoderTest
context.get(HpackContext.STATIC_SIZE+1).getSize()+context.get(HpackContext.STATIC_SIZE+2).getSize()));
}
@Test
public void testResize()
{
HttpFields fields = new HttpFields();
fields.add("host", "localhost0");
fields.add("cookie","abcdefghij");
HpackEncoder encoder = new HpackEncoder(4096);
ByteBuffer buffer = BufferUtil.allocate(4096);
int pos = BufferUtil.flipToFill(buffer);
encoder.encodeMaxDynamicTableSize(buffer,0);
encoder.setRemoteMaxDynamicTableSize(50);
encoder.encode(buffer,new MetaData(HttpVersion.HTTP_2,fields));
BufferUtil.flipToFlush(buffer,pos);
HpackContext context = encoder.getHpackContext();
Assert.assertThat(context.getMaxDynamicTableSize(),Matchers.is(50));
Assert.assertThat(context.size(),Matchers.is(1));
}
}

View File

@ -18,15 +18,18 @@
package org.eclipse.jetty.http2.hpack;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.DateGenerator;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.MetaData.Response;
@ -35,10 +38,6 @@ import org.eclipse.jetty.util.BufferUtil;
import org.junit.Assert;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
public class HpackTest
{
final static HttpField ServerJetty = new PreEncodedHttpField(HttpHeader.SERVER,"jetty");
@ -46,7 +45,7 @@ public class HpackTest
final static HttpField Date = new PreEncodedHttpField(HttpHeader.DATE,DateGenerator.formatDate(TimeUnit.NANOSECONDS.toMillis(System.nanoTime())));
@Test
public void encodeDecodeResponseTest()
public void encodeDecodeResponseTest() throws Exception
{
HpackEncoder encoder = new HpackEncoder();
HpackDecoder decoder = new HpackDecoder(4096,8192);
@ -99,7 +98,7 @@ public class HpackTest
}
@Test
public void encodeDecodeTooLargeTest()
public void encodeDecodeTooLargeTest() throws Exception
{
HpackEncoder encoder = new HpackEncoder();
HpackDecoder decoder = new HpackDecoder(4096,164);
@ -131,14 +130,14 @@ public class HpackTest
decoder.decode(buffer);
Assert.fail();
}
catch(BadMessageException e)
catch(HpackException.SessionException e)
{
assertEquals(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431,e.getCode());
assertThat(e.getMessage(),containsString("Header too large"));
}
}
@Test
public void evictReferencedFieldTest()
public void evictReferencedFieldTest() throws Exception
{
HpackEncoder encoder = new HpackEncoder(200,200);
HpackDecoder decoder = new HpackDecoder(200,1024);

View File

@ -50,17 +50,6 @@ public class HuffmanTest
}
}
@Test
public void testDecodeTrailingFF() throws Exception
{
for (String[] test:tests)
{
byte[] encoded=TypeUtil.fromHexString(test[1]+"FF");
String decoded=Huffman.decode(ByteBuffer.wrap(encoded));
Assert.assertEquals(test[0],test[2],decoded);
}
}
@Test
public void testEncode() throws Exception
{

View File

@ -1,3 +1,3 @@
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
org.eclipse.jetty.http2.LEVEL=INFO
org.eclipse.jetty.http2.hpack.LEVEL=INFO
#org.eclipse.jetty.http2.LEVEL=DEBUG
#org.eclipse.jetty.http2.hpack.LEVEL=DEBUG

View File

@ -23,7 +23,6 @@ import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Queue;
import java.util.function.BiFunction;
@ -165,10 +164,8 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements Stream.Listen
HttpExchange exchange = getHttpExchange();
if (exchange == null)
return;
ErrorCode error = ErrorCode.from(frame.getError());
String reason = error == null ? "reset" : error.name().toLowerCase(Locale.ENGLISH);
exchange.getRequest().abort(new IOException(reason));
int error = frame.getError();
exchange.getRequest().abort(new IOException(ErrorCode.toString(error, "reset_code_" + error)));
}
@Override
@ -178,6 +175,13 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements Stream.Listen
return true;
}
@Override
public void onFailure(Stream stream, int error, String reason, Callback callback)
{
responseFailure(new IOException(String.format("%s/%s", ErrorCode.toString(error, null), reason)));
callback.succeeded();
}
private void notifyContent(HttpExchange exchange, DataFrame frame, Callback callback)
{
contentNotifier.offer(new DataInfo(exchange, frame, callback));

View File

@ -576,6 +576,40 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest
Assert.assertArrayEquals(bytes, response.getContent());
}
@Test
public void testInvalidResponseHPack() throws Exception
{
start(new ServerSessionListener.Adapter()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
// Produce an invalid HPACK block by adding a request pseudo-header to the response.
HttpFields fields = new HttpFields();
fields.put(":method", "get");
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, fields, 0);
int streamId = stream.getId();
HeadersFrame responseFrame = new HeadersFrame(streamId, response, null, false);
Callback.Completable callback = new Callback.Completable();
stream.headers(responseFrame, callback);
byte[] bytes = "hello".getBytes(StandardCharsets.US_ASCII);
callback.thenRun(() -> stream.data(new DataFrame(streamId, ByteBuffer.wrap(bytes), true), Callback.NOOP));
return null;
}
});
CountDownLatch latch = new CountDownLatch(1);
client.newRequest("localhost", connector.getLocalPort())
.timeout(5, TimeUnit.SECONDS)
.send(result ->
{
if (result.isFailed())
latch.countDown();
});
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
}
@Ignore
@Test
public void testExternalServer() throws Exception

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-parent</artifactId>
@ -10,55 +11,76 @@
<artifactId>http2-server</artifactId>
<name>Jetty :: HTTP2 :: Server</name>
<properties>
<properties>
<bundle-symbolic-name>${project.groupId}.server</bundle-symbolic-name>
</properties>
<build>
<plugins>
<plugin>
<groupId>com.github.madgnome</groupId>
<artifactId>h2spec-maven-plugin</artifactId>
<configuration>
<mainClass>org.eclipse.jetty.http2.server.H2SpecServer</mainClass>
<skip>${skipTests}</skip>
<!-- TODO: remove the exclusion when upgrading to h2spec 2.1.1+ -->
<excludeSpecs>
<excludeSpec>5.1 - closed: Sends a DATA frame</excludeSpec>
</excludeSpecs>
</configuration>
<executions>
<execution>
<id>h2spec</id>
<phase>test</phase>
<goals>
<goal>h2spec</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.alpn</groupId>
<artifactId>alpn-api</artifactId>
<version>${alpn.api.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlets</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-server</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.alpn</groupId>
<artifactId>alpn-api</artifactId>
<version>${alpn.api.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlets</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-server</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -50,6 +50,7 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
private int initialStreamRecvWindow = 512 * 1024;
private int maxConcurrentStreams = 128;
private int maxHeaderBlockFragment = 0;
private int maxFrameLength = Frame.DEFAULT_MAX_LENGTH;
private int maxSettingsKeys = SettingsFrame.DEFAULT_MAX_KEYS;
private FlowControlStrategy.Factory flowControlStrategyFactory = () -> new BufferingFlowControlStrategy(0.5F);
private long streamIdleTimeout;
@ -146,6 +147,17 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
this.streamIdleTimeout = streamIdleTimeout;
}
@ManagedAttribute("The max frame length in bytes")
public int getMaxFrameLength()
{
return maxFrameLength;
}
public void setMaxFrameLength(int maxFrameLength)
{
this.maxFrameLength = maxFrameLength;
}
@ManagedAttribute("The max number of keys in all SETTINGS frames")
public int getMaxSettingsKeys()
{
@ -217,6 +229,7 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
session.setWriteThreshold(getHttpConfiguration().getOutputBufferSize());
ServerParser parser = newServerParser(connector, session);
parser.setMaxFrameLength(getMaxFrameLength());
parser.setMaxSettingsKeys(getMaxSettingsKeys());
HTTP2Connection connection = new HTTP2ServerConnection(connector.getByteBufferPool(), connector.getExecutor(),

View File

@ -115,13 +115,10 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF
@Override
public void onClose(Session session, GoAwayFrame frame, Callback callback)
{
ErrorCode error = ErrorCode.from(frame.getError());
if (error == null)
error = ErrorCode.STREAM_CLOSED_ERROR;
String reason = frame.tryConvertPayload();
if (reason != null && !reason.isEmpty())
reason = " (" + reason + ")";
getConnection().onSessionFailure(new EofException("HTTP/2 " + error + reason), callback);
getConnection().onSessionFailure(new EofException(String.format("Close %s/%s", ErrorCode.toString(frame.getError(), null), reason)), callback);
}
@Override
@ -156,10 +153,13 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF
@Override
public void onReset(Stream stream, ResetFrame frame, Callback callback)
{
ErrorCode error = ErrorCode.from(frame.getError());
if (error == null)
error = ErrorCode.CANCEL_STREAM_ERROR;
getConnection().onStreamFailure((IStream)stream, new EofException("HTTP/2 " + error), callback);
getConnection().onStreamFailure((IStream)stream, new EofException("Reset " + ErrorCode.toString(frame.getError(), null)), callback);
}
@Override
public void onFailure(Stream stream, int error, String reason, Callback callback)
{
getConnection().onStreamFailure((IStream)stream, new EofException(String.format("Failure %s/%s", ErrorCode.toString(error, null), reason)), callback);
}
@Override

View File

@ -83,16 +83,39 @@ public class HTTP2ServerSession extends HTTP2Session implements ServerParser.Lis
if (LOG.isDebugEnabled())
LOG.debug("Received {}", frame);
int streamId = frame.getStreamId();
if (!isClientStream(streamId))
{
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "invalid_stream_id");
return;
}
IStream stream = getStream(streamId);
MetaData metaData = frame.getMetaData();
if (metaData.isRequest())
{
IStream stream = createRemoteStream(frame.getStreamId());
if (stream != null)
if (stream == null)
{
onStreamOpened(stream);
stream.process(frame, Callback.NOOP);
Stream.Listener listener = notifyNewStream(stream, frame);
stream.setListener(listener);
if (isRemoteStreamClosed(streamId))
{
onConnectionFailure(ErrorCode.STREAM_CLOSED_ERROR.code, "unexpected_headers_frame");
}
else
{
stream = createRemoteStream(streamId);
if (stream != null)
{
onStreamOpened(stream);
stream.process(frame, Callback.NOOP);
Stream.Listener listener = notifyNewStream(stream, frame);
stream.setListener(listener);
}
}
}
else
{
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "duplicate_stream");
}
}
else if (metaData.isResponse())
@ -102,8 +125,6 @@ public class HTTP2ServerSession extends HTTP2Session implements ServerParser.Lis
else
{
// Trailers.
int streamId = frame.getStreamId();
IStream stream = getStream(streamId);
if (stream != null)
{
stream.process(frame, Callback.NOOP);
@ -112,7 +133,8 @@ public class HTTP2ServerSession extends HTTP2Session implements ServerParser.Lis
else
{
if (LOG.isDebugEnabled())
LOG.debug("Ignoring {}, stream #{} not found", frame, streamId);
LOG.debug("Stream #{} not found", streamId);
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_headers_frame");
}
}
}

View File

@ -0,0 +1,48 @@
//
// ========================================================================
// Copyright (c) 1995-2018 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.http2.server;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
/**
* HTTP/2 server to run the 'h2spec' tool against.
*/
public class H2SpecServer
{
public static void main(String[] args) throws Exception
{
int port = Integer.parseInt(args[0]);
Server server = new Server();
HttpConfiguration http_config = new HttpConfiguration();
http_config.setRequestHeaderSize(16 * 1024);
HttpConnectionFactory http = new HttpConnectionFactory(http_config);
HTTP2CServerConnectionFactory h2c = new HTTP2CServerConnectionFactory(http_config);
ServerConnector connector = new ServerConnector(server, http, h2c);
connector.setPort(port);
server.addConnector(connector);
server.start();
}
}

View File

@ -887,6 +887,11 @@
<artifactId>buildnumber-maven-plugin</artifactId>
<version>1.4</version>
</plugin>
<plugin>
<groupId>com.github.madgnome</groupId>
<artifactId>h2spec-maven-plugin</artifactId>
<version>0.3</version>
</plugin>
</plugins>
</pluginManagement>
</build>

View File

@ -1,7 +0,0 @@
body {color: #2E2E2E; font-family:sans-serif; font-size:90%;}
h1 {font-variant: small-caps; font-size:130%; letter-spacing: 0.1em;}
h2 {font-variant: small-caps; font-size:100%; letter-spacing: 0.1em; margin-top:2em}
h3 {font-size:100%; letter-spacing: 0.1em;}
span.pass { color: green; }
span.fail { color:red; }