diff --git a/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/HTTP2Test.java b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/HTTP2Test.java index 132733ee8c2..b5be288a82b 100644 --- a/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/HTTP2Test.java +++ b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/HTTP2Test.java @@ -27,7 +27,6 @@ import java.util.Map; import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -46,6 +45,7 @@ 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.SettingsFrame; +import org.eclipse.jetty.http2.parser.RateControl; import org.eclipse.jetty.http2.parser.ServerParser; import org.eclipse.jetty.http2.server.RawHTTP2ServerConnectionFactory; import org.eclipse.jetty.server.Connector; @@ -152,7 +152,7 @@ public class HTTP2Test extends AbstractTest start(new HttpServlet() { @Override - protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.getOutputStream().write(content); } @@ -200,7 +200,7 @@ public class HTTP2Test 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 { IO.copy(request.getInputStream(), response.getOutputStream()); } @@ -244,7 +244,7 @@ public class HTTP2Test 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 { int download = request.getIntHeader(downloadBytes); byte[] content = new byte[download]; @@ -287,7 +287,7 @@ public class HTTP2Test extends AbstractTest start(new HttpServlet() { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) { response.setStatus(status); } @@ -322,7 +322,7 @@ public class HTTP2Test extends AbstractTest start(new HttpServlet() { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) { assertEquals(host, request.getServerName()); assertEquals(port, request.getServerPort()); @@ -747,7 +747,7 @@ public class HTTP2Test extends AbstractTest RawHTTP2ServerConnectionFactory connectionFactory = new RawHTTP2ServerConnectionFactory(new HttpConfiguration(), serverListener) { @Override - protected ServerParser newServerParser(Connector connector, ServerParser.Listener listener) + protected ServerParser newServerParser(Connector connector, ServerParser.Listener listener, RateControl rateControl) { return super.newServerParser(connector, new ServerParser.Listener.Wrapper(listener) { @@ -757,7 +757,7 @@ public class HTTP2Test extends AbstractTest super.onGoAway(frame); goAwayLatch.countDown(); } - }); + }, rateControl); } }; prepareServer(connectionFactory); 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..a8fb651121a 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 @@ -57,6 +57,7 @@ import org.eclipse.jetty.util.AtomicBiInteger; import org.eclipse.jetty.util.Atomics; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.CountingCallback; +import org.eclipse.jetty.util.MathUtils; import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.Retainable; import org.eclipse.jetty.util.annotation.ManagedAttribute; @@ -460,47 +461,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 (MathUtils.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 (MathUtils.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); } } @@ -515,19 +502,6 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio 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) { 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..01c6d29e570 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) @@ -239,4 +245,9 @@ public abstract class BodyParser LOG.info("Failure while notifying listener " + listener, x); } } + + protected boolean rateControlOnEvent(Object o) + { + return headerParser.getRateControl().onEvent(o); + } } 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..fc2e03e97a0 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,6 +23,7 @@ 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 @@ -43,7 +44,15 @@ public class ContinuationBodyParser extends BodyParser protected void emptyBody(ByteBuffer buffer) { if (hasFlag(Flags.END_HEADERS)) - onHeaders(); + { + onHeaders(buffer); + } + else + { + ContinuationFrame frame = new ContinuationFrame(getStreamId(), hasFlag(Flags.END_HEADERS)); + if (!rateControlOnEvent(frame)) + connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_continuation_frame_rate"); + } } @Override @@ -81,7 +90,7 @@ public class ContinuationBodyParser extends BodyParser headerBlockFragments.storeFragment(buffer, length, last); reset(); if (last) - return onHeaders(); + return onHeaders(buffer); return true; } } @@ -94,15 +103,20 @@ public class ContinuationBodyParser extends BodyParser return false; } - private boolean onHeaders() + private boolean onHeaders(ByteBuffer buffer) { ByteBuffer headerBlock = headerBlockFragments.complete(); MetaData metaData = headerBlockParser.parse(headerBlock, headerBlock.remaining()); + if (metaData == null) + return true; 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()); + if (metaData == HeaderBlockParser.STREAM_FAILURE) + { + if (!rateControlOnEvent(frame)) + return connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_continuation_frame_rate"); + } notifyHeaders(frame); return true; } 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..be0ede0a5cb 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 @@ -48,9 +48,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() && !rateControlOnEvent(frame)) + connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_data_frame_rate"); + else + onData(frame); + } } @Override @@ -134,7 +142,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/HeaderParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/HeaderParser.java index 0e4d2dc5e9e..e735d9ada3e 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/HeaderParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/HeaderParser.java @@ -30,14 +30,24 @@ import org.eclipse.jetty.http2.frames.FrameType; */ public class HeaderParser { + private final RateControl rateControl; private State state = State.LENGTH; private int cursor; - private int length; private int type; private int flags; private int streamId; + public HeaderParser(RateControl rateControl) + { + this.rateControl = rateControl; + } + + public RateControl getRateControl() + { + return rateControl; + } + protected void reset() { state = State.LENGTH; 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..3054d5ee439 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 @@ -61,17 +61,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 (!rateControlOnEvent(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 +185,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 (!rateControlOnEvent(frame)) + connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_headers_frame_rate"); + } } } else @@ -230,6 +244,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..7545b6fe159 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,18 +54,22 @@ 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 boolean continuation; private State state = State.HEADER; public Parser(ByteBufferPool byteBufferPool, Listener listener, int maxDynamicTableSize, int maxHeaderSize) + { + this(byteBufferPool, listener, maxDynamicTableSize, maxHeaderSize, RateControl.NO_RATE_CONTROL); + } + + public Parser(ByteBufferPool byteBufferPool, Listener listener, int maxDynamicTableSize, int maxHeaderSize, RateControl rateControl) { this.byteBufferPool = byteBufferPool; this.listener = listener; - this.headerParser = new HeaderParser(); + this.headerParser = new HeaderParser(rateControl == null ? RateControl.NO_RATE_CONTROL : rateControl); this.hpackDecoder = new HpackDecoder(maxDynamicTableSize, maxHeaderSize); - this.maxFrameLength = Frame.DEFAULT_MAX_LENGTH; this.bodyParsers = new BodyParser[FrameType.values().length]; } 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..e56e573236e 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 @@ -66,7 +66,7 @@ public class PingBodyParser extends BodyParser if (buffer.remaining() >= 8) { buffer.get(payload); - return onPing(payload); + return onPing(buffer, payload); } else { @@ -80,7 +80,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 +92,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 (!rateControlOnEvent(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..914e9387b30 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 @@ -103,7 +103,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 +114,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 (!rateControlOnEvent(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..be8f82a7cf2 --- /dev/null +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/RateControl.java @@ -0,0 +1,39 @@ +// +// ======================================================================== +// 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 +{ + public static final RateControl NO_RATE_CONTROL = event -> true; + + /** + *

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 true IFF 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/ServerParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/ServerParser.java index 49d40f8fd58..067e263cd33 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/ServerParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/ServerParser.java @@ -37,9 +37,9 @@ public class ServerParser extends Parser private State state = State.PREFACE; private boolean notifyPreface = true; - public ServerParser(ByteBufferPool byteBufferPool, Listener listener, int maxDynamicTableSize, int maxHeaderSize) + public ServerParser(ByteBufferPool byteBufferPool, Listener listener, int maxDynamicTableSize, int maxHeaderSize, RateControl rateControl) { - super(byteBufferPool, listener, maxDynamicTableSize, maxHeaderSize); + super(byteBufferPool, listener, maxDynamicTableSize, maxHeaderSize, rateControl); this.listener = listener; this.prefaceParser = new PrefaceParser(listener); } 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..b58fa8a8b51 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; @@ -72,7 +73,12 @@ public class SettingsBodyParser extends BodyParser @Override protected void emptyBody(ByteBuffer buffer) { - onSettings(buffer, new HashMap<>()); + boolean isReply = hasFlag(Flags.ACK); + SettingsFrame frame = new SettingsFrame(Collections.emptyMap(), isReply); + if (!isReply && !rateControlOnEvent(frame)) + connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_settings_frame_rate"); + else + onSettings(frame); } @Override @@ -200,6 +206,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 +218,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(RateControl.NO_RATE_CONTROL), 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..90293fee9db 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,6 +20,9 @@ package org.eclipse.jetty.http2.parser; import java.nio.ByteBuffer; +import org.eclipse.jetty.http2.ErrorCode; +import org.eclipse.jetty.http2.frames.UnknownFrame; + public class UnknownBodyParser extends BodyParser { private int cursor; @@ -34,7 +37,11 @@ public class UnknownBodyParser extends BodyParser { int length = cursor == 0 ? getBodyLength() : cursor; cursor = consume(buffer, length); - return cursor == 0; + boolean parsed = cursor == 0; + if (parsed && !rateControlOnEvent(new UnknownFrame(getFrameType()))) + return connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_unknown_frame_rate"); + + 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..00a2c769737 --- /dev/null +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/WindowRateControl.java @@ -0,0 +1,65 @@ +// +// ======================================================================== +// 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; +import java.util.concurrent.atomic.AtomicInteger; + +/** + *

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. The size of the queue is maintained + * separately in an AtomicInteger and if it 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 AtomicInteger size = new AtomicInteger(); + 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) + break; + if (events.remove(time)) + size.decrementAndGet(); + } + events.add(now + window); + return size.incrementAndGet() <= 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..04685dcb743 --- /dev/null +++ b/jetty-http2/http2-common/src/test/java/org/eclipse/jetty/http2/frames/FrameFloodTest.java @@ -0,0 +1,161 @@ +// +// ======================================================================== +// 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, 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-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackDecoder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackDecoder.java index 34c47201932..bb1d3ede5fb 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackDecoder.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackDecoder.java @@ -22,6 +22,7 @@ import java.nio.ByteBuffer; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpTokens; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http2.hpack.HpackContext.Entry; import org.eclipse.jetty.util.TypeUtil; @@ -175,14 +176,35 @@ public class HpackDecoder name = Huffman.decode(buffer, length); else name = toASCIIString(buffer, length); - for (int i = 0; i < name.length(); i++) + check: + for (int i = name.length(); i-- > 0;) { char c = name.charAt(i); - if (c >= 'A' && c <= 'Z') + if (c > 0xff) { - _builder.streamException("Uppercase header name %s", name); + _builder.streamException("Illegal header name %s", name); break; } + HttpTokens.Token token = HttpTokens.TOKENS[0xFF & c]; + switch (token.getType()) + { + case ALPHA: + if (c >= 'A' && c <= 'Z') + { + _builder.streamException("Uppercase header name %s", name); + break check; + } + break; + + case COLON: + case TCHAR: + case DIGIT: + break; + + default: + _builder.streamException("Illegal header name %s", name); + break check; + } } header = HttpHeader.CACHE.get(name); } diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java index f10f792fb44..ce1f8b36cbe 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java @@ -74,11 +74,13 @@ public class MetaDataBuilder { HttpHeader header = field.getHeader(); String name = field.getName(); + if (name == null || name.length() == 0) + throw new HpackException.SessionException("Header size 0"); String value = field.getValue(); int fieldSize = name.length() + (value == null ? 0 : value.length()); _size += fieldSize + 32; if (_size > _maxSize) - throw new HpackException.SessionException("Header Size %d > %d", _size, _maxSize); + throw new HpackException.SessionException("Header size %d > %d", _size, _maxSize); if (field instanceof StaticTableHttpField) { @@ -113,7 +115,7 @@ public class MetaDataBuilder { case C_STATUS: if (checkPseudoHeader(header, _status)) - _status = Integer.valueOf(field.getIntValue()); + _status = field.getIntValue(); _response = true; break; diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackDecoderTest.java b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackDecoderTest.java index 3aa3da34e69..791ecd3d060 100644 --- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackDecoderTest.java +++ b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackDecoderTest.java @@ -26,6 +26,7 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http2.hpack.HpackException.CompressionException; +import org.eclipse.jetty.http2.hpack.HpackException.SessionException; import org.eclipse.jetty.http2.hpack.HpackException.StreamException; import org.eclipse.jetty.util.TypeUtil; import org.hamcrest.Matchers; @@ -43,6 +44,21 @@ import static org.junit.jupiter.api.Assertions.fail; public class HpackDecoderTest { + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 0 | 0 | 0 | + +---+---+-----------------------+ + | H | Name Length (7+) | + +---+---------------------------+ + | Name String (Length octets) | + +---+---------------------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length octets) | + +-------------------------------+ + */ + @Test public void testDecodeD_3() throws Exception { @@ -253,14 +269,14 @@ public class HpackDecoderTest decoder.decode(buffer); fail(); } - catch (HpackException.SessionException e) + catch (SessionException e) { assertThat(e.getMessage(), Matchers.startsWith("Unknown index")); } } /* 8.1.2.1. Pseudo-Header Fields */ - @Test() + @Test public void test8_1_2_1_PsuedoHeaderFields() throws Exception { // 1:Sends a HEADERS frame that contains a unknown pseudo-header field @@ -312,7 +328,7 @@ public class HpackDecoderTest } } - @Test() + @Test public void test8_1_2_2_ConnectionSpecificHeaderFields() throws Exception { MetaDataBuilder mdb; @@ -349,7 +365,7 @@ public class HpackDecoderTest assertNotNull(mdb.build()); } - @Test() + @Test public void test8_1_2_3_RequestPseudoHeaderFields() throws Exception { { @@ -368,7 +384,7 @@ public class HpackDecoderTest mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http")); mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080")); mdb.emit(new HttpField(HttpHeader.C_PATH, "")); - StreamException ex = assertThrows(StreamException.class, () -> mdb.build()); + StreamException ex = assertThrows(StreamException.class, mdb::build); assertThat(ex.getMessage(), Matchers.containsString("No Path")); } @@ -378,7 +394,7 @@ public class HpackDecoderTest mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http")); mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080")); mdb.emit(new HttpField(HttpHeader.C_PATH, "/")); - StreamException ex = assertThrows(StreamException.class, () -> mdb.build()); + StreamException ex = assertThrows(StreamException.class, mdb::build); assertThat(ex.getMessage(), Matchers.containsString("No Method")); } @@ -388,7 +404,7 @@ public class HpackDecoderTest mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET")); mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080")); mdb.emit(new HttpField(HttpHeader.C_PATH, "/")); - StreamException ex = assertThrows(StreamException.class, () -> mdb.build()); + StreamException ex = assertThrows(StreamException.class, mdb::build); assertThat(ex.getMessage(), Matchers.containsString("No Scheme")); } @@ -398,7 +414,7 @@ public class HpackDecoderTest 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")); - StreamException ex = assertThrows(StreamException.class, () -> mdb.build()); + StreamException ex = assertThrows(StreamException.class, mdb::build); assertThat(ex.getMessage(), Matchers.containsString("No Path")); } @@ -410,7 +426,7 @@ public class HpackDecoderTest mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http")); mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080")); mdb.emit(new HttpField(HttpHeader.C_PATH, "/")); - StreamException ex = assertThrows(StreamException.class, () -> mdb.build()); + StreamException ex = assertThrows(StreamException.class, mdb::build); assertThat(ex.getMessage(), Matchers.containsString("Duplicate")); } @@ -423,12 +439,12 @@ public class HpackDecoderTest mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080")); mdb.emit(new HttpField(HttpHeader.C_PATH, "/")); - StreamException ex = assertThrows(StreamException.class, () -> mdb.build()); + StreamException ex = assertThrows(StreamException.class, mdb::build); assertThat(ex.getMessage(), Matchers.containsString("Duplicate")); } } - @Test() + @Test public void testHuffmanEncodedStandard() throws Exception { HpackDecoder decoder = new HpackDecoder(4096, 8192); @@ -446,8 +462,8 @@ public class HpackDecoderTest } /* 5.2.1: Sends a Huffman-encoded string literal representation with padding longer than 7 bits */ - @Test() - public void testHuffmanEncodedExtraPadding() throws Exception + @Test + public void testHuffmanEncodedExtraPadding() { HpackDecoder decoder = new HpackDecoder(4096, 8192); @@ -458,8 +474,8 @@ public class HpackDecoderTest } /* 5.2.2: Sends a Huffman-encoded string literal representation padded by zero */ - @Test() - public void testHuffmanEncodedZeroPadding() throws Exception + @Test + public void testHuffmanEncodedZeroPadding() { HpackDecoder decoder = new HpackDecoder(4096, 8192); @@ -471,8 +487,8 @@ public class HpackDecoderTest } /* 5.2.3: Sends a Huffman-encoded string literal representation containing the EOS symbol */ - @Test() - public void testHuffmanEncodedWithEOS() throws Exception + @Test + public void testHuffmanEncodedWithEOS() { HpackDecoder decoder = new HpackDecoder(4096, 8192); @@ -483,8 +499,8 @@ public class HpackDecoderTest assertThat(ex.getMessage(), Matchers.containsString("EOS in content")); } - @Test() - public void testHuffmanEncodedOneIncompleteOctet() throws Exception + @Test + public void testHuffmanEncodedOneIncompleteOctet() { HpackDecoder decoder = new HpackDecoder(4096, 8192); @@ -495,8 +511,8 @@ public class HpackDecoderTest assertThat(ex.getMessage(), Matchers.containsString("Bad termination")); } - @Test() - public void testHuffmanEncodedTwoIncompleteOctet() throws Exception + @Test + public void testHuffmanEncodedTwoIncompleteOctet() { HpackDecoder decoder = new HpackDecoder(4096, 8192); @@ -506,4 +522,49 @@ public class HpackDecoderTest CompressionException ex = assertThrows(CompressionException.class, () -> decoder.decode(buffer)); assertThat(ex.getMessage(), Matchers.containsString("Bad termination")); } + + @Test + public void testZeroLengthName() + { + HpackDecoder decoder = new HpackDecoder(4096, 8192); + + String encoded = "00000130"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); + SessionException ex = assertThrows(SessionException.class, () -> decoder.decode(buffer)); + assertThat(ex.getMessage(), Matchers.containsString("Header size 0")); + } + + @Test + public void testZeroLengthValue() throws Exception + { + HpackDecoder decoder = new HpackDecoder(4096, 8192); + + String encoded = "00016800"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); + MetaData metaData = decoder.decode(buffer); + assertThat(metaData.getFields().size(), is(1)); + assertThat(metaData.getFields().get("h"), is("")); + } + + @Test + public void testUpperCaseName() + { + HpackDecoder decoder = new HpackDecoder(4096, 8192); + + String encoded = "0001480130"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); + StreamException ex = assertThrows(StreamException.class, () -> decoder.decode(buffer)); + assertThat(ex.getMessage(), Matchers.containsString("Uppercase header")); + } + + @Test + public void testWhiteSpaceName() + { + HpackDecoder decoder = new HpackDecoder(4096, 8192); + + String encoded = "0001200130"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); + StreamException ex = assertThrows(StreamException.class, () -> decoder.decode(buffer)); + assertThat(ex.getMessage(), Matchers.containsString("Illegal header")); + } } diff --git a/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2Test.java b/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2Test.java index 6e782a738b0..9304ae519c8 100644 --- a/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2Test.java +++ b/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2Test.java @@ -18,7 +18,6 @@ package org.eclipse.jetty.http2.client.http; -import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; @@ -38,7 +37,6 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.UnaryOperator; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -63,6 +61,7 @@ import org.eclipse.jetty.http2.frames.HeadersFrame; import org.eclipse.jetty.http2.frames.ResetFrame; 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.server.RawHTTP2ServerConnectionFactory; import org.eclipse.jetty.io.ByteBufferPool; @@ -187,7 +186,7 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest start(new AbstractHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) { baseRequest.setHandled(true); HttpVersion version = HttpVersion.fromString(request.getProtocol()); @@ -313,7 +312,7 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest start(new AbstractHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) { baseRequest.setHandled(true); assertEquals(path, request.getRequestURI()); @@ -337,7 +336,7 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest start(new AbstractHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) { baseRequest.setHandled(true); assertEquals(path, request.getRequestURI()); @@ -501,7 +500,7 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest x.printStackTrace(); } } - }, 4096, 8192); + }, 4096, 8192, RateControl.NO_RATE_CONTROL); parser.init(UnaryOperator.identity()); byte[] bytes = new byte[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..7b5319efbc5 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 @@ -237,7 +251,7 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne session.setInitialSessionRecvWindow(getInitialSessionRecvWindow()); session.setWriteThreshold(getHttpConfiguration().getOutputBufferSize()); - ServerParser parser = newServerParser(connector, session); + ServerParser parser = newServerParser(connector, session, getRateControl()); parser.setMaxFrameLength(getMaxFrameLength()); parser.setMaxSettingsKeys(getMaxSettingsKeys()); @@ -249,9 +263,9 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne protected abstract ServerSessionListener newSessionListener(Connector connector, EndPoint endPoint); - protected ServerParser newServerParser(Connector connector, ServerParser.Listener listener) + protected ServerParser newServerParser(Connector connector, ServerParser.Listener listener, RateControl rateControl) { - return new ServerParser(connector.getByteBufferPool(), listener, getMaxDynamicTableSize(), getHttpConfiguration().getRequestHeaderSize()); + return new ServerParser(connector.getByteBufferPool(), listener, getMaxDynamicTableSize(), getHttpConfiguration().getRequestHeaderSize(), rateControl); } @ManagedObject("The container of HTTP/2 sessions") diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/MathUtils.java b/jetty-util/src/main/java/org/eclipse/jetty/util/MathUtils.java new file mode 100644 index 00000000000..6c522299b21 --- /dev/null +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/MathUtils.java @@ -0,0 +1,46 @@ +// +// ======================================================================== +// 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.util; + +public class MathUtils +{ + private MathUtils() + { + } + + /** + * Returns whether the sum of the arguments overflows an {@code int}. + * + * @param a the first value + * @param b the second value + * @return whether the sum of the arguments overflows an {@code int} + */ + public static boolean sumOverflows(int a, int b) + { + try + { + Math.addExact(a, b); + return false; + } + catch (ArithmeticException x) + { + return true; + } + } +}