Implemented PUSH_PROMISE generator/parser.

This commit is contained in:
Simone Bordet 2014-06-16 11:10:55 +02:00
parent 907d303774
commit 849360717e
10 changed files with 596 additions and 5 deletions

View File

@ -40,6 +40,7 @@ import org.eclipse.jetty.http2.frames.GoAwayFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.PingFrame;
import org.eclipse.jetty.http2.frames.PriorityFrame;
import org.eclipse.jetty.http2.frames.PushPromiseFrame;
import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
@ -178,6 +179,13 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
return false;
}
@Override
public boolean onPushPromise(PushPromiseFrame frame)
{
// TODO
return false;
}
@Override
public boolean onPing(PingFrame frame)
{

View File

@ -0,0 +1,51 @@
//
// ========================================================================
// 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 org.eclipse.jetty.http2.hpack.MetaData;
public class PushPromiseFrame extends Frame
{
private final int streamId;
private final int promisedStreamId;
private final MetaData metaData;
public PushPromiseFrame(int streamId, int promisedStreamId, MetaData metaData)
{
super(FrameType.PUSH_PROMISE);
this.streamId = streamId;
this.promisedStreamId = promisedStreamId;
this.metaData = metaData;
}
public int getStreamId()
{
return streamId;
}
public int getPromisedStreamId()
{
return promisedStreamId;
}
public MetaData getMetaData()
{
return metaData;
}
}

View File

@ -49,7 +49,7 @@ public class Generator
this.generators[FrameType.PRIORITY.getType()] = new PriorityGenerator(headerGenerator);
this.generators[FrameType.RST_STREAM.getType()] = new ResetGenerator(headerGenerator);
this.generators[FrameType.SETTINGS.getType()] = new SettingsGenerator(headerGenerator);
this.generators[FrameType.PUSH_PROMISE.getType()] = null; // TODO
this.generators[FrameType.PUSH_PROMISE.getType()] = new PushPromiseGenerator(headerGenerator, encoder);
this.generators[FrameType.PING.getType()] = new PingGenerator(headerGenerator);
this.generators[FrameType.GO_AWAY.getType()] = new GoAwayGenerator(headerGenerator);
this.generators[FrameType.WINDOW_UPDATE.getType()] = new WindowUpdateGenerator(headerGenerator);

View File

@ -43,10 +43,10 @@ public class HeadersGenerator extends FrameGenerator
public void generate(ByteBufferPool.Lease lease, Frame frame)
{
HeadersFrame headersFrame = (HeadersFrame)frame;
generate(lease, headersFrame.getStreamId(), headersFrame.getMetaData(), !headersFrame.isEndStream());
generateHeaders(lease, headersFrame.getStreamId(), headersFrame.getMetaData(), !headersFrame.isEndStream());
}
private void generate(ByteBufferPool.Lease lease, int streamId, MetaData metaData, boolean contentFollows)
public void generateHeaders(ByteBufferPool.Lease lease, int streamId, MetaData metaData, boolean contentFollows)
{
if (streamId < 0)
throw new IllegalArgumentException("Invalid stream id: " + streamId);

View File

@ -0,0 +1,55 @@
package org.eclipse.jetty.http2.generator;
import java.nio.ByteBuffer;
import org.eclipse.jetty.http2.frames.Flag;
import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.FrameType;
import org.eclipse.jetty.http2.frames.PushPromiseFrame;
import org.eclipse.jetty.http2.hpack.HpackEncoder;
import org.eclipse.jetty.http2.hpack.MetaData;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.BufferUtil;
public class PushPromiseGenerator extends FrameGenerator
{
private final HpackEncoder encoder;
public PushPromiseGenerator(HeaderGenerator headerGenerator, HpackEncoder encoder)
{
super(headerGenerator);
this.encoder = encoder;
}
@Override
public void generate(ByteBufferPool.Lease lease, Frame frame)
{
PushPromiseFrame pushPromiseFrame = (PushPromiseFrame)frame;
generatePushPromise(lease, pushPromiseFrame.getStreamId(), pushPromiseFrame.getPromisedStreamId(), pushPromiseFrame.getMetaData());
}
public void generatePushPromise(ByteBufferPool.Lease lease, int streamId, int promisedStreamId, MetaData metaData)
{
if (streamId < 0)
throw new IllegalArgumentException("Invalid stream id: " + streamId);
if (promisedStreamId < 0)
throw new IllegalArgumentException("Invalid promised stream id: " + promisedStreamId);
encoder.encode(metaData, lease);
long length = lease.getTotalLength();
if (length > Frame.MAX_LENGTH)
throw new IllegalArgumentException("Invalid headers, too big");
// Space for the promised streamId.
length += 4;
int flags = Flag.END_HEADERS;
ByteBuffer header = generateHeader(lease, FrameType.PUSH_PROMISE, (int)length, flags, streamId);
header.putInt(promisedStreamId);
BufferUtil.flipToFlush(header, 0);
lease.prepend(header, true);
}
}

View File

@ -26,6 +26,7 @@ import org.eclipse.jetty.http2.frames.GoAwayFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.PingFrame;
import org.eclipse.jetty.http2.frames.PriorityFrame;
import org.eclipse.jetty.http2.frames.PushPromiseFrame;
import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
@ -148,6 +149,19 @@ public abstract class BodyParser
}
}
protected boolean notifyPushPromise(PushPromiseFrame frame)
{
try
{
return listener.onPushPromise(frame);
}
catch (Throwable x)
{
LOG.info("Failure while notifying listener " + listener, x);
return false;
}
}
protected boolean notifyPing(PingFrame frame)
{
try

View File

@ -26,6 +26,7 @@ import org.eclipse.jetty.http2.frames.GoAwayFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.PingFrame;
import org.eclipse.jetty.http2.frames.PriorityFrame;
import org.eclipse.jetty.http2.frames.PushPromiseFrame;
import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
@ -56,7 +57,7 @@ public class Parser
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.PUSH_PROMISE.getType()] = null; // TODO
bodyParsers[FrameType.PUSH_PROMISE.getType()] = new PushPromiseBodyParser(headerParser, listener, headerBlockParser);
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);
@ -172,6 +173,8 @@ public class Parser
public boolean onSettings(SettingsFrame frame);
public boolean onPushPromise(PushPromiseFrame frame);
public boolean onPing(PingFrame frame);
public boolean onGoAway(GoAwayFrame frame);
@ -212,6 +215,12 @@ public class Parser
return false;
}
@Override
public boolean onPushPromise(PushPromiseFrame frame)
{
return false;
}
@Override
public boolean onPing(PingFrame frame)
{

View File

@ -0,0 +1,190 @@
//
// ========================================================================
// 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 org.eclipse.jetty.http2.frames.PushPromiseFrame;
import org.eclipse.jetty.http2.hpack.MetaData;
public class PushPromiseBodyParser extends BodyParser
{
private final HeaderBlockParser headerBlockParser;
private State state = State.PREPARE;
private int cursor;
private int length;
private int paddingLength;
private int streamId;
public PushPromiseBodyParser(HeaderParser headerParser, Parser.Listener listener, HeaderBlockParser headerBlockParser)
{
super(headerParser, listener);
this.headerBlockParser = headerBlockParser;
}
private void reset()
{
state = State.PREPARE;
cursor = 0;
length = 0;
paddingLength = 0;
streamId = 0;
}
@Override
public Result parse(ByteBuffer buffer)
{
boolean loop = false;
while (buffer.hasRemaining() || loop)
{
switch (state)
{
case PREPARE:
{
// SPEC: wrong streamId is treated as connection error.
if (getStreamId() == 0)
{
return notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR, "invalid_push_promise_frame");
}
length = getBodyLength();
if (isPaddingHigh())
{
state = State.PADDING_HIGH;
}
else if (isPaddingLow())
{
state = State.PADDING_LOW;
}
else
{
state = State.STREAM_ID;
}
break;
}
case PADDING_HIGH:
{
paddingLength = (buffer.get() & 0xFF) << 8;
--length;
state = State.PADDING_LOW;
if (length < 1 + 256)
{
return notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR, "invalid_push_promise_frame_padding");
}
break;
}
case PADDING_LOW:
{
paddingLength += buffer.get() & 0xFF;
--length;
length -= paddingLength;
state = State.STREAM_ID;
loop = length == 0;
if (length < 0)
{
return notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR, "invalid_push_promise_frame_padding");
}
break;
}
case STREAM_ID:
{
if (buffer.remaining() >= 4)
{
streamId = buffer.getInt();
streamId &= 0x7F_FF_FF_FF;
length -= 4;
state = State.HEADERS;
if (length < 1)
{
return notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR, "invalid_push_promise_frame");
}
}
else
{
state = State.STREAM_ID_BYTES;
cursor = 4;
}
break;
}
case STREAM_ID_BYTES:
{
int currByte = buffer.get() & 0xFF;
--cursor;
streamId += currByte << (8 * cursor);
--length;
if (cursor > 0 && length <= 0)
{
return notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR, "invalid_push_promise_frame");
}
if (cursor == 0)
{
streamId &= 0x7F_FF_FF_FF;
state = State.HEADERS;
if (length <= 0)
{
return notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR, "invalid_push_promise_frame");
}
}
break;
}
case HEADERS:
{
MetaData metaData = headerBlockParser.parse(buffer, length);
if (metaData != null)
{
state = State.PADDING;
loop = paddingLength == 0;
if (onPushPromise(streamId, metaData))
{
return Result.ASYNC;
}
}
break;
}
case PADDING:
{
int size = Math.min(buffer.remaining(), paddingLength);
buffer.position(buffer.position() + size);
paddingLength -= size;
if (paddingLength == 0)
{
reset();
return Result.COMPLETE;
}
break;
}
default:
{
throw new IllegalStateException();
}
}
}
return Result.PENDING;
}
private boolean onPushPromise(int streamId, MetaData metaData)
{
PushPromiseFrame frame = new PushPromiseFrame(getStreamId(), streamId, metaData);
return notifyPushPromise(frame);
}
private enum State
{
PREPARE, PADDING_HIGH, PADDING_LOW, STREAM_ID, STREAM_ID_BYTES, HEADERS, PADDING
}
}

View File

@ -18,17 +18,131 @@
package org.eclipse.jetty.http2.frames;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http2.generator.HeaderGenerator;
import org.eclipse.jetty.http2.generator.HeadersGenerator;
import org.eclipse.jetty.http2.hpack.HpackEncoder;
import org.eclipse.jetty.http2.hpack.MetaData;
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.Ignore;
import org.junit.Test;
public class HeadersGenerateParseTest
{
private final ByteBufferPool byteBufferPool = new MappedByteBufferPool();
@Ignore
@Test
public void testGenerateParse() throws Exception
{
// TODO
HeadersGenerator generator = new HeadersGenerator(new HeaderGenerator(), new HpackEncoder());
int streamId = 13;
HttpFields fields = new HttpFields();
fields.put("Accept", "text/html");
fields.put("User-Agent", "Jetty");
MetaData.Request metaData = new MetaData.Request(HttpScheme.HTTP, "GET", "localhost:8080", "localhost", 8080, "/path", fields);
// Iterate a few times to be sure generator and parser are properly reset.
final List<HeadersFrame> frames = new ArrayList<>();
for (int i = 0; i < 2; ++i)
{
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
generator.generateHeaders(lease, streamId, metaData, false);
Parser parser = new Parser(byteBufferPool, new Parser.Listener.Adapter()
{
@Override
public boolean onHeaders(HeadersFrame frame)
{
frames.add(frame);
return false;
}
});
frames.clear();
for (ByteBuffer buffer : lease.getByteBuffers())
{
while (buffer.hasRemaining())
{
parser.parse(buffer);
}
}
Assert.assertEquals(1, frames.size());
HeadersFrame frame = frames.get(0);
Assert.assertEquals(streamId, frame.getStreamId());
Assert.assertTrue(frame.isEndStream());
MetaData.Request request = (MetaData.Request)frame.getMetaData();
Assert.assertSame(metaData.getScheme(), request.getScheme());
Assert.assertEquals(metaData.getMethod(), request.getMethod());
Assert.assertEquals(metaData.getAuthority(), request.getAuthority());
Assert.assertEquals(metaData.getHost(), request.getHost());
Assert.assertEquals(metaData.getPort(), request.getPort());
Assert.assertEquals(metaData.getPath(), request.getPath());
for (int j = 0; j < fields.size(); ++j)
{
HttpField field = fields.getField(j);
Assert.assertTrue(request.getFields().contains(field));
}
}
}
@Test
public void testGenerateParseOneByteAtATime() throws Exception
{
HeadersGenerator generator = new HeadersGenerator(new HeaderGenerator(), new HpackEncoder());
int streamId = 13;
HttpFields fields = new HttpFields();
fields.put("Accept", "text/html");
fields.put("User-Agent", "Jetty");
MetaData.Request metaData = new MetaData.Request(HttpScheme.HTTP, "GET", "localhost:8080", "localhost", 8080, "/path", fields);
final List<HeadersFrame> frames = new ArrayList<>();
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
generator.generateHeaders(lease, streamId, metaData, false);
Parser parser = new Parser(byteBufferPool, new Parser.Listener.Adapter()
{
@Override
public boolean onHeaders(HeadersFrame frame)
{
frames.add(frame);
return false;
}
});
for (ByteBuffer buffer : lease.getByteBuffers())
{
while (buffer.hasRemaining())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
}
}
Assert.assertEquals(1, frames.size());
HeadersFrame frame = frames.get(0);
Assert.assertEquals(streamId, frame.getStreamId());
Assert.assertTrue(frame.isEndStream());
MetaData.Request request = (MetaData.Request)frame.getMetaData();
Assert.assertSame(metaData.getScheme(), request.getScheme());
Assert.assertEquals(metaData.getMethod(), request.getMethod());
Assert.assertEquals(metaData.getAuthority(), request.getAuthority());
Assert.assertEquals(metaData.getHost(), request.getHost());
Assert.assertEquals(metaData.getPort(), request.getPort());
Assert.assertEquals(metaData.getPath(), request.getPath());
for (int j = 0; j < fields.size(); ++j)
{
HttpField field = fields.getField(j);
Assert.assertTrue(request.getFields().contains(field));
}
}
}

View File

@ -0,0 +1,150 @@
//
// ========================================================================
// 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.List;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http2.generator.HeaderGenerator;
import org.eclipse.jetty.http2.generator.PushPromiseGenerator;
import org.eclipse.jetty.http2.hpack.HpackEncoder;
import org.eclipse.jetty.http2.hpack.MetaData;
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.Ignore;
import org.junit.Test;
public class PushPromiseGenerateParseTest
{
private final ByteBufferPool byteBufferPool = new MappedByteBufferPool();
@Ignore
@Test
public void testGenerateParse() throws Exception
{
PushPromiseGenerator generator = new PushPromiseGenerator(new HeaderGenerator(), new HpackEncoder());
int streamId = 13;
int promisedStreamId = 17;
HttpFields fields = new HttpFields();
fields.put("Accept", "text/html");
fields.put("User-Agent", "Jetty");
MetaData.Request metaData = new MetaData.Request(HttpScheme.HTTP, "GET", "localhost:8080", "localhost", 8080, "/path", fields);
// Iterate a few times to be sure generator and parser are properly reset.
final List<PushPromiseFrame> frames = new ArrayList<>();
for (int i = 0; i < 2; ++i)
{
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
generator.generatePushPromise(lease, streamId, promisedStreamId, metaData);
Parser parser = new Parser(byteBufferPool, new Parser.Listener.Adapter()
{
@Override
public boolean onPushPromise(PushPromiseFrame frame)
{
frames.add(frame);
return false;
}
});
frames.clear();
for (ByteBuffer buffer : lease.getByteBuffers())
{
while (buffer.hasRemaining())
{
parser.parse(buffer);
}
}
Assert.assertEquals(1, frames.size());
PushPromiseFrame frame = frames.get(0);
Assert.assertEquals(streamId, frame.getStreamId());
Assert.assertEquals(promisedStreamId, frame.getPromisedStreamId());
MetaData.Request request = (MetaData.Request)frame.getMetaData();
Assert.assertSame(metaData.getScheme(), request.getScheme());
Assert.assertEquals(metaData.getMethod(), request.getMethod());
Assert.assertEquals(metaData.getAuthority(), request.getAuthority());
Assert.assertEquals(metaData.getHost(), request.getHost());
Assert.assertEquals(metaData.getPort(), request.getPort());
Assert.assertEquals(metaData.getPath(), request.getPath());
for (int j = 0; j < fields.size(); ++j)
{
HttpField field = fields.getField(j);
Assert.assertTrue(request.getFields().contains(field));
}
}
}
@Test
public void testGenerateParseOneByteAtATime() throws Exception
{
PushPromiseGenerator generator = new PushPromiseGenerator(new HeaderGenerator(), new HpackEncoder());
int streamId = 13;
int promisedStreamId = 17;
HttpFields fields = new HttpFields();
fields.put("Accept", "text/html");
fields.put("User-Agent", "Jetty");
MetaData.Request metaData = new MetaData.Request(HttpScheme.HTTP, "GET", "localhost:8080", "localhost", 8080, "/path", fields);
final List<PushPromiseFrame> frames = new ArrayList<>();
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
generator.generatePushPromise(lease, streamId, promisedStreamId, metaData);
Parser parser = new Parser(byteBufferPool, new Parser.Listener.Adapter()
{
@Override
public boolean onPushPromise(PushPromiseFrame frame)
{
frames.add(frame);
return false;
}
});
for (ByteBuffer buffer : lease.getByteBuffers())
{
while (buffer.hasRemaining())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
}
}
Assert.assertEquals(1, frames.size());
PushPromiseFrame frame = frames.get(0);
Assert.assertEquals(streamId, frame.getStreamId());
Assert.assertEquals(promisedStreamId, frame.getPromisedStreamId());
MetaData.Request request = (MetaData.Request)frame.getMetaData();
Assert.assertSame(metaData.getScheme(), request.getScheme());
Assert.assertEquals(metaData.getMethod(), request.getMethod());
Assert.assertEquals(metaData.getAuthority(), request.getAuthority());
Assert.assertEquals(metaData.getHost(), request.getHost());
Assert.assertEquals(metaData.getPort(), request.getPort());
Assert.assertEquals(metaData.getPath(), request.getPath());
for (int j = 0; j < fields.size(); ++j)
{
HttpField field = fields.getField(j);
Assert.assertTrue(request.getFields().contains(field));
}
}
}