From 2a485be6c13e5f84df80c77299d239cefd932311 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Fri, 6 Jun 2014 11:23:50 +0200 Subject: [PATCH] Implemented parser and generator for SETTINGS frame. --- .../jetty/http2/frames/SettingsFrame.java | 43 +++++ .../jetty/http2/generator/Generator.java | 19 ++ .../jetty/http2/parser/BodyParser.java | 39 +++- .../jetty/http2/parser/DataBodyParser.java | 14 +- .../eclipse/jetty/http2/parser/ErrorCode.java | 36 ++++ .../jetty/http2/parser/GoAwayBodyParser.java | 6 +- .../jetty/http2/parser/HeaderParser.java | 8 + .../eclipse/jetty/http2/parser/Parser.java | 93 ++++++++-- .../jetty/http2/parser/PingBodyParser.java | 4 +- .../http2/parser/PriorityBodyParser.java | 6 +- .../jetty/http2/parser/ResetBodyParser.java | 4 +- .../http2/parser/SettingsBodyParser.java | 148 +++++++++++++++ .../http2/parser/WindowUpdateBodyParser.java | 4 +- .../http2/frames/DataGenerateParseTest.java | 13 ++ .../frames/SettingsGenerateParseTest.java | 171 ++++++++++++++++++ 15 files changed, 564 insertions(+), 44 deletions(-) create mode 100644 jetty-http2/src/main/java/org/eclipse/jetty/http2/frames/SettingsFrame.java create mode 100644 jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/ErrorCode.java create mode 100644 jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/SettingsBodyParser.java create mode 100644 jetty-http2/src/test/main/org/eclipse/jetty/http2/frames/SettingsGenerateParseTest.java diff --git a/jetty-http2/src/main/java/org/eclipse/jetty/http2/frames/SettingsFrame.java b/jetty-http2/src/main/java/org/eclipse/jetty/http2/frames/SettingsFrame.java new file mode 100644 index 00000000000..5fe64b0a863 --- /dev/null +++ b/jetty-http2/src/main/java/org/eclipse/jetty/http2/frames/SettingsFrame.java @@ -0,0 +1,43 @@ +// +// ======================================================================== +// Copyright (c) 1995-2014 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.util.Map; + +public class SettingsFrame +{ + private final Map settings; + private final boolean reply; + + public SettingsFrame(Map settings, boolean reply) + { + this.settings = settings; + this.reply = reply; + } + + public Map getSettings() + { + return settings; + } + + public boolean isReply() + { + return reply; + } +} diff --git a/jetty-http2/src/main/java/org/eclipse/jetty/http2/generator/Generator.java b/jetty-http2/src/main/java/org/eclipse/jetty/http2/generator/Generator.java index 587ee4a19f2..daedce95c2e 100644 --- a/jetty-http2/src/main/java/org/eclipse/jetty/http2/generator/Generator.java +++ b/jetty-http2/src/main/java/org/eclipse/jetty/http2/generator/Generator.java @@ -21,6 +21,7 @@ package org.eclipse.jetty.http2.generator; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.eclipse.jetty.http2.frames.DataFrame; import org.eclipse.jetty.http2.frames.FrameType; @@ -118,6 +119,24 @@ public class Generator return result; } + public Result generateSettings(Map settings, boolean reply) + { + Result result = new Result(byteBufferPool); + + ByteBuffer header = generateHeader(FrameType.SETTINGS, 5 * settings.size(), reply ? 0x01 : 0x00, 0); + + for (Map.Entry entry : settings.entrySet()) + { + header.put(entry.getKey().byteValue()); + header.putInt(entry.getValue()); + } + + BufferUtil.flipToFlush(header, 0); + result.add(header, true); + + return result; + } + public Result generatePing(byte[] payload, boolean reply) { if (payload.length != 8) diff --git a/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/BodyParser.java b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/BodyParser.java index 09630c685b5..1d9040c152f 100644 --- a/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/BodyParser.java +++ b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/BodyParser.java @@ -25,6 +25,7 @@ import org.eclipse.jetty.http2.frames.GoAwayFrame; import org.eclipse.jetty.http2.frames.PingFrame; import org.eclipse.jetty.http2.frames.PriorityFrame; import org.eclipse.jetty.http2.frames.ResetFrame; +import org.eclipse.jetty.http2.frames.SettingsFrame; import org.eclipse.jetty.http2.frames.WindowUpdateFrame; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -44,6 +45,12 @@ public abstract class BodyParser public abstract Result parse(ByteBuffer buffer); + protected boolean emptyBody() + { + notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR, "invalid_frame"); + return false; + } + protected boolean hasFlag(int bit) { return headerParser.hasFlag(bit); @@ -74,11 +81,6 @@ public abstract class BodyParser return headerParser.getLength(); } - protected void reset() - { - headerParser.reset(); - } - protected boolean notifyData(DataFrame frame) { try @@ -118,6 +120,19 @@ public abstract class BodyParser } } + protected boolean notifySettings(SettingsFrame frame) + { + try + { + return listener.onSettings(frame); + } + catch (Throwable x) + { + LOG.info("Failure while notifying listener " + listener, x); + return false; + } + } + protected boolean notifyPing(PingFrame frame) { try @@ -157,6 +172,20 @@ public abstract class BodyParser } } + protected Result notifyConnectionFailure(int error, String reason) + { + try + { + listener.onConnectionFailure(error, reason); + return Result.ASYNC; + } + catch (Throwable x) + { + LOG.info("Failure while notifying listener " + listener, x); + return Result.ASYNC; + } + } + public enum Result { PENDING, ASYNC, COMPLETE diff --git a/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/DataBodyParser.java b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/DataBodyParser.java index 781794b5100..f230d651d09 100644 --- a/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/DataBodyParser.java +++ b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/DataBodyParser.java @@ -21,6 +21,7 @@ package org.eclipse.jetty.http2.parser; import java.nio.ByteBuffer; import org.eclipse.jetty.http2.frames.DataFrame; +import org.eclipse.jetty.util.BufferUtil; public class DataBodyParser extends BodyParser { @@ -33,15 +34,19 @@ public class DataBodyParser extends BodyParser super(headerParser, listener); } - @Override - protected void reset() + private void reset() { - super.reset(); state = State.PREPARE; paddingLength = 0; length = 0; } + @Override + protected boolean emptyBody() + { + return onData(BufferUtil.EMPTY_BUFFER, false); + } + @Override public Result parse(ByteBuffer buffer) { @@ -133,8 +138,7 @@ public class DataBodyParser extends BodyParser private boolean onData(ByteBuffer buffer, boolean fragment) { - boolean end = isEndStream(); - DataFrame frame = new DataFrame(getStreamId(), buffer, fragment ? false : end); + DataFrame frame = new DataFrame(getStreamId(), buffer, !fragment && isEndStream()); return notifyData(frame); } diff --git a/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/ErrorCode.java b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/ErrorCode.java new file mode 100644 index 00000000000..b94a19b8764 --- /dev/null +++ b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/ErrorCode.java @@ -0,0 +1,36 @@ +// +// ======================================================================== +// Copyright (c) 1995-2014 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; + +public interface ErrorCode +{ + public static final int NO_ERROR = 0; + public static final int PROTOCOL_ERROR = 1; + public static final int INTERNAL_ERROR = 2; + public static final int FLOW_CONTROL_ERROR = 3; + public static final int SETTINGS_TIMEOUT_ERROR = 4; + public static final int STREAM_CLOSED_ERROR = 5; + public static final int FRAME_SIZE_ERROR = 6; + public static final int REFUSED_STREAM_ERROR = 7; + public static final int CANCEL_STREAM_ERROR = 8; + public static final int COMPRESSION_ERROR = 9; + public static final int HTTP_CONNECT_ERROR = 10; + public static final int ENHANCE_YOUR_CALM_ERROR = 11; + public static final int INADEQUATE_SECURITY_ERROR = 12; +} diff --git a/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/GoAwayBodyParser.java b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/GoAwayBodyParser.java index 0d6ecf17f62..45671bc6969 100644 --- a/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/GoAwayBodyParser.java +++ b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/GoAwayBodyParser.java @@ -26,7 +26,6 @@ public class GoAwayBodyParser extends BodyParser { private State state = State.LAST_STREAM_ID; private int cursor; - private int lastStreamId; private int error; private byte[] payload; @@ -36,13 +35,10 @@ public class GoAwayBodyParser extends BodyParser super(headerParser, listener); } - @Override - protected void reset() + private void reset() { - super.reset(); state = State.LAST_STREAM_ID; cursor = 0; - lastStreamId = 0; error = 0; payload = null; diff --git a/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/HeaderParser.java b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/HeaderParser.java index 87d2a9750ae..fe72e1c2962 100644 --- a/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/HeaderParser.java +++ b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/HeaderParser.java @@ -43,6 +43,14 @@ public class HeaderParser streamId = 0; } + /** + * Parses the header bytes in the given {@code buffer}; only the header + * bytes are consumed, therefore the buffer may contain unconsumed bytes. + * + * @param buffer the buffer to parse + * @return true if a whole header was parsed, false if not enough header + * bytes were present in the buffer + */ public boolean parse(ByteBuffer buffer) { while (buffer.hasRemaining()) diff --git a/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/Parser.java b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/Parser.java index fa6ff4cb956..01281c21bca 100644 --- a/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/Parser.java +++ b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/Parser.java @@ -26,20 +26,27 @@ import org.eclipse.jetty.http2.frames.GoAwayFrame; import org.eclipse.jetty.http2.frames.PingFrame; import org.eclipse.jetty.http2.frames.PriorityFrame; import org.eclipse.jetty.http2.frames.ResetFrame; +import org.eclipse.jetty.http2.frames.SettingsFrame; import org.eclipse.jetty.http2.frames.WindowUpdateFrame; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; public class Parser { + private static final Logger LOG = Log.getLogger(Parser.class); + private final HeaderParser headerParser = new HeaderParser(); private final BodyParser[] bodyParsers = new BodyParser[FrameType.values().length]; + private final Listener listener; private State state = State.HEADER; - private BodyParser bodyParser; public Parser(Listener listener) { + this.listener = listener; bodyParsers[FrameType.DATA.getType()] = new DataBodyParser(headerParser, listener); bodyParsers[FrameType.PRIORITY.getType()] = new PriorityBodyParser(headerParser, listener); bodyParsers[FrameType.RST_STREAM.getType()] = new ResetBodyParser(headerParser, listener); + bodyParsers[FrameType.SETTINGS.getType()] = new SettingsBodyParser(headerParser, listener); bodyParsers[FrameType.PING.getType()] = new PingBodyParser(headerParser, listener); bodyParsers[FrameType.GO_AWAY.getType()] = new GoAwayBodyParser(headerParser, listener); bodyParsers[FrameType.WINDOW_UPDATE.getType()] = new WindowUpdateBodyParser(headerParser, listener); @@ -47,37 +54,67 @@ public class Parser private void reset() { + headerParser.reset(); state = State.HEADER; } public boolean parse(ByteBuffer buffer) { - while (buffer.hasRemaining()) + while (true) { switch (state) { case HEADER: { - if (headerParser.parse(buffer)) - { - int type = headerParser.getFrameType(); - bodyParser = bodyParsers[type]; - state = State.BODY; - } + if (!headerParser.parse(buffer)) + return false; + state = State.BODY; break; } case BODY: { - BodyParser.Result result = bodyParser.parse(buffer); - if (result == BodyParser.Result.ASYNC) + int type = headerParser.getFrameType(); + if (type < 0 || type >= bodyParsers.length) { - // The content will be processed asynchronously, stop parsing; - // the asynchronous operation will eventually resume parsing. - return true; + notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR, "unknown_frame_type_" + type); + return false; } - else if (result == BodyParser.Result.COMPLETE) + BodyParser bodyParser = bodyParsers[type]; + if (headerParser.getLength() == 0) { + boolean async = bodyParser.emptyBody(); reset(); + if (async) + return true; + if (!buffer.hasRemaining()) + return false; + } + else + { + BodyParser.Result result = bodyParser.parse(buffer); + switch (result) + { + case PENDING: + { + // Not enough bytes. + return false; + } + case ASYNC: + { + // The content will be processed asynchronously, stop parsing; + // the asynchronous operation will eventually resume parsing. + return true; + } + case COMPLETE: + { + reset(); + break; + } + default: + { + throw new IllegalStateException(); + } + } } break; } @@ -87,7 +124,18 @@ public class Parser } } } - return false; + } + + protected void notifyConnectionFailure(int error, String reason) + { + try + { + listener.onConnectionFailure(error, reason); + } + catch (Throwable x) + { + LOG.info("Failure while notifying listener " + listener, x); + } } public interface Listener @@ -98,12 +146,16 @@ public class Parser public boolean onReset(ResetFrame frame); + public boolean onSettings(SettingsFrame frame); + public boolean onPing(PingFrame frame); public boolean onGoAway(GoAwayFrame frame); public boolean onWindowUpdate(WindowUpdateFrame frame); + public void onConnectionFailure(int error, String reason); + public static class Adapter implements Listener { @Override @@ -124,6 +176,12 @@ public class Parser return false; } + @Override + public boolean onSettings(SettingsFrame frame) + { + return false; + } + @Override public boolean onPing(PingFrame frame) { @@ -141,6 +199,11 @@ public class Parser { return false; } + + @Override + public void onConnectionFailure(int error, String reason) + { + } } } diff --git a/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/PingBodyParser.java b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/PingBodyParser.java index 262a79975b3..173a5ea57b0 100644 --- a/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/PingBodyParser.java +++ b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/PingBodyParser.java @@ -33,10 +33,8 @@ public class PingBodyParser extends BodyParser super(headerParser, listener); } - @Override - protected void reset() + private void reset() { - super.reset(); state = State.PAYLOAD; cursor = 0; payload = null; diff --git a/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/PriorityBodyParser.java b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/PriorityBodyParser.java index b56574a063f..e37785e0739 100644 --- a/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/PriorityBodyParser.java +++ b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/PriorityBodyParser.java @@ -26,7 +26,6 @@ public class PriorityBodyParser extends BodyParser { private State state = State.EXCLUSIVE; private int cursor; - private boolean exclusive; private int streamId; @@ -35,13 +34,10 @@ public class PriorityBodyParser extends BodyParser super(headerParser, listener); } - @Override - protected void reset() + private void reset() { - super.reset(); state = State.EXCLUSIVE; cursor = 0; - exclusive = false; streamId = 0; } diff --git a/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/ResetBodyParser.java b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/ResetBodyParser.java index d9fed1d3716..fe9171352f9 100644 --- a/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/ResetBodyParser.java +++ b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/ResetBodyParser.java @@ -33,10 +33,8 @@ public class ResetBodyParser extends BodyParser super(headerParser, listener); } - @Override - protected void reset() + private void reset() { - super.reset(); state = State.ERROR; cursor = 0; error = 0; diff --git a/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/SettingsBodyParser.java b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/SettingsBodyParser.java new file mode 100644 index 00000000000..18098ebb87c --- /dev/null +++ b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/SettingsBodyParser.java @@ -0,0 +1,148 @@ +// +// ======================================================================== +// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http2.parser; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jetty.http2.frames.SettingsFrame; + +public class SettingsBodyParser extends BodyParser +{ + private State state = State.PREPARE; + private int cursor; + private int length; + private int settingId; + private int settingValue; + private Map settings; + + public SettingsBodyParser(HeaderParser headerParser, Parser.Listener listener) + { + super(headerParser, listener); + } + + private void reset() + { + state = State.PREPARE; + cursor = 0; + length = 0; + settingId = 0; + settingValue = 0; + settings = null; + } + + @Override + protected boolean emptyBody() + { + return onSettings(new HashMap()) == Result.ASYNC; + } + + @Override + public Result parse(ByteBuffer buffer) + { + while (buffer.hasRemaining()) + { + switch (state) + { + case PREPARE: + { + length = getBodyLength(); + settings = new HashMap<>(); + state = State.SETTING_ID; + if (length == 0) + { + return onSettings(settings); + } + break; + } + case SETTING_ID: + { + settingId = buffer.get() & 0xFF; + state = State.SETTING_VALUE; + --length; + if (length == 0) + { + return notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR, "invalid_settings"); + } + break; + } + case SETTING_VALUE: + { + if (buffer.remaining() >= 4) + { + settingValue = buffer.getInt(); + settings.put(settingId, settingValue); + state = State.SETTING_ID; + length -= 4; + if (length == 0) + { + return onSettings(settings); + } + } + else + { + cursor = 4; + settingValue = 0; + state = State.SETTING_VALUE_BYTES; + } + break; + } + case SETTING_VALUE_BYTES: + { + int currByte = buffer.get() & 0xFF; + --cursor; + settingValue += currByte << (8 * cursor); + --length; + if (cursor > 0 && length == 0) + { + return notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR, "invalid_settings"); + } + if (cursor == 0) + { + settings.put(settingId, settingValue); + state = State.SETTING_ID; + if (length == 0) + { + return onSettings(settings); + } + } + break; + } + default: + { + throw new IllegalStateException(); + } + } + } + return Result.PENDING; + } + + private Result onSettings(Map settings) + { + SettingsFrame frame = new SettingsFrame(settings, hasFlag(0x1)); + reset(); + return notifySettings(frame) ? Result.ASYNC : Result.COMPLETE; + } + + private enum State + { + PREPARE, SETTING_ID, SETTING_VALUE, SETTING_VALUE_BYTES + } +} diff --git a/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/WindowUpdateBodyParser.java b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/WindowUpdateBodyParser.java index b651c9d392a..0ed06b81428 100644 --- a/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/WindowUpdateBodyParser.java +++ b/jetty-http2/src/main/java/org/eclipse/jetty/http2/parser/WindowUpdateBodyParser.java @@ -33,10 +33,8 @@ public class WindowUpdateBodyParser extends BodyParser super(headerParser, listener); } - @Override - protected void reset() + private void reset() { - super.reset(); state = State.WINDOW_DELTA; cursor = 0; windowDelta = 0; diff --git a/jetty-http2/src/test/main/org/eclipse/jetty/http2/frames/DataGenerateParseTest.java b/jetty-http2/src/test/main/org/eclipse/jetty/http2/frames/DataGenerateParseTest.java index 4670f3fffe7..de879438987 100644 --- a/jetty-http2/src/test/main/org/eclipse/jetty/http2/frames/DataGenerateParseTest.java +++ b/jetty-http2/src/test/main/org/eclipse/jetty/http2/frames/DataGenerateParseTest.java @@ -27,6 +27,7 @@ import org.eclipse.jetty.http2.generator.Generator; import org.eclipse.jetty.http2.parser.Parser; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.MappedByteBufferPool; +import org.eclipse.jetty.util.BufferUtil; import org.junit.Assert; import org.junit.Test; @@ -43,6 +44,18 @@ public class DataGenerateParseTest random.nextBytes(largeContent); } + @Test + public void testGenerateParseNoContentNoPadding() + { + ByteBuffer content = BufferUtil.EMPTY_BUFFER; + List frames = testGenerateParse(0, content); + Assert.assertEquals(1, frames.size()); + DataFrame frame = frames.get(0); + Assert.assertTrue(frame.getStreamId() != 0); + Assert.assertTrue(frame.isEnd()); + Assert.assertEquals(content, frame.getData()); + } + @Test public void testGenerateParseSmallContentNoPadding() { diff --git a/jetty-http2/src/test/main/org/eclipse/jetty/http2/frames/SettingsGenerateParseTest.java b/jetty-http2/src/test/main/org/eclipse/jetty/http2/frames/SettingsGenerateParseTest.java new file mode 100644 index 00000000000..f52f383b3e9 --- /dev/null +++ b/jetty-http2/src/test/main/org/eclipse/jetty/http2/frames/SettingsGenerateParseTest.java @@ -0,0 +1,171 @@ +// +// ======================================================================== +// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http2.frames; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.jetty.http2.generator.Generator; +import org.eclipse.jetty.http2.parser.ErrorCode; +import org.eclipse.jetty.http2.parser.Parser; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.MappedByteBufferPool; +import org.junit.Assert; +import org.junit.Test; + +public class SettingsGenerateParseTest +{ + private final ByteBufferPool byteBufferPool = new MappedByteBufferPool(); + + @Test + public void testGenerateParseNoSettings() throws Exception + { + List frames = testGenerateParse(Collections.emptyMap()); + Assert.assertEquals(1, frames.size()); + SettingsFrame frame = frames.get(0); + Assert.assertEquals(0, frame.getSettings().size()); + Assert.assertTrue(frame.isReply()); + } + + @Test + public void testGenerateParseSettings() throws Exception + { + Map settings1 = new HashMap<>(); + int key1 = 13; + Integer value1 = 17; + settings1.put(key1, value1); + int key2 = 19; + Integer value2 = 23; + settings1.put(key2, value2); + List frames = testGenerateParse(settings1); + Assert.assertEquals(1, frames.size()); + SettingsFrame frame = frames.get(0); + Map settings2 = frame.getSettings(); + Assert.assertEquals(2, settings2.size()); + Assert.assertEquals(value1, settings2.get(key1)); + Assert.assertEquals(value2, settings2.get(key2)); + } + + private List testGenerateParse(Map settings) + { + Generator generator = new Generator(byteBufferPool); + + // Iterate a few times to be sure generator and parser are properly reset. + final List frames = new ArrayList<>(); + for (int i = 0; i < 2; ++i) + { + Generator.Result result = generator.generateSettings(settings, true); + Parser parser = new Parser(new Parser.Listener.Adapter() + { + @Override + public boolean onSettings(SettingsFrame frame) + { + frames.add(frame); + return false; + } + }); + + frames.clear(); + for (ByteBuffer buffer : result.getByteBuffers()) + { + while (buffer.hasRemaining()) + { + parser.parse(buffer); + } + } + } + + return frames; + } + + @Test + public void testGenerateParseInvalidSettings() throws Exception + { + Generator generator = new Generator(byteBufferPool); + Map settings1 = new HashMap<>(); + settings1.put(13, 17); + Generator.Result result = generator.generateSettings(settings1, true); + // Modify the length of the frame to make it invalid + ByteBuffer bytes = result.getByteBuffers().get(0); + bytes.putShort(0, (short)(bytes.getShort(0) - 1)); + + final AtomicInteger errorRef = new AtomicInteger(); + Parser parser = new Parser(new Parser.Listener.Adapter() + { + @Override + public void onConnectionFailure(int error, String reason) + { + errorRef.set(error); + } + }); + + for (ByteBuffer buffer : result.getByteBuffers()) + { + while (buffer.hasRemaining()) + { + parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()})); + } + } + + Assert.assertEquals(ErrorCode.PROTOCOL_ERROR, errorRef.get()); + } + + @Test + public void testGenerateParseOneByteAtATime() throws Exception + { + Generator generator = new Generator(byteBufferPool); + + Map settings1 = new HashMap<>(); + int key = 13; + Integer value = 17; + settings1.put(key, value); + + final List frames = new ArrayList<>(); + Generator.Result result = generator.generateSettings(settings1, true); + Parser parser = new Parser(new Parser.Listener.Adapter() + { + @Override + public boolean onSettings(SettingsFrame frame) + { + frames.add(frame); + return false; + } + }); + + for (ByteBuffer buffer : result.getByteBuffers()) + { + while (buffer.hasRemaining()) + { + parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()})); + } + } + + Assert.assertEquals(1, frames.size()); + SettingsFrame frame = frames.get(0); + Map settings2 = frame.getSettings(); + Assert.assertEquals(1, settings2.size()); + Assert.assertEquals(value, settings2.get(key)); + Assert.assertTrue(frame.isReply()); + } +}