From c367ea8a85f66b8d8f811740aafe808dbf086ed7 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Wed, 8 Jul 2015 18:55:47 +0200 Subject: [PATCH] 441020 - Support HEADERS followed by CONTINUATION+. --- .../client/HTTP2ClientConnectionFactory.java | 2 +- .../jetty/http2/generator/Generator.java | 10 +- .../http2/generator/HeadersGenerator.java | 63 ++++++-- .../http2/generator/PushPromiseGenerator.java | 30 ++-- .../http2/parser/ContinuationBodyParser.java | 81 +++++++++- .../http2/parser/HeaderBlockFragments.java | 95 ++++++++++++ .../jetty/http2/parser/HeadersBodyParser.java | 54 +++++-- .../eclipse/jetty/http2/parser/Parser.java | 30 +++- .../jetty/http2/hpack/HpackEncoder.java | 72 ++++----- .../AbstractHTTP2ServerConnectionFactory.java | 13 +- .../jetty/http2/server/HTTP2ServerTest.java | 144 ++++++++++++++++++ 11 files changed, 496 insertions(+), 98 deletions(-) create mode 100644 jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/HeaderBlockFragments.java diff --git a/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientConnectionFactory.java b/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientConnectionFactory.java index be089eabc63..8b641f0fb30 100644 --- a/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientConnectionFactory.java +++ b/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientConnectionFactory.java @@ -63,7 +63,7 @@ public class HTTP2ClientConnectionFactory implements ClientConnectionFactory @SuppressWarnings("unchecked") Promise promise = (Promise)context.get(SESSION_PROMISE_CONTEXT_KEY); - Generator generator = new Generator(byteBufferPool, 4096); + Generator generator = new Generator(byteBufferPool); FlowControlStrategy flowControl = newFlowControlStrategy(); HTTP2ClientSession session = new HTTP2ClientSession(scheduler, endPoint, generator, listener, flowControl); Parser parser = new Parser(byteBufferPool, session, 4096, 8192); diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/generator/Generator.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/generator/Generator.java index 0f082e3a939..5d6ae067561 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/generator/Generator.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/generator/Generator.java @@ -34,18 +34,18 @@ public class Generator public Generator(ByteBufferPool byteBufferPool) { - this(byteBufferPool, 4096); + this(byteBufferPool, 4096, 0); } - public Generator(ByteBufferPool byteBufferPool, int headerTableSize) + public Generator(ByteBufferPool byteBufferPool, int maxDynamicTableSize, int maxHeaderBlockFragment) { this.byteBufferPool = byteBufferPool; headerGenerator = new HeaderGenerator(); - hpackEncoder = new HpackEncoder(headerTableSize); + hpackEncoder = new HpackEncoder(maxDynamicTableSize); this.generators = new FrameGenerator[FrameType.values().length]; - this.generators[FrameType.HEADERS.getType()] = new HeadersGenerator(headerGenerator, hpackEncoder); + this.generators[FrameType.HEADERS.getType()] = new HeadersGenerator(headerGenerator, hpackEncoder, maxHeaderBlockFragment); 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); @@ -53,7 +53,7 @@ public class Generator 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); - this.generators[FrameType.CONTINUATION.getType()] = null; // TODO + this.generators[FrameType.CONTINUATION.getType()] = null; // Never generated explicitly. this.generators[FrameType.PREFACE.getType()] = new PrefaceGenerator(); this.generators[FrameType.DISCONNECT.getType()] = new DisconnectGenerator(); diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/generator/HeadersGenerator.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/generator/HeadersGenerator.java index a14c16410b2..b6bb95b3858 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/generator/HeadersGenerator.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/generator/HeadersGenerator.java @@ -32,11 +32,18 @@ import org.eclipse.jetty.util.BufferUtil; public class HeadersGenerator extends FrameGenerator { private final HpackEncoder encoder; + private final int maxHeaderBlockFragment; public HeadersGenerator(HeaderGenerator headerGenerator, HpackEncoder encoder) + { + this(headerGenerator, encoder, 0); + } + + public HeadersGenerator(HeaderGenerator headerGenerator, HpackEncoder encoder, int maxHeaderBlockFragment) { super(headerGenerator); this.encoder = encoder; + this.maxHeaderBlockFragment = maxHeaderBlockFragment; } @Override @@ -53,22 +60,50 @@ public class HeadersGenerator extends FrameGenerator int maxFrameSize = getMaxFrameSize(); - // The lease may already contain other buffers, - // compute the bytes generated by the encoder only. - int leaseSize = lease.getSize(); - long leaseLength = lease.getTotalLength(); - encoder.encode(metaData, lease, maxFrameSize); - long headersLength = lease.getTotalLength() - leaseLength; - if (headersLength > maxFrameSize) - throw new IllegalArgumentException(String.format("Invalid headers, too big for max frame size: %d > %d", headersLength, maxFrameSize)); + ByteBuffer hpacked = lease.acquire(maxFrameSize, false); + BufferUtil.clearToFill(hpacked); + encoder.encode(hpacked, metaData); + int hpackedLength = hpacked.position(); + BufferUtil.flipToFlush(hpacked, 0); - int flags = Flags.END_HEADERS; - if (!contentFollows) - flags |= Flags.END_STREAM; + // Split into CONTINUATION frames if necessary. + if (maxHeaderBlockFragment > 0 && hpackedLength > maxHeaderBlockFragment) + { + int flags = contentFollows ? Flags.NONE : Flags.END_STREAM; + ByteBuffer header = generateHeader(lease, FrameType.HEADERS, maxHeaderBlockFragment, flags, streamId); + BufferUtil.flipToFlush(header, 0); + lease.append(header, true); + hpacked.limit(maxHeaderBlockFragment); + lease.append(hpacked.slice(), false); - ByteBuffer header = generateHeader(lease, FrameType.HEADERS, (int)headersLength, flags, streamId); + int position = maxHeaderBlockFragment; + int limit = position + maxHeaderBlockFragment; + while (limit < hpackedLength) + { + hpacked.position(position).limit(limit); + header = generateHeader(lease, FrameType.CONTINUATION, maxHeaderBlockFragment, Flags.NONE, streamId); + BufferUtil.flipToFlush(header, 0); + lease.append(header, true); + lease.append(hpacked.slice(), false); + position += maxHeaderBlockFragment; + limit += maxHeaderBlockFragment; + } - BufferUtil.flipToFlush(header, 0); - lease.insert(leaseSize, header, true); + hpacked.position(position).limit(hpackedLength); + header = generateHeader(lease, FrameType.CONTINUATION, hpacked.remaining(), Flags.END_HEADERS, streamId); + BufferUtil.flipToFlush(header, 0); + lease.append(header, true); + lease.append(hpacked, true); + } + else + { + int flags = Flags.END_HEADERS; + if (!contentFollows) + flags |= Flags.END_STREAM; + ByteBuffer header = generateHeader(lease, FrameType.HEADERS, hpackedLength, flags, streamId); + BufferUtil.flipToFlush(header, 0); + lease.append(header, true); + lease.append(hpacked, true); + } } } diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/generator/PushPromiseGenerator.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/generator/PushPromiseGenerator.java index a048badad4a..647e4a9d048 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/generator/PushPromiseGenerator.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/generator/PushPromiseGenerator.java @@ -54,28 +54,24 @@ public class PushPromiseGenerator extends FrameGenerator throw new IllegalArgumentException("Invalid promised stream id: " + promisedStreamId); int maxFrameSize = getMaxFrameSize(); - // The promised streamId. - int fixedLength = 4; - maxFrameSize -= fixedLength; + // The promised streamId space. + int extraSpace = 4; + maxFrameSize -= extraSpace; - // The lease may already contain other buffers, - // compute the bytes generated by the encoder only. - int leaseSize = lease.getSize(); - long leaseLength = lease.getTotalLength(); - encoder.encode(metaData, lease, maxFrameSize); - long headersLength = lease.getTotalLength() - leaseLength; - if (headersLength > maxFrameSize) - throw new IllegalArgumentException(String.format("Invalid headers, too big for max frame size: %d > %d", headersLength, maxFrameSize)); - - // Space for the promised streamId. - headersLength += fixedLength; + ByteBuffer hpacked = lease.acquire(maxFrameSize, false); + BufferUtil.clearToFill(hpacked); + encoder.encode(hpacked, metaData); + int hpackedLength = hpacked.position(); + BufferUtil.flipToFlush(hpacked, 0); + int length = hpackedLength + extraSpace; int flags = Flags.END_HEADERS; - ByteBuffer header = generateHeader(lease, FrameType.PUSH_PROMISE, (int)headersLength, flags, streamId); + ByteBuffer header = generateHeader(lease, FrameType.PUSH_PROMISE, length, flags, streamId); header.putInt(promisedStreamId); - BufferUtil.flipToFlush(header, 0); - lease.insert(leaseSize, header, true); + + lease.append(header, true); + lease.append(hpacked, true); } } 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 f58ab89c062..01acaf7d4a7 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 @@ -21,22 +21,95 @@ package org.eclipse.jetty.http2.parser; 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.HeadersFrame; public class ContinuationBodyParser extends BodyParser { private final HeaderBlockParser headerBlockParser; + private final HeaderBlockFragments headerBlockFragments; + private State state = State.PREPARE; + private int length; - public ContinuationBodyParser(HeaderParser headerParser, Parser.Listener listener, HeaderBlockParser headerBlockParser) + public ContinuationBodyParser(HeaderParser headerParser, Parser.Listener listener, HeaderBlockParser headerBlockParser, HeaderBlockFragments headerBlockFragments) { super(headerParser, listener); this.headerBlockParser = headerBlockParser; + this.headerBlockFragments = headerBlockFragments; + } + + @Override + protected void emptyBody(ByteBuffer buffer) + { + reset(); + if (hasFlag(Flags.END_HEADERS)) + onHeaders(); } @Override public boolean parse(ByteBuffer buffer) { - MetaData metaData = headerBlockParser.parse(buffer, getBodyLength()); - // TODO: CONTINUATION frames are not supported for now, we just parse them to keep HPACK happy. - return metaData != null; + while (buffer.hasRemaining()) + { + switch (state) + { + case PREPARE: + { + // SPEC: wrong streamId is treated as connection error. + if (getStreamId() == 0) + return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_continuation_frame"); + + if (getStreamId() != headerBlockFragments.getStreamId()) + return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_continuation_stream"); + + length = getBodyLength(); + state = State.FRAGMENT; + break; + } + case FRAGMENT: + { + int remaining = buffer.remaining(); + if (remaining < length) + { + headerBlockFragments.storeFragment(buffer, remaining, false); + length -= remaining; + } + else + { + boolean last = hasFlag(Flags.END_HEADERS); + headerBlockFragments.storeFragment(buffer, length, last); + reset(); + if (last) + onHeaders(); + return true; + } + } + default: + { + throw new IllegalStateException(); + } + } + } + return false; + } + + private void onHeaders() + { + ByteBuffer headerBlock = headerBlockFragments.complete(); + MetaData metaData = headerBlockParser.parse(headerBlock, headerBlock.remaining()); + HeadersFrame frame = new HeadersFrame(getStreamId(), metaData, headerBlockFragments.getPriorityFrame(), headerBlockFragments.isEndStream()); + notifyHeaders(frame); + } + + private void reset() + { + state = State.PREPARE; + length = 0; + } + + private enum State + { + PREPARE, FRAGMENT } } diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/HeaderBlockFragments.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/HeaderBlockFragments.java new file mode 100644 index 00000000000..a9c609f611f --- /dev/null +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/HeaderBlockFragments.java @@ -0,0 +1,95 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 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.PriorityFrame; + +public class HeaderBlockFragments +{ + private PriorityFrame priorityFrame; + private boolean endStream; + private int streamId; + private ByteBuffer storage; + + public void storeFragment(ByteBuffer fragment, int length, boolean last) + { + if (storage == null) + { + int space = last ? length : length * 2; + storage = ByteBuffer.allocate(space); + } + + // Grow the storage if necessary. + if (storage.remaining() < length) + { + int space = last ? length : length * 2; + int capacity = storage.position() + space; + ByteBuffer newStorage = ByteBuffer.allocate(capacity); + storage.flip(); + newStorage.put(storage); + storage = newStorage; + } + + // Copy the fragment into the storage. + int limit = fragment.limit(); + fragment.limit(fragment.position() + length); + storage.put(fragment); + fragment.limit(limit); + } + + public PriorityFrame getPriorityFrame() + { + return priorityFrame; + } + + public void setPriorityFrame(PriorityFrame priorityFrame) + { + this.priorityFrame = priorityFrame; + } + + public boolean isEndStream() + { + return endStream; + } + + public void setEndStream(boolean endStream) + { + this.endStream = endStream; + } + + public ByteBuffer complete() + { + ByteBuffer result = storage; + storage = null; + result.flip(); + return result; + } + + public int getStreamId() + { + return streamId; + } + + public void setStreamId(int streamId) + { + this.streamId = streamId; + } +} 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 728f9d165c1..7d413f39678 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 @@ -30,6 +30,7 @@ import org.eclipse.jetty.util.BufferUtil; public class HeadersBodyParser extends BodyParser { private final HeaderBlockParser headerBlockParser; + private final HeaderBlockFragments headerBlockFragments; private State state = State.PREPARE; private int cursor; private int length; @@ -38,10 +39,11 @@ public class HeadersBodyParser extends BodyParser private int streamId; private int weight; - public HeadersBodyParser(HeaderParser headerParser, Parser.Listener listener, HeaderBlockParser headerBlockParser) + public HeadersBodyParser(HeaderParser headerParser, Parser.Listener listener, HeaderBlockParser headerBlockParser, HeaderBlockFragments headerBlockFragments) { super(headerParser, listener); this.headerBlockParser = headerBlockParser; + this.headerBlockFragments = headerBlockFragments; } private void reset() @@ -58,8 +60,18 @@ public class HeadersBodyParser extends BodyParser @Override protected void emptyBody(ByteBuffer buffer) { - MetaData metaData = headerBlockParser.parse(BufferUtil.EMPTY_BUFFER, 0); - onHeaders(0, 0, false, metaData); + if (hasFlag(Flags.END_HEADERS)) + { + MetaData metaData = headerBlockParser.parse(BufferUtil.EMPTY_BUFFER, 0); + onHeaders(0, 0, false, metaData); + } + else + { + headerBlockFragments.setStreamId(getStreamId()); + headerBlockFragments.setEndStream(isEndStream()); + if (hasFlag(Flags.PRIORITY)) + headerBlockFragments.setPriorityFrame(new PriorityFrame(streamId, getStreamId(), weight, exclusive)); + } reset(); } @@ -77,10 +89,6 @@ public class HeadersBodyParser extends BodyParser if (getStreamId() == 0) return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_headers_frame"); - // For now we don't support HEADERS frames that don't have END_HEADERS. - if (!hasFlag(Flags.END_HEADERS)) - return connectionFailure(buffer, ErrorCode.INTERNAL_ERROR.code, "unsupported_headers_frame"); - length = getBodyLength(); if (isPadding()) @@ -162,12 +170,34 @@ public class HeadersBodyParser extends BodyParser } case HEADERS: { - MetaData metaData = headerBlockParser.parse(buffer, length); - if (metaData != null) + if (hasFlag(Flags.END_HEADERS)) { - state = State.PADDING; - loop = paddingLength == 0; - onHeaders(streamId, weight, exclusive, metaData); + MetaData metaData = headerBlockParser.parse(buffer, length); + if (metaData != null) + { + state = State.PADDING; + loop = paddingLength == 0; + onHeaders(streamId, weight, exclusive, metaData); + } + } + else + { + int remaining = buffer.remaining(); + if (remaining < length) + { + headerBlockFragments.storeFragment(buffer, remaining, false); + length -= remaining; + } + else + { + headerBlockFragments.setStreamId(getStreamId()); + headerBlockFragments.setEndStream(isEndStream()); + if (hasFlag(Flags.PRIORITY)) + headerBlockFragments.setPriorityFrame(new PriorityFrame(streamId, getStreamId(), weight, exclusive)); + headerBlockFragments.storeFragment(buffer, length, false); + state = State.PADDING; + loop = paddingLength == 0; + } } break; } 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 bde82938f3b..62eecb39ed6 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 @@ -21,6 +21,7 @@ package org.eclipse.jetty.http2.parser; import java.nio.ByteBuffer; import org.eclipse.jetty.http2.ErrorCode; +import org.eclipse.jetty.http2.Flags; import org.eclipse.jetty.http2.frames.DataFrame; import org.eclipse.jetty.http2.frames.FrameType; import org.eclipse.jetty.http2.frames.GoAwayFrame; @@ -49,6 +50,7 @@ public class Parser private final Listener listener; private final HeaderParser headerParser; private final BodyParser[] bodyParsers; + private boolean continuation; private State state = State.HEADER; public Parser(ByteBufferPool byteBufferPool, Listener listener, int maxDynamicTableSize, int maxHeaderSize) @@ -58,9 +60,10 @@ public class Parser this.bodyParsers = new BodyParser[FrameType.values().length]; HeaderBlockParser headerBlockParser = new HeaderBlockParser(byteBufferPool, new HpackDecoder(maxDynamicTableSize, maxHeaderSize)); + HeaderBlockFragments headerBlockFragments = new HeaderBlockFragments(); bodyParsers[FrameType.DATA.getType()] = new DataBodyParser(headerParser, listener); - bodyParsers[FrameType.HEADERS.getType()] = new HeadersBodyParser(headerParser, listener, headerBlockParser); + bodyParsers[FrameType.HEADERS.getType()] = new HeadersBodyParser(headerParser, listener, headerBlockParser, headerBlockFragments); 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); @@ -68,7 +71,7 @@ public class Parser 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); - bodyParsers[FrameType.CONTINUATION.getType()] = new ContinuationBodyParser(headerParser, listener, headerBlockParser); + bodyParsers[FrameType.CONTINUATION.getType()] = new ContinuationBodyParser(headerParser, listener, headerBlockParser, headerBlockFragments); } private void reset() @@ -100,6 +103,29 @@ public class Parser { if (!headerParser.parse(buffer)) return; + + if (continuation) + { + if (headerParser.getFrameType() != FrameType.CONTINUATION.getType()) + { + // SPEC: CONTINUATION frames must be consecutive. + BufferUtil.clear(buffer); + notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "continuation_frame_expected"); + return; + } + if (headerParser.hasFlag(Flags.END_HEADERS)) + { + continuation = false; + } + } + else + { + if (headerParser.getFrameType() == FrameType.HEADERS.getType() && + !headerParser.hasFlag(Flags.END_HEADERS)) + { + continuation = true; + } + } state = State.BODY; break; } diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java index 65b8a952105..3cddcd0133d 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java @@ -31,30 +31,28 @@ import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.PreEncodedHttpField; import org.eclipse.jetty.http2.hpack.HpackContext.Entry; import org.eclipse.jetty.http2.hpack.HpackContext.StaticEntry; -import org.eclipse.jetty.io.ByteBufferPool.Lease; -import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; public class HpackEncoder -{ +{ public static final Logger LOG = Log.getLogger(HpackEncoder.class); - + private final static HttpField[] __status= new HttpField[599]; - - - final static EnumSet __DO_NOT_HUFFMAN = + + + final static EnumSet __DO_NOT_HUFFMAN = EnumSet.of( HttpHeader.AUTHORIZATION, HttpHeader.CONTENT_MD5, HttpHeader.PROXY_AUTHENTICATE, HttpHeader.PROXY_AUTHORIZATION); - - final static EnumSet __DO_NOT_INDEX = + + final static EnumSet __DO_NOT_INDEX = EnumSet.of( - // HttpHeader.C_PATH, // TODO more data needed - // HttpHeader.DATE, // TODO more data needed + // HttpHeader.C_PATH, // TODO more data needed + // HttpHeader.DATE, // TODO more data needed HttpHeader.AUTHORIZATION, HttpHeader.CONTENT_MD5, HttpHeader.CONTENT_RANGE, @@ -71,35 +69,35 @@ public class HpackEncoder HttpHeader.LAST_MODIFIED, HttpHeader.SET_COOKIE, HttpHeader.SET_COOKIE2); - - final static EnumSet __NEVER_INDEX = + + final static EnumSet __NEVER_INDEX = EnumSet.of( HttpHeader.AUTHORIZATION, HttpHeader.SET_COOKIE, HttpHeader.SET_COOKIE2); - + static { for (HttpStatus.Code code : HttpStatus.Code.values()) __status[code.getCode()]=new PreEncodedHttpField(HttpHeader.C_STATUS,Integer.toString(code.getCode())); } - + private final HpackContext _context; private final boolean _debug; private int _remoteMaxDynamicTableSize; private int _localMaxDynamicTableSize; - + public HpackEncoder() { this(4096,4096); } - + public HpackEncoder(int localMaxDynamicTableSize) { this(localMaxDynamicTableSize,4096); } - + public HpackEncoder(int localMaxDynamicTableSize,int remoteMaxDynamicTableSize) { _context=new HpackContext(remoteMaxDynamicTableSize); @@ -112,27 +110,17 @@ public class HpackEncoder { return _context; } - + public void setRemoteMaxDynamicTableSize(int remoteMaxDynamicTableSize) { _remoteMaxDynamicTableSize=remoteMaxDynamicTableSize; } - + public void setLocalMaxDynamicTableSize(int localMaxDynamicTableSize) { _localMaxDynamicTableSize=localMaxDynamicTableSize; } - - // TODO better handling of buffer size - public void encode(MetaData metadata,Lease lease,int buffersize) - { - ByteBuffer buffer = lease.acquire(buffersize,false); - lease.append(buffer,true); - BufferUtil.clearToFill(buffer); - encode(buffer,metadata); - BufferUtil.flipToFlush(buffer,0); - } - + public void encode(ByteBuffer buffer, MetaData metadata) { if (LOG.isDebugEnabled()) @@ -188,7 +176,7 @@ public class HpackEncoder public void encode(ByteBuffer buffer, HttpField field) { final int p=_debug?buffer.position():-1; - + String encoding=null; // Is there an entry for the field? @@ -215,18 +203,18 @@ public class HpackEncoder { // Unknown field entry, so we will have to send literally. final boolean indexed; - + // But do we know it's name? HttpHeader header = field.getHeader(); - + // Select encoding strategy if (header==null) { // Select encoding strategy for unknown header names Entry name = _context.get(field.getName()); - + if (field instanceof PreEncodedHttpField) - { + { int i=buffer.position(); ((PreEncodedHttpField)field).putTo(buffer,HttpVersion.HTTP_2); byte b=buffer.get(i); @@ -237,7 +225,7 @@ public class HpackEncoder // has the custom header name been seen before? else if (name==null) { - // unknown name and value, so let's index this just in case it is + // unknown name and value, so let's index this just in case it is // the first time we have seen a custom name or a custom field. // unless the name is changing, this is worthwhile indexed=true; @@ -257,13 +245,13 @@ public class HpackEncoder encoding="LitHuffNHuffV!Idx"; } } - else + else { // Select encoding strategy for known header names Entry name = _context.get(header); if (field instanceof PreEncodedHttpField) - { + { // Preencoded field int i=buffer.position(); ((PreEncodedHttpField)field).putTo(buffer,HttpVersion.HTTP_2); @@ -320,9 +308,9 @@ public class HpackEncoder int e=buffer.position(); if (LOG.isDebugEnabled()) LOG.debug("encode {}:'{}' to '{}'",encoding,field,TypeUtil.toHexString(buffer.array(),buffer.arrayOffset()+p,e-p)); - } + } } - + private void encodeName(ByteBuffer buffer, byte mask, int bits, String name, Entry entry) { buffer.put(mask); @@ -339,7 +327,7 @@ public class HpackEncoder NBitInteger.encode(buffer,bits,_context.index(entry)); } } - + static void encodeValue(ByteBuffer buffer, boolean huffman, String value) { if (huffman) 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 e9bbb49728f..f625aaa2329 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 @@ -38,6 +38,7 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne private int maxDynamicTableSize = 4096; private int initialStreamSendWindow = FlowControlStrategy.DEFAULT_WINDOW_SIZE; private int maxConcurrentStreams = -1; + private int maxHeaderBlockFragment = 0; private final HttpConfiguration httpConfiguration; public AbstractHTTP2ServerConnectionFactory(@Name("config") HttpConfiguration httpConfiguration) @@ -81,6 +82,16 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne this.maxConcurrentStreams = maxConcurrentStreams; } + public int getMaxHeaderBlockFragment() + { + return maxHeaderBlockFragment; + } + + public void setMaxHeaderBlockFragment(int maxHeaderBlockFragment) + { + this.maxHeaderBlockFragment = maxHeaderBlockFragment; + } + public HttpConfiguration getHttpConfiguration() { return httpConfiguration; @@ -91,7 +102,7 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne { ServerSessionListener listener = newSessionListener(connector, endPoint); - Generator generator = new Generator(connector.getByteBufferPool(), getMaxDynamicTableSize()); + Generator generator = new Generator(connector.getByteBufferPool(), getMaxDynamicTableSize(), getMaxHeaderBlockFragment()); FlowControlStrategy flowControl = newFlowControlStrategy(); HTTP2ServerSession session = new HTTP2ServerSession(connector.getScheduler(), endPoint, generator, listener, flowControl); session.setMaxLocalStreams(getMaxConcurrentStreams()); diff --git a/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2ServerTest.java b/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2ServerTest.java index 29f8d22b3ed..0e104fac08e 100644 --- a/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2ServerTest.java +++ b/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2ServerTest.java @@ -26,10 +26,13 @@ import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; + import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -38,12 +41,15 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http2.ErrorCode; +import org.eclipse.jetty.http2.Flags; import org.eclipse.jetty.http2.frames.DataFrame; +import org.eclipse.jetty.http2.frames.FrameType; 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.PrefaceFrame; import org.eclipse.jetty.http2.frames.SettingsFrame; +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.ManagedSelector; @@ -390,4 +396,142 @@ public class HTTP2ServerTest extends AbstractServerTest logger.setHideStacks(false); } } + + @Test + public void testRequestWithContinuationFrames() throws Exception + { + testRequestWithContinuationFrames(new Callable() + { + @Override + public ByteBufferPool.Lease call() throws Exception + { + ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool); + generator.control(lease, new PrefaceFrame()); + MetaData.Request metaData = newRequest("GET", new HttpFields()); + generator.control(lease, new HeadersFrame(1, metaData, null, true)); + return lease; + } + }); + } + + @Test + public void testRequestWithContinuationFramesWithEmptyHeadersFrame() throws Exception + { + testRequestWithContinuationFrames(new Callable() + { + @Override + public ByteBufferPool.Lease call() throws Exception + { + ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool); + generator.control(lease, new PrefaceFrame()); + MetaData.Request metaData = newRequest("GET", new HttpFields()); + generator.control(lease, new HeadersFrame(1, metaData, null, true)); + // Take the HeadersFrame header and set the length to zero. + List buffers = lease.getByteBuffers(); + ByteBuffer headersFrameHeader = buffers.get(1); + headersFrameHeader.put(0, (byte)0); + headersFrameHeader.putShort(1, (short)0); + // Insert a CONTINUATION frame header for the body of the HEADERS frame. + lease.insert(2, buffers.get(3).slice(), false); + return lease; + } + }); + } + + @Test + public void testRequestWithContinuationFramesWithEmptyContinuationFrame() throws Exception + { + testRequestWithContinuationFrames(new Callable() + { + @Override + public ByteBufferPool.Lease call() throws Exception + { + ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool); + generator.control(lease, new PrefaceFrame()); + MetaData.Request metaData = newRequest("GET", new HttpFields()); + generator.control(lease, new HeadersFrame(1, metaData, null, true)); + // Take the ContinuationFrame header, duplicate it, and set the length to zero. + List buffers = lease.getByteBuffers(); + ByteBuffer continuationFrameHeader = buffers.get(3); + ByteBuffer duplicate = ByteBuffer.allocate(continuationFrameHeader.remaining()); + duplicate.put(continuationFrameHeader).flip(); + continuationFrameHeader.flip(); + continuationFrameHeader.put(0, (byte)0); + continuationFrameHeader.putShort(1, (short)0); + // Insert a CONTINUATION frame header for the body of the previous CONTINUATION frame. + lease.insert(4, duplicate, false); + return lease; + } + }); + } + + @Test + public void testRequestWithContinuationFramesWithEmptyLastContinuationFrame() throws Exception + { + testRequestWithContinuationFrames(new Callable() + { + @Override + public ByteBufferPool.Lease call() throws Exception + { + ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool); + generator.control(lease, new PrefaceFrame()); + MetaData.Request metaData = newRequest("GET", new HttpFields()); + generator.control(lease, new HeadersFrame(1, metaData, null, true)); + // Take the last CONTINUATION frame and reset the flag. + List buffers = lease.getByteBuffers(); + ByteBuffer continuationFrameHeader = buffers.get(buffers.size() - 2); + continuationFrameHeader.put(4, (byte)0); + // Add a last, empty, CONTINUATION frame. + ByteBuffer last = ByteBuffer.wrap(new byte[]{ + 0, 0, 0, // Length + (byte)FrameType.CONTINUATION.getType(), + (byte)Flags.END_HEADERS, + 0, 0, 0, 1 // Stream ID + }); + lease.append(last, false); + return lease; + } + }); + } + + private void testRequestWithContinuationFrames(Callable frames) throws Exception + { + final CountDownLatch serverLatch = new CountDownLatch(1); + startServer(new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + serverLatch.countDown(); + } + }); + generator = new Generator(byteBufferPool, 4096, 4); + + ByteBufferPool.Lease lease = frames.call(); + + try (Socket client = new Socket("localhost", connector.getLocalPort())) + { + OutputStream output = client.getOutputStream(); + for (ByteBuffer buffer : lease.getByteBuffers()) + output.write(BufferUtil.toArray(buffer)); + output.flush(); + + Assert.assertTrue(serverLatch.await(5, TimeUnit.SECONDS)); + + final CountDownLatch clientLatch = new CountDownLatch(1); + Parser parser = new Parser(byteBufferPool, new Parser.Listener.Adapter() + { + @Override + public void onHeaders(HeadersFrame frame) + { + if (frame.isEndStream()) + clientLatch.countDown(); + } + }, 4096, 8192); + boolean closed = parseResponse(client, parser); + + Assert.assertTrue(clientLatch.await(5, TimeUnit.SECONDS)); + Assert.assertFalse(closed); + } + } }