Issue #3978 - HTTP/2 vulnerabilities.
Implemented rate control for HTTP/2 frames using a single RateControl object to avoid that each individual vulnerability is within limits, but combined they still overload the server. Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
parent
47759b3f9b
commit
cfe1baa048
|
@ -459,12 +459,6 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
int streamId = frame.getStreamId();
|
||||
int windowDelta = frame.getWindowDelta();
|
||||
if (streamId > 0)
|
||||
{
|
||||
if (windowDelta == 0)
|
||||
{
|
||||
reset(new ResetFrame(streamId, ErrorCode.PROTOCOL_ERROR.code), Callback.NOOP);
|
||||
}
|
||||
else
|
||||
{
|
||||
IStream stream = getStream(streamId);
|
||||
if (stream != null)
|
||||
|
@ -486,13 +480,6 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_window_update_frame");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (windowDelta == 0)
|
||||
{
|
||||
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "invalid_window_update_frame");
|
||||
}
|
||||
else
|
||||
{
|
||||
int sessionSendWindow = updateSendWindow(0);
|
||||
|
@ -502,7 +489,6 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
onWindowUpdate(null, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStreamFailure(int streamId, int error, String reason)
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2019 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 ContinuationFrame extends Frame
|
||||
{
|
||||
private final int streamId;
|
||||
private final boolean endHeaders;
|
||||
|
||||
public ContinuationFrame(int streamId, boolean endHeaders)
|
||||
{
|
||||
super(FrameType.CONTINUATION);
|
||||
this.streamId = streamId;
|
||||
this.endHeaders = endHeaders;
|
||||
}
|
||||
|
||||
public int getStreamId()
|
||||
{
|
||||
return streamId;
|
||||
}
|
||||
|
||||
public boolean isEndHeaders()
|
||||
{
|
||||
return endHeaders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("%s#%d{end=%b}", super.toString(), getStreamId(), isEndHeaders());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2019 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 UnknownFrame extends Frame
|
||||
{
|
||||
private final int frameType;
|
||||
|
||||
public UnknownFrame(int frameType)
|
||||
{
|
||||
super(null);
|
||||
this.frameType = frameType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("%s,t=%d", super.toString(), frameType);
|
||||
}
|
||||
}
|
|
@ -96,6 +96,11 @@ public abstract class BodyParser
|
|||
return headerParser.getLength();
|
||||
}
|
||||
|
||||
protected int getFrameType()
|
||||
{
|
||||
return headerParser.getFrameType();
|
||||
}
|
||||
|
||||
protected void notifyData(DataFrame frame)
|
||||
{
|
||||
try
|
||||
|
@ -223,9 +228,10 @@ public abstract class BodyParser
|
|||
}
|
||||
}
|
||||
|
||||
protected void streamFailure(int streamId, int error, String reason)
|
||||
protected boolean streamFailure(int streamId, int error, String reason)
|
||||
{
|
||||
notifyStreamFailure(streamId, error, reason);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void notifyStreamFailure(int streamId, int error, String reason)
|
||||
|
|
|
@ -23,28 +23,39 @@ import java.nio.ByteBuffer;
|
|||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http2.ErrorCode;
|
||||
import org.eclipse.jetty.http2.Flags;
|
||||
import org.eclipse.jetty.http2.frames.ContinuationFrame;
|
||||
import org.eclipse.jetty.http2.frames.HeadersFrame;
|
||||
|
||||
public class ContinuationBodyParser extends BodyParser
|
||||
{
|
||||
private final HeaderBlockParser headerBlockParser;
|
||||
private final HeaderBlockFragments headerBlockFragments;
|
||||
private final RateControl rateControl;
|
||||
private State state = State.PREPARE;
|
||||
private int length;
|
||||
|
||||
public ContinuationBodyParser(HeaderParser headerParser, Parser.Listener listener, HeaderBlockParser headerBlockParser, HeaderBlockFragments headerBlockFragments)
|
||||
public ContinuationBodyParser(HeaderParser headerParser, Parser.Listener listener, HeaderBlockParser headerBlockParser, HeaderBlockFragments headerBlockFragments, RateControl rateControl)
|
||||
{
|
||||
super(headerParser, listener);
|
||||
this.headerBlockParser = headerBlockParser;
|
||||
this.headerBlockFragments = headerBlockFragments;
|
||||
this.rateControl = rateControl;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void emptyBody(ByteBuffer buffer)
|
||||
{
|
||||
if (hasFlag(Flags.END_HEADERS))
|
||||
{
|
||||
onHeaders();
|
||||
}
|
||||
else
|
||||
{
|
||||
ContinuationFrame frame = new ContinuationFrame(getStreamId(), hasFlag(Flags.END_HEADERS));
|
||||
if (rateControl != null && !rateControl.onEvent(frame))
|
||||
connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_continuation_frame_rate");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean parse(ByteBuffer buffer)
|
||||
|
|
|
@ -26,14 +26,16 @@ import org.eclipse.jetty.util.BufferUtil;
|
|||
|
||||
public class DataBodyParser extends BodyParser
|
||||
{
|
||||
private final RateControl rateControl;
|
||||
private State state = State.PREPARE;
|
||||
private int padding;
|
||||
private int paddingLength;
|
||||
private int length;
|
||||
|
||||
public DataBodyParser(HeaderParser headerParser, Parser.Listener listener)
|
||||
public DataBodyParser(HeaderParser headerParser, Parser.Listener listener, RateControl rateControl)
|
||||
{
|
||||
super(headerParser, listener);
|
||||
this.rateControl = rateControl;
|
||||
}
|
||||
|
||||
private void reset()
|
||||
|
@ -48,9 +50,17 @@ public class DataBodyParser extends BodyParser
|
|||
protected void emptyBody(ByteBuffer buffer)
|
||||
{
|
||||
if (isPadding())
|
||||
{
|
||||
connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_data_frame");
|
||||
}
|
||||
else
|
||||
onData(BufferUtil.EMPTY_BUFFER, false, 0);
|
||||
{
|
||||
DataFrame frame = new DataFrame(getStreamId(), BufferUtil.EMPTY_BUFFER, isEndStream());
|
||||
if (!isEndStream() && rateControl != null && !rateControl.onEvent(frame))
|
||||
connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_data_frame_rate");
|
||||
else
|
||||
onData(frame);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -134,7 +144,11 @@ public class DataBodyParser extends BodyParser
|
|||
|
||||
private void onData(ByteBuffer buffer, boolean fragment, int padding)
|
||||
{
|
||||
DataFrame frame = new DataFrame(getStreamId(), buffer, !fragment && isEndStream(), padding);
|
||||
onData(new DataFrame(getStreamId(), buffer, !fragment && isEndStream(), padding));
|
||||
}
|
||||
|
||||
private void onData(DataFrame frame)
|
||||
{
|
||||
notifyData(frame);
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ public class HeadersBodyParser extends BodyParser
|
|||
{
|
||||
private final HeaderBlockParser headerBlockParser;
|
||||
private final HeaderBlockFragments headerBlockFragments;
|
||||
private final RateControl rateControl;
|
||||
private State state = State.PREPARE;
|
||||
private int cursor;
|
||||
private int length;
|
||||
|
@ -40,11 +41,12 @@ public class HeadersBodyParser extends BodyParser
|
|||
private int parentStreamId;
|
||||
private int weight;
|
||||
|
||||
public HeadersBodyParser(HeaderParser headerParser, Parser.Listener listener, HeaderBlockParser headerBlockParser, HeaderBlockFragments headerBlockFragments)
|
||||
public HeadersBodyParser(HeaderParser headerParser, Parser.Listener listener, HeaderBlockParser headerBlockParser, HeaderBlockFragments headerBlockFragments, RateControl rateControl)
|
||||
{
|
||||
super(headerParser, listener);
|
||||
this.headerBlockParser = headerBlockParser;
|
||||
this.headerBlockFragments = headerBlockFragments;
|
||||
this.rateControl = rateControl;
|
||||
}
|
||||
|
||||
private void reset()
|
||||
|
@ -61,17 +63,23 @@ public class HeadersBodyParser extends BodyParser
|
|||
@Override
|
||||
protected void emptyBody(ByteBuffer buffer)
|
||||
{
|
||||
if (hasFlag(Flags.END_HEADERS))
|
||||
if (hasFlag(Flags.PRIORITY))
|
||||
{
|
||||
connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_headers_priority_frame");
|
||||
}
|
||||
else if (hasFlag(Flags.END_HEADERS))
|
||||
{
|
||||
MetaData metaData = headerBlockParser.parse(BufferUtil.EMPTY_BUFFER, 0);
|
||||
onHeaders(0, 0, false, metaData);
|
||||
HeadersFrame frame = new HeadersFrame(getStreamId(), metaData, null, isEndStream());
|
||||
if (rateControl != null && !rateControl.onEvent(frame))
|
||||
connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_headers_frame_rate");
|
||||
else
|
||||
onHeaders(frame);
|
||||
}
|
||||
else
|
||||
{
|
||||
headerBlockFragments.setStreamId(getStreamId());
|
||||
headerBlockFragments.setEndStream(isEndStream());
|
||||
if (hasFlag(Flags.PRIORITY))
|
||||
connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_headers_priority_frame");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,8 +187,16 @@ public class HeadersBodyParser extends BodyParser
|
|||
state = State.PADDING;
|
||||
loop = paddingLength == 0;
|
||||
if (metaData != HeaderBlockParser.STREAM_FAILURE)
|
||||
{
|
||||
onHeaders(parentStreamId, weight, exclusive, metaData);
|
||||
}
|
||||
else
|
||||
{
|
||||
HeadersFrame frame = new HeadersFrame(getStreamId(), metaData, null, isEndStream());
|
||||
if (rateControl != null && !rateControl.onEvent(frame))
|
||||
connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_headers_frame_rate");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -230,6 +246,11 @@ public class HeadersBodyParser extends BodyParser
|
|||
if (hasFlag(Flags.PRIORITY))
|
||||
priorityFrame = new PriorityFrame(getStreamId(), parentStreamId, weight, exclusive);
|
||||
HeadersFrame frame = new HeadersFrame(getStreamId(), metaData, priorityFrame, isEndStream());
|
||||
onHeaders(frame);
|
||||
}
|
||||
|
||||
private void onHeaders(HeadersFrame frame)
|
||||
{
|
||||
notifyHeaders(frame);
|
||||
}
|
||||
|
||||
|
|
|
@ -54,8 +54,9 @@ public class Parser
|
|||
private final HpackDecoder hpackDecoder;
|
||||
private final BodyParser[] bodyParsers;
|
||||
private UnknownBodyParser unknownBodyParser;
|
||||
private int maxFrameLength;
|
||||
private int maxFrameLength = Frame.DEFAULT_MAX_LENGTH;
|
||||
private int maxSettingsKeys = SettingsFrame.DEFAULT_MAX_KEYS;
|
||||
private RateControl rateControl;
|
||||
private boolean continuation;
|
||||
private State state = State.HEADER;
|
||||
|
||||
|
@ -65,26 +66,26 @@ public class Parser
|
|||
this.listener = listener;
|
||||
this.headerParser = new HeaderParser();
|
||||
this.hpackDecoder = new HpackDecoder(maxDynamicTableSize, maxHeaderSize);
|
||||
this.maxFrameLength = Frame.DEFAULT_MAX_LENGTH;
|
||||
this.bodyParsers = new BodyParser[FrameType.values().length];
|
||||
}
|
||||
|
||||
public void init(UnaryOperator<Listener> wrapper)
|
||||
{
|
||||
Listener listener = wrapper.apply(this.listener);
|
||||
unknownBodyParser = new UnknownBodyParser(headerParser, listener);
|
||||
RateControl rateControl = getRateControl();
|
||||
unknownBodyParser = new UnknownBodyParser(headerParser, listener, rateControl);
|
||||
HeaderBlockParser headerBlockParser = new HeaderBlockParser(headerParser, byteBufferPool, hpackDecoder, unknownBodyParser);
|
||||
HeaderBlockFragments headerBlockFragments = new HeaderBlockFragments();
|
||||
bodyParsers[FrameType.DATA.getType()] = new DataBodyParser(headerParser, listener);
|
||||
bodyParsers[FrameType.HEADERS.getType()] = new HeadersBodyParser(headerParser, listener, headerBlockParser, headerBlockFragments);
|
||||
bodyParsers[FrameType.PRIORITY.getType()] = new PriorityBodyParser(headerParser, listener);
|
||||
bodyParsers[FrameType.DATA.getType()] = new DataBodyParser(headerParser, listener, rateControl);
|
||||
bodyParsers[FrameType.HEADERS.getType()] = new HeadersBodyParser(headerParser, listener, headerBlockParser, headerBlockFragments, rateControl);
|
||||
bodyParsers[FrameType.PRIORITY.getType()] = new PriorityBodyParser(headerParser, listener, rateControl);
|
||||
bodyParsers[FrameType.RST_STREAM.getType()] = new ResetBodyParser(headerParser, listener);
|
||||
bodyParsers[FrameType.SETTINGS.getType()] = new SettingsBodyParser(headerParser, listener, getMaxSettingsKeys());
|
||||
bodyParsers[FrameType.SETTINGS.getType()] = new SettingsBodyParser(headerParser, listener, getMaxSettingsKeys(), rateControl);
|
||||
bodyParsers[FrameType.PUSH_PROMISE.getType()] = new PushPromiseBodyParser(headerParser, listener, headerBlockParser);
|
||||
bodyParsers[FrameType.PING.getType()] = new PingBodyParser(headerParser, listener);
|
||||
bodyParsers[FrameType.PING.getType()] = new PingBodyParser(headerParser, listener, rateControl);
|
||||
bodyParsers[FrameType.GO_AWAY.getType()] = new GoAwayBodyParser(headerParser, listener);
|
||||
bodyParsers[FrameType.WINDOW_UPDATE.getType()] = new WindowUpdateBodyParser(headerParser, listener);
|
||||
bodyParsers[FrameType.CONTINUATION.getType()] = new ContinuationBodyParser(headerParser, listener, headerBlockParser, headerBlockFragments);
|
||||
bodyParsers[FrameType.CONTINUATION.getType()] = new ContinuationBodyParser(headerParser, listener, headerBlockParser, headerBlockFragments, rateControl);
|
||||
}
|
||||
|
||||
private void reset()
|
||||
|
@ -235,6 +236,16 @@ public class Parser
|
|||
this.maxSettingsKeys = maxSettingsKeys;
|
||||
}
|
||||
|
||||
public RateControl getRateControl()
|
||||
{
|
||||
return rateControl;
|
||||
}
|
||||
|
||||
public void setRateControl(RateControl rateControl)
|
||||
{
|
||||
this.rateControl = rateControl;
|
||||
}
|
||||
|
||||
protected void notifyConnectionFailure(int error, String reason)
|
||||
{
|
||||
try
|
||||
|
|
|
@ -26,13 +26,15 @@ import org.eclipse.jetty.http2.frames.PingFrame;
|
|||
|
||||
public class PingBodyParser extends BodyParser
|
||||
{
|
||||
private final RateControl rateControl;
|
||||
private State state = State.PREPARE;
|
||||
private int cursor;
|
||||
private byte[] payload;
|
||||
|
||||
public PingBodyParser(HeaderParser headerParser, Parser.Listener listener)
|
||||
public PingBodyParser(HeaderParser headerParser, Parser.Listener listener, RateControl rateControl)
|
||||
{
|
||||
super(headerParser, listener);
|
||||
this.rateControl = rateControl;
|
||||
}
|
||||
|
||||
private void reset()
|
||||
|
@ -66,7 +68,7 @@ public class PingBodyParser extends BodyParser
|
|||
if (buffer.remaining() >= 8)
|
||||
{
|
||||
buffer.get(payload);
|
||||
return onPing(payload);
|
||||
return onPing(buffer, payload);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -80,7 +82,7 @@ public class PingBodyParser extends BodyParser
|
|||
payload[8 - cursor] = buffer.get();
|
||||
--cursor;
|
||||
if (cursor == 0)
|
||||
return onPing(payload);
|
||||
return onPing(buffer, payload);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
@ -92,9 +94,11 @@ public class PingBodyParser extends BodyParser
|
|||
return false;
|
||||
}
|
||||
|
||||
private boolean onPing(byte[] payload)
|
||||
private boolean onPing(ByteBuffer buffer, byte[] payload)
|
||||
{
|
||||
PingFrame frame = new PingFrame(payload, hasFlag(Flags.ACK));
|
||||
if (rateControl != null && !rateControl.onEvent(frame))
|
||||
return connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_ping_frame_rate");
|
||||
reset();
|
||||
notifyPing(frame);
|
||||
return true;
|
||||
|
|
|
@ -25,14 +25,16 @@ import org.eclipse.jetty.http2.frames.PriorityFrame;
|
|||
|
||||
public class PriorityBodyParser extends BodyParser
|
||||
{
|
||||
private final RateControl rateControl;
|
||||
private State state = State.PREPARE;
|
||||
private int cursor;
|
||||
private boolean exclusive;
|
||||
private int parentStreamId;
|
||||
|
||||
public PriorityBodyParser(HeaderParser headerParser, Parser.Listener listener)
|
||||
public PriorityBodyParser(HeaderParser headerParser, Parser.Listener listener, RateControl rateControl)
|
||||
{
|
||||
super(headerParser, listener);
|
||||
this.rateControl = rateControl;
|
||||
}
|
||||
|
||||
private void reset()
|
||||
|
@ -103,7 +105,7 @@ public class PriorityBodyParser extends BodyParser
|
|||
if (getStreamId() == parentStreamId)
|
||||
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_priority_frame");
|
||||
int weight = (buffer.get() & 0xFF) + 1;
|
||||
return onPriority(parentStreamId, weight, exclusive);
|
||||
return onPriority(buffer, parentStreamId, weight, exclusive);
|
||||
}
|
||||
default:
|
||||
{
|
||||
|
@ -114,9 +116,11 @@ public class PriorityBodyParser extends BodyParser
|
|||
return false;
|
||||
}
|
||||
|
||||
private boolean onPriority(int parentStreamId, int weight, boolean exclusive)
|
||||
private boolean onPriority(ByteBuffer buffer, int parentStreamId, int weight, boolean exclusive)
|
||||
{
|
||||
PriorityFrame frame = new PriorityFrame(getStreamId(), parentStreamId, weight, exclusive);
|
||||
if (rateControl != null && !rateControl.onEvent(frame))
|
||||
return connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_priority_frame_rate");
|
||||
reset();
|
||||
notifyPriority(frame);
|
||||
return true;
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2019 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;
|
||||
|
||||
/**
|
||||
* Controls rate of events via {@link #onEvent(Object)}.
|
||||
*/
|
||||
public interface RateControl
|
||||
{
|
||||
/**
|
||||
* <p>Applications should call this method when they want to signal an
|
||||
* event that is subject to rate control.</p>
|
||||
* <p>Implementations should return true if the event does not exceed
|
||||
* the desired rate, or false to signal that the event exceeded the
|
||||
* desired rate.</p>
|
||||
*
|
||||
* @param event the event subject to rate control
|
||||
* @return whether the rate is within limits
|
||||
*/
|
||||
public boolean onEvent(Object event);
|
||||
}
|
|
@ -19,6 +19,7 @@
|
|||
package org.eclipse.jetty.http2.parser;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
@ -35,6 +36,7 @@ public class SettingsBodyParser extends BodyParser
|
|||
private static final Logger LOG = Log.getLogger(SettingsBodyParser.class);
|
||||
|
||||
private final int maxKeys;
|
||||
private final RateControl rateControl;
|
||||
private State state = State.PREPARE;
|
||||
private int cursor;
|
||||
private int length;
|
||||
|
@ -45,13 +47,14 @@ public class SettingsBodyParser extends BodyParser
|
|||
|
||||
public SettingsBodyParser(HeaderParser headerParser, Parser.Listener listener)
|
||||
{
|
||||
this(headerParser, listener, SettingsFrame.DEFAULT_MAX_KEYS);
|
||||
this(headerParser, listener, SettingsFrame.DEFAULT_MAX_KEYS, null);
|
||||
}
|
||||
|
||||
public SettingsBodyParser(HeaderParser headerParser, Parser.Listener listener, int maxKeys)
|
||||
public SettingsBodyParser(HeaderParser headerParser, Parser.Listener listener, int maxKeys, RateControl rateControl)
|
||||
{
|
||||
super(headerParser, listener);
|
||||
this.maxKeys = maxKeys;
|
||||
this.rateControl = rateControl;
|
||||
}
|
||||
|
||||
protected void reset()
|
||||
|
@ -72,7 +75,11 @@ public class SettingsBodyParser extends BodyParser
|
|||
@Override
|
||||
protected void emptyBody(ByteBuffer buffer)
|
||||
{
|
||||
onSettings(buffer, new HashMap<>());
|
||||
SettingsFrame frame = new SettingsFrame(Collections.emptyMap(), hasFlag(Flags.ACK));
|
||||
if (rateControl != null && !rateControl.onEvent(frame))
|
||||
connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_settings_frame");
|
||||
else
|
||||
onSettings(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -200,6 +207,11 @@ public class SettingsBodyParser extends BodyParser
|
|||
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_settings_max_frame_size");
|
||||
|
||||
SettingsFrame frame = new SettingsFrame(settings, hasFlag(Flags.ACK));
|
||||
return onSettings(frame);
|
||||
}
|
||||
|
||||
private boolean onSettings(SettingsFrame frame)
|
||||
{
|
||||
reset();
|
||||
notifySettings(frame);
|
||||
return true;
|
||||
|
@ -207,40 +219,25 @@ public class SettingsBodyParser extends BodyParser
|
|||
|
||||
public static SettingsFrame parseBody(final ByteBuffer buffer)
|
||||
{
|
||||
final int bodyLength = buffer.remaining();
|
||||
final AtomicReference<SettingsFrame> frameRef = new AtomicReference<>();
|
||||
SettingsBodyParser parser = new SettingsBodyParser(null, null)
|
||||
AtomicReference<SettingsFrame> frameRef = new AtomicReference<>();
|
||||
SettingsBodyParser parser = new SettingsBodyParser(new HeaderParser(), new Parser.Listener.Adapter()
|
||||
{
|
||||
@Override
|
||||
protected int getStreamId()
|
||||
public void onSettings(SettingsFrame frame)
|
||||
{
|
||||
return 0;
|
||||
frameRef.set(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getBodyLength()
|
||||
{
|
||||
return bodyLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onSettings(ByteBuffer buffer, Map<Integer, Integer> settings)
|
||||
{
|
||||
frameRef.set(new SettingsFrame(settings, false));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean connectionFailure(ByteBuffer buffer, int error, String reason)
|
||||
public void onConnectionFailure(int error, String reason)
|
||||
{
|
||||
frameRef.set(null);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
if (bodyLength == 0)
|
||||
parser.emptyBody(buffer);
|
||||
else
|
||||
});
|
||||
if (buffer.hasRemaining())
|
||||
parser.parse(buffer);
|
||||
else
|
||||
parser.emptyBody(buffer);
|
||||
return frameRef.get();
|
||||
}
|
||||
|
||||
|
|
|
@ -20,13 +20,19 @@ package org.eclipse.jetty.http2.parser;
|
|||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.eclipse.jetty.http2.ErrorCode;
|
||||
import org.eclipse.jetty.http2.frames.Frame;
|
||||
import org.eclipse.jetty.http2.frames.UnknownFrame;
|
||||
|
||||
public class UnknownBodyParser extends BodyParser
|
||||
{
|
||||
private final RateControl rateControl;
|
||||
private int cursor;
|
||||
|
||||
public UnknownBodyParser(HeaderParser headerParser, Parser.Listener listener)
|
||||
public UnknownBodyParser(HeaderParser headerParser, Parser.Listener listener, RateControl rateControl)
|
||||
{
|
||||
super(headerParser, listener);
|
||||
this.rateControl = rateControl;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -34,7 +40,14 @@ public class UnknownBodyParser extends BodyParser
|
|||
{
|
||||
int length = cursor == 0 ? getBodyLength() : cursor;
|
||||
cursor = consume(buffer, length);
|
||||
return cursor == 0;
|
||||
boolean parsed = cursor == 0;
|
||||
if (parsed && rateControl != null)
|
||||
{
|
||||
Frame frame = new UnknownFrame(getFrameType());
|
||||
if (!rateControl.onEvent(frame))
|
||||
return connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_unknown_frame");
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private int consume(ByteBuffer buffer, int length)
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2019 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.time.Duration;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
/**
|
||||
* <p>An implementation of {@link RateControl} that limits the number of
|
||||
* events within a time period.</p>
|
||||
* <p>Events are kept in a queue and for each event the queue is first
|
||||
* drained of the old events outside the time window, and then the new
|
||||
* event is added to the queue. If the size of the queue exceeds the max
|
||||
* number of events then {@link #onEvent(Object)} returns {@code false}.</p>
|
||||
*/
|
||||
public class WindowRateControl implements RateControl
|
||||
{
|
||||
private final Queue<Long> events = new ConcurrentLinkedQueue<>();
|
||||
private final int maxEvents;
|
||||
private final long window;
|
||||
|
||||
public WindowRateControl(int maxEvents, Duration window)
|
||||
{
|
||||
this.maxEvents = maxEvents;
|
||||
this.window = window.toNanos();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onEvent(Object event)
|
||||
{
|
||||
long now = System.nanoTime();
|
||||
while (true)
|
||||
{
|
||||
Long time = events.peek();
|
||||
if (time == null)
|
||||
break;
|
||||
if (now - time < window)
|
||||
break;
|
||||
events.poll();
|
||||
}
|
||||
events.add(now);
|
||||
return events.size() <= maxEvents;
|
||||
}
|
||||
}
|
|
@ -61,7 +61,7 @@ public class WindowUpdateBodyParser extends BodyParser
|
|||
if (buffer.remaining() >= 4)
|
||||
{
|
||||
windowDelta = buffer.getInt() & 0x7F_FF_FF_FF;
|
||||
return onWindowUpdate(windowDelta);
|
||||
return onWindowUpdate(buffer, windowDelta);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -78,7 +78,7 @@ public class WindowUpdateBodyParser extends BodyParser
|
|||
if (cursor == 0)
|
||||
{
|
||||
windowDelta &= 0x7F_FF_FF_FF;
|
||||
return onWindowUpdate(windowDelta);
|
||||
return onWindowUpdate(buffer, windowDelta);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -91,9 +91,17 @@ public class WindowUpdateBodyParser extends BodyParser
|
|||
return false;
|
||||
}
|
||||
|
||||
private boolean onWindowUpdate(int windowDelta)
|
||||
private boolean onWindowUpdate(ByteBuffer buffer, int windowDelta)
|
||||
{
|
||||
WindowUpdateFrame frame = new WindowUpdateFrame(getStreamId(), windowDelta);
|
||||
int streamId = getStreamId();
|
||||
if (windowDelta == 0)
|
||||
{
|
||||
if (streamId == 0)
|
||||
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_window_update_frame");
|
||||
else
|
||||
return streamFailure(streamId, ErrorCode.PROTOCOL_ERROR.code, "invalid_window_update_frame");
|
||||
}
|
||||
WindowUpdateFrame frame = new WindowUpdateFrame(streamId, windowDelta);
|
||||
reset();
|
||||
notifyWindowUpdate(frame);
|
||||
return true;
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2019 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.time.Duration;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http2.Flags;
|
||||
import org.eclipse.jetty.http2.hpack.HpackEncoder;
|
||||
import org.eclipse.jetty.http2.parser.Parser;
|
||||
import org.eclipse.jetty.http2.parser.WindowRateControl;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.io.MappedByteBufferPool;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.lessThan;
|
||||
|
||||
public class FrameFloodTest
|
||||
{
|
||||
private final ByteBufferPool byteBufferPool = new MappedByteBufferPool();
|
||||
|
||||
// Frame structure:
|
||||
// | Len0 | Len1 | Len2 | Type | Flags | StreamID0 |StreamID1 |StreamID2 |StreamID3 | Payload... |
|
||||
|
||||
private byte[] frameFrom(int length, int frameType, int flags, int streamId, byte[] payload)
|
||||
{
|
||||
byte[] result = new byte[3 + 1 + 1 + 4 + payload.length];
|
||||
result[0] = (byte)((length >>> 16) & 0xFF);
|
||||
result[1] = (byte)((length >>> 8) & 0xFF);
|
||||
result[2] = (byte)(length & 0xFF);
|
||||
result[3] = (byte)frameType;
|
||||
result[4] = (byte)flags;
|
||||
result[5] = (byte)((streamId >>> 24) & 0xFF);
|
||||
result[6] = (byte)((streamId >>> 16) & 0xFF);
|
||||
result[7] = (byte)((streamId >>> 8) & 0xFF);
|
||||
result[8] = (byte)(streamId & 0xFF);
|
||||
System.arraycopy(payload, 0, result, 9, payload.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDataFrameFlood()
|
||||
{
|
||||
byte[] payload = new byte[0];
|
||||
testFrameFlood(null, frameFrom(payload.length, FrameType.DATA.getType(), 0, 13, payload));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHeadersFrameFlood()
|
||||
{
|
||||
byte[] payload = new byte[0];
|
||||
testFrameFlood(null, frameFrom(payload.length, FrameType.HEADERS.getType(), Flags.END_HEADERS, 13, payload));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidHeadersFrameFlood()
|
||||
{
|
||||
// Invalid MetaData (no method, no scheme, etc).
|
||||
MetaData.Request metadata = new MetaData.Request(null, (String)null, null, null, HttpVersion.HTTP_2, null, -1);
|
||||
HpackEncoder encoder = new HpackEncoder();
|
||||
ByteBuffer buffer = ByteBuffer.allocate(1024);
|
||||
encoder.encode(buffer, metadata);
|
||||
buffer.flip();
|
||||
byte[] payload = new byte[buffer.remaining()];
|
||||
buffer.get(payload);
|
||||
testFrameFlood(null, frameFrom(payload.length, FrameType.HEADERS.getType(), Flags.END_HEADERS, 13, payload));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPriorityFrameFlood()
|
||||
{
|
||||
byte[] payload = new byte[]{0, 0, 0, 7, 0};
|
||||
testFrameFlood(null, frameFrom(payload.length, FrameType.PRIORITY.getType(), 0, 13, payload));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSettingsFrameFlood()
|
||||
{
|
||||
byte[] payload = new byte[0];
|
||||
testFrameFlood(null, frameFrom(payload.length, FrameType.SETTINGS.getType(), 0, 0, payload));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPingFrameFlood()
|
||||
{
|
||||
byte[] payload = {0, 0, 0, 0, 0, 0, 0, 0};
|
||||
testFrameFlood(null, frameFrom(payload.length, FrameType.PING.getType(), 0, 0, payload));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContinuationFrameFlood()
|
||||
{
|
||||
int streamId = 13;
|
||||
byte[] headersPayload = new byte[0];
|
||||
byte[] headersBytes = frameFrom(headersPayload.length, FrameType.HEADERS.getType(), 0, streamId, headersPayload);
|
||||
byte[] continuationPayload = new byte[0];
|
||||
testFrameFlood(headersBytes, frameFrom(continuationPayload.length, FrameType.CONTINUATION.getType(), 0, streamId, continuationPayload));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnknownFrameFlood()
|
||||
{
|
||||
byte[] payload = {0, 0, 0, 0};
|
||||
testFrameFlood(null, frameFrom(payload.length, 64, 0, 0, payload));
|
||||
}
|
||||
|
||||
private void testFrameFlood(byte[] preamble, byte[] bytes)
|
||||
{
|
||||
AtomicBoolean failed = new AtomicBoolean();
|
||||
Parser parser = new Parser(byteBufferPool, new Parser.Listener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public void onConnectionFailure(int error, String reason)
|
||||
{
|
||||
failed.set(true);
|
||||
}
|
||||
}, 4096, 8192);
|
||||
parser.setRateControl(new WindowRateControl(8, Duration.ofSeconds(1)));
|
||||
parser.init(UnaryOperator.identity());
|
||||
|
||||
if (preamble != null)
|
||||
{
|
||||
ByteBuffer buffer = ByteBuffer.wrap(preamble);
|
||||
while (buffer.hasRemaining())
|
||||
{
|
||||
parser.parse(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
while (!failed.get())
|
||||
{
|
||||
ByteBuffer buffer = ByteBuffer.wrap(bytes);
|
||||
while (buffer.hasRemaining())
|
||||
{
|
||||
parser.parse(buffer);
|
||||
}
|
||||
assertThat("too many frames allowed", ++count, lessThan(1024));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@
|
|||
package org.eclipse.jetty.http2.server;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
|
@ -34,7 +35,9 @@ import org.eclipse.jetty.http2.api.server.ServerSessionListener;
|
|||
import org.eclipse.jetty.http2.frames.Frame;
|
||||
import org.eclipse.jetty.http2.frames.SettingsFrame;
|
||||
import org.eclipse.jetty.http2.generator.Generator;
|
||||
import org.eclipse.jetty.http2.parser.RateControl;
|
||||
import org.eclipse.jetty.http2.parser.ServerParser;
|
||||
import org.eclipse.jetty.http2.parser.WindowRateControl;
|
||||
import org.eclipse.jetty.io.Connection;
|
||||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.server.AbstractConnectionFactory;
|
||||
|
@ -58,6 +61,7 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
|
|||
private int maxHeaderBlockFragment = 0;
|
||||
private int maxFrameLength = Frame.DEFAULT_MAX_LENGTH;
|
||||
private int maxSettingsKeys = SettingsFrame.DEFAULT_MAX_KEYS;
|
||||
private RateControl rateControl = new WindowRateControl(20, Duration.ofSeconds(1));
|
||||
private FlowControlStrategy.Factory flowControlStrategyFactory = () -> new BufferingFlowControlStrategy(0.5F);
|
||||
private long streamIdleTimeout;
|
||||
|
||||
|
@ -178,6 +182,16 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
|
|||
this.maxSettingsKeys = maxSettingsKeys;
|
||||
}
|
||||
|
||||
public RateControl getRateControl()
|
||||
{
|
||||
return rateControl;
|
||||
}
|
||||
|
||||
public void setRateControl(RateControl rateControl)
|
||||
{
|
||||
this.rateControl = rateControl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return -1
|
||||
* @deprecated feature removed, no replacement
|
||||
|
@ -240,6 +254,7 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
|
|||
ServerParser parser = newServerParser(connector, session);
|
||||
parser.setMaxFrameLength(getMaxFrameLength());
|
||||
parser.setMaxSettingsKeys(getMaxSettingsKeys());
|
||||
parser.setRateControl(getRateControl());
|
||||
|
||||
HTTP2Connection connection = new HTTP2ServerConnection(connector.getByteBufferPool(), connector.getExecutor(),
|
||||
endPoint, httpConfiguration, parser, session, getInputBufferSize(), listener);
|
||||
|
|
Loading…
Reference in New Issue