Implemented parser and generator for SETTINGS frame.

This commit is contained in:
Simone Bordet 2014-06-06 11:23:50 +02:00
parent 54577057bb
commit 2a485be6c1
15 changed files with 564 additions and 44 deletions

View File

@ -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<Integer, Integer> settings;
private final boolean reply;
public SettingsFrame(Map<Integer, Integer> settings, boolean reply)
{
this.settings = settings;
this.reply = reply;
}
public Map<Integer, Integer> getSettings()
{
return settings;
}
public boolean isReply()
{
return reply;
}
}

View File

@ -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<Integer, Integer> settings, boolean reply)
{
Result result = new Result(byteBufferPool);
ByteBuffer header = generateHeader(FrameType.SETTINGS, 5 * settings.size(), reply ? 0x01 : 0x00, 0);
for (Map.Entry<Integer, Integer> 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)

View File

@ -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

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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())

View File

@ -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)
{
}
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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<Integer, Integer> 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<Integer, Integer>()) == 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<Integer, Integer> 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
}
}

View File

@ -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;

View File

@ -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<DataFrame> 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()
{

View File

@ -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<SettingsFrame> frames = testGenerateParse(Collections.<Integer, Integer>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<Integer, Integer> settings1 = new HashMap<>();
int key1 = 13;
Integer value1 = 17;
settings1.put(key1, value1);
int key2 = 19;
Integer value2 = 23;
settings1.put(key2, value2);
List<SettingsFrame> frames = testGenerateParse(settings1);
Assert.assertEquals(1, frames.size());
SettingsFrame frame = frames.get(0);
Map<Integer, Integer> settings2 = frame.getSettings();
Assert.assertEquals(2, settings2.size());
Assert.assertEquals(value1, settings2.get(key1));
Assert.assertEquals(value2, settings2.get(key2));
}
private List<SettingsFrame> testGenerateParse(Map<Integer, Integer> settings)
{
Generator generator = new Generator(byteBufferPool);
// Iterate a few times to be sure generator and parser are properly reset.
final List<SettingsFrame> 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<Integer, Integer> 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<Integer, Integer> settings1 = new HashMap<>();
int key = 13;
Integer value = 17;
settings1.put(key, value);
final List<SettingsFrame> 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<Integer, Integer> settings2 = frame.getSettings();
Assert.assertEquals(1, settings2.size());
Assert.assertEquals(value, settings2.get(key));
Assert.assertTrue(frame.isReply());
}
}