From cfe1baa048296e90f95773db680ca4d5ef24e0a2 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Sat, 17 Aug 2019 22:51:39 +0200 Subject: [PATCH] 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 --- .../org/eclipse/jetty/http2/HTTP2Session.java | 46 ++--- .../jetty/http2/frames/ContinuationFrame.java | 48 ++++++ .../jetty/http2/frames/UnknownFrame.java | 36 ++++ .../jetty/http2/parser/BodyParser.java | 8 +- .../http2/parser/ContinuationBodyParser.java | 13 +- .../jetty/http2/parser/DataBodyParser.java | 20 ++- .../jetty/http2/parser/HeadersBodyParser.java | 31 +++- .../eclipse/jetty/http2/parser/Parser.java | 29 +++- .../jetty/http2/parser/PingBodyParser.java | 12 +- .../http2/parser/PriorityBodyParser.java | 10 +- .../jetty/http2/parser/RateControl.java | 37 ++++ .../http2/parser/SettingsBodyParser.java | 51 +++--- .../jetty/http2/parser/UnknownBodyParser.java | 17 +- .../jetty/http2/parser/WindowRateControl.java | 61 +++++++ .../http2/parser/WindowUpdateBodyParser.java | 16 +- .../jetty/http2/frames/FrameFloodTest.java | 162 ++++++++++++++++++ .../AbstractHTTP2ServerConnectionFactory.java | 15 ++ 17 files changed, 523 insertions(+), 89 deletions(-) create mode 100644 jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/ContinuationFrame.java create mode 100644 jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/UnknownFrame.java create mode 100644 jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/RateControl.java create mode 100644 jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/WindowRateControl.java create mode 100644 jetty-http2/http2-common/src/test/java/org/eclipse/jetty/http2/frames/FrameFloodTest.java diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java index 770f1d07f28..1908884b48c 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java @@ -460,47 +460,33 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio int windowDelta = frame.getWindowDelta(); if (streamId > 0) { - if (windowDelta == 0) + IStream stream = getStream(streamId); + if (stream != null) { - 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)) { - 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); - } + reset(new ResetFrame(streamId, ErrorCode.FLOW_CONTROL_ERROR.code), Callback.NOOP); } else { - if (!isRemoteStreamClosed(streamId)) - onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_window_update_frame"); + stream.process(frame, Callback.NOOP); + onWindowUpdate(stream, frame); } } + else + { + if (!isRemoteStreamClosed(streamId)) + onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_window_update_frame"); + } } else { - if (windowDelta == 0) - { - onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "invalid_window_update_frame"); - } + int sessionSendWindow = updateSendWindow(0); + if (sumOverflows(sessionSendWindow, windowDelta)) + onConnectionFailure(ErrorCode.FLOW_CONTROL_ERROR.code, "invalid_flow_control_window"); else - { - int sessionSendWindow = updateSendWindow(0); - if (sumOverflows(sessionSendWindow, windowDelta)) - onConnectionFailure(ErrorCode.FLOW_CONTROL_ERROR.code, "invalid_flow_control_window"); - else - onWindowUpdate(null, frame); - } + onWindowUpdate(null, frame); } } diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/ContinuationFrame.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/ContinuationFrame.java new file mode 100644 index 00000000000..39d69310ce8 --- /dev/null +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/ContinuationFrame.java @@ -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()); + } +} diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/UnknownFrame.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/UnknownFrame.java new file mode 100644 index 00000000000..ffd88682b2a --- /dev/null +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/UnknownFrame.java @@ -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); + } +} diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/BodyParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/BodyParser.java index 82d18f96a84..c85aa9ba8bf 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/BodyParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/BodyParser.java @@ -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) diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/ContinuationBodyParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/ContinuationBodyParser.java index 65e47d1c827..b14cabf4e7d 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/ContinuationBodyParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/ContinuationBodyParser.java @@ -23,27 +23,38 @@ 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 diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/DataBodyParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/DataBodyParser.java index ac9e7bab991..5ea1a5bca81 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/DataBodyParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/DataBodyParser.java @@ -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); } diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/HeadersBodyParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/HeadersBodyParser.java index febdefb6c25..9c53eed6a9e 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/HeadersBodyParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/HeadersBodyParser.java @@ -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,7 +187,15 @@ 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); } diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/Parser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/Parser.java index 15920bebcdd..3b0efca7c20 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/Parser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/Parser.java @@ -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 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 diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PingBodyParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PingBodyParser.java index 8cee350e91e..675ba6d14ac 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PingBodyParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PingBodyParser.java @@ -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; diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PriorityBodyParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PriorityBodyParser.java index a9d11398987..67629f4fa09 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PriorityBodyParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PriorityBodyParser.java @@ -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; diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/RateControl.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/RateControl.java new file mode 100644 index 00000000000..16d82fa4e77 --- /dev/null +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/RateControl.java @@ -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 +{ + /** + *

Applications should call this method when they want to signal an + * event that is subject to rate control.

+ *

Implementations should return true if the event does not exceed + * the desired rate, or false to signal that the event exceeded the + * desired rate.

+ * + * @param event the event subject to rate control + * @return whether the rate is within limits + */ + public boolean onEvent(Object event); +} diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/SettingsBodyParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/SettingsBodyParser.java index 741dd95981d..3bf278f1089 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/SettingsBodyParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/SettingsBodyParser.java @@ -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 frameRef = new AtomicReference<>(); - SettingsBodyParser parser = new SettingsBodyParser(null, null) + AtomicReference 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 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(); } diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/UnknownBodyParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/UnknownBodyParser.java index dbb29bfede2..0d3f3840490 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/UnknownBodyParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/UnknownBodyParser.java @@ -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) diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/WindowRateControl.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/WindowRateControl.java new file mode 100644 index 00000000000..f2cef157e47 --- /dev/null +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/WindowRateControl.java @@ -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; + +/** + *

An implementation of {@link RateControl} that limits the number of + * events within a time period.

+ *

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}.

+ */ +public class WindowRateControl implements RateControl +{ + private final Queue 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; + } +} diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/WindowUpdateBodyParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/WindowUpdateBodyParser.java index 78505540939..2c3ec6b736c 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/WindowUpdateBodyParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/WindowUpdateBodyParser.java @@ -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; diff --git a/jetty-http2/http2-common/src/test/java/org/eclipse/jetty/http2/frames/FrameFloodTest.java b/jetty-http2/http2-common/src/test/java/org/eclipse/jetty/http2/frames/FrameFloodTest.java new file mode 100644 index 00000000000..aa9f8073068 --- /dev/null +++ b/jetty-http2/http2-common/src/test/java/org/eclipse/jetty/http2/frames/FrameFloodTest.java @@ -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)); + } + } +} diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/AbstractHTTP2ServerConnectionFactory.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/AbstractHTTP2ServerConnectionFactory.java index 82312d11e17..5824d276277 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/AbstractHTTP2ServerConnectionFactory.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/AbstractHTTP2ServerConnectionFactory.java @@ -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);