Adding support for HTTP/2 binary headers

Motivation:

The HTTP/2 spec does not restrict headers to being String. The current
implementation of the HTTP/2 codec uses Strings as header keys and
values. We should change this so that header keys and values allow
binary values.

Modifications:

Making Http2Headers based on AsciiString, which is a wrapper around a
byte[].
Various changes throughout the HTTP/2 codec to use the new interface.

Result:

HTTP/2 codec no longer requires string headers.
This commit is contained in:
nmittler 2014-09-13 13:13:28 -07:00
parent 94deea409e
commit 43d097d25a
33 changed files with 3243 additions and 2184 deletions

View File

@ -14,6 +14,13 @@
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_ENCODING;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_LENGTH;
import static io.netty.handler.codec.http.HttpHeaders.Values.DEFLATE;
import static io.netty.handler.codec.http.HttpHeaders.Values.GZIP;
import static io.netty.handler.codec.http.HttpHeaders.Values.IDENTITY;
import static io.netty.handler.codec.http.HttpHeaders.Values.XDEFLATE;
import static io.netty.handler.codec.http.HttpHeaders.Values.XGZIP;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
@ -29,8 +36,8 @@ import io.netty.handler.codec.http.HttpHeaders;
* to the {@code content-encoding} header for each stream.
*/
public class DecompressorHttp2FrameReader extends DefaultHttp2FrameReader {
private static final AsciiString CONTENT_ENCODING_LOWER_CASE = HttpHeaders.Names.CONTENT_ENCODING.toLowerCase();
private static final AsciiString CONTENT_LENGTH_LOWER_CASE = HttpHeaders.Names.CONTENT_LENGTH.toLowerCase();
private static final AsciiString CONTENT_ENCODING_LOWER_CASE = CONTENT_ENCODING.toLowerCase();
private static final AsciiString CONTENT_LENGTH_LOWER_CASE = CONTENT_LENGTH.toLowerCase();
private static final Http2ConnectionAdapter CLEAN_UP_LISTENER = new Http2ConnectionAdapter() {
@Override
public void streamRemoved(Http2Stream stream) {
@ -78,12 +85,12 @@ public class DecompressorHttp2FrameReader extends DefaultHttp2FrameReader {
* @throws Http2Exception If the specified encoding is not not supported and warrants an exception
*/
protected EmbeddedChannel newContentDecoder(CharSequence contentEncoding) throws Http2Exception {
if (HttpHeaders.Values.GZIP.equalsIgnoreCase(contentEncoding) ||
HttpHeaders.Values.XGZIP.equalsIgnoreCase(contentEncoding)) {
if (GZIP.equalsIgnoreCase(contentEncoding) ||
XGZIP.equalsIgnoreCase(contentEncoding)) {
return new EmbeddedChannel(ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP));
}
if (HttpHeaders.Values.DEFLATE.equalsIgnoreCase(contentEncoding) ||
HttpHeaders.Values.XDEFLATE.equalsIgnoreCase(contentEncoding)) {
if (DEFLATE.equalsIgnoreCase(contentEncoding) ||
XDEFLATE.equalsIgnoreCase(contentEncoding)) {
final ZlibWrapper wrapper = strict ? ZlibWrapper.ZLIB : ZlibWrapper.ZLIB_OR_NONE;
// To be strict, 'deflate' means ZLIB, but some servers were not implemented correctly.
return new EmbeddedChannel(ZlibCodecFactory.newZlibDecoder(wrapper));
@ -101,7 +108,7 @@ public class DecompressorHttp2FrameReader extends DefaultHttp2FrameReader {
* @return the expected content encoding of the new content.
* @throws Http2Exception if the {@code contentEncoding} is not supported and warrants an exception
*/
protected CharSequence getTargetContentEncoding(
protected AsciiString getTargetContentEncoding(
@SuppressWarnings("UnusedParameters") CharSequence contentEncoding) throws Http2Exception {
return HttpHeaders.Values.IDENTITY;
}
@ -114,28 +121,29 @@ public class DecompressorHttp2FrameReader extends DefaultHttp2FrameReader {
* @param endOfStream Indicates if the stream has ended
* @throws Http2Exception If the {@code content-encoding} is not supported
*/
private void initDecoder(int streamId, Http2Headers.Builder builder, boolean endOfStream)
private void initDecoder(int streamId, Http2Headers headers, boolean endOfStream)
throws Http2Exception {
// Convert the names into a case-insensitive map.
final Http2Stream stream = connection.stream(streamId);
if (stream != null) {
EmbeddedChannel decoder = stream.decompressor();
if (decoder == null) {
if (!endOfStream) {
// Determine the content encoding.
CharSequence contentEncoding = builder.get(CONTENT_ENCODING_LOWER_CASE);
AsciiString contentEncoding = headers.get(CONTENT_ENCODING_LOWER_CASE);
if (contentEncoding == null) {
contentEncoding = HttpHeaders.Values.IDENTITY;
contentEncoding = IDENTITY;
}
decoder = newContentDecoder(contentEncoding);
if (decoder != null) {
stream.decompressor(decoder);
// Decode the content and remove or replace the existing headers
// so that the message looks like a decoded message.
CharSequence targetContentEncoding = getTargetContentEncoding(contentEncoding);
if (HttpHeaders.Values.IDENTITY.equalsIgnoreCase(targetContentEncoding)) {
builder.remove(CONTENT_ENCODING_LOWER_CASE);
AsciiString targetContentEncoding = getTargetContentEncoding(contentEncoding);
if (IDENTITY.equalsIgnoreCase(targetContentEncoding)) {
headers.remove(CONTENT_ENCODING_LOWER_CASE);
} else {
builder.set(CONTENT_ENCODING_LOWER_CASE, targetContentEncoding);
headers.set(CONTENT_ENCODING_LOWER_CASE, targetContentEncoding);
}
}
}
@ -146,7 +154,7 @@ public class DecompressorHttp2FrameReader extends DefaultHttp2FrameReader {
// The content length will be for the compressed data. Since we will decompress the data
// this content-length will not be correct. Instead of queuing messages or delaying sending
// header frames...just remove the content-length header
builder.remove(CONTENT_LENGTH_LOWER_CASE);
headers.remove(CONTENT_LENGTH_LOWER_CASE);
}
}
}
@ -226,18 +234,18 @@ public class DecompressorHttp2FrameReader extends DefaultHttp2FrameReader {
}
@Override
protected void notifyListenerOnHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers.Builder builder,
protected void notifyListenerOnHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
int streamDependency, short weight, boolean exclusive, int padding, boolean endOfStream,
Http2FrameListener listener) throws Http2Exception {
initDecoder(streamId, builder, endOfStream);
super.notifyListenerOnHeadersRead(ctx, streamId, builder, streamDependency, weight,
initDecoder(streamId, headers, endOfStream);
super.notifyListenerOnHeadersRead(ctx, streamId, headers, streamDependency, weight,
exclusive, padding, endOfStream, listener);
}
@Override
protected void notifyListenerOnHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers.Builder builder,
protected void notifyListenerOnHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
int padding, boolean endOfStream, Http2FrameListener listener) throws Http2Exception {
initDecoder(streamId, builder, endOfStream);
super.notifyListenerOnHeadersRead(ctx, streamId, builder, padding, endOfStream, listener);
initDecoder(streamId, headers, endOfStream);
super.notifyListenerOnHeadersRead(ctx, streamId, headers, padding, endOfStream, listener);
}
}

View File

@ -372,16 +372,16 @@ public class DefaultHttp2FrameReader implements Http2FrameReader {
listener.onDataRead(ctx, streamId, data, padding, endOfStream);
}
protected void notifyListenerOnHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers.Builder builder,
protected void notifyListenerOnHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
int streamDependency, short weight, boolean exclusive, int padding,
boolean endOfStream, Http2FrameListener listener) throws Http2Exception {
listener.onHeadersRead(ctx, streamId, builder.build(), streamDependency,
listener.onHeadersRead(ctx, streamId, headers, streamDependency,
weight, exclusive, padding, endOfStream);
}
protected void notifyListenerOnHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers.Builder builder,
protected void notifyListenerOnHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
int padding, boolean endOfStream, Http2FrameListener listener) throws Http2Exception {
listener.onHeadersRead(ctx, streamId, builder.build(), padding, endOfStream);
listener.onHeadersRead(ctx, streamId, headers, padding, endOfStream);
}
private void readDataFrame(ChannelHandlerContext ctx, ByteBuf payload,
@ -428,7 +428,7 @@ public class DefaultHttp2FrameReader implements Http2FrameReader {
final HeadersBlockBuilder hdrBlockBuilder = headersBlockBuilder();
hdrBlockBuilder.addFragment(fragment, ctx.alloc(), endOfHeaders);
if (endOfHeaders) {
notifyListenerOnHeadersRead(ctx, headersStreamId, hdrBlockBuilder.builder(),
notifyListenerOnHeadersRead(ctx, headersStreamId, hdrBlockBuilder.headers(),
streamDependency, weight, exclusive, padding, headersFlags.endOfStream(), listener);
close();
}
@ -454,7 +454,7 @@ public class DefaultHttp2FrameReader implements Http2FrameReader {
final HeadersBlockBuilder hdrBlockBuilder = headersBlockBuilder();
hdrBlockBuilder.addFragment(fragment, ctx.alloc(), endOfHeaders);
if (endOfHeaders) {
notifyListenerOnHeadersRead(ctx, headersStreamId, hdrBlockBuilder.builder(), padding,
notifyListenerOnHeadersRead(ctx, headersStreamId, hdrBlockBuilder.headers(), padding,
headersFlags.endOfStream(), listener);
close();
}
@ -525,7 +525,7 @@ public class DefaultHttp2FrameReader implements Http2FrameReader {
Http2FrameListener listener) throws Http2Exception {
headersBlockBuilder().addFragment(fragment, ctx.alloc(), endOfHeaders);
if (endOfHeaders) {
Http2Headers headers = headersBlockBuilder().builder().build();
Http2Headers headers = headersBlockBuilder().headers();
listener.onPushPromiseRead(ctx, pushPromiseStreamId, promisedStreamId, headers,
padding);
close();
@ -676,7 +676,7 @@ public class DefaultHttp2FrameReader implements Http2FrameReader {
* Builds the headers from the completed headers block. After this is called, this builder
* should not be called again.
*/
Http2Headers.Builder builder() throws Http2Exception {
Http2Headers headers() throws Http2Exception {
try {
return headersDecoder.decodeHeaders(headerBlock);
} finally {

View File

@ -12,601 +12,149 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.AUTHORITY;
import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.METHOD;
import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.SCHEME;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.BinaryHeaders;
import io.netty.handler.codec.DefaultBinaryHeaders;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeSet;
public class DefaultHttp2Headers extends DefaultBinaryHeaders implements Http2Headers {
/**
* An immutable collection of headers sent or received via HTTP/2.
*/
public final class DefaultHttp2Headers extends Http2Headers {
private static final int MAX_VALUE_LENGTH = 0xFFFF; // Length is a 16-bit field
private static final int BUCKET_SIZE = 17;
private final HeaderEntry[] entries;
private final HeaderEntry head;
private final int size;
private DefaultHttp2Headers(Builder builder) {
entries = builder.entries;
head = builder.head;
size = builder.size;
public DefaultHttp2Headers() {
}
@Override
public String get(CharSequence name) {
if (name == null) {
throw new NullPointerException("name");
}
int h = hash(name);
int i = index(h);
HeaderEntry e = entries[i];
while (e != null) {
if (e.hash == h && eq(name, e.key)) {
return e.value;
}
e = e.next;
}
return null;
public Http2Headers add(AsciiString name, AsciiString value) {
super.add(name, value);
return this;
}
@Override
public List<String> getAll(CharSequence name) {
if (name == null) {
throw new NullPointerException("name");
}
LinkedList<String> values = new LinkedList<String>();
int h = hash(name);
int i = index(h);
HeaderEntry e = entries[i];
while (e != null) {
if (e.hash == h && eq(name, e.key)) {
values.addFirst(e.value);
}
e = e.next;
}
return values;
public Http2Headers add(AsciiString name, Iterable<AsciiString> values) {
super.add(name, values);
return this;
}
@Override
public List<Entry<String, String>> entries() {
List<Map.Entry<String, String>> all = new LinkedList<Map.Entry<String, String>>();
HeaderEntry e = head.after;
while (e != head) {
all.add(e);
e = e.after;
}
return all;
public Http2Headers add(AsciiString name, AsciiString... values) {
super.add(name, values);
return this;
}
@Override
public boolean contains(CharSequence name) {
return get(name) != null;
public Http2Headers add(BinaryHeaders headers) {
super.add(headers);
return this;
}
@Override
public boolean isEmpty() {
return size == 0;
public Http2Headers set(AsciiString name, AsciiString value) {
super.set(name, value);
return this;
}
@Override
public int size() {
return size;
public Http2Headers set(AsciiString name, Iterable<AsciiString> values) {
super.set(name, values);
return this;
}
@Override
public Set<String> names() {
Set<String> names = new TreeSet<String>();
HeaderEntry e = head.after;
while (e != head) {
names.add(e.key);
e = e.after;
}
return names;
public Http2Headers set(AsciiString name, AsciiString... values) {
super.set(name, values);
return this;
}
@Override
public Iterator<Entry<String, String>> iterator() {
return new HeaderIterator();
public Http2Headers set(BinaryHeaders headers) {
super.set(headers);
return this;
}
@Override
public String forEach(HeaderVisitor visitor) {
if (isEmpty()) {
return null;
}
HeaderEntry e = head.after;
do {
if (visitor.visit(e)) {
e = e.after;
} else {
return e.getKey();
}
} while (e != head);
return null;
public Http2Headers setAll(BinaryHeaders headers) {
super.setAll(headers);
return this;
}
/**
* Short cut for {@code new DefaultHttp2Headers.Builder()}.
*/
public static Builder newBuilder() {
return new Builder();
@Override
public Http2Headers clear() {
super.clear();
return this;
}
/**
* Builds instances of {@link DefaultHttp2Headers}.
*/
public static class Builder implements Http2Headers.Builder {
private HeaderEntry[] entries;
private HeaderEntry head;
private Http2Headers buildResults;
private int size;
public Builder() {
clear();
}
@Override
public String get(CharSequence name) {
if (name == null) {
throw new NullPointerException("name");
}
int h = hash(name);
int i = index(h);
HeaderEntry e = entries[i];
while (e != null) {
if (e.hash == h && eq(name, e.key)) {
return e.value;
}
e = e.next;
}
return null;
}
@Override
public List<String> getAll(CharSequence name) {
if (name == null) {
throw new NullPointerException("name");
}
LinkedList<String> values = new LinkedList<String>();
int h = hash(name);
int i = index(h);
HeaderEntry e = entries[i];
while (e != null) {
if (e.hash == h && eq(name, e.key)) {
values.addFirst(e.value);
}
e = e.next;
}
return values;
}
@Override
public void set(Http2Headers headers) {
// No need to lazy copy the previous results, since we're starting from scratch.
clear();
for (Map.Entry<String, String> entry : headers) {
add(entry.getKey(), entry.getValue());
}
}
@Override
public Builder add(CharSequence name, Object value) {
return add(name.toString(), value);
}
@Override
public Builder add(String name, Object value) {
// If this is the first call on the builder since the last build, copy the previous
// results.
lazyCopy();
String lowerCaseName = name.toLowerCase();
validateHeaderName(lowerCaseName);
String strVal = toString(value);
validateHeaderValue(strVal);
int nameHash = hash(lowerCaseName);
int hashTableIndex = index(nameHash);
add0(nameHash, hashTableIndex, lowerCaseName, strVal);
return this;
}
@Override
public Builder remove(CharSequence name) {
if (name == null) {
throw new NullPointerException("name");
}
// If this is the first call on the builder since the last build, copy the previous
// results.
lazyCopy();
remove0(name);
return this;
}
@Override
public Builder remove(String name) {
if (name == null) {
throw new NullPointerException("name");
}
// If this is the first call on the builder since the last build, copy the previous
// results.
lazyCopy();
remove0(name.toLowerCase());
return this;
}
@Override
public Builder set(CharSequence name, Object value) {
return set(name.toString(), value);
}
@Override
public Builder set(String name, Object value) {
// If this is the first call on the builder since the last build, copy the previous
// results.
lazyCopy();
String lowerCaseName = name.toLowerCase();
validateHeaderName(lowerCaseName);
String strVal = toString(value);
validateHeaderValue(strVal);
int nameHash = hash(lowerCaseName);
int hashTableIndex = index(nameHash);
remove0(nameHash, hashTableIndex, lowerCaseName);
add0(nameHash, hashTableIndex, lowerCaseName, strVal);
return this;
}
@Override
public Builder set(String name, Iterable<?> values) {
if (values == null) {
throw new NullPointerException("values");
}
// If this is the first call on the builder since the last build, copy the previous
// results.
lazyCopy();
String lowerCaseName = name.toLowerCase();
validateHeaderName(lowerCaseName);
int nameHash = hash(lowerCaseName);
int hashTableIndex = index(nameHash);
remove0(nameHash, hashTableIndex, lowerCaseName);
for (Object v : values) {
if (v == null) {
break;
}
String strVal = toString(v);
validateHeaderValue(strVal);
add0(nameHash, hashTableIndex, lowerCaseName, strVal);
}
return this;
}
@Override
public int size() {
return size;
}
@Override
public Builder clear() {
// No lazy copy required, since we're just creating a new array.
entries = new HeaderEntry[BUCKET_SIZE];
head = new HeaderEntry(-1, null, null);
head.before = head.after = head;
buildResults = null;
size = 0;
return this;
}
@Override
public Builder method(String method) {
return set(METHOD.value(), method);
}
@Override
public Builder scheme(String scheme) {
return set(SCHEME.value(), scheme);
}
@Override
public Builder authority(String authority) {
return set(AUTHORITY.value(), authority);
}
@Override
public Builder path(String path) {
return set(PseudoHeaderName.PATH.value(), path);
}
@Override
public Builder status(String status) {
return set(PseudoHeaderName.STATUS.value(), status);
}
@Override
public DefaultHttp2Headers build() {
// If this is the first call on the builder since the last build, copy the previous
// results.
lazyCopy();
// Give the multimap over to the headers instance and save the build results for
// future lazy copies if this builder is used again later.
DefaultHttp2Headers headers = new DefaultHttp2Headers(this);
buildResults = headers;
return headers;
}
/**
* Performs a lazy copy of the last build results, if there are any. For the typical use
* case, headers will only be built once so no copy will be required. If the any method is
* called on the builder after that, it will force a copy of the most recently created
* headers object.
*/
private void lazyCopy() {
if (buildResults != null) {
set(buildResults);
buildResults = null;
}
}
private void add0(int hash, int hashTableIndex, final String name, final String value) {
// Update the hash table.
HeaderEntry e = entries[hashTableIndex];
HeaderEntry newEntry;
entries[hashTableIndex] = newEntry = new HeaderEntry(hash, name, value);
newEntry.next = e;
// Update the linked list.
newEntry.addBefore(head);
size++;
}
private void remove0(final CharSequence name) {
final int nameHash = hash(name);
final int hashTableIndex = index(nameHash);
remove0(nameHash, hashTableIndex, name);
}
private void remove0(int hash, int hashTableIndex, CharSequence name) {
HeaderEntry e = entries[hashTableIndex];
if (e == null) {
return;
}
for (;;) {
if (e.hash == hash && eq(name, e.key)) {
e.remove();
size--;
HeaderEntry next = e.next;
if (next != null) {
entries[hashTableIndex] = next;
e = next;
} else {
entries[hashTableIndex] = null;
return;
}
} else {
break;
}
}
for (;;) {
HeaderEntry next = e.next;
if (next == null) {
break;
}
if (next.hash == hash && eq(name, next.key)) {
e.next = next.next;
next.remove();
size--;
} else {
e = next;
}
}
}
private static String toString(Object value) {
if (value == null) {
return null;
}
return value.toString();
}
/**
* Validate a HTTP2 header value. Does not validate max length.
*/
private static void validateHeaderValue(String value) {
if (value == null) {
throw new NullPointerException("value");
}
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
if (c == 0) {
throw new IllegalArgumentException("value contains null character: " + value);
}
}
}
/**
* Validate a HTTP/2 header name.
*/
private static void validateHeaderName(String name) {
if (name == null) {
throw new NullPointerException("name");
}
if (name.isEmpty()) {
throw new IllegalArgumentException("name cannot be length zero");
}
// Since name may only contain ascii characters, for valid names
// name.length() returns the number of bytes when UTF-8 encoded.
if (name.length() > MAX_VALUE_LENGTH) {
throw new IllegalArgumentException("name exceeds allowable length: " + name);
}
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (c == 0) {
throw new IllegalArgumentException("name contains null character: " + name);
}
if (c > 127) {
throw new IllegalArgumentException("name contains non-ascii character: " + name);
}
}
// If the name looks like an HTTP/2 pseudo-header, validate it against the list of
// valid pseudo-headers.
if (name.startsWith(PSEUDO_HEADER_PREFIX)) {
if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) {
throw new IllegalArgumentException("Invalid HTTP/2 Pseudo-header: " + name);
}
}
}
@Override
public Http2Headers forEachEntry(final BinaryHeaders.BinaryHeaderVisitor visitor) {
super.forEachEntry(visitor);
return this;
}
private static int hash(CharSequence name) {
int h = 0;
for (int i = name.length() - 1; i >= 0; i--) {
char c = name.charAt(i);
if (c >= 'A' && c <= 'Z') {
c += 32;
}
h = 31 * h + c;
}
if (h > 0) {
return h;
} else if (h == Integer.MIN_VALUE) {
return Integer.MAX_VALUE;
} else {
return -h;
}
@Override
public int hashCode() {
return super.hashCode();
}
private static boolean eq(CharSequence name1, CharSequence name2) {
int nameLen = name1.length();
if (nameLen != name2.length()) {
@Override
public boolean equals(Object o) {
if (!(o instanceof Http2Headers)) {
return false;
}
for (int i = nameLen - 1; i >= 0; i--) {
char c1 = name1.charAt(i);
char c2 = name2.charAt(i);
if (c1 != c2) {
if (c1 >= 'A' && c1 <= 'Z') {
c1 += 32;
}
if (c2 >= 'A' && c2 <= 'Z') {
c2 += 32;
}
if (c1 != c2) {
return false;
}
}
}
return true;
return super.equals((BinaryHeaders) o);
}
private static int index(int hash) {
return hash % BUCKET_SIZE;
@Override
public Http2Headers method(AsciiString value) {
set(PseudoHeaderName.METHOD.value(), value);
return this;
}
private final class HeaderIterator implements Iterator<Map.Entry<String, String>> {
private HeaderEntry current = head;
@Override
public boolean hasNext() {
return current.after != head;
}
@Override
public Entry<String, String> next() {
current = current.after;
if (current == head) {
throw new NoSuchElementException();
}
return current;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
@Override
public Http2Headers scheme(AsciiString value) {
set(PseudoHeaderName.SCHEME.value(), value);
return this;
}
private static final class HeaderEntry implements Map.Entry<String, String> {
final int hash;
final String key;
final String value;
HeaderEntry next;
HeaderEntry before, after;
@Override
public Http2Headers authority(AsciiString value) {
set(PseudoHeaderName.AUTHORITY.value(), value);
return this;
}
HeaderEntry(int hash, String key, String value) {
this.hash = hash;
this.key = key;
this.value = value;
}
@Override
public Http2Headers path(AsciiString value) {
set(PseudoHeaderName.PATH.value(), value);
return this;
}
void remove() {
before.after = after;
after.before = before;
}
@Override
public Http2Headers status(AsciiString value) {
set(PseudoHeaderName.STATUS.value(), value);
return this;
}
void addBefore(HeaderEntry e) {
after = e;
before = e.before;
before.after = this;
after.before = this;
}
@Override
public AsciiString method() {
return get(PseudoHeaderName.METHOD.value());
}
@Override
public String getKey() {
return key;
}
@Override
public AsciiString scheme() {
return get(PseudoHeaderName.SCHEME.value());
}
@Override
public String getValue() {
return value;
}
@Override
public AsciiString authority() {
return get(PseudoHeaderName.AUTHORITY.value());
}
@Override
public String setValue(String value) {
throw new UnsupportedOperationException();
}
@Override
public AsciiString path() {
return get(PseudoHeaderName.PATH.value());
}
@Override
public String toString() {
return key + '=' + value;
}
@Override
public AsciiString status() {
return get(PseudoHeaderName.STATUS.value());
}
}

View File

@ -19,11 +19,12 @@ import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_TABLE_S
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_MAX_HEADER_SIZE;
import static io.netty.handler.codec.http2.Http2Error.COMPRESSION_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.protocolError;
import static io.netty.util.CharsetUtil.UTF_8;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream;
import io.netty.handler.codec.AsciiString;
import java.io.IOException;
import java.io.InputStream;
import com.twitter.hpack.Decoder;
import com.twitter.hpack.HeaderListener;
@ -65,37 +66,43 @@ public class DefaultHttp2HeadersDecoder implements Http2HeadersDecoder {
}
@Override
public Http2Headers.Builder decodeHeaders(ByteBuf headerBlock) throws Http2Exception {
public Http2Headers decodeHeaders(ByteBuf headerBlock) throws Http2Exception {
InputStream in = new ByteBufInputStream(headerBlock);
try {
final DefaultHttp2Headers.Builder headersBuilder = new DefaultHttp2Headers.Builder();
final Http2Headers headers = new DefaultHttp2Headers();
HeaderListener listener = new HeaderListener() {
@Override
public void addHeader(byte[] key, byte[] value, boolean sensitive) {
String keyString = new String(key, UTF_8);
String valueString = new String(value, UTF_8);
headersBuilder.add(keyString, valueString);
headers.add(new AsciiString(key, false), new AsciiString(value, false));
}
};
decoder.decode(new ByteBufInputStream(headerBlock), listener);
decoder.decode(in, listener);
boolean truncated = decoder.endHeaderBlock();
if (truncated) {
// TODO: what's the right thing to do here?
}
if (headersBuilder.size() > maxHeaderListSize) {
if (headers.size() > maxHeaderListSize) {
throw protocolError("Number of headers (%d) exceeds maxHeaderListSize (%d)",
headersBuilder.size(), maxHeaderListSize);
headers.size(), maxHeaderListSize);
}
return headersBuilder;
return headers;
} catch (IOException e) {
throw new Http2Exception(COMPRESSION_ERROR, e.getMessage());
} catch (Throwable e) {
// Default handler for any other types of errors that may have occurred. For example,
// the the Header builder throws IllegalArgumentException if the key or value was invalid
// Default handler for any other types of errors that may have occurred. For example,
// the the Header builder throws IllegalArgumentException if the key or value was
// invalid
// for any reason (e.g. the key was an invalid pseudo-header).
throw new Http2Exception(Http2Error.PROTOCOL_ERROR, e.getMessage(), e);
} finally {
try {
in.close();
} catch (IOException e) {
throw new Http2Exception(Http2Error.INTERNAL_ERROR, e.getMessage(), e);
}
}
}
}

View File

@ -17,16 +17,15 @@ package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_TABLE_SIZE;
import static io.netty.handler.codec.http2.Http2Exception.protocolError;
import static io.netty.handler.codec.http2.Http2Headers.PSEUDO_HEADER_PREFIX;
import static io.netty.util.CharsetUtil.UTF_8;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.BinaryHeaders;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
@ -34,7 +33,7 @@ import com.twitter.hpack.Encoder;
public class DefaultHttp2HeadersEncoder implements Http2HeadersEncoder {
private final Encoder encoder;
private final ByteBuf tableSizeChangeOutput = Unpooled.buffer();
private final ByteArrayOutputStream tableSizeChangeOutput = new ByteArrayOutputStream();
private final Set<String> sensitiveHeaders = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
private int maxHeaderListSize = Integer.MAX_VALUE;
@ -49,6 +48,7 @@ public class DefaultHttp2HeadersEncoder implements Http2HeadersEncoder {
@Override
public void encodeHeaders(Http2Headers headers, ByteBuf buffer) throws Http2Exception {
final OutputStream stream = new ByteBufOutputStream(buffer);
try {
if (headers.size() > maxHeaderListSize) {
throw protocolError("Number of headers (%d) exceeds maxHeaderListSize (%d)",
@ -57,28 +57,37 @@ public class DefaultHttp2HeadersEncoder implements Http2HeadersEncoder {
// If there was a change in the table size, serialize the output from the encoder
// resulting from that change.
if (tableSizeChangeOutput.isReadable()) {
buffer.writeBytes(tableSizeChangeOutput);
tableSizeChangeOutput.clear();
if (tableSizeChangeOutput.size() > 0) {
buffer.writeBytes(tableSizeChangeOutput.toByteArray());
tableSizeChangeOutput.reset();
}
OutputStream stream = new ByteBufOutputStream(buffer);
// Write pseudo headers first as required by the HTTP/2 spec.
for (Http2Headers.PseudoHeaderName pseudoHeader : Http2Headers.PseudoHeaderName.values()) {
String name = pseudoHeader.value();
String value = headers.get(name);
AsciiString name = pseudoHeader.value();
AsciiString value = headers.get(name);
if (value != null) {
encodeHeader(name, value, stream);
}
}
for (Entry<String, String> header : headers) {
if (!header.getKey().startsWith(PSEUDO_HEADER_PREFIX)) {
encodeHeader(header.getKey(), header.getValue(), stream);
headers.forEachEntry(new BinaryHeaders.BinaryHeaderVisitor() {
@Override
public boolean visit(AsciiString name, AsciiString value) throws Exception {
if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) {
encodeHeader(name, value, stream);
}
return true;
}
}
});
} catch (IOException e) {
throw Http2Exception.format(Http2Error.COMPRESSION_ERROR,
"Failed encoding headers block: %s", e.getMessage());
} finally {
try {
stream.close();
} catch (IOException e) {
throw new Http2Exception(Http2Error.INTERNAL_ERROR, e.getMessage(), e);
}
}
}
@ -86,7 +95,7 @@ public class DefaultHttp2HeadersEncoder implements Http2HeadersEncoder {
public void maxHeaderTableSize(int size) throws Http2Exception {
try {
// No headers should be emitted. If they are, we throw.
encoder.setMaxHeaderTableSize(new ByteBufOutputStream(tableSizeChangeOutput), size);
encoder.setMaxHeaderTableSize(tableSizeChangeOutput, size);
} catch (IOException e) {
throw new Http2Exception(Http2Error.COMPRESSION_ERROR, e.getMessage(), e);
}
@ -110,8 +119,8 @@ public class DefaultHttp2HeadersEncoder implements Http2HeadersEncoder {
return maxHeaderListSize;
}
private void encodeHeader(String key, String value, OutputStream stream) throws IOException {
private void encodeHeader(AsciiString key, AsciiString value, OutputStream stream) throws IOException {
boolean sensitive = sensitiveHeaders.contains(key);
encoder.encodeHeader(stream, key.getBytes(UTF_8), value.getBytes(UTF_8), sensitive);
encoder.encodeHeader(stream, key.array(), value.array(), sensitive);
}
}

View File

@ -20,11 +20,6 @@ import io.netty.channel.ChannelPromise;
import io.netty.channel.ChannelPromiseAggregator;
import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import java.net.URI;
import java.util.Map;
/**
* Light weight wrapper around {@link DelegatingHttp2ConnectionHandler} to provide HTTP/1.x object to HTTP/2 encoding
@ -45,66 +40,6 @@ public class DelegatingHttp2HttpConnectionHandler extends DelegatingHttp2Connect
super(connection, listener);
}
/**
* Add HTTP/2 headers based upon HTTP/1.x headers from a {@link HttpHeaders}
*
* @param httpHeaders The HTTP/1.x request object to pull headers from
* @param http2Headers The HTTP/2 headers object to add headers to
*/
private void addHeaders(HttpHeaders httpHeaders, DefaultHttp2Headers.Builder http2Headers) {
String value = httpHeaders.get(HttpHeaders.Names.HOST);
if (value != null) {
URI hostUri = URI.create(value);
// The authority MUST NOT include the deprecated "userinfo" subcomponent
value = hostUri.getAuthority();
if (value != null) {
http2Headers.authority(value.replaceFirst("^.*@", ""));
}
value = hostUri.getScheme();
if (value != null) {
http2Headers.scheme(value);
}
httpHeaders.remove(HttpHeaders.Names.HOST);
}
// Consume the Authority extension header if present
value = httpHeaders.get(HttpUtil.ExtensionHeaders.Names.AUTHORITY);
if (value != null) {
http2Headers.authority(value);
httpHeaders.remove(HttpUtil.ExtensionHeaders.Names.AUTHORITY);
}
// Consume the Scheme extension header if present
value = httpHeaders.get(HttpUtil.ExtensionHeaders.Names.SCHEME);
if (value != null) {
http2Headers.scheme(value);
httpHeaders.remove(HttpUtil.ExtensionHeaders.Names.SCHEME);
}
}
/**
* Add HTTP/2 headers based upon HTTP/1.x headers from a {@link HttpRequest}
*
* @param httpRequest The HTTP/1.x request object to pull headers from
* @param http2Headers The HTTP/2 headers object to add headers to
*/
private void addRequestHeaders(HttpRequest httpRequest, DefaultHttp2Headers.Builder http2Headers) {
http2Headers.path(httpRequest.uri());
http2Headers.method(httpRequest.method().toString());
addHeaders(httpRequest.headers(), http2Headers);
}
/**
* Add HTTP/2 headers based upon HTTP/1.x headers from a {@link HttpRequest}
*
* @param httpResponse The HTTP/1.x response object to pull headers from
* @param http2Headers The HTTP/2 headers object to add headers to
*/
private void addResponseHeaders(HttpResponse httpResponse, DefaultHttp2Headers.Builder http2Headers) {
http2Headers.status(Integer.toString(httpResponse.status().code()));
addHeaders(httpResponse.headers(), http2Headers);
}
/**
* Get the next stream id either from the {@link HttpHeaders} object or HTTP/2 codec
*
@ -114,7 +49,7 @@ public class DelegatingHttp2HttpConnectionHandler extends DelegatingHttp2Connect
*/
private int getStreamId(HttpHeaders httpHeaders) throws Http2Exception {
int streamId = 0;
String value = httpHeaders.get(HttpUtil.ExtensionHeaders.Names.STREAM_ID);
String value = httpHeaders.get(HttpUtil.ExtensionHeaderNames.STREAM_ID.text());
if (value == null) {
streamId = nextStreamId();
} else {
@ -124,7 +59,6 @@ public class DelegatingHttp2HttpConnectionHandler extends DelegatingHttp2Connect
throw Http2Exception.format(Http2Error.INTERNAL_ERROR,
"Invalid user-specified stream id value '%s'", value);
}
httpHeaders.remove(HttpUtil.ExtensionHeaders.Names.STREAM_ID);
}
return streamId;
@ -134,52 +68,33 @@ public class DelegatingHttp2HttpConnectionHandler extends DelegatingHttp2Connect
* Handles conversion of a {@link FullHttpMessage} to HTTP/2 frames.
*/
@Override
@SuppressWarnings("deprecation")
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
if (msg instanceof FullHttpMessage) {
FullHttpMessage httpMsg = (FullHttpMessage) msg;
boolean hasData = httpMsg.content().isReadable();
// Convert and write the headers.
HttpHeaders httpHeaders = httpMsg.headers();
DefaultHttp2Headers.Builder http2Headers = DefaultHttp2Headers.newBuilder();
if (msg instanceof HttpRequest) {
addRequestHeaders((HttpRequest) msg, http2Headers);
} else if (msg instanceof HttpResponse) {
addResponseHeaders((HttpResponse) msg, http2Headers);
}
// Provide the user the opportunity to specify the streamId
int streamId = 0;
try {
streamId = getStreamId(httpHeaders);
streamId = getStreamId(httpMsg.headers());
} catch (Http2Exception e) {
httpMsg.release();
promise.setFailure(e);
return;
}
// The Connection, Keep-Alive, Proxy-Connection, Transfer-Encoding,
// and Upgrade headers are not valid and MUST not be sent.
httpHeaders.remove(HttpHeaders.Names.CONNECTION);
httpHeaders.remove(HttpHeaders.Names.KEEP_ALIVE);
httpHeaders.remove(HttpHeaders.Names.PROXY_CONNECTION);
httpHeaders.remove(HttpHeaders.Names.TRANSFER_ENCODING);
// Add the HTTP headers which have not been consumed above
for (Map.Entry<String, String> entry : httpHeaders.entries()) {
http2Headers.add(entry.getKey(), entry.getValue());
}
// Convert and write the headers.
Http2Headers http2Headers = HttpUtil.toHttp2Headers(httpMsg);
if (hasData) {
ChannelPromiseAggregator promiseAggregator = new ChannelPromiseAggregator(promise);
ChannelPromise headerPromise = ctx.newPromise();
ChannelPromise dataPromise = ctx.newPromise();
promiseAggregator.add(headerPromise, dataPromise);
writeHeaders(ctx, streamId, http2Headers.build(), 0, false, headerPromise);
writeHeaders(ctx, streamId, http2Headers, 0, false, headerPromise);
writeData(ctx, streamId, httpMsg.content(), 0, true, dataPromise);
} else {
writeHeaders(ctx, streamId, http2Headers.build(), 0, true, promise);
writeHeaders(ctx, streamId, http2Headers, 0, true, promise);
}
} else {
ctx.write(msg, promise);

View File

@ -0,0 +1,142 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.BinaryHeaders;
import io.netty.handler.codec.EmptyBinaryHeaders;
public final class EmptyHttp2Headers extends EmptyBinaryHeaders implements Http2Headers {
public static final EmptyHttp2Headers INSTANCE = new EmptyHttp2Headers();
private EmptyHttp2Headers() {
}
@Override
public EmptyHttp2Headers add(AsciiString name, AsciiString value) {
super.add(name, value);
return this;
}
@Override
public EmptyHttp2Headers add(AsciiString name, Iterable<AsciiString> values) {
super.add(name, values);
return this;
}
@Override
public EmptyHttp2Headers add(AsciiString name, AsciiString... values) {
super.add(name, values);
return this;
}
@Override
public EmptyHttp2Headers add(BinaryHeaders headers) {
super.add(headers);
return this;
}
@Override
public EmptyHttp2Headers set(AsciiString name, AsciiString value) {
super.set(name, value);
return this;
}
@Override
public EmptyHttp2Headers set(AsciiString name, Iterable<AsciiString> values) {
super.set(name, values);
return this;
}
@Override
public EmptyHttp2Headers set(AsciiString name, AsciiString... values) {
super.set(name, values);
return this;
}
@Override
public EmptyHttp2Headers set(BinaryHeaders headers) {
super.set(headers);
return this;
}
@Override
public EmptyHttp2Headers setAll(BinaryHeaders headers) {
super.setAll(headers);
return this;
}
@Override
public EmptyHttp2Headers clear() {
return this;
}
@Override
public EmptyHttp2Headers forEachEntry(BinaryHeaderVisitor visitor) {
super.forEachEntry(visitor);
return this;
}
@Override
public EmptyHttp2Headers method(AsciiString method) {
throw new UnsupportedOperationException();
}
@Override
public EmptyHttp2Headers scheme(AsciiString status) {
throw new UnsupportedOperationException();
}
@Override
public EmptyHttp2Headers authority(AsciiString authority) {
throw new UnsupportedOperationException();
}
@Override
public EmptyHttp2Headers path(AsciiString path) {
throw new UnsupportedOperationException();
}
@Override
public EmptyHttp2Headers status(AsciiString status) {
throw new UnsupportedOperationException();
}
@Override
public AsciiString method() {
return get(PseudoHeaderName.METHOD.value());
}
@Override
public AsciiString scheme() {
return get(PseudoHeaderName.SCHEME.value());
}
@Override
public AsciiString authority() {
return get(PseudoHeaderName.AUTHORITY.value());
}
@Override
public AsciiString path() {
return get(PseudoHeaderName.PATH.value());
}
@Override
public AsciiString status() {
return get(PseudoHeaderName.STATUS.value());
}
}

View File

@ -15,72 +15,16 @@
package io.netty.handler.codec.http2;
import java.util.Collections;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.BinaryHeaders;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
/**
* An immutable collection of headers sent or received via HTTP/2.
* A collection of headers sent or received via HTTP/2.
*/
public abstract class Http2Headers implements Iterable<Entry<String, String>> {
public static final Http2Headers EMPTY_HEADERS = new Http2Headers() {
@Override
public String get(CharSequence name) {
return null;
}
@Override
public List<String> getAll(CharSequence name) {
return Collections.emptyList();
}
@Override
public List<Entry<String, String>> entries() {
return Collections.emptyList();
}
@Override
public boolean contains(CharSequence name) {
return false;
}
@Override
public boolean isEmpty() {
return true;
}
@Override
public int size() {
return 0;
}
@Override
public Set<String> names() {
return Collections.emptySet();
}
@Override
public Iterator<Entry<String, String>> iterator() {
return entries().iterator();
}
@Override
public String forEach(HeaderVisitor visitor) {
return null;
}
};
/**
* The prefix used to denote an HTTP/2 psuedo-header.
*/
public static String PSEUDO_HEADER_PREFIX = ":";
public interface Http2Headers extends BinaryHeaders {
/**
* HTTP/2 pseudo-headers names.
@ -89,326 +33,133 @@ public abstract class Http2Headers implements Iterable<Entry<String, String>> {
/**
* {@code :method}.
*/
METHOD(PSEUDO_HEADER_PREFIX + "method"),
METHOD(":method"),
/**
* {@code :scheme}.
*/
SCHEME(PSEUDO_HEADER_PREFIX + "scheme"),
SCHEME(":scheme"),
/**
* {@code :authority}.
*/
AUTHORITY(PSEUDO_HEADER_PREFIX + "authority"),
AUTHORITY(":authority"),
/**
* {@code :path}.
*/
PATH(PSEUDO_HEADER_PREFIX + "path"),
PATH(":path"),
/**
* {@code :status}.
*/
STATUS(PSEUDO_HEADER_PREFIX + "status");
STATUS(":status");
private final String value;
PseudoHeaderName(String value) {
this.value = value;
private final AsciiString value;
private static final Set<AsciiString> PSEUDO_HEADERS = new HashSet<AsciiString>();
static {
for (PseudoHeaderName pseudoHeader : PseudoHeaderName.values()) {
PSEUDO_HEADERS.add(pseudoHeader.value());
}
}
public String value() {
PseudoHeaderName(String value) {
this.value = new AsciiString(value);
}
public AsciiString value() {
// Return a slice so that the buffer gets its own reader index.
return value;
}
/**
* Indicates whether the given header name is a valid HTTP/2 pseudo header.
*/
public static boolean isPseudoHeader(String header) {
if (header == null || !header.startsWith(Http2Headers.PSEUDO_HEADER_PREFIX)) {
// Not a pseudo-header.
return false;
}
// Check the header name against the set of valid pseudo-headers.
for (PseudoHeaderName pseudoHeader : PseudoHeaderName.values()) {
String pseudoHeaderName = pseudoHeader.value();
if (pseudoHeaderName.equals(header)) {
// It's a valid pseudo-header.
return true;
}
}
return false;
public static boolean isPseudoHeader(AsciiString header) {
return PSEUDO_HEADERS.contains(header);
}
}
/**
* Returns the {@link Set} of all header names.
*/
public abstract Set<String> names();
@Override
Http2Headers add(AsciiString name, AsciiString value);
@Override
Http2Headers add(AsciiString name, Iterable<AsciiString> values);
@Override
Http2Headers add(AsciiString name, AsciiString... values);
@Override
Http2Headers add(BinaryHeaders headers);
@Override
Http2Headers set(AsciiString name, AsciiString value);
@Override
Http2Headers set(AsciiString name, Iterable<AsciiString> values);
@Override
Http2Headers set(AsciiString name, AsciiString... values);
@Override
Http2Headers set(BinaryHeaders headers);
@Override
Http2Headers setAll(BinaryHeaders headers);
@Override
Http2Headers clear();
@Override
Http2Headers forEachEntry(BinaryHeaderVisitor visitor);
/**
* Returns the header value with the specified header name. If there is more than one header
* value for the specified header name, the first value is returned.
*
* @return the header value or {@code null} if there is no such header
* Sets the {@link PseudoHeaderName#METHOD} header or {@code null} if there is no such header
*/
public abstract String get(CharSequence name);
Http2Headers method(AsciiString value);
/**
* Returns the header values with the specified header name.
*
* @return the {@link List} of header values. An empty list if there is no such header.
* Sets the {@link PseudoHeaderName#SCHEME} header if there is no such header
*/
public abstract List<String> getAll(CharSequence name);
Http2Headers scheme(AsciiString value);
/**
* Returns all header names and values that this frame contains.
*
* @return the {@link List} of the header name-value pairs. An empty list if there is no header
* in this message.
* Sets the {@link PseudoHeaderName#AUTHORITY} header or {@code null} if there is no such header
*/
public abstract List<Map.Entry<String, String>> entries();
Http2Headers authority(AsciiString value);
/**
* Returns {@code true} if and only if there is a header with the specified header name.
* Sets the {@link PseudoHeaderName#PATH} header or {@code null} if there is no such header
*/
public abstract boolean contains(CharSequence name);
Http2Headers path(AsciiString value);
/**
* Checks if no header exists.
* Sets the {@link PseudoHeaderName#STATUS} header or {@code null} if there is no such header
*/
public abstract boolean isEmpty();
/**
* Gets the number of headers contained in this object.
*/
public abstract int size();
/**
* Allows a means to reduce GC pressure while iterating over a collection
*/
public interface HeaderVisitor {
/**
* @return
* <ul>
* <li>{@code true} if the processor wants to continue the loop and handle the entry.</li>
* <li>{@code false} if the processor wants to stop handling headers and abort the loop.</li>
* </ul>
*/
boolean visit(Map.Entry<String, String> entry);
}
/**
* Iterates over the entries contained within this header object in no guaranteed order
* @return {@code null} if the visitor iterated to or beyond the end of the headers.
* The last-visited header name If the {@link HeaderVisitor#visit(Entry)} returned {@code false}.
*/
public abstract String forEach(HeaderVisitor visitor);
/**
* Interface for the Builder pattern for {@link Http2Headers}.
*/
public interface Builder {
/**
* Build all the collected headers into a {@link Http2Headers}.
* @return The {@link Http2Headers} object which this builder has been used for
*/
Http2Headers build();
/**
* Gets the number of headers contained in this object.
*/
int size();
/**
* Clears all values from this collection.
*/
Builder clear();
/**
* Returns the header value with the specified header name. If there is more than one header
* value for the specified header name, the first value is returned.
* <p>
* Note that all HTTP2 headers names are lower case and this method will not force {@code name} to lower case.
* @return the header value or {@code null} if there is no such header
*/
String get(CharSequence name);
/**
* Returns the header values with the specified header name.
* <p>
* Note that all HTTP2 headers names are lower case and this method will not force {@code name} to lower case.
* @return the {@link List} of header values. An empty list if there is no such header.
*/
List<String> getAll(CharSequence name);
/**
* Clears all existing headers from this collection and replaces them with the given header
* set.
*/
void set(Http2Headers headers);
/**
* Adds the given header to the collection.
* @throws IllegalArgumentException if the name or value of this header is invalid for any reason.
*/
Builder add(CharSequence name, Object value);
/**
* Adds the given header to the collection.
* @throws IllegalArgumentException if the name or value of this header is invalid for any reason.
*/
Builder add(String name, Object value);
/**
* Removes the header with the given name from this collection.
* This method will <b>not</b> force the {@code name} to lower case before looking for a match.
*/
Builder remove(CharSequence name);
/**
* Removes the header with the given name from this collection.
* This method will force the {@code name} to lower case before looking for a match.
*/
Builder remove(String name);
/**
* Sets the given header in the collection, replacing any previous values.
* @throws IllegalArgumentException if the name or value of this header is invalid for any reason.
*/
Builder set(CharSequence name, Object value);
/**
* Sets the given header in the collection, replacing any previous values.
* @throws IllegalArgumentException if the name or value of this header is invalid for any reason.
*/
Builder set(String name, Object value);
/**
* Sets the given header in the collection, replacing any previous values.
* @throws IllegalArgumentException if the name or value of this header is invalid for any reason.
*/
Builder set(String name, Iterable<?> values);
/**
* Sets the {@link PseudoHeaderName#METHOD} header.
*/
Builder method(String method);
/**
* Sets the {@link PseudoHeaderName#SCHEME} header.
*/
Builder scheme(String scheme);
/**
* Sets the {@link PseudoHeaderName#AUTHORITY} header.
*/
Builder authority(String authority);
/**
* Sets the {@link PseudoHeaderName#PATH} header.
*/
Builder path(String path);
/**
* Sets the {@link PseudoHeaderName#STATUS} header.
*/
Builder status(String status);
}
Http2Headers status(AsciiString value);
/**
* Gets the {@link PseudoHeaderName#METHOD} header or {@code null} if there is no such header
*/
public final String method() {
return get(PseudoHeaderName.METHOD.value());
}
AsciiString method();
/**
* Gets the {@link PseudoHeaderName#SCHEME} header or {@code null} if there is no such header
*/
public final String scheme() {
return get(PseudoHeaderName.SCHEME.value());
}
AsciiString scheme();
/**
* Gets the {@link PseudoHeaderName#AUTHORITY} header or {@code null} if there is no such header
*/
public final String authority() {
return get(PseudoHeaderName.AUTHORITY.value());
}
AsciiString authority();
/**
* Gets the {@link PseudoHeaderName#PATH} header or {@code null} if there is no such header
*/
public final String path() {
return get(PseudoHeaderName.PATH.value());
}
AsciiString path();
/**
* Gets the {@link PseudoHeaderName#STATUS} header or {@code null} if there is no such header
*/
public final String status() {
return get(PseudoHeaderName.STATUS.value());
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
for (String name : names()) {
result = prime * result + name.hashCode();
Set<String> values = new TreeSet<String>(getAll(name));
for (String value : values) {
result = prime * result + value.hashCode();
}
}
return result;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Http2Headers)) {
return false;
}
Http2Headers other = (Http2Headers) o;
// First, check that the set of names match.
Set<String> names = names();
if (!names.equals(other.names())) {
return false;
}
// Compare the values for each name.
for (String name : names) {
List<String> values = getAll(name);
List<String> otherValues = other.getAll(name);
if (values.size() != otherValues.size()) {
return false;
}
// Convert the values to a set and remove values from the other object to see if
// they match.
Set<String> valueSet = new HashSet<String>(values);
valueSet.removeAll(otherValues);
if (!valueSet.isEmpty()) {
return false;
}
}
// They match.
return true;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("Http2Headers[");
for (Map.Entry<String, String> header : this) {
builder.append(header.getKey());
builder.append(':');
builder.append(header.getValue());
builder.append(',');
}
builder.append(']');
return builder.toString();
}
AsciiString status();
}

View File

@ -25,7 +25,7 @@ public interface Http2HeadersDecoder {
/**
* Decodes the given headers block and returns the headers.
*/
Http2Headers.Builder decodeHeaders(ByteBuf headerBlock) throws Http2Exception;
Http2Headers decodeHeaders(ByteBuf headerBlock) throws Http2Exception;
/**
* Sets the new max header table size for this decoder.

View File

@ -15,13 +15,49 @@
package io.netty.handler.codec.http2;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.BinaryHeaders;
import io.netty.handler.codec.TextHeaderProcessor;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderUtil;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Provides utility methods and constants for the HTTP/2 to HTTP conversion
*/
@SuppressWarnings("deprecation")
public final class HttpUtil {
/**
* The set of headers that should not be directly copied when converting headers from HTTP to HTTP/2.
*/
private static final Set<CharSequence> HTTP_TO_HTTP2_HEADER_BLACKLIST = new HashSet<CharSequence>();
static {
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaders.Names.CONNECTION.toLowerCase());
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaders.Names.KEEP_ALIVE.toLowerCase());
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaders.Names.PROXY_CONNECTION.toLowerCase());
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaders.Names.TRANSFER_ENCODING.toLowerCase());
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaders.Names.HOST.toLowerCase());
// These are already defined as lower-case.
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.STREAM_ID.text());
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.AUTHORITY.text());
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.SCHEME.text());
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.PATH.text());
}
/**
* This will be the method used for {@link HttpRequest} objects generated
* out of the HTTP message flow defined in
@ -48,60 +84,67 @@ public final class HttpUtil {
/**
* Provides the HTTP header extensions used to carry HTTP/2 information in HTTP objects
*/
public static final class ExtensionHeaders {
public static final class Names {
private Names() { }
public enum ExtensionHeaderNames {
/**
* HTTP extension header which will identify the stream id from the HTTP/2 event(s)
* responsible for generating a {@code HttpObject}
* <p>
* {@code "x-http2-stream-id"}
*/
STREAM_ID("x-http2-stream-id"),
/**
* HTTP extension header which will identify the stream id
* from the HTTP/2 event(s) responsible for generating a {@code HttpObject}
* <p>
* {@code "x-http2-stream-id"}
*/
public static final AsciiString STREAM_ID = new AsciiString("x-http2-stream-id");
/**
* HTTP extension header which will identify the authority pseudo header
* from the HTTP/2 event(s) responsible for generating a {@code HttpObject}
* <p>
* {@code "x-http2-authority"}
*/
public static final AsciiString AUTHORITY = new AsciiString("x-http2-authority");
/**
* HTTP extension header which will identify the scheme pseudo header
* from the HTTP/2 event(s) responsible for generating a {@code HttpObject}
* <p>
* {@code "x-http2-scheme"}
*/
public static final AsciiString SCHEME = new AsciiString("x-http2-scheme");
/**
* HTTP extension header which will identify the path pseudo header
* from the HTTP/2 event(s) responsible for generating a {@code HttpObject}
* <p>
* {@code "x-http2-path"}
*/
public static final AsciiString PATH = new AsciiString("x-http2-path");
/**
* HTTP extension header which will identify the stream id used to create this stream
* in a HTTP/2 push promise frame
* <p>
* {@code "x-http2-stream-promise-id"}
*/
public static final AsciiString STREAM_PROMISE_ID = new AsciiString("x-http2-stream-promise-id");
/**
* HTTP extension header which will identify the stream id which this stream is dependent on.
* This stream will be a child node of the stream id associated with this header value.
* <p>
* {@code "x-http2-stream-dependency-id"}
*/
public static final AsciiString STREAM_DEPENDENCY_ID = new AsciiString("x-http2-stream-dependency-id");
/**
* HTTP extension header which will identify the weight
* (if non-default and the priority is not on the default stream) of the associated HTTP/2 stream
* responsible responsible for generating a {@code HttpObject}
* <p>
* {@code "x-http2-stream-weight"}
*/
public static final AsciiString STREAM_WEIGHT = new AsciiString("x-http2-stream-weight");
/**
* HTTP extension header which will identify the authority pseudo header from the HTTP/2
* event(s) responsible for generating a {@code HttpObject}
* <p>
* {@code "x-http2-authority"}
*/
AUTHORITY("x-http2-authority"),
/**
* HTTP extension header which will identify the scheme pseudo header from the HTTP/2
* event(s) responsible for generating a {@code HttpObject}
* <p>
* {@code "x-http2-scheme"}
*/
SCHEME("x-http2-scheme"),
/**
* HTTP extension header which will identify the path pseudo header from the HTTP/2 event(s)
* responsible for generating a {@code HttpObject}
* <p>
* {@code "x-http2-path"}
*/
PATH("x-http2-path"),
/**
* HTTP extension header which will identify the stream id used to create this stream in a
* HTTP/2 push promise frame
* <p>
* {@code "x-http2-stream-promise-id"}
*/
STREAM_PROMISE_ID("x-http2-stream-promise-id"),
/**
* HTTP extension header which will identify the stream id which this stream is dependent
* on. This stream will be a child node of the stream id associated with this header value.
* <p>
* {@code "x-http2-stream-dependency-id"}
*/
STREAM_DEPENDENCY_ID("x-http2-stream-dependency-id"),
/**
* HTTP extension header which will identify the weight (if non-default and the priority is
* not on the default stream) of the associated HTTP/2 stream responsible responsible for
* generating a {@code HttpObject}
* <p>
* {@code "x-http2-stream-weight"}
*/
STREAM_WEIGHT("x-http2-stream-weight");
private final AsciiString text;
private ExtensionHeaderNames(String text) {
this.text = new AsciiString(text);
}
public AsciiString text() {
return text;
}
}
@ -112,7 +155,7 @@ public final class HttpUtil {
* @return The HTTP/1.x status
* @throws Http2Exception If there is a problem translating from HTTP/2 to HTTP/1.x
*/
public static HttpResponseStatus parseStatus(String status) throws Http2Exception {
public static HttpResponseStatus parseStatus(AsciiString status) throws Http2Exception {
HttpResponseStatus result = null;
try {
result = HttpResponseStatus.parseLine(status);
@ -127,4 +170,210 @@ public final class HttpUtil {
}
return result;
}
/**
* Create a new object to contain the response data
*
* @param streamId The stream associated with the response
* @param http2Headers The initial set of HTTP/2 headers to create the response with
* @param validateHttpHeaders
* <ul>
* <li>{@code true} to validate HTTP headers in the http-codec</li>
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
* </ul>
* @return A new response object which represents headers/data
* @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, Map)}
*/
public static FullHttpResponse toHttpResponse(int streamId, Http2Headers http2Headers,
boolean validateHttpHeaders) throws Http2Exception {
HttpResponseStatus status = parseStatus(http2Headers.status());
// HTTP/2 does not define a way to carry the version or reason phrase that is included in an
// HTTP/1.1 status line.
FullHttpResponse msg =
new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, validateHttpHeaders);
addHttp2ToHttpHeaders(streamId, http2Headers, msg, false);
return msg;
}
/**
* Create a new object to contain the request data
*
* @param streamId The stream associated with the request
* @param http2Headers The initial set of HTTP/2 headers to create the request with
* @param validateHttpHeaders
* <ul>
* <li>{@code true} to validate HTTP headers in the http-codec</li>
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
* </ul>
* @return A new request object which represents headers/data
* @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, Map)}
*/
public static FullHttpRequest toHttpRequest(int streamId, Http2Headers http2Headers, boolean validateHttpHeaders)
throws Http2Exception {
// HTTP/2 does not define a way to carry the version identifier that is
// included in the HTTP/1.1 request line.
FullHttpRequest msg =
new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(http2Headers
.method().toString()), http2Headers.path().toString(), validateHttpHeaders);
addHttp2ToHttpHeaders(streamId, http2Headers, msg, false);
return msg;
}
/**
* Translate and add HTTP/2 headers to HTTP/1.x headers
*
* @param streamId The stream associated with {@code sourceHeaders}
* @param sourceHeaders The HTTP/2 headers to convert
* @param destinationMessage The object which will contain the resulting HTTP/1.x headers
* @param addToTrailer {@code true} to add to trailing headers. {@code false} to add to initial
* headers.
* @throws Http2Exception If not all HTTP/2 headers can be translated to HTTP/1.x
*/
public static void addHttp2ToHttpHeaders(int streamId, Http2Headers sourceHeaders,
FullHttpMessage destinationMessage, boolean addToTrailer)
throws Http2Exception {
HttpHeaders headers = addToTrailer ? destinationMessage.trailingHeaders() : destinationMessage.headers();
boolean request = destinationMessage instanceof HttpRequest;
Http2ToHttpHeaderTranslator visitor = new Http2ToHttpHeaderTranslator(headers, request);
sourceHeaders.forEachEntry(visitor);
if (visitor.cause() != null) {
throw visitor.cause();
}
headers.remove(HttpHeaders.Names.TRANSFER_ENCODING);
headers.remove(HttpHeaders.Names.TRAILER);
if (!addToTrailer) {
headers.set(ExtensionHeaderNames.STREAM_ID.text(), streamId);
HttpHeaderUtil.setKeepAlive(destinationMessage, true);
}
}
/**
* Converts the given HTTP/1.x headers into HTTP/2 headers.
*/
public static Http2Headers toHttp2Headers(FullHttpMessage in) {
final Http2Headers out = new DefaultHttp2Headers();
HttpHeaders inHeaders = in.headers();
if (in instanceof HttpRequest) {
HttpRequest request = (HttpRequest) in;
out.path(new AsciiString(request.uri()));
out.method(new AsciiString(request.method().toString()));
String value = inHeaders.get(HttpHeaders.Names.HOST);
if (value != null) {
URI hostUri = URI.create(value);
// The authority MUST NOT include the deprecated "userinfo" subcomponent
value = hostUri.getAuthority();
if (value != null) {
out.authority(new AsciiString(value.replaceFirst("^.*@", "")));
}
value = hostUri.getScheme();
if (value != null) {
out.scheme(new AsciiString(value));
}
}
// Consume the Authority extension header if present
value = inHeaders.get(ExtensionHeaderNames.AUTHORITY.text());
if (value != null) {
out.authority(new AsciiString(value));
}
// Consume the Scheme extension header if present
value = inHeaders.get(ExtensionHeaderNames.SCHEME.text());
if (value != null) {
out.scheme(new AsciiString(value));
}
} else if (in instanceof HttpResponse) {
HttpResponse response = (HttpResponse) in;
out.status(new AsciiString(Integer.toString(response.status().code())));
}
// Add the HTTP headers which have not been consumed above
inHeaders.forEachEntry(new TextHeaderProcessor() {
@Override
public boolean process(CharSequence name, CharSequence value) throws Exception {
AsciiString aName = AsciiString.of(name);
if (!HTTP_TO_HTTP2_HEADER_BLACKLIST.contains(aName.toLowerCase())) {
AsciiString aValue = AsciiString.of(value);
out.add(aName, aValue);
}
return true;
}
});
return out;
}
/**
* A visitor which translates HTTP/2 headers to HTTP/1 headers
*/
private static final class Http2ToHttpHeaderTranslator implements BinaryHeaders.BinaryHeaderVisitor {
/**
* Translations from HTTP/2 header name to the HTTP/1.x equivalent.
*/
private static final Map<AsciiString, String> REQUEST_HEADER_TRANSLATIONS =
new HashMap<AsciiString, String>();
private static final Map<AsciiString, String> RESPONSE_HEADER_TRANSLATIONS =
new HashMap<AsciiString, String>();
static {
RESPONSE_HEADER_TRANSLATIONS.put(Http2Headers.PseudoHeaderName.AUTHORITY.value(),
ExtensionHeaderNames.AUTHORITY.text().toString());
RESPONSE_HEADER_TRANSLATIONS.put(Http2Headers.PseudoHeaderName.SCHEME.value(),
ExtensionHeaderNames.SCHEME.text().toString());
REQUEST_HEADER_TRANSLATIONS.putAll(RESPONSE_HEADER_TRANSLATIONS);
RESPONSE_HEADER_TRANSLATIONS.put(Http2Headers.PseudoHeaderName.PATH.value(),
ExtensionHeaderNames.PATH.text().toString());
}
private final HttpHeaders output;
private final Map<AsciiString, String> translations;
private Http2Exception e;
/**
* Create a new instance
*
* @param output The HTTP/1.x headers object to store the results of the translation
* @param request if {@code true}, translates headers using the request translation map.
* Otherwise uses the response translation map.
*/
public Http2ToHttpHeaderTranslator(HttpHeaders output, boolean request) {
this.output = output;
translations = request? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS;
}
@Override
public boolean visit(AsciiString name, AsciiString value) {
String translatedName = translations.get(name);
if (translatedName != null || !Http2Headers.PseudoHeaderName.isPseudoHeader(name)) {
if (translatedName == null) {
translatedName = name.toString();
}
// http://tools.ietf.org/html/draft-ietf-httpbis-http2-14#section-8.1.2.3
// All headers that start with ':' are only valid in HTTP/2 context
if (translatedName.isEmpty() || translatedName.charAt(0) == ':') {
e = Http2Exception
.protocolError("Unknown HTTP/2 header '%s' encountered in translation to HTTP/1.x",
translatedName);
return false;
} else {
output.add(translatedName, value.toString());
}
}
return true;
}
/**
* Get any exceptions encountered while translating HTTP/2 headers to HTTP/1.x headers
*
* @return
* <ul>
* <li>{@code null} if no exceptions where encountered</li>
* <li>Otherwise an exception describing what went wrong</li>
* </ul>
*/
public Http2Exception cause() {
return e;
}
}
}

View File

@ -17,26 +17,14 @@ package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.TooLongFrameException;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderUtil;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http2.Http2Headers.HeaderVisitor;
import io.netty.util.collection.IntObjectHashMap;
import io.netty.util.collection.IntObjectMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* This adapter provides just header/data events from the HTTP message flow defined
* here <a href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-14#section-8.1.">HTTP/2 Spec Message Flow</a>
@ -48,27 +36,6 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter {
private final ImmediateSendDetector sendDetector;
protected final IntObjectMap<FullHttpMessage> messageMap;
private static final Set<String> HEADERS_TO_EXCLUDE;
private static final Map<String, String> HEADER_NAME_TRANSLATIONS_REQUEST;
private static final Map<String, String> HEADER_NAME_TRANSLATIONS_RESPONSE;
static {
HEADERS_TO_EXCLUDE = new HashSet<String>();
HEADER_NAME_TRANSLATIONS_REQUEST = new HashMap<String, String>();
HEADER_NAME_TRANSLATIONS_RESPONSE = new HashMap<String, String>();
for (Http2Headers.PseudoHeaderName http2HeaderName : Http2Headers.PseudoHeaderName.values()) {
HEADERS_TO_EXCLUDE.add(http2HeaderName.value());
}
HEADER_NAME_TRANSLATIONS_RESPONSE.put(Http2Headers.PseudoHeaderName.AUTHORITY.value(),
HttpUtil.ExtensionHeaders.Names.AUTHORITY.toString());
HEADER_NAME_TRANSLATIONS_RESPONSE.put(Http2Headers.PseudoHeaderName.SCHEME.value(),
HttpUtil.ExtensionHeaders.Names.SCHEME.toString());
HEADER_NAME_TRANSLATIONS_REQUEST.putAll(HEADER_NAME_TRANSLATIONS_RESPONSE);
HEADER_NAME_TRANSLATIONS_RESPONSE.put(Http2Headers.PseudoHeaderName.PATH.value(),
HttpUtil.ExtensionHeaders.Names.PATH.toString());
}
/**
* Creates a new instance
*
@ -189,8 +156,8 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter {
*/
protected FullHttpMessage newMessage(int streamId, Http2Headers headers, boolean validateHttpHeaders)
throws Http2Exception {
return connection.isServer() ? newHttpRequest(streamId, headers, validateHttpHeaders) :
newHttpResponse(streamId, headers, validateHttpHeaders);
return connection.isServer() ? HttpUtil.toHttpRequest(streamId, headers,
validateHttpHeaders) : HttpUtil.toHttpResponse(streamId, headers, validateHttpHeaders);
}
/**
@ -224,7 +191,7 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter {
msg = newMessage(streamId, headers, validateHttpHeaders);
} else if (allowAppend) {
try {
addHttp2ToHttpHeaders(streamId, headers, msg, appendToTrailer);
HttpUtil.addHttp2ToHttpHeaders(streamId, headers, msg, appendToTrailer);
} catch (Http2Exception e) {
removeMessage(streamId);
throw e;
@ -316,7 +283,7 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter {
promisedStreamId);
}
msg.headers().set(HttpUtil.ExtensionHeaders.Names.STREAM_PROMISE_ID, streamId);
msg.headers().set(HttpUtil.ExtensionHeaderNames.STREAM_PROMISE_ID.text(), streamId);
processHeadersEnd(ctx, promisedStreamId, msg, false);
}
@ -384,150 +351,4 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter {
return null;
}
}
/**
* Create a new object to contain the response data
*
* @param streamId The stream associated with the response
* @param http2Headers The initial set of HTTP/2 headers to create the response with
* @param validateHttpHeaders
* <ul>
* <li>{@code true} to validate HTTP headers in the http-codec</li>
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
* </ul>
* @return A new response object which represents headers/data
* @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, Map)}
*/
private static FullHttpMessage newHttpResponse(int streamId, Http2Headers http2Headers, boolean validateHttpHeaders)
throws Http2Exception {
HttpResponseStatus status = HttpUtil.parseStatus(http2Headers.status());
// HTTP/2 does not define a way to carry the version or reason phrase that is included in an HTTP/1.1
// status line.
FullHttpMessage msg = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, validateHttpHeaders);
addHttp2ToHttpHeaders(streamId, http2Headers, msg, false, HEADER_NAME_TRANSLATIONS_RESPONSE);
return msg;
}
/**
* Create a new object to contain the request data
*
* @param streamId The stream associated with the request
* @param http2Headers The initial set of HTTP/2 headers to create the request with
* @param validateHttpHeaders
* <ul>
* <li>{@code true} to validate HTTP headers in the http-codec</li>
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
* </ul>
* @return A new request object which represents headers/data
* @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, Map)}
*/
private static FullHttpMessage newHttpRequest(int streamId, Http2Headers http2Headers, boolean validateHttpHeaders)
throws Http2Exception {
// HTTP/2 does not define a way to carry the version identifier that is
// included in the HTTP/1.1 request line.
FullHttpMessage msg = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,
HttpMethod.valueOf(http2Headers.method()), http2Headers.path(), validateHttpHeaders);
addHttp2ToHttpHeaders(streamId, http2Headers, msg, false, HEADER_NAME_TRANSLATIONS_REQUEST);
return msg;
}
/**
* Translate and add HTTP/2 headers to HTTP/1.x headers
*
* @param streamId The stream associated with {@code sourceHeaders}
* @param sourceHeaders The HTTP/2 headers to convert
* @param destinationMessage The object which will contain the resulting HTTP/1.x headers
* @param addToTrailer {@code true} to add to trailing headers. {@code false} to add to initial headers.
* @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, Map)}
*/
private static void addHttp2ToHttpHeaders(int streamId, Http2Headers sourceHeaders,
FullHttpMessage destinationMessage, boolean addToTrailer) throws Http2Exception {
addHttp2ToHttpHeaders(streamId, sourceHeaders, destinationMessage, addToTrailer,
(destinationMessage instanceof FullHttpRequest) ? HEADER_NAME_TRANSLATIONS_REQUEST
: HEADER_NAME_TRANSLATIONS_RESPONSE);
}
/**
* Translate and add HTTP/2 headers to HTTP/1.x headers
*
* @param streamId The stream associated with {@code sourceHeaders}
* @param sourceHeaders The HTTP/2 headers to convert
* @param destinationMessage The object which will contain the resulting HTTP/1.x headers
* @param addToTrailer {@code true} to add to trailing headers. {@code false} to add to initial headers.
* @param translations A map used to help translate HTTP/2 headers to HTTP/1.x headers
* @throws Http2Exception If not all HTTP/2 headers can be translated to HTTP/1.x
*/
private static void addHttp2ToHttpHeaders(int streamId, Http2Headers sourceHeaders,
FullHttpMessage destinationMessage, boolean addToTrailer, Map<String, String> translations)
throws Http2Exception {
HttpHeaders headers = addToTrailer ? destinationMessage.trailingHeaders() : destinationMessage.headers();
HttpAdapterVisitor visitor = new HttpAdapterVisitor(headers, translations);
sourceHeaders.forEach(visitor);
if (visitor.exception() != null) {
throw visitor.exception();
}
headers.remove(HttpHeaders.Names.TRANSFER_ENCODING);
headers.remove(HttpHeaders.Names.TRAILER);
if (!addToTrailer) {
headers.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, streamId);
HttpHeaderUtil.setKeepAlive(destinationMessage, true);
}
}
/**
* A visitor which translates HTTP/2 headers to HTTP/1 headers
*/
private static final class HttpAdapterVisitor implements HeaderVisitor {
private Map<String, String> translations;
private HttpHeaders headers;
private Http2Exception e;
/**
* Create a new instance
*
* @param headers The HTTP/1.x headers object to store the results of the translation
* @param translations A map used to help translate HTTP/2 headers to HTTP/1.x headers
*/
public HttpAdapterVisitor(HttpHeaders headers, Map<String, String> translations) {
this.translations = translations;
this.headers = headers;
this.e = null;
}
@Override
public boolean visit(Entry<String, String> entry) {
String translatedName = translations.get(entry.getKey());
if (translatedName != null || !HEADERS_TO_EXCLUDE.contains(entry.getKey())) {
if (translatedName == null) {
translatedName = entry.getKey();
}
// http://tools.ietf.org/html/draft-ietf-httpbis-http2-14#section-8.1.2.3
// All headers that start with ':' are only valid in HTTP/2 context
if (translatedName.isEmpty() || translatedName.charAt(0) == ':') {
e = Http2Exception
.protocolError("Unknown HTTP/2 header '%s' encountered in translation to HTTP/1.x",
translatedName);
return false;
} else {
headers.add(translatedName, entry.getValue());
}
}
return true;
}
/**
* Get any exceptions encountered while translating HTTP/2 headers to HTTP/1.x headers
*
* @return
* <ul>
* <li>{@code null} if no exceptions where encountered</li>
* <li>Otherwise an exception describing what went wrong</li>
* </ul>
*/
public Http2Exception exception() {
return e;
}
}
}

View File

@ -15,6 +15,7 @@
package io.netty.handler.codec.http2;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.TextHeaderProcessor;
import io.netty.handler.codec.TooLongFrameException;
import io.netty.handler.codec.http.DefaultHttpHeaders;
@ -30,6 +31,12 @@ import io.netty.util.collection.IntObjectMap;
* the header/data message flow is more likely.
*/
public final class InboundHttp2ToHttpPriorityAdapter extends InboundHttp2ToHttpAdapter {
private static final AsciiString OUT_OF_MESSAGE_SEQUENCE_METHOD = new AsciiString(
HttpUtil.OUT_OF_MESSAGE_SEQUENCE_METHOD.toString());
private static final AsciiString OUT_OF_MESSAGE_SEQUENCE_PATH = new AsciiString(
HttpUtil.OUT_OF_MESSAGE_SEQUENCE_PATH);
private static final AsciiString OUT_OF_MESSAGE_SEQUENCE_RETURN_CODE = new AsciiString(
HttpUtil.OUT_OF_MESSAGE_SEQUENCE_RETURN_CODE.toString());
private final IntObjectMap<HttpHeaders> outOfMessageFlowHeaders;
/**
@ -150,33 +157,32 @@ public final class InboundHttp2ToHttpPriorityAdapter extends InboundHttp2ToHttpA
* @param headers The headers to remove the priority tree elements from
*/
private void removePriorityRelatedHeaders(HttpHeaders headers) {
headers.remove(HttpUtil.ExtensionHeaders.Names.STREAM_DEPENDENCY_ID);
headers.remove(HttpUtil.ExtensionHeaders.Names.STREAM_WEIGHT);
headers.remove(HttpUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text());
headers.remove(HttpUtil.ExtensionHeaderNames.STREAM_WEIGHT.text());
}
/**
* Initializes the pseudo header fields for out of message flow HTTP/2 headers
* @param builder The builder to set the pseudo header values
* @param headers The headers to be initialized with pseudo header values
*/
private void initializePseudoHeaders(DefaultHttp2Headers.Builder builder) {
private void initializePseudoHeaders(Http2Headers headers) {
if (connection.isServer()) {
builder.method(HttpUtil.OUT_OF_MESSAGE_SEQUENCE_METHOD.toString())
.path(HttpUtil.OUT_OF_MESSAGE_SEQUENCE_PATH);
headers.method(OUT_OF_MESSAGE_SEQUENCE_METHOD).path(OUT_OF_MESSAGE_SEQUENCE_PATH);
} else {
builder.status(HttpUtil.OUT_OF_MESSAGE_SEQUENCE_RETURN_CODE.toString());
headers.status(OUT_OF_MESSAGE_SEQUENCE_RETURN_CODE);
}
}
/**
* Add all the HTTP headers into the HTTP/2 headers {@code builder} object
* @param headers The HTTP headers to translate to HTTP/2
* @param builder The container for the HTTP/2 headers
* Add all the HTTP headers into the HTTP/2 headers object
* @param httpHeaders The HTTP headers to translate to HTTP/2
* @param http2Headers The target HTTP/2 headers
*/
private void addHttpHeadersToHttp2Headers(HttpHeaders headers, final DefaultHttp2Headers.Builder builder) {
headers.forEachEntry(new TextHeaderProcessor() {
private void addHttpHeadersToHttp2Headers(HttpHeaders httpHeaders, final Http2Headers http2Headers) {
httpHeaders.forEachEntry(new TextHeaderProcessor() {
@Override
public boolean process(CharSequence name, CharSequence value) throws Exception {
builder.add(name, value);
http2Headers.add(new AsciiString(name), new AsciiString(value));
return true;
}
});
@ -209,7 +215,7 @@ public final class InboundHttp2ToHttpPriorityAdapter extends InboundHttp2ToHttpA
// and the HTTP message flow exists in OPEN.
if (parent != null && !parent.equals(connection.connectionStream())) {
HttpHeaders headers = new DefaultHttpHeaders();
headers.set(HttpUtil.ExtensionHeaders.Names.STREAM_DEPENDENCY_ID, parent.id());
headers.set(HttpUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), parent.id());
importOutOfMessageFlowHeaders(stream.id(), headers);
}
} else {
@ -218,7 +224,7 @@ public final class InboundHttp2ToHttpPriorityAdapter extends InboundHttp2ToHttpA
removePriorityRelatedHeaders(msg.trailingHeaders());
} else if (!parent.equals(connection.connectionStream())) {
HttpHeaders headers = getActiveHeaders(msg);
headers.set(HttpUtil.ExtensionHeaders.Names.STREAM_DEPENDENCY_ID, parent.id());
headers.set(HttpUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), parent.id());
}
}
}
@ -236,7 +242,7 @@ public final class InboundHttp2ToHttpPriorityAdapter extends InboundHttp2ToHttpA
} else {
headers = getActiveHeaders(msg);
}
headers.set(HttpUtil.ExtensionHeaders.Names.STREAM_WEIGHT, stream.weight());
headers.set(HttpUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), stream.weight());
}
@Override
@ -244,15 +250,15 @@ public final class InboundHttp2ToHttpPriorityAdapter extends InboundHttp2ToHttpA
boolean exclusive) throws Http2Exception {
FullHttpMessage msg = messageMap.get(streamId);
if (msg == null) {
HttpHeaders headers = outOfMessageFlowHeaders.remove(streamId);
if (headers == null) {
HttpHeaders httpHeaders = outOfMessageFlowHeaders.remove(streamId);
if (httpHeaders == null) {
throw Http2Exception.protocolError("Priority Frame recieved for unknown stream id %d", streamId);
}
DefaultHttp2Headers.Builder builder = DefaultHttp2Headers.newBuilder();
initializePseudoHeaders(builder);
addHttpHeadersToHttp2Headers(headers, builder);
msg = newMessage(streamId, builder.build(), validateHttpHeaders);
Http2Headers http2Headers = new DefaultHttp2Headers();
initializePseudoHeaders(http2Headers);
addHttpHeadersToHttp2Headers(httpHeaders, http2Headers);
msg = newMessage(streamId, http2Headers, validateHttpHeaders);
fireChannelRead(ctx, msg, streamId);
}
}

View File

@ -14,6 +14,10 @@
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_ENCODING;
import static io.netty.handler.codec.http.HttpHeaders.Values.DEFLATE;
import static io.netty.handler.codec.http.HttpHeaders.Values.GZIP;
import static io.netty.handler.codec.http2.Http2TestUtil.as;
import static io.netty.handler.codec.http2.Http2TestUtil.runInChannel;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals;
@ -38,9 +42,9 @@ import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.compression.ZlibCodecFactory;
import io.netty.handler.codec.compression.ZlibWrapper;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http2.Http2TestUtil.Http2Runnable;
import io.netty.util.NetUtil;
import io.netty.util.concurrent.Future;
@ -61,6 +65,9 @@ import org.mockito.MockitoAnnotations;
* Test for data decompression in the HTTP/2 codec.
*/
public class DataCompressionHttp2Test {
private static final AsciiString GET = as("GET");
private static final AsciiString POST = as("POST");
private static final AsciiString PATH = as("/some/path");
private List<ByteBuf> dataCapture;
@Mock
@ -73,7 +80,6 @@ public class DataCompressionHttp2Test {
private ServerBootstrap sb;
private Bootstrap cb;
private Channel serverChannel;
private Channel serverConnectedChannel;
private Channel clientChannel;
private CountDownLatch serverLatch;
private CountDownLatch clientLatch;
@ -103,7 +109,6 @@ public class DataCompressionHttp2Test {
serverConnection), serverListener, serverLatch, false);
p.addLast("reader", serverAdapter);
p.addLast(Http2CodecUtil.ignoreSettingsHandler());
serverConnectedChannel = ch;
}
});
@ -149,8 +154,8 @@ public class DataCompressionHttp2Test {
@Test
public void justHeadersNoData() throws Exception {
final Http2Headers headers = new DefaultHttp2Headers.Builder().method("GET").path("/some/path")
.set(HttpHeaders.Names.CONTENT_ENCODING, HttpHeaders.Values.GZIP).build();
final Http2Headers headers =
new DefaultHttp2Headers().method(GET).path(PATH).set(CONTENT_ENCODING, GZIP);
// Required because the decompressor intercepts the onXXXRead events before
// our {@link Http2TestUtil$FrameAdapter} does.
Http2TestUtil.FrameAdapter.getOrCreateStream(serverConnection, 3, false);
@ -173,8 +178,8 @@ public class DataCompressionHttp2Test {
final EmbeddedChannel encoder = new EmbeddedChannel(ZlibCodecFactory.newZlibEncoder(ZlibWrapper.GZIP));
try {
final ByteBuf encodedData = encodeData(data, encoder);
final Http2Headers headers = new DefaultHttp2Headers.Builder().method("POST").path("/some/path")
.set(HttpHeaders.Names.CONTENT_ENCODING, HttpHeaders.Values.GZIP).build();
final Http2Headers headers =
new DefaultHttp2Headers().method(POST).path(PATH).set(CONTENT_ENCODING.toLowerCase(), GZIP);
// Required because the decompressor intercepts the onXXXRead events before
// our {@link Http2TestUtil$FrameAdapter} does.
Http2TestUtil.FrameAdapter.getOrCreateStream(serverConnection, 3, false);
@ -207,8 +212,8 @@ public class DataCompressionHttp2Test {
final EmbeddedChannel encoder = new EmbeddedChannel(ZlibCodecFactory.newZlibEncoder(ZlibWrapper.GZIP));
try {
final ByteBuf encodedData = encodeData(data, encoder);
final Http2Headers headers = new DefaultHttp2Headers.Builder().method("POST").path("/some/path")
.set(HttpHeaders.Names.CONTENT_ENCODING, HttpHeaders.Values.GZIP).build();
final Http2Headers headers =
new DefaultHttp2Headers().method(POST).path(PATH).set(CONTENT_ENCODING.toLowerCase(), GZIP);
// Required because the decompressor intercepts the onXXXRead events before
// our {@link Http2TestUtil$FrameAdapter} does.
Http2TestUtil.FrameAdapter.getOrCreateStream(serverConnection, 3, false);
@ -244,8 +249,8 @@ public class DataCompressionHttp2Test {
try {
final ByteBuf encodedData1 = encodeData(data1, encoder);
final ByteBuf encodedData2 = encodeData(data2, encoder);
final Http2Headers headers = new DefaultHttp2Headers.Builder().method("POST").path("/some/path")
.set(HttpHeaders.Names.CONTENT_ENCODING, HttpHeaders.Values.GZIP).build();
final Http2Headers headers =
new DefaultHttp2Headers().method(POST).path(PATH).set(CONTENT_ENCODING.toLowerCase(), GZIP);
// Required because the decompressor intercepts the onXXXRead events before
// our {@link Http2TestUtil$FrameAdapter} does.
Http2TestUtil.FrameAdapter.getOrCreateStream(serverConnection, 3, false);
@ -288,8 +293,9 @@ public class DataCompressionHttp2Test {
data.writeByte((byte) 'a');
}
final ByteBuf encodedData = encodeData(data, encoder);
final Http2Headers headers = new DefaultHttp2Headers.Builder().method("POST").path("/some/path")
.set(HttpHeaders.Names.CONTENT_ENCODING, HttpHeaders.Values.DEFLATE).build();
final Http2Headers headers =
new DefaultHttp2Headers().method(POST).path(PATH)
.set(CONTENT_ENCODING.toLowerCase(), DEFLATE);
// Required because the decompressor intercepts the onXXXRead events before
// our {@link Http2TestUtil$FrameAdapter} does.
Http2TestUtil.FrameAdapter.getOrCreateStream(serverConnection, 3, false);
@ -362,10 +368,6 @@ public class DataCompressionHttp2Test {
serverLatch.await(5, SECONDS);
}
private void awaitClient() throws Exception {
clientLatch.await(5, SECONDS);
}
private ChannelHandlerContext ctxClient() {
return clientChannel.pipeline().firstContext();
}
@ -373,12 +375,4 @@ public class DataCompressionHttp2Test {
private ChannelPromise newPromiseClient() {
return ctxClient().newPromise();
}
private ChannelHandlerContext ctxServer() {
return serverConnectedChannel.pipeline().firstContext();
}
private ChannelPromise newPromiseServer() {
return ctxServer().newPromise();
}
}

View File

@ -16,6 +16,8 @@
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_UNSIGNED_INT;
import static io.netty.handler.codec.http2.Http2TestUtil.as;
import static io.netty.handler.codec.http2.Http2TestUtil.randomString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -257,7 +259,7 @@ public class DefaultHttp2FrameIOTest {
@Test
public void emptyHeadersShouldRoundtrip() throws Exception {
Http2Headers headers = Http2Headers.EMPTY_HEADERS;
Http2Headers headers = EmptyHttp2Headers.INSTANCE;
writer.writeHeaders(ctx, 1, headers, 0, true, promise);
ByteBuf frame = captureWrite();
@ -271,7 +273,7 @@ public class DefaultHttp2FrameIOTest {
@Test
public void emptyHeadersWithPaddingShouldRoundtrip() throws Exception {
Http2Headers headers = Http2Headers.EMPTY_HEADERS;
Http2Headers headers = EmptyHttp2Headers.INSTANCE;
writer.writeHeaders(ctx, 1, headers, 0xFF, true, promise);
ByteBuf frame = captureWrite();
@ -283,6 +285,19 @@ public class DefaultHttp2FrameIOTest {
}
}
@Test
public void binaryHeadersWithoutPriorityShouldRoundtrip() throws Exception {
Http2Headers headers = dummyBinaryHeaders();
writer.writeHeaders(ctx, 1, headers, 0, true, promise);
ByteBuf frame = captureWrite();
try {
reader.readFrame(ctx, frame, listener);
verify(listener).onHeadersRead(eq(ctx), eq(1), eq(headers), eq(0), eq(true));
} finally {
frame.release();
}
}
@Test
public void headersWithoutPriorityShouldRoundtrip() throws Exception {
Http2Headers headers = dummyHeaders();
@ -373,7 +388,7 @@ public class DefaultHttp2FrameIOTest {
@Test
public void emptypushPromiseShouldRoundtrip() throws Exception {
Http2Headers headers = Http2Headers.EMPTY_HEADERS;
Http2Headers headers = EmptyHttp2Headers.INSTANCE;
writer.writePushPromise(ctx, 1, 2, headers, 0, promise);
ByteBuf frame = captureWrite();
@ -447,18 +462,27 @@ public class DefaultHttp2FrameIOTest {
return alloc.buffer().writeBytes("abcdefgh".getBytes(CharsetUtil.UTF_8));
}
private static Http2Headers dummyBinaryHeaders() {
DefaultHttp2Headers headers = new DefaultHttp2Headers();
for (int ix = 0; ix < 10; ++ix) {
headers.add(randomString(), randomString());
}
return headers;
}
private static Http2Headers dummyHeaders() {
return DefaultHttp2Headers.newBuilder().method("GET").scheme("https").authority("example.org")
.path("/some/path").add("accept", "*/*").build();
return new DefaultHttp2Headers().method(as("GET")).scheme(as("https"))
.authority(as("example.org")).path(as("/some/path"))
.add(as("accept"), as("*/*"));
}
private static Http2Headers largeHeaders() {
DefaultHttp2Headers.Builder builder = DefaultHttp2Headers.newBuilder();
DefaultHttp2Headers headers = new DefaultHttp2Headers();
for (int i = 0; i < 100; ++i) {
String key = "this-is-a-test-header-key-" + i;
String value = "this-is-a-test-header-value-" + i;
builder.add(key, value);
headers.add(as(key), as(value));
}
return builder.build();
return headers;
}
}

View File

@ -15,6 +15,8 @@
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2TestUtil.as;
import static io.netty.handler.codec.http2.Http2TestUtil.randomBytes;
import static io.netty.util.CharsetUtil.UTF_8;
import static org.junit.Assert.assertEquals;
import io.netty.buffer.ByteBuf;
@ -41,34 +43,28 @@ public class DefaultHttp2HeadersDecoderTest {
@Test
public void decodeShouldSucceed() throws Exception {
final ByteBuf buf = encode(":method", "GET", "akey", "avalue");
ByteBuf buf = encode(b(":method"), b("GET"), b("akey"), b("avalue"), randomBytes(), randomBytes());
try {
Http2Headers headers = decoder.decodeHeaders(buf).build();
assertEquals(2, headers.size());
assertEquals("GET", headers.method());
assertEquals("avalue", headers.get("akey"));
Http2Headers headers = decoder.decodeHeaders(buf);
assertEquals(3, headers.size());
assertEquals("GET", headers.method().toString());
assertEquals("avalue", headers.get(as("akey")).toString());
} finally {
buf.release();
}
}
@Test(expected = Http2Exception.class)
public void decodeWithInvalidPseudoHeaderShouldFail() throws Exception {
final ByteBuf buf = encode(":invalid", "GET", "akey", "avalue");
try {
decoder.decodeHeaders(buf);
} finally {
buf.release();
}
private byte[] b(String string) {
return string.getBytes(UTF_8);
}
private ByteBuf encode(String... entries) throws Exception {
final Encoder encoder = new Encoder();
final ByteArrayOutputStream stream = new ByteArrayOutputStream();
private ByteBuf encode(byte[]... entries) throws Exception {
Encoder encoder = new Encoder();
ByteArrayOutputStream stream = new ByteArrayOutputStream();
for (int ix = 0; ix < entries.length;) {
String key = entries[ix++];
String value = entries[ix++];
encoder.encodeHeader(stream, key.getBytes(UTF_8), value.getBytes(UTF_8), false);
byte[] key = entries[ix++];
byte[] value = entries[ix++];
encoder.encodeHeader(stream, key, value, false);
}
return Unpooled.wrappedBuffer(stream.toByteArray());
}

View File

@ -15,6 +15,7 @@
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2TestUtil.as;
import static org.junit.Assert.assertTrue;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
@ -36,9 +37,8 @@ public class DefaultHttp2HeadersEncoderTest {
@Test
public void encodeShouldSucceed() throws Http2Exception {
DefaultHttp2Headers headers = DefaultHttp2Headers.newBuilder().method("GET").add("a", "1").add("a", "2")
.build();
final ByteBuf buf = Unpooled.buffer();
Http2Headers headers = headers();
ByteBuf buf = Unpooled.buffer();
try {
encoder.encodeHeaders(headers, buf);
assertTrue(buf.writerIndex() > 0);
@ -49,10 +49,13 @@ public class DefaultHttp2HeadersEncoderTest {
@Test(expected = Http2Exception.class)
public void headersExceedMaxSetSizeShouldFail() throws Http2Exception {
DefaultHttp2Headers headers = DefaultHttp2Headers.newBuilder().method("GET").add("a", "1").add("a", "2")
.build();
Http2Headers headers = headers();
encoder.maxHeaderListSize(2);
encoder.encodeHeaders(headers, Unpooled.buffer());
}
private Http2Headers headers() {
return new DefaultHttp2Headers().method(as("GET")).add(as("a"), as("1"))
.add(as("a"), as("2"));
}
}

View File

@ -1,113 +0,0 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import org.junit.Test;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import static org.junit.Assert.*;
/**
* Tests for {@link DefaultHttp2Headers}.
*/
public class DefaultHttp2HeadersTest {
@Test
public void duplicateKeysShouldStoreAllValues() {
DefaultHttp2Headers headers =
DefaultHttp2Headers.newBuilder().add("a", "1").add("a", "2")
.add("a", "3").build();
List<String> aValues = headers.getAll("a");
assertEquals(3, aValues.size());
assertEquals(3, headers.size());
assertEquals("1", aValues.get(0));
assertEquals("2", aValues.get(1));
assertEquals("3", aValues.get(2));
}
@Test
public void setHeaderShouldReplacePrevious() {
DefaultHttp2Headers headers =
DefaultHttp2Headers.newBuilder().add("a", "1").add("a", "2")
.add("a", "3").set("a", "4").build();
assertEquals(1, headers.size());
assertEquals("4", headers.get("a"));
}
@Test
public void setHeadersShouldReplacePrevious() {
DefaultHttp2Headers headers =
DefaultHttp2Headers.newBuilder().add("a", "1").add("a", "2")
.add("a", "3").set("a", Arrays.asList("4", "5")).build();
assertEquals(2, headers.size());
List<String> list = headers.getAll("a");
assertEquals(2, list.size());
assertEquals("4", list.get(0));
assertEquals("5", list.get(1));
}
@Test(expected = NoSuchElementException.class)
public void iterateEmptyHeadersShouldThrow() {
Iterator<Map.Entry<String, String>> iterator =
DefaultHttp2Headers.newBuilder().build().iterator();
assertFalse(iterator.hasNext());
iterator.next();
}
@Test
public void iterateHeadersShouldReturnAllValues() {
Set<String> headers = new HashSet<String>();
headers.add("a:1");
headers.add("a:2");
headers.add("a:3");
headers.add("b:1");
headers.add("b:2");
headers.add("c:1");
// Build the headers from the input set.
DefaultHttp2Headers.Builder builder = DefaultHttp2Headers.newBuilder();
for (String header : headers) {
String[] parts = header.split(":");
builder.add(parts[0], parts[1]);
}
// Now iterate through the headers, removing them from the original set.
for (Map.Entry<String, String> entry : builder.build()) {
assertTrue(headers.remove(entry.getKey() + ':' + entry.getValue()));
}
// Make sure we removed them all.
assertTrue(headers.isEmpty());
}
@Test(expected = IllegalArgumentException.class)
public void addInvalidPseudoHeaderShouldFail() {
DefaultHttp2Headers.newBuilder().add(":a", "1");
}
@Test(expected = IllegalArgumentException.class)
public void setInvalidPseudoHeaderShouldFail() {
DefaultHttp2Headers.newBuilder().set(":a", "1");
}
}

View File

@ -26,7 +26,6 @@ import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
import static io.netty.handler.codec.http2.Http2Error.NO_ERROR;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.protocolError;
import static io.netty.handler.codec.http2.Http2Headers.EMPTY_HEADERS;
import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_LOCAL;
import static io.netty.handler.codec.http2.Http2Stream.State.OPEN;
import static io.netty.handler.codec.http2.Http2Stream.State.RESERVED_LOCAL;
@ -277,7 +276,7 @@ public class DelegatingHttp2ConnectionHandlerTest {
@Test
public void headersReadAfterGoAwayShouldBeIgnored() throws Exception {
when(remote.isGoAwayReceived()).thenReturn(true);
decode().onHeadersRead(ctx, STREAM_ID, EMPTY_HEADERS, 0, false);
decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false);
verify(remote, never()).createStream(eq(STREAM_ID), eq(false));
// Verify that the event was absorbed and not propagated to the oberver.
@ -288,53 +287,54 @@ public class DelegatingHttp2ConnectionHandlerTest {
@Test
public void headersReadForUnknownStreamShouldCreateStream() throws Exception {
when(remote.createStream(eq(5), eq(false))).thenReturn(stream);
decode().onHeadersRead(ctx, 5, EMPTY_HEADERS, 0, false);
decode().onHeadersRead(ctx, 5, EmptyHttp2Headers.INSTANCE, 0, false);
verify(remote).createStream(eq(5), eq(false));
verify(listener).onHeadersRead(eq(ctx), eq(5), eq(EMPTY_HEADERS), eq(0), eq(DEFAULT_PRIORITY_WEIGHT),
eq(false), eq(0), eq(false));
verify(listener).onHeadersRead(eq(ctx), eq(5), eq(EmptyHttp2Headers.INSTANCE), eq(0),
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false));
}
@Test
public void headersReadForUnknownStreamShouldCreateHalfClosedStream() throws Exception {
when(remote.createStream(eq(5), eq(true))).thenReturn(stream);
decode().onHeadersRead(ctx, 5, EMPTY_HEADERS, 0, true);
decode().onHeadersRead(ctx, 5, EmptyHttp2Headers.INSTANCE, 0, true);
verify(remote).createStream(eq(5), eq(true));
verify(listener).onHeadersRead(eq(ctx), eq(5), eq(EMPTY_HEADERS), eq(0), eq(DEFAULT_PRIORITY_WEIGHT),
eq(false), eq(0), eq(true));
verify(listener).onHeadersRead(eq(ctx), eq(5), eq(EmptyHttp2Headers.INSTANCE), eq(0),
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true));
}
@Test
public void headersReadForPromisedStreamShouldHalfOpenStream() throws Exception {
when(stream.state()).thenReturn(RESERVED_REMOTE);
decode().onHeadersRead(ctx, STREAM_ID, EMPTY_HEADERS, 0, false);
decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false);
verify(stream).openForPush();
verify(listener).onHeadersRead(eq(ctx), eq(STREAM_ID), eq(EMPTY_HEADERS), eq(0), eq(DEFAULT_PRIORITY_WEIGHT),
eq(false), eq(0), eq(false));
verify(listener).onHeadersRead(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0),
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false));
}
@Test
public void headersReadForPromisedStreamShouldCloseStream() throws Exception {
when(stream.state()).thenReturn(RESERVED_REMOTE);
decode().onHeadersRead(ctx, STREAM_ID, EMPTY_HEADERS, 0, true);
decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, true);
verify(stream).openForPush();
verify(stream).close();
verify(listener).onHeadersRead(eq(ctx), eq(STREAM_ID), eq(EMPTY_HEADERS), eq(0), eq(DEFAULT_PRIORITY_WEIGHT),
eq(false), eq(0), eq(true));
verify(listener).onHeadersRead(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0),
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true));
}
@Test
public void pushPromiseReadAfterGoAwayShouldBeIgnored() throws Exception {
when(remote.isGoAwayReceived()).thenReturn(true);
decode().onPushPromiseRead(ctx, STREAM_ID, PUSH_STREAM_ID, EMPTY_HEADERS, 0);
decode().onPushPromiseRead(ctx, STREAM_ID, PUSH_STREAM_ID, EmptyHttp2Headers.INSTANCE, 0);
verify(remote, never()).reservePushStream(anyInt(), any(Http2Stream.class));
verify(listener, never()).onPushPromiseRead(eq(ctx), anyInt(), anyInt(), any(Http2Headers.class), anyInt());
}
@Test
public void pushPromiseReadShouldSucceed() throws Exception {
decode().onPushPromiseRead(ctx, STREAM_ID, PUSH_STREAM_ID, EMPTY_HEADERS, 0);
decode().onPushPromiseRead(ctx, STREAM_ID, PUSH_STREAM_ID, EmptyHttp2Headers.INSTANCE, 0);
verify(remote).reservePushStream(eq(PUSH_STREAM_ID), eq(stream));
verify(listener).onPushPromiseRead(eq(ctx), eq(STREAM_ID), eq(PUSH_STREAM_ID), eq(EMPTY_HEADERS), eq(0));
verify(listener).onPushPromiseRead(eq(ctx), eq(STREAM_ID), eq(PUSH_STREAM_ID),
eq(EmptyHttp2Headers.INSTANCE), eq(0));
}
@Test
@ -528,7 +528,8 @@ public class DelegatingHttp2ConnectionHandlerTest {
@Test
public void headersWriteAfterGoAwayShouldFail() throws Exception {
when(connection.isGoAway()).thenReturn(true);
ChannelFuture future = handler.writeHeaders(ctx, 5, EMPTY_HEADERS, 0, (short) 255, false, 0, false, promise);
ChannelFuture future = handler.writeHeaders(
ctx, 5, EmptyHttp2Headers.INSTANCE, 0, (short) 255, false, 0, false, promise);
verify(local, never()).createStream(anyInt(), anyBoolean());
verify(writer, never()).writeHeaders(eq(ctx), anyInt(), any(Http2Headers.class), anyInt(), anyBoolean(),
eq(promise));
@ -538,54 +539,56 @@ public class DelegatingHttp2ConnectionHandlerTest {
@Test
public void headersWriteForUnknownStreamShouldCreateStream() throws Exception {
when(local.createStream(eq(5), eq(false))).thenReturn(stream);
handler.writeHeaders(ctx, 5, EMPTY_HEADERS, 0, false, promise);
handler.writeHeaders(ctx, 5, EmptyHttp2Headers.INSTANCE, 0, false, promise);
verify(local).createStream(eq(5), eq(false));
verify(writer).writeHeaders(eq(ctx), eq(5), eq(EMPTY_HEADERS), eq(0), eq(DEFAULT_PRIORITY_WEIGHT), eq(false),
eq(0), eq(false), eq(promise));
verify(writer).writeHeaders(eq(ctx), eq(5), eq(EmptyHttp2Headers.INSTANCE), eq(0),
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise));
}
@Test
public void headersWriteShouldCreateHalfClosedStream() throws Exception {
when(local.createStream(eq(5), eq(true))).thenReturn(stream);
handler.writeHeaders(ctx, 5, EMPTY_HEADERS, 0, true, promise);
handler.writeHeaders(ctx, 5, EmptyHttp2Headers.INSTANCE, 0, true, promise);
verify(local).createStream(eq(5), eq(true));
verify(writer).writeHeaders(eq(ctx), eq(5), eq(EMPTY_HEADERS), eq(0), eq(DEFAULT_PRIORITY_WEIGHT), eq(false),
eq(0), eq(true), eq(promise));
verify(writer).writeHeaders(eq(ctx), eq(5), eq(EmptyHttp2Headers.INSTANCE), eq(0),
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise));
}
@Test
public void headersWriteShouldOpenStreamForPush() throws Exception {
when(stream.state()).thenReturn(RESERVED_LOCAL);
handler.writeHeaders(ctx, STREAM_ID, EMPTY_HEADERS, 0, false, promise);
handler.writeHeaders(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false, promise);
verify(stream).openForPush();
verify(stream, never()).closeLocalSide();
verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), eq(EMPTY_HEADERS), eq(0), eq(DEFAULT_PRIORITY_WEIGHT),
eq(false), eq(0), eq(false), eq(promise));
verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0),
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise));
}
@Test
public void headersWriteShouldClosePushStream() throws Exception {
when(stream.state()).thenReturn(RESERVED_LOCAL).thenReturn(HALF_CLOSED_LOCAL);
handler.writeHeaders(ctx, STREAM_ID, EMPTY_HEADERS, 0, true, promise);
handler.writeHeaders(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, true, promise);
verify(stream).openForPush();
verify(stream).closeLocalSide();
verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), eq(EMPTY_HEADERS), eq(0), eq(DEFAULT_PRIORITY_WEIGHT),
eq(false), eq(0), eq(true), eq(promise));
verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0),
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise));
}
@Test
public void pushPromiseWriteAfterGoAwayShouldFail() throws Exception {
when(connection.isGoAway()).thenReturn(true);
ChannelFuture future = handler.writePushPromise(ctx, STREAM_ID, PUSH_STREAM_ID, EMPTY_HEADERS, 0, promise);
ChannelFuture future =
handler.writePushPromise(ctx, STREAM_ID, PUSH_STREAM_ID,
EmptyHttp2Headers.INSTANCE, 0, promise);
assertTrue(future.awaitUninterruptibly().cause() instanceof Http2Exception);
}
@Test
public void pushPromiseWriteShouldReserveStream() throws Exception {
handler.writePushPromise(ctx, STREAM_ID, PUSH_STREAM_ID, EMPTY_HEADERS, 0, promise);
handler.writePushPromise(ctx, STREAM_ID, PUSH_STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, promise);
verify(local).reservePushStream(eq(PUSH_STREAM_ID), eq(stream));
verify(writer).writePushPromise(eq(ctx), eq(STREAM_ID), eq(PUSH_STREAM_ID), eq(EMPTY_HEADERS), eq(0),
eq(promise));
verify(writer).writePushPromise(eq(ctx), eq(STREAM_ID), eq(PUSH_STREAM_ID),
eq(EmptyHttp2Headers.INSTANCE), eq(0), eq(promise));
}
@Test

View File

@ -18,6 +18,7 @@ import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpMethod.POST;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import static io.netty.handler.codec.http2.Http2CodecUtil.ignoreSettingsHandler;
import static io.netty.handler.codec.http2.Http2TestUtil.as;
import static io.netty.util.CharsetUtil.UTF_8;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals;
@ -144,16 +145,19 @@ public class DelegatingHttp2HttpConnectionHandlerTest {
final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, "/example");
try {
final HttpHeaders httpHeaders = request.headers();
httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 5);
httpHeaders.set(HttpHeaders.Names.HOST, "http://my-user_name@www.example.org:5555/example");
httpHeaders.set(HttpUtil.ExtensionHeaders.Names.AUTHORITY, "www.example.org:5555");
httpHeaders.set(HttpUtil.ExtensionHeaders.Names.SCHEME, "http");
httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
httpHeaders.set(HttpHeaders.Names.HOST,
"http://my-user_name@www.example.org:5555/example");
httpHeaders.set(HttpUtil.ExtensionHeaderNames.AUTHORITY.text(), "www.example.org:5555");
httpHeaders.set(HttpUtil.ExtensionHeaderNames.SCHEME.text(), "http");
httpHeaders.add("foo", "goo");
httpHeaders.add("foo", "goo2");
httpHeaders.add("foo2", "goo2");
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("GET").path("/example")
.authority("www.example.org:5555").scheme("http").add("foo", "goo").add("foo", "goo2")
.add("foo2", "goo2").build();
final Http2Headers http2Headers =
new DefaultHttp2Headers().method(as("GET")).path(as("/example"))
.authority(as("www.example.org:5555")).scheme(as("http"))
.add(as("foo"), as("goo")).add(as("foo"), as("goo2"))
.add(as("foo2"), as("goo2"));
ChannelPromise writePromise = newPromise();
ChannelFuture writeFuture = clientChannel.writeAndFlush(request, writePromise);
@ -162,11 +166,10 @@ public class DelegatingHttp2HttpConnectionHandlerTest {
writeFuture.awaitUninterruptibly(2, SECONDS);
assertTrue(writeFuture.isSuccess());
awaitRequests();
final ArgumentCaptor<ByteBuf> dataCaptor = ArgumentCaptor.forClass(ByteBuf.class);
verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(5), eq(http2Headers), eq(0),
anyShort(), anyBoolean(), eq(0), eq(true));
verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(5),
eq(http2Headers), eq(0), anyShort(), anyBoolean(), eq(0), eq(true));
verify(serverListener, never()).onDataRead(any(ChannelHandlerContext.class), anyInt(),
dataCaptor.capture(), anyInt(), anyBoolean());
any(ByteBuf.class), anyInt(), anyBoolean());
} finally {
request.release();
}
@ -184,9 +187,11 @@ public class DelegatingHttp2HttpConnectionHandlerTest {
httpHeaders.add("foo", "goo");
httpHeaders.add("foo", "goo2");
httpHeaders.add("foo2", "goo2");
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("POST").path("/example")
.authority("www.example.org:5555").scheme("http").add("foo", "goo").add("foo", "goo2")
.add("foo2", "goo2").build();
final Http2Headers http2Headers =
new DefaultHttp2Headers().method(as("POST")).path(as("/example"))
.authority(as("www.example.org:5555")).scheme(as("http"))
.add(as("foo"), as("goo")).add(as("foo"), as("goo2"))
.add(as("foo2"), as("goo2"));
ChannelPromise writePromise = newPromise();
ChannelFuture writeFuture = clientChannel.writeAndFlush(request, writePromise);

View File

@ -15,6 +15,8 @@
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2TestUtil.as;
import static io.netty.handler.codec.http2.Http2TestUtil.randomString;
import static io.netty.handler.codec.http2.Http2TestUtil.runInChannel;
import static io.netty.util.CharsetUtil.UTF_8;
import static java.util.concurrent.TimeUnit.SECONDS;
@ -133,8 +135,7 @@ public class Http2ConnectionRoundtripTest {
@Test
public void flowControlProperlyChunksLargeMessage() throws Exception {
final Http2Headers headers = new DefaultHttp2Headers.Builder().method("GET").scheme("https")
.authority("example.org").path("/some/path/resource2").build();
final Http2Headers headers = dummyHeaders();
// Create a large message to send.
final int length = 10485760; // 10MB
@ -179,8 +180,7 @@ public class Http2ConnectionRoundtripTest {
@Test
public void stressTest() throws Exception {
final Http2Headers headers = new DefaultHttp2Headers.Builder().method("GET").scheme("https")
.authority("example.org").path("/some/path/resource2").build();
final Http2Headers headers = dummyHeaders();
final String text = "hello world";
final String pingMsg = "12345678";
final ByteBuf data = Unpooled.copiedBuffer(text.getBytes());
@ -257,4 +257,9 @@ public class Http2ConnectionRoundtripTest {
}
}
}
private Http2Headers dummyHeaders() {
return new DefaultHttp2Headers().method(as("GET")).scheme(as("https"))
.authority(as("example.org")).path(as("/some/path/resource2")).add(randomString(), randomString());
}
}

View File

@ -15,16 +15,18 @@
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2TestUtil.as;
import static io.netty.handler.codec.http2.Http2TestUtil.randomString;
import static io.netty.handler.codec.http2.Http2TestUtil.runInChannel;
import static io.netty.util.CharsetUtil.UTF_8;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
@ -153,9 +155,7 @@ public class Http2FrameRoundtripTest {
@Test
public void headersFrameWithoutPriorityShouldMatch() throws Exception {
final Http2Headers headers =
new DefaultHttp2Headers.Builder().method("GET").scheme("https")
.authority("example.org").path("/some/path/resource2").build();
final Http2Headers headers = headers();
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
@ -170,9 +170,7 @@ public class Http2FrameRoundtripTest {
@Test
public void headersFrameWithPriorityShouldMatch() throws Exception {
final Http2Headers headers =
new DefaultHttp2Headers.Builder().method("GET").scheme("https")
.authority("example.org").path("/some/path/resource2").build();
final Http2Headers headers = headers();
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
@ -244,9 +242,7 @@ public class Http2FrameRoundtripTest {
@Test
public void pushPromiseFrameShouldMatch() throws Exception {
final Http2Headers headers =
new DefaultHttp2Headers.Builder().method("GET").scheme("https")
.authority("example.org").path("/some/path/resource2").build();
final Http2Headers headers = headers();
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
@ -306,9 +302,7 @@ public class Http2FrameRoundtripTest {
@Test
public void stressTest() throws Exception {
final Http2Headers headers =
new DefaultHttp2Headers.Builder().method("GET").scheme("https")
.authority("example.org").path("/some/path/resource2").build();
final Http2Headers headers = headers();
final String text = "hello world";
final ByteBuf data = Unpooled.copiedBuffer(text.getBytes());
try {
@ -359,4 +353,9 @@ public class Http2FrameRoundtripTest {
private ChannelPromise newPromise() {
return ctx().newPromise();
}
private Http2Headers headers() {
return new DefaultHttp2Headers().method(as("GET")).scheme(as("https"))
.authority(as("example.org")).path(as("/some/path/resource2")).add(randomString(), randomString());
}
}

View File

@ -15,6 +15,8 @@
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2TestUtil.as;
import static io.netty.handler.codec.http2.Http2TestUtil.randomString;
import static org.junit.Assert.assertEquals;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
@ -46,47 +48,37 @@ public class Http2HeaderBlockIOTest {
@Test
public void roundtripShouldBeSuccessful() throws Http2Exception {
Http2Headers in =
new DefaultHttp2Headers.Builder().method("GET").scheme("https")
.authority("example.org").path("/some/path/resource2")
.add("accept", "image/png").add("cache-control", "no-cache")
.add("custom", "value1").add("custom", "value2")
.add("custom", "value3").add("custom", "custom4").build();
Http2Headers in = headers();
assertRoundtripSuccessful(in);
}
@Test
public void successiveCallsShouldSucceed() throws Http2Exception {
Http2Headers in =
new DefaultHttp2Headers.Builder().method("GET").scheme("https")
.authority("example.org").path("/some/path")
.add("accept", "*/*").build();
new DefaultHttp2Headers().method(as("GET")).scheme(as("https"))
.authority(as("example.org")).path(as("/some/path"))
.add(as("accept"), as("*/*"));
assertRoundtripSuccessful(in);
in =
new DefaultHttp2Headers.Builder().method("GET").scheme("https")
.authority("example.org").path("/some/path/resource1")
.add("accept", "image/jpeg").add("cache-control", "no-cache")
.build();
new DefaultHttp2Headers().method(as("GET")).scheme(as("https"))
.authority(as("example.org")).path(as("/some/path/resource1"))
.add(as("accept"), as("image/jpeg"))
.add(as("cache-control"), as("no-cache"));
assertRoundtripSuccessful(in);
in =
new DefaultHttp2Headers.Builder().method("GET").scheme("https")
.authority("example.org").path("/some/path/resource2")
.add("accept", "image/png").add("cache-control", "no-cache")
.build();
new DefaultHttp2Headers().method(as("GET")).scheme(as("https"))
.authority(as("example.org")).path(as("/some/path/resource2"))
.add(as("accept"), as("image/png"))
.add(as("cache-control"), as("no-cache"));
assertRoundtripSuccessful(in);
}
@Test
public void setMaxHeaderSizeShouldBeSuccessful() throws Http2Exception {
encoder.maxHeaderTableSize(10);
Http2Headers in =
new DefaultHttp2Headers.Builder().method("GET").scheme("https")
.authority("example.org").path("/some/path/resource2")
.add("accept", "image/png").add("cache-control", "no-cache")
.add("custom", "value1").add("custom", "value2")
.add("custom", "value3").add("custom", "custom4").build();
Http2Headers in = headers();
assertRoundtripSuccessful(in);
assertEquals(10, decoder.maxHeaderTableSize());
}
@ -94,7 +86,16 @@ public class Http2HeaderBlockIOTest {
private void assertRoundtripSuccessful(Http2Headers in) throws Http2Exception {
encoder.encodeHeaders(in, buffer);
Http2Headers out = decoder.decodeHeaders(buffer).build();
Http2Headers out = decoder.decodeHeaders(buffer);
assertEquals(in, out);
}
private Http2Headers headers() {
return new DefaultHttp2Headers().method(as("GET")).scheme(as("https"))
.authority(as("example.org")).path(as("/some/path/resource2"))
.add(as("accept"), as("image/png")).add(as("cache-control"), as("no-cache"))
.add(as("custom"), as("value1")).add(as("custom"), as("value2"))
.add(as("custom"), as("value3")).add(as("custom"), as("custom4"))
.add(randomString(), randomString());
}
}

View File

@ -17,9 +17,11 @@ package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
/**
@ -49,7 +51,38 @@ final class Http2TestUtil {
});
}
private Http2TestUtil() { }
/**
* Converts a {@link String} into an {@link AsciiString}.
*/
public static AsciiString as(String value) {
return new AsciiString(value);
}
/**
* Converts a byte array into an {@link AsciiString}.
*/
public static AsciiString as(byte[] value) {
return new AsciiString(value);
}
/**
* Returns a byte array filled with random data.
*/
public static byte[] randomBytes() {
byte[] data = new byte[100];
new Random().nextBytes(data);
return data;
}
/**
* Returns an {@link AsciiString} that wraps a randomly-filled byte array.
*/
public static AsciiString randomString() {
return as(randomBytes());
}
private Http2TestUtil() {
}
static class FrameAdapter extends ByteToMessageDecoder {
private final boolean copyBufs;

View File

@ -14,6 +14,7 @@
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2TestUtil.as;
import static io.netty.handler.codec.http2.Http2TestUtil.runInChannel;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals;
@ -166,12 +167,14 @@ public class InboundHttp2ToHttpAdapterTest {
"/some/path/resource2", true);
try {
HttpHeaders httpHeaders = request.headers();
httpHeaders.set(HttpUtil.ExtensionHeaders.Names.SCHEME, "https");
httpHeaders.set(HttpUtil.ExtensionHeaders.Names.AUTHORITY, "example.org");
httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpUtil.ExtensionHeaderNames.SCHEME.text(), "https");
httpHeaders.set(HttpUtil.ExtensionHeaderNames.AUTHORITY.text(), "example.org");
httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, 0);
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("GET").scheme("https")
.authority("example.org").path("/some/path/resource2").build();
final Http2Headers http2Headers =
new DefaultHttp2Headers().method(as("GET")).scheme(as("https"))
.authority(as("example.org"))
.path(as("/some/path/resource2"));
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
@ -197,10 +200,10 @@ public class InboundHttp2ToHttpAdapterTest {
"/some/path/resource2", content, true);
try {
HttpHeaders httpHeaders = request.headers();
httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length());
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("GET")
.path("/some/path/resource2").build();
final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET"))
.path(as("/some/path/resource2"));
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
@ -227,10 +230,10 @@ public class InboundHttp2ToHttpAdapterTest {
"/some/path/resource2", content, true);
try {
HttpHeaders httpHeaders = request.headers();
httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length());
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("GET")
.path("/some/path/resource2").build();
final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET"))
.path(as("/some/path/resource2"));
final int midPoint = text.length() / 2;
runInChannel(clientChannel, new Http2Runnable() {
@Override
@ -261,10 +264,10 @@ public class InboundHttp2ToHttpAdapterTest {
"/some/path/resource2", content, true);
try {
HttpHeaders httpHeaders = request.headers();
httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length());
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("GET")
.path("/some/path/resource2").build();
final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET"))
.path(as("/some/path/resource2"));
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
@ -295,16 +298,19 @@ public class InboundHttp2ToHttpAdapterTest {
"/some/path/resource2", content, true);
try {
HttpHeaders httpHeaders = request.headers();
httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length());
HttpHeaders trailingHeaders = request.trailingHeaders();
trailingHeaders.set("FoO", "goo");
trailingHeaders.set("foO2", "goo2");
trailingHeaders.add("fOo2", "goo3");
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("GET")
.path("/some/path/resource2").build();
final Http2Headers http2Headers2 = new DefaultHttp2Headers.Builder().set("foo", "goo").set("foo2", "goo2")
.add("foo2", "goo3").build();
final Http2Headers http2Headers =
new DefaultHttp2Headers().method(as("GET")).path(
as("/some/path/resource2"));
final Http2Headers http2Headers2 =
new DefaultHttp2Headers().set(as("foo"), as("goo"))
.set(as("foo2"), as("goo2"))
.add(as("foo2"), as("goo3"));
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
@ -332,16 +338,19 @@ public class InboundHttp2ToHttpAdapterTest {
"/some/path/resource2", content, true);
try {
HttpHeaders httpHeaders = request.headers();
httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length());
HttpHeaders trailingHeaders = request.trailingHeaders();
trailingHeaders.set("Foo", "goo");
trailingHeaders.set("fOo2", "goo2");
trailingHeaders.add("foO2", "goo3");
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("GET")
.path("/some/path/resource2").build();
final Http2Headers http2Headers2 = new DefaultHttp2Headers.Builder().set("foo", "goo").set("foo2", "goo2")
.add("foo2", "goo3").build();
final Http2Headers http2Headers =
new DefaultHttp2Headers().method(as("GET")).path(
as("/some/path/resource2"));
final Http2Headers http2Headers2 =
new DefaultHttp2Headers().set(as("foo"), as("goo"))
.set(as("foo2"), as("goo2"))
.add(as("foo2"), as("goo3"));
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
@ -374,17 +383,17 @@ public class InboundHttp2ToHttpAdapterTest {
"/some/path/resource2", content2, true);
try {
HttpHeaders httpHeaders = request.headers();
httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length());
HttpHeaders httpHeaders2 = request2.headers();
httpHeaders2.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 5);
httpHeaders2.set(HttpUtil.ExtensionHeaders.Names.STREAM_DEPENDENCY_ID, 3);
httpHeaders2.set(HttpUtil.ExtensionHeaders.Names.STREAM_WEIGHT, 123);
httpHeaders2.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
httpHeaders2.set(HttpUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), 3);
httpHeaders2.set(HttpUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), 123);
httpHeaders2.set(HttpHeaders.Names.CONTENT_LENGTH, text2.length());
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("PUT")
.path("/some/path/resource").build();
final Http2Headers http2Headers2 = new DefaultHttp2Headers.Builder().method("PUT")
.path("/some/path/resource2").build();
final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("PUT"))
.path(as("/some/path/resource"));
final Http2Headers http2Headers2 = new DefaultHttp2Headers().method(as("PUT"))
.path(as("/some/path/resource2"));
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
@ -423,19 +432,19 @@ public class InboundHttp2ToHttpAdapterTest {
HttpUtil.OUT_OF_MESSAGE_SEQUENCE_METHOD, HttpUtil.OUT_OF_MESSAGE_SEQUENCE_PATH, true);
try {
HttpHeaders httpHeaders = request.headers();
httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length());
HttpHeaders httpHeaders2 = request2.headers();
httpHeaders2.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 5);
httpHeaders2.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
httpHeaders2.set(HttpHeaders.Names.CONTENT_LENGTH, text2.length());
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("PUT")
.path("/some/path/resource").build();
final Http2Headers http2Headers2 = new DefaultHttp2Headers.Builder().method("PUT")
.path("/some/path/resource2").build();
final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("PUT"))
.path(as("/some/path/resource"));
final Http2Headers http2Headers2 = new DefaultHttp2Headers().method(as("PUT"))
.path(as("/some/path/resource2"));
HttpHeaders httpHeaders3 = request3.headers();
httpHeaders3.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 5);
httpHeaders3.set(HttpUtil.ExtensionHeaders.Names.STREAM_DEPENDENCY_ID, 3);
httpHeaders3.set(HttpUtil.ExtensionHeaders.Names.STREAM_WEIGHT, 222);
httpHeaders3.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
httpHeaders3.set(HttpUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), 3);
httpHeaders3.set(HttpUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), 222);
httpHeaders3.set(HttpHeaders.Names.CONTENT_LENGTH, 0);
runInChannel(clientChannel, new Http2Runnable() {
@Override
@ -477,20 +486,20 @@ public class InboundHttp2ToHttpAdapterTest {
HttpMethod.GET, "/push/test", true);
try {
HttpHeaders httpHeaders = response.headers();
httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length());
HttpHeaders httpHeaders2 = response2.headers();
httpHeaders2.set(HttpUtil.ExtensionHeaders.Names.SCHEME, "https");
httpHeaders2.set(HttpUtil.ExtensionHeaders.Names.AUTHORITY, "example.org");
httpHeaders2.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 5);
httpHeaders2.set(HttpUtil.ExtensionHeaders.Names.STREAM_PROMISE_ID, 3);
httpHeaders2.set(HttpUtil.ExtensionHeaderNames.SCHEME.text(), "https");
httpHeaders2.set(HttpUtil.ExtensionHeaderNames.AUTHORITY.text(), "example.org");
httpHeaders2.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
httpHeaders2.set(HttpUtil.ExtensionHeaderNames.STREAM_PROMISE_ID.text(), 3);
httpHeaders2.set(HttpHeaders.Names.CONTENT_LENGTH, text2.length());
httpHeaders = request.headers();
httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, 0);
final Http2Headers http2Headers3 = new DefaultHttp2Headers.Builder().method("GET")
.path("/push/test").build();
final Http2Headers http2Headers3 = new DefaultHttp2Headers().method(as("GET"))
.path(as("/push/test"));
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
@ -504,9 +513,10 @@ public class InboundHttp2ToHttpAdapterTest {
capturedRequests = requestCaptor.getAllValues();
assertEquals(request, capturedRequests.get(0));
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().status("200").build();
final Http2Headers http2Headers2 = new DefaultHttp2Headers.Builder().status("201").scheme("https")
.authority("example.org").build();
final Http2Headers http2Headers = new DefaultHttp2Headers().status(as("200"));
final Http2Headers http2Headers2 =
new DefaultHttp2Headers().status(as("201")).scheme(as("https"))
.authority(as("example.org"));
runInChannel(serverConnectedChannel, new Http2Runnable() {
@Override
public void run() {
@ -535,11 +545,15 @@ public class InboundHttp2ToHttpAdapterTest {
final FullHttpMessage request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT, "/info/test",
true);
HttpHeaders httpHeaders = request.headers();
httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.set(HttpHeaders.Names.EXPECT, HttpHeaders.Values.CONTINUE);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, 0);
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("PUT").path("/info/test")
.set(HttpHeaders.Names.EXPECT.toString(), HttpHeaders.Values.CONTINUE).build();
final Http2Headers http2Headers =
new DefaultHttp2Headers()
.method(as("PUT"))
.path(as("/info/test"))
.set(as(HttpHeaders.Names.EXPECT.toString()),
as(HttpHeaders.Values.CONTINUE.toString()));
final FullHttpMessage response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
final String text = "a big payload";
final ByteBuf payload = Unpooled.copiedBuffer(text.getBytes());
@ -563,9 +577,9 @@ public class InboundHttp2ToHttpAdapterTest {
reset(serverListener);
httpHeaders = response.headers();
httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, 0);
final Http2Headers http2HeadersResponse = new DefaultHttp2Headers.Builder().status("100").build();
final Http2Headers http2HeadersResponse = new DefaultHttp2Headers().status(as("100"));
runInChannel(serverConnectedChannel, new Http2Runnable() {
@Override
public void run() {
@ -600,9 +614,9 @@ public class InboundHttp2ToHttpAdapterTest {
setClientLatch(1);
httpHeaders = response2.headers();
httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, 0);
final Http2Headers http2HeadersResponse2 = new DefaultHttp2Headers.Builder().status("200").build();
final Http2Headers http2HeadersResponse2 = new DefaultHttp2Headers().status(as("200"));
runInChannel(serverConnectedChannel, new Http2Runnable() {
@Override
public void run() {

View File

@ -21,6 +21,7 @@ import io.netty.util.internal.EmptyArrays;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
@ -34,6 +35,78 @@ import java.util.regex.PatternSyntaxException;
public final class AsciiString implements CharSequence, Comparable<CharSequence> {
public static final AsciiString EMPTY_STRING = new AsciiString("");
public static final Comparator<AsciiString> CASE_INSENSITIVE_ORDER = new Comparator<AsciiString>() {
@Override
public int compare(AsciiString o1, AsciiString o2) {
return CHARSEQUENCE_CASE_INSENSITIVE_ORDER.compare(o1, o2);
}
};
public static final Comparator<CharSequence> CHARSEQUENCE_CASE_INSENSITIVE_ORDER =
new Comparator<CharSequence>() {
@Override
public int compare(CharSequence o1, CharSequence o2) {
if (o1 == o2) {
return 0;
}
AsciiString a1 = o1 instanceof AsciiString ? (AsciiString) o1 : null;
AsciiString a2 = o2 instanceof AsciiString ? (AsciiString) o2 : null;
int result;
int length1 = o1.length();
int length2 = o2.length();
int minLength = Math.min(length1, length2);
if (a1 != null && a2 != null) {
byte[] thisValue = a1.value;
byte[] thatValue = a2.value;
for (int i = 0; i < minLength; i++) {
byte v1 = thisValue[i];
byte v2 = thatValue[i];
if (v1 == v2) {
continue;
}
int c1 = toLowerCase(v1) & 0xFF;
int c2 = toLowerCase(v2) & 0xFF;
result = c1 - c2;
if (result != 0) {
return result;
}
}
} else if (a1 != null) {
byte[] thisValue = a1.value;
for (int i = 0; i < minLength; i++) {
int c1 = toLowerCase(thisValue[i]) & 0xFF;
int c2 = toLowerCase(o2.charAt(i));
result = c1 - c2;
if (result != 0) {
return result;
}
}
} else if (a2 != null) {
byte[] thatValue = a2.value;
for (int i = 0; i < minLength; i++) {
int c1 = toLowerCase(o1.charAt(i));
int c2 = toLowerCase(thatValue[i]) & 0xFF;
result = c1 - c2;
if (result != 0) {
return result;
}
}
} else {
for (int i = 0; i < minLength; i++) {
int c1 = toLowerCase(o1.charAt(i));
int c2 = toLowerCase(o2.charAt(i));
result = c1 - c2;
if (result != 0) {
return result;
}
}
}
return length1 - length2;
}
};
/**
* Returns the case-insensitive hash code of the specified string. Note that this method uses the same hashing
@ -105,6 +178,14 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence>
return a.equals(b);
}
/**
* Returns an {@link AsciiString} containing the given character sequence. If the given string
* is already a {@link AsciiString}, just returns the same instance.
*/
public static AsciiString of(CharSequence string) {
return string instanceof AsciiString ? (AsciiString) string : new AsciiString(string);
}
private final byte[] value;
private String string;
private int hash;
@ -429,43 +510,7 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence>
* if {@code string} is {@code null}.
*/
public int compareToIgnoreCase(CharSequence string) {
if (this == string) {
return 0;
}
int result;
int length1 = length();
int length2 = string.length();
int minLength = Math.min(length1, length2);
byte[] thisValue = value;
if (string instanceof AsciiString) {
AsciiString that = (AsciiString) string;
byte[] thatValue = that.value;
for (int i = 0; i < minLength; i ++) {
byte v1 = thisValue[i];
byte v2 = thatValue[i];
if (v1 == v2) {
continue;
}
int c1 = toLowerCase(v1) & 0xFF;
int c2 = toLowerCase(v2) & 0xFF;
result = c1 - c2;
if (result != 0) {
return result;
}
}
} else {
for (int i = 0; i < minLength; i ++) {
int c1 = toLowerCase(thisValue[i]) & 0xFF;
int c2 = toLowerCase(string.charAt(i));
result = c1 - c2;
if (result != 0) {
return result;
}
}
}
return length1 - length2;
return CHARSEQUENCE_CASE_INSENSITIVE_ORDER.compare(this, string);
}
/**

View File

@ -0,0 +1,369 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.handler.codec;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
/**
* A typical {@code AsciiString} multimap used by protocols that use binary headers (such as HTTP/2)
* for the representation of arbitrary key-value data. {@link AsciiString} is just a wrapper around
* a byte array but provides some additional utility when handling text data.
*/
public interface BinaryHeaders extends Iterable<Map.Entry<AsciiString, AsciiString>> {
/**
* A visitor that helps reduce GC pressure while iterating over a collection of {@link BinaryHeaders}.
*/
public interface BinaryHeaderVisitor {
/**
* @return
* <ul>
* <li>{@code true} if the processor wants to continue the loop and handle the entry.</li>
* <li>{@code false} if the processor wants to stop handling headers and abort the loop.</li>
* </ul>
*/
boolean visit(AsciiString name, AsciiString value) throws Exception;
}
/**
* Returns the value of a header with the specified name. If there are
* more than one values for the specified name, the first value is returned.
*
* @param name the name of the header to search
* @return the first header value if the header is found.
* {@code null} if there's no such header.
*/
AsciiString get(AsciiString name);
/**
* Returns the value of a header with the specified name. If there are
* more than one values for the specified name, the first value is returned.
*
* @param name the name of the header to search
* @param defaultValue the default value
* @return the first header value if the header is found.
* {@code defaultValue} if there's no such header.
*/
AsciiString get(AsciiString name, AsciiString defaultValue);
/**
* Returns and removes the value of a header with the specified name. If there are
* more than one values for the specified name, the first value is returned.
*
* @param name the name of the header to search
* @return the first header value or {@code null} if there is no such header
*/
AsciiString getAndRemove(AsciiString name);
/**
* Returns and removes the value of a header with the specified name. If there are
* more than one values for the specified name, the first value is returned.
*
* @param name the name of the header to search
* @param defaultValue the default value
* @return the first header value or {@code defaultValue} if there is no such header
*/
AsciiString getAndRemove(AsciiString name, AsciiString defaultValue);
/**
* Returns the values of headers with the specified name
*
* @param name The name of the headers to search
* @return A {@link List} of header values which will be empty if no values are found
*/
List<AsciiString> getAll(AsciiString name);
/**
* Returns and Removes the values of headers with the specified name
*
* @param name The name of the headers to search
* @return A {@link List} of header values which will be empty if no values are found
*/
List<AsciiString> getAllAndRemove(AsciiString name);
/**
* Returns a new {@link List} that contains all headers in this object. Note that modifying the
* returned {@link List} will not affect the state of this object. If you intend to enumerate over the header
* entries only, use {@link #iterator()} instead, which has much less overhead.
*/
List<Entry<AsciiString, AsciiString>> entries();
/**
* Returns {@code true} if and only if this collection contains the header with the specified name.
*
* @param name The name of the header to search for
* @return {@code true} if at least one header is found
*/
boolean contains(AsciiString name);
/**
* Returns the number of header entries in this collection.
*/
int size();
/**
* Returns {@code true} if and only if this collection contains no header entries.
*/
boolean isEmpty();
/**
* Returns a new {@link Set} that contains the names of all headers in this object. Note that modifying the
* returned {@link Set} will not affect the state of this object. If you intend to enumerate over the header
* entries only, use {@link #iterator()} instead, which has much less overhead.
*/
Set<AsciiString> names();
/**
* Adds a new header with the specified name and value.
*
* If the specified value is not a {@link String}, it is converted
* into a {@link String} by {@link Object#toString()}, except in the cases
* of {@link java.util.Date} and {@link java.util.Calendar}, which are formatted to the date
* format defined in <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1">RFC2616</a>.
*
* @param name the name of the header being added
* @param value the value of the header being added
*
* @return {@code this}
*/
BinaryHeaders add(AsciiString name, AsciiString value);
/**
* Adds a new header with the specified name and values.
*
* This getMethod can be represented approximately as the following code:
* <pre>
* for (Object v: values) {
* if (v == null) {
* break;
* }
* headers.add(name, v);
* }
* </pre>
*
* @param name the name of the headers being set
* @param values the values of the headers being set
* @return {@code this}
*/
BinaryHeaders add(AsciiString name, Iterable<AsciiString> values);
/**
* Adds a new header with the specified name and values.
*
* This getMethod can be represented approximately as the following code:
* <pre>
* for (Object v: values) {
* if (v == null) {
* break;
* }
* headers.add(name, v);
* }
* </pre>
*
* @param name the name of the headers being set
* @param values the values of the headers being set
* @return {@code this}
*/
BinaryHeaders add(AsciiString name, AsciiString... values);
/**
* Adds all header entries of the specified {@code headers}.
*
* @return {@code this}
*/
BinaryHeaders add(BinaryHeaders headers);
/**
* Sets a header with the specified name and value.
*
* If there is an existing header with the same name, it is removed.
* If the specified value is not a {@link String}, it is converted into a
* {@link String} by {@link Object#toString()}, except for {@link java.util.Date}
* and {@link java.util.Calendar}, which are formatted to the date format defined in
* <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1">RFC2616</a>.
*
* @param name The name of the header being set
* @param value The value of the header being set
* @return {@code this}
*/
BinaryHeaders set(AsciiString name, AsciiString value);
/**
* Sets a header with the specified name and values.
*
* If there is an existing header with the same name, it is removed.
* This getMethod can be represented approximately as the following code:
* <pre>
* headers.remove(name);
* for (Object v: values) {
* if (v == null) {
* break;
* }
* headers.add(name, v);
* }
* </pre>
*
* @param name the name of the headers being set
* @param values the values of the headers being set
* @return {@code this}
*/
BinaryHeaders set(AsciiString name, Iterable<AsciiString> values);
/**
* Sets a header with the specified name and values.
*
* If there is an existing header with the same name, it is removed.
* This getMethod can be represented approximately as the following code:
* <pre>
* headers.remove(name);
* for (Object v: values) {
* if (v == null) {
* break;
* }
* headers.add(name, v);
* }
* </pre>
*
* @param name the name of the headers being set
* @param values the values of the headers being set
* @return {@code this}
*/
BinaryHeaders set(AsciiString name, AsciiString... values);
/**
* Cleans the current header entries and copies all header entries of the specified {@code headers}.
*
* @return {@code this}
*/
BinaryHeaders set(BinaryHeaders headers);
/**
* Retains all current headers but calls {@link #set(AsciiString, Object)} for each entry in {@code headers}
* @param headers The headers used to {@link #set(AsciiString, Object)} values in this instance
* @return {@code this}
*/
BinaryHeaders setAll(BinaryHeaders headers);
/**
* Removes the header with the specified name.
*
* @param name The name of the header to remove
* @return {@code true} if and only if at least one entry has been removed
*/
boolean remove(AsciiString name);
/**
* Removes all headers.
*
* @return {@code this}
*/
BinaryHeaders clear();
/**
* Returns {@code true} if a header with the name and value exists.
*
* @param name the header name
* @param value the header value
* @return {@code true} if it contains it {@code false} otherwise
*/
boolean contains(AsciiString name, AsciiString value);
@Override
Iterator<Entry<AsciiString, AsciiString>> iterator();
BinaryHeaders forEachEntry(BinaryHeaderVisitor visitor);
/**
* Common utilities for {@link BinaryHeaders}.
*/
public static final class Utils {
private static final int HASH_CODE_PRIME = 31;
/**
* Generates a hash code for a {@link BinaryHeaders} object.
*/
public static int hashCode(BinaryHeaders headers) {
int result = 1;
for (AsciiString name : headers.names()) {
result = HASH_CODE_PRIME * result + name.hashCode();
Set<AsciiString> values = new TreeSet<AsciiString>(headers.getAll(name));
for (AsciiString value : values) {
result = HASH_CODE_PRIME * result + value.hashCode();
}
}
return result;
}
/**
* Compares the contents of two {@link BinaryHeaders} objects.
*/
public static boolean equals(BinaryHeaders h1, BinaryHeaders h2) {
// First, check that the set of names match.
Set<AsciiString> names = h1.names();
if (!names.equals(h2.names())) {
return false;
}
// Compare the values for each name.
for (AsciiString name : names) {
List<AsciiString> values = h1.getAll(name);
List<AsciiString> otherValues = h2.getAll(name);
if (values.size() != otherValues.size()) {
return false;
}
// Convert the values to a set and remove values from the other object to see if
// they match.
Set<AsciiString> valueSet = new HashSet<AsciiString>(values);
valueSet.removeAll(otherValues);
if (!valueSet.isEmpty()) {
return false;
}
}
return true;
}
/**
* Generates a {@link String} representation of the {@link BinaryHeaders}, assuming all of
* the names and values are {@code UTF-8} strings.
*/
public static String toStringUtf8(BinaryHeaders headers) {
StringBuilder builder =
new StringBuilder(headers.getClass().getSimpleName()).append('[');
boolean first = true;
Set<AsciiString> names = new TreeSet<AsciiString>(headers.names());
for (AsciiString name : names) {
Set<AsciiString> valueSet = new TreeSet<AsciiString>(headers.getAll(name));
for (AsciiString value : valueSet) {
if (!first) {
builder.append(", ");
}
first = false;
builder.append(name).append(": ").append(value);
}
}
return builder.append("]").toString();
}
}
}

View File

@ -0,0 +1,347 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec;
import io.netty.util.internal.PlatformDependent;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
public class DefaultBinaryHeaders implements BinaryHeaders {
private final HeaderMap.ValueUnmarshaller<AsciiString> VALUE_UNMARSHALLER =
new HeaderMap.ValueUnmarshaller<AsciiString>() {
@Override
public AsciiString unmarshal(CharSequence value) {
return (AsciiString) value;
}
};
private final BinaryHeaderVisitor addAll = new BinaryHeaderVisitor() {
@Override
public boolean visit(AsciiString name, AsciiString value) throws Exception {
add(name, value);
return true;
}
};
private final BinaryHeaderVisitor setAll = new BinaryHeaderVisitor() {
@Override
public boolean visit(AsciiString name, AsciiString value) throws Exception {
set(name, value);
return true;
}
};
private final HeaderMap headers;
public DefaultBinaryHeaders() {
// Binary headers are case-sensitive. It's up the HTTP/1 translation layer to convert headers to
// lowercase.
headers = new HeaderMap(false);
}
@Override
public BinaryHeaders add(AsciiString name, AsciiString value) {
headers.add(name, value);
return this;
}
@Override
public BinaryHeaders add(AsciiString name, Iterable<AsciiString> values) {
headers.add(name, values);
return this;
}
@Override
public BinaryHeaders add(AsciiString name, AsciiString... values) {
headers.add(name, values);
return this;
}
@Override
public BinaryHeaders add(BinaryHeaders headers) {
checkNotNull(headers, "headers");
add0(headers);
return this;
}
private void add0(BinaryHeaders headers) {
if (headers.isEmpty()) {
return;
}
if (headers instanceof DefaultBinaryHeaders) {
this.headers.add(((DefaultBinaryHeaders) headers).headers);
} else {
forEachEntry(addAll);
}
}
@Override
public boolean remove(AsciiString name) {
return headers.remove(name);
}
@Override
public BinaryHeaders set(AsciiString name, AsciiString value) {
headers.set(name, value);
return this;
}
@Override
public BinaryHeaders set(AsciiString name, Iterable<AsciiString> values) {
headers.set(name, values);
return this;
}
@Override
public BinaryHeaders set(AsciiString name, AsciiString... values) {
headers.set(name, values);
return this;
}
@Override
public BinaryHeaders set(BinaryHeaders headers) {
checkNotNull(headers, "headers");
clear();
add0(headers);
return this;
}
@Override
public BinaryHeaders setAll(BinaryHeaders headers) {
checkNotNull(headers, "headers");
if (headers instanceof DefaultBinaryHeaders) {
this.headers.setAll(((DefaultBinaryHeaders) headers).headers);
} else {
forEachEntry(setAll);
}
return this;
}
@Override
public BinaryHeaders clear() {
headers.clear();
return this;
}
@Override
public AsciiString get(AsciiString name) {
return (AsciiString) headers.get(name);
}
@Override
public AsciiString get(AsciiString name, AsciiString defaultValue) {
AsciiString v = get(name);
if (v == null) {
return defaultValue;
}
return v;
}
@Override
public AsciiString getAndRemove(AsciiString name) {
return (AsciiString) headers.getAndRemove(name);
}
@Override
public AsciiString getAndRemove(AsciiString name, AsciiString defaultValue) {
AsciiString v = getAndRemove(name);
if (v == null) {
return defaultValue;
}
return v;
}
@Override
public List<AsciiString> getAll(AsciiString name) {
return headers.getAll(name, VALUE_UNMARSHALLER);
}
@Override
public List<AsciiString> getAllAndRemove(AsciiString name) {
return headers.getAllAndRemove(name, VALUE_UNMARSHALLER);
}
@Override
public List<Map.Entry<AsciiString, AsciiString>> entries() {
int size = size();
@SuppressWarnings("unchecked")
final Map.Entry<AsciiString, AsciiString>[] all = new Map.Entry[size];
headers.forEachEntry(new HeaderMap.EntryVisitor() {
int cnt;
@Override
public boolean visit(Entry<CharSequence, CharSequence> entry) {
all[cnt++] = new AsciiStringHeaderEntry(entry);
return true;
}
});
return Arrays.asList(all);
}
@Override
public Iterator<Entry<AsciiString, AsciiString>> iterator() {
return new AsciiStringHeaderIterator();
}
@Override
public boolean contains(AsciiString name) {
return get(name) != null;
}
@Override
public int size() {
return headers.size();
}
@Override
public boolean isEmpty() {
return headers.isEmpty();
}
@Override
public boolean contains(AsciiString name, AsciiString value) {
return contains(name, value, false);
}
public boolean contains(AsciiString name, AsciiString value, boolean ignoreCase) {
return headers.contains(name, value);
}
@Override
public Set<AsciiString> names() {
return names(headers.isIgnoreCase());
}
/**
* Get the set of names for all text headers
* @param caseInsensitive {@code true} if names should be added in a case insensitive
* @return The set of names for all text headers
*/
public Set<AsciiString> names(boolean caseInsensitive) {
final Set<AsciiString> names = caseInsensitive ? new TreeSet<AsciiString>(AsciiString.CASE_INSENSITIVE_ORDER)
: new LinkedHashSet<AsciiString>(size());
headers.forEachName(new HeaderMap.NameVisitor() {
@Override
public boolean visit(CharSequence name) {
names.add((AsciiString) name);
return true;
}
});
return names;
}
@Override
public BinaryHeaders forEachEntry(final BinaryHeaders.BinaryHeaderVisitor visitor) {
headers.forEachEntry(new HeaderMap.EntryVisitor() {
@Override
public boolean visit(Entry<CharSequence, CharSequence> entry) {
try {
return visitor.visit((AsciiString) entry.getKey(),
(AsciiString) entry.getValue());
} catch (Exception e) {
PlatformDependent.throwException(e);
return false;
}
}
});
return this;
}
@Override
public int hashCode() {
return Utils.hashCode(this);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof BinaryHeaders)) {
return false;
}
return Utils.equals(this, (BinaryHeaders) o);
}
@Override
public String toString() {
return Utils.toStringUtf8(this);
}
static <T> void checkNotNull(T value, String name) {
if (value == null) {
throw new NullPointerException(name);
}
}
private static final class AsciiStringHeaderEntry implements Map.Entry<AsciiString, AsciiString> {
private final Entry<CharSequence, CharSequence> entry;
AsciiStringHeaderEntry(Entry<CharSequence, CharSequence> entry) {
this.entry = entry;
}
@Override
public AsciiString getKey() {
return (AsciiString) entry.getKey();
}
@Override
public AsciiString getValue() {
return (AsciiString) entry.getValue();
}
@Override
public AsciiString setValue(AsciiString value) {
checkNotNull(value, "value");
return (AsciiString) entry.setValue(value);
}
@Override
public String toString() {
return entry.toString();
}
}
private final class AsciiStringHeaderIterator implements Iterator<Map.Entry<AsciiString, AsciiString>> {
private Iterator<Entry<CharSequence, CharSequence>> iter = headers.iterator();
@Override
public boolean hasNext() {
return iter.hasNext();
}
@Override
public Entry<AsciiString, AsciiString> next() {
Entry<CharSequence, CharSequence> entry = iter.next();
return new AsciiStringHeaderEntry(entry);
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
}

View File

@ -23,46 +23,64 @@ import java.text.DateFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeSet;
import java.util.HashSet;
import java.util.TimeZone;
import java.util.TreeSet;
public class DefaultTextHeaders implements TextHeaders {
private static final int HASH_CODE_PRIME = 31;
private static final int BUCKET_SIZE = 17;
private final HeaderMap.NameConverter NAME_CONVERTER = new HeaderMap.NameConverter() {
@Override
public CharSequence convertName(CharSequence name) {
return DefaultTextHeaders.this.convertName(name);
}
};
private final HeaderMap.ValueMarshaller VALUE_MARSHALLER = new HeaderMap.ValueMarshaller() {
@Override
public CharSequence marshal(Object value) {
return convertValue(value);
}
};
private final HeaderMap.ValueUnmarshaller<String> VALUE_UNMARSHALLER =
new HeaderMap.ValueUnmarshaller<String>() {
@Override
public String unmarshal(CharSequence value) {
return value.toString();
}
};
private static int index(int hash) {
return Math.abs(hash % BUCKET_SIZE);
}
private final TextHeaderProcessor addAll = new TextHeaderProcessor() {
@Override
public boolean process(CharSequence name, CharSequence value) throws Exception {
headers.add(name, value);
return true;
}
};
private final HeaderEntry[] entries = new HeaderEntry[BUCKET_SIZE];
private final HeaderEntry head = new HeaderEntry(this);
private final boolean ignoreCase;
int size;
private final TextHeaderProcessor setAll = new TextHeaderProcessor() {
@Override
public boolean process(CharSequence name, CharSequence value) throws Exception {
headers.set(name, value);
return true;
}
};
private final HeaderMap headers;
public DefaultTextHeaders() {
this(true);
}
public DefaultTextHeaders(boolean ignoreCase) {
head.before = head.after = head;
this.ignoreCase = ignoreCase;
}
protected int hashCode(CharSequence name) {
return AsciiString.caseInsensitiveHashCode(name);
headers = new HeaderMap(ignoreCase, NAME_CONVERTER);
}
protected CharSequence convertName(CharSequence name) {
@ -82,86 +100,28 @@ public class DefaultTextHeaders implements TextHeaders {
return value.toString();
}
protected boolean nameEquals(CharSequence a, CharSequence b) {
return equals(a, b, ignoreCase);
}
protected boolean valueEquals(CharSequence a, CharSequence b, boolean ignoreCase) {
return equals(a, b, ignoreCase);
}
private static boolean equals(CharSequence a, CharSequence b, boolean ignoreCase) {
if (ignoreCase) {
return AsciiString.equalsIgnoreCase(a, b);
} else {
return AsciiString.equals(a, b);
}
}
@Override
public TextHeaders add(CharSequence name, Object value) {
name = convertName(name);
CharSequence convertedVal = convertValue(value);
int h = hashCode(name);
int i = index(h);
add0(h, i, name, convertedVal);
headers.add(name, convertedVal);
return this;
}
@Override
public TextHeaders add(CharSequence name, Iterable<?> values) {
name = convertName(name);
if (values == null) {
throw new NullPointerException("values");
}
int h = hashCode(name);
int i = index(h);
for (Object v: values) {
if (v == null) {
break;
}
CharSequence convertedVal = convertValue(v);
add0(h, i, name, convertedVal);
}
headers.addConvertedValues(name, VALUE_MARSHALLER, values);
return this;
}
@Override
public TextHeaders add(CharSequence name, Object... values) {
name = convertName(name);
if (values == null) {
throw new NullPointerException("values");
}
int h = hashCode(name);
int i = index(h);
for (Object v: values) {
if (v == null) {
break;
}
CharSequence convertedVal = convertValue(v);
add0(h, i, name, convertedVal);
}
headers.addConvertedValues(name, VALUE_MARSHALLER, values);
return this;
}
private void add0(int h, int i, CharSequence name, CharSequence value) {
// Update the hash table.
HeaderEntry e = entries[i];
HeaderEntry newEntry;
entries[i] = newEntry = new HeaderEntry(this, h, name, value);
newEntry.next = e;
// Update the linked list.
newEntry.addBefore(head);
}
@Override
public TextHeaders add(TextHeaders headers) {
if (headers == null) {
throw new NullPointerException("headers");
}
checkNotNull(headers, "headers");
add0(headers);
return this;
@ -173,124 +133,33 @@ public class DefaultTextHeaders implements TextHeaders {
}
if (headers instanceof DefaultTextHeaders) {
DefaultTextHeaders m = (DefaultTextHeaders) headers;
HeaderEntry e = m.head.after;
while (e != m.head) {
CharSequence name = e.name;
name = convertName(name);
add(name, convertValue(e.value));
e = e.after;
}
this.headers.add(((DefaultTextHeaders) headers).headers);
} else {
for (Entry<CharSequence, CharSequence> e: headers.unconvertedEntries()) {
add(e.getKey(), e.getValue());
}
headers.forEachEntry(addAll);
}
}
@Override
public boolean remove(CharSequence name) {
if (name == null) {
throw new NullPointerException("name");
}
int h = hashCode(name);
int i = index(h);
return remove0(h, i, name);
}
private boolean remove0(int h, int i, CharSequence name) {
HeaderEntry e = entries[i];
if (e == null) {
return false;
}
boolean removed = false;
for (;;) {
if (e.hash == h && nameEquals(e.name, name)) {
e.remove();
HeaderEntry next = e.next;
if (next != null) {
entries[i] = next;
e = next;
} else {
entries[i] = null;
return true;
}
removed = true;
} else {
break;
}
}
for (;;) {
HeaderEntry next = e.next;
if (next == null) {
break;
}
if (next.hash == h && nameEquals(next.name, name)) {
e.next = next.next;
next.remove();
removed = true;
} else {
e = next;
}
}
return removed;
return headers.remove(name);
}
@Override
public TextHeaders set(CharSequence name, Object value) {
name = convertName(name);
CharSequence convertedVal = convertValue(value);
int h = hashCode(name);
int i = index(h);
remove0(h, i, name);
add0(h, i, name, convertedVal);
headers.set(name, convertedVal);
return this;
}
@Override
public TextHeaders set(CharSequence name, Iterable<?> values) {
name = convertName(name);
if (values == null) {
throw new NullPointerException("values");
}
int h = hashCode(name);
int i = index(h);
remove0(h, i, name);
for (Object v: values) {
if (v == null) {
break;
}
CharSequence convertedVal = convertValue(v);
add0(h, i, name, convertedVal);
}
headers.set(name, VALUE_MARSHALLER, values);
return this;
}
@Override
public TextHeaders set(CharSequence name, Object... values) {
name = convertName(name);
if (values == null) {
throw new NullPointerException("values");
}
int h = hashCode(name);
int i = index(h);
remove0(h, i, name);
for (Object v: values) {
if (v == null) {
break;
}
CharSequence convertedVal = convertValue(v);
add0(h, i, name, convertedVal);
}
headers.set(name, VALUE_MARSHALLER, values);
return this;
}
@ -312,18 +181,9 @@ public class DefaultTextHeaders implements TextHeaders {
}
if (headers instanceof DefaultTextHeaders) {
DefaultTextHeaders m = (DefaultTextHeaders) headers;
HeaderEntry e = m.head.after;
while (e != m.head) {
CharSequence name = e.name;
name = convertName(name);
set(name, convertValue(e.value));
e = e.after;
}
this.headers.setAll(((DefaultTextHeaders) headers).headers);
} else {
for (Entry<CharSequence, CharSequence> e: headers.unconvertedEntries()) {
set(e.getKey(), e.getValue());
}
headers.forEachEntry(setAll);
}
return this;
@ -331,34 +191,13 @@ public class DefaultTextHeaders implements TextHeaders {
@Override
public TextHeaders clear() {
Arrays.fill(entries, null);
head.before = head.after = head;
size = 0;
headers.clear();
return this;
}
@Override
public CharSequence getUnconverted(CharSequence name) {
if (name == null) {
throw new NullPointerException("name");
}
int h = hashCode(name);
int i = index(h);
HeaderEntry e = entries[i];
CharSequence value = null;
// loop until the first header was found
while (e != null) {
if (e.hash == h && nameEquals(e.name, name)) {
value = e.value;
}
e = e.next;
}
if (value != null) {
return value;
}
return null;
return headers.get(name);
}
@Override
@ -477,52 +316,7 @@ public class DefaultTextHeaders implements TextHeaders {
@Override
public CharSequence getUnconvertedAndRemove(CharSequence name) {
if (name == null) {
throw new NullPointerException("name");
}
int h = hashCode(name);
int i = index(h);
HeaderEntry e = entries[i];
if (e == null) {
return null;
}
CharSequence value = null;
for (;;) {
if (e.hash == h && nameEquals(e.name, name)) {
value = e.value;
e.remove();
HeaderEntry next = e.next;
if (next != null) {
entries[i] = next;
e = next;
} else {
entries[i] = null;
return value;
}
} else {
break;
}
}
for (;;) {
HeaderEntry next = e.next;
if (next == null) {
break;
}
if (next.hash == h && nameEquals(next.name, name)) {
value = next.value;
e.next = next.next;
next.remove();
} else {
e = next;
}
}
if (value != null) {
return value;
}
return null;
return headers.getAndRemove(name);
}
@Override
@ -641,176 +435,45 @@ public class DefaultTextHeaders implements TextHeaders {
@Override
public List<CharSequence> getAllUnconverted(CharSequence name) {
if (name == null) {
throw new NullPointerException("name");
}
List<CharSequence> values = new ArrayList<CharSequence>(4);
int h = hashCode(name);
int i = index(h);
HeaderEntry e = entries[i];
while (e != null) {
if (e.hash == h && nameEquals(e.name, name)) {
values.add(e.getValue());
}
e = e.next;
}
Collections.reverse(values);
return values;
return headers.getAll(name);
}
@Override
public List<String> getAll(CharSequence name) {
if (name == null) {
throw new NullPointerException("name");
}
List<String> values = new ArrayList<String>(4);
int h = hashCode(name);
int i = index(h);
HeaderEntry e = entries[i];
while (e != null) {
if (e.hash == h && nameEquals(e.name, name)) {
values.add(e.getValue().toString());
}
e = e.next;
}
Collections.reverse(values);
return values;
return headers.getAll(name, VALUE_UNMARSHALLER);
}
@Override
public List<String> getAllAndRemove(CharSequence name) {
if (name == null) {
throw new NullPointerException("name");
}
int h = hashCode(name);
int i = index(h);
HeaderEntry e = entries[i];
if (e == null) {
return null;
}
List<String> values = new ArrayList<String>(4);
for (;;) {
if (e.hash == h && nameEquals(e.name, name)) {
values.add(e.getValue().toString());
e.remove();
HeaderEntry next = e.next;
if (next != null) {
entries[i] = next;
e = next;
} else {
entries[i] = null;
Collections.reverse(values);
return values;
}
} else {
break;
}
}
for (;;) {
HeaderEntry next = e.next;
if (next == null) {
break;
}
if (next.hash == h && nameEquals(next.name, name)) {
values.add(next.getValue().toString());
e.next = next.next;
next.remove();
} else {
e = next;
}
}
Collections.reverse(values);
return values;
return headers.getAll(name, VALUE_UNMARSHALLER);
}
@Override
public List<CharSequence> getAllUnconvertedAndRemove(CharSequence name) {
if (name == null) {
throw new NullPointerException("name");
}
int h = hashCode(name);
int i = index(h);
HeaderEntry e = entries[i];
if (e == null) {
return null;
}
List<CharSequence> values = new ArrayList<CharSequence>(4);
for (;;) {
if (e.hash == h && nameEquals(e.name, name)) {
values.add(e.getValue());
e.remove();
HeaderEntry next = e.next;
if (next != null) {
entries[i] = next;
e = next;
} else {
entries[i] = null;
Collections.reverse(values);
return values;
}
} else {
break;
}
}
for (;;) {
HeaderEntry next = e.next;
if (next == null) {
break;
}
if (next.hash == h && nameEquals(next.name, name)) {
values.add(next.getValue());
e.next = next.next;
next.remove();
} else {
e = next;
}
}
Collections.reverse(values);
return values;
return headers.getAllAndRemove(name);
}
@Override
public List<Map.Entry<String, String>> entries() {
int cnt = 0;
int size = size();
@SuppressWarnings("unchecked")
Map.Entry<String, String>[] all = new Map.Entry[size];
final Map.Entry<String, String>[] all = new Map.Entry[size];
HeaderEntry e = head.after;
while (e != head) {
all[cnt ++] = new StringHeaderEntry(e);
e = e.after;
}
headers.forEachEntry(new HeaderMap.EntryVisitor() {
int cnt;
@Override
public boolean visit(Entry<CharSequence, CharSequence> entry) {
all[cnt++] = new StringHeaderEntry(entry);
return true;
}
});
assert size == cnt;
return Arrays.asList(all);
}
@Override
public List<Map.Entry<CharSequence, CharSequence>> unconvertedEntries() {
int cnt = 0;
int size = size();
@SuppressWarnings("unchecked")
Map.Entry<CharSequence, CharSequence>[] all = new Map.Entry[size];
HeaderEntry e = head.after;
while (e != head) {
all[cnt ++] = e;
e = e.after;
}
assert size == cnt;
return Arrays.asList(all);
return headers.entries();
}
@Override
@ -820,7 +483,7 @@ public class DefaultTextHeaders implements TextHeaders {
@Override
public Iterator<Entry<CharSequence, CharSequence>> unconvertedIterator() {
return new HeaderIterator();
return headers.iterator();
}
@Override
@ -830,12 +493,12 @@ public class DefaultTextHeaders implements TextHeaders {
@Override
public int size() {
return size;
return headers.size();
}
@Override
public boolean isEmpty() {
return head == head.after;
return headers.isEmpty();
}
@Override
@ -845,34 +508,13 @@ public class DefaultTextHeaders implements TextHeaders {
@Override
public boolean contains(CharSequence name, Object value, boolean ignoreCase) {
if (name == null) {
throw new NullPointerException("name");
}
int h = hashCode(name);
int i = index(h);
CharSequence convertedVal = convertValue(value);
HeaderEntry e = entries[i];
while (e != null) {
if (e.hash == h && nameEquals(e.name, name)) {
if (valueEquals(e.value, convertedVal, ignoreCase)) {
return true;
}
}
e = e.next;
}
return false;
return headers.contains(name, convertedVal, ignoreCase);
}
@Override
public Set<CharSequence> unconvertedNames() {
Set<CharSequence> names = new LinkedHashSet<CharSequence>(size());
HeaderEntry e = head.after;
while (e != head) {
names.add(e.getKey());
e = e.after;
}
return names;
return headers.names();
}
@Override
@ -886,43 +528,38 @@ public class DefaultTextHeaders implements TextHeaders {
* @return The set of names for all text headers
*/
public Set<String> names(boolean caseInsensitive) {
Set<String> names = caseInsensitive ? new TreeSet<String>(String.CASE_INSENSITIVE_ORDER)
: new LinkedHashSet<String>(size());
HeaderEntry e = head.after;
while (e != head) {
names.add(e.getKey().toString());
e = e.after;
}
final Set<String> names =
caseInsensitive ? new TreeSet<String>(String.CASE_INSENSITIVE_ORDER)
: new LinkedHashSet<String>(size());
headers.forEachName(new HeaderMap.NameVisitor() {
@Override
public boolean visit(CharSequence name) {
names.add(name.toString());
return true;
}
});
return names;
}
@Override
public TextHeaders forEachEntry(TextHeaderProcessor processor) {
HeaderEntry e = head.after;
try {
while (e != head) {
if (!processor.process(e.getKey(), e.getValue())) {
break;
public TextHeaders forEachEntry(final TextHeaderProcessor processor) {
headers.forEachEntry(new HeaderMap.EntryVisitor() {
@Override
public boolean visit(Entry<CharSequence, CharSequence> entry) {
try {
return processor.process(entry.getKey(), entry.getValue());
} catch (Exception ex) {
PlatformDependent.throwException(ex);
return false;
}
e = e.after;
}
} catch (Exception ex) {
PlatformDependent.throwException(ex);
}
});
return this;
}
@Override
public int hashCode() {
int result = 1;
for (String name : names(true)) {
result = HASH_CODE_PRIME * result + name.hashCode();
Set<String> values = new TreeSet<String>(getAll(name));
for (String value : values) {
result = HASH_CODE_PRIME * result + value.hashCode();
}
}
return result;
return headers.hashCode();
}
@Override
@ -959,66 +596,9 @@ public class DefaultTextHeaders implements TextHeaders {
return true;
}
private static final class HeaderEntry implements Map.Entry<CharSequence, CharSequence> {
private final DefaultTextHeaders parent;
final int hash;
final CharSequence name;
CharSequence value;
HeaderEntry next;
HeaderEntry before, after;
HeaderEntry(DefaultTextHeaders parent, int hash, CharSequence name, CharSequence value) {
this.parent = parent;
this.hash = hash;
this.name = name;
this.value = value;
}
HeaderEntry(DefaultTextHeaders parent) {
this.parent = parent;
hash = -1;
name = null;
value = null;
}
void remove() {
before.after = after;
after.before = before;
parent.size --;
}
void addBefore(HeaderEntry e) {
after = e;
before = e.before;
before.after = this;
after.before = this;
parent.size ++;
}
@Override
public CharSequence getKey() {
return name;
}
@Override
public CharSequence getValue() {
return value;
}
@Override
public CharSequence setValue(CharSequence value) {
if (value == null) {
throw new NullPointerException("value");
}
value = parent.convertValue(value);
CharSequence oldValue = this.value;
this.value = value;
return oldValue;
}
@Override
public String toString() {
return name.toString() + '=' + value.toString();
private static <T> void checkNotNull(T value, String name) {
if (value == null) {
throw new NullPointerException(name);
}
}
@ -1058,50 +638,20 @@ public class DefaultTextHeaders implements TextHeaders {
}
}
private final class HeaderIterator implements Iterator<Map.Entry<CharSequence, CharSequence>> {
private final class StringHeaderIterator implements Iterator<Entry<String, String>> {
private HeaderEntry current = head;
private Iterator<Entry<CharSequence, CharSequence>> iter = headers.iterator();
@Override
public boolean hasNext() {
return current.after != head;
}
@Override
public Entry<CharSequence, CharSequence> next() {
current = current.after;
if (current == head) {
throw new NoSuchElementException();
}
return current;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
private final class StringHeaderIterator implements Iterator<Map.Entry<String, String>> {
private HeaderEntry current = head;
@Override
public boolean hasNext() {
return current.after != head;
return iter.hasNext();
}
@Override
public Entry<String, String> next() {
current = current.after;
Entry<CharSequence, CharSequence> next = iter.next();
if (current == head) {
throw new NoSuchElementException();
}
return new StringHeaderEntry(current);
return new StringHeaderEntry(next);
}
@Override

View File

@ -0,0 +1,169 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.handler.codec;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
public class EmptyBinaryHeaders implements BinaryHeaders {
@Override
public AsciiString get(AsciiString name) {
return null;
}
@Override
public AsciiString get(AsciiString name, AsciiString defaultValue) {
return defaultValue;
}
@Override
public AsciiString getAndRemove(AsciiString name) {
return null;
}
@Override
public AsciiString getAndRemove(AsciiString name, AsciiString defaultValue) {
return defaultValue;
}
@Override
public List<AsciiString> getAll(AsciiString name) {
return Collections.emptyList();
}
@Override
public List<AsciiString> getAllAndRemove(AsciiString name) {
return Collections.emptyList();
}
@Override
public List<Entry<AsciiString, AsciiString>> entries() {
return Collections.emptyList();
}
@Override
public boolean contains(AsciiString name) {
return false;
}
@Override
public int size() {
return 0;
}
@Override
public boolean isEmpty() {
return true;
}
@Override
public Set<AsciiString> names() {
return Collections.emptySet();
}
@Override
public BinaryHeaders add(AsciiString name, AsciiString value) {
throw new UnsupportedOperationException("read only");
}
@Override
public BinaryHeaders add(AsciiString name, Iterable<AsciiString> values) {
throw new UnsupportedOperationException("read only");
}
@Override
public BinaryHeaders add(AsciiString name, AsciiString... values) {
throw new UnsupportedOperationException("read only");
}
@Override
public BinaryHeaders add(BinaryHeaders headers) {
throw new UnsupportedOperationException("read only");
}
@Override
public BinaryHeaders set(AsciiString name, AsciiString value) {
throw new UnsupportedOperationException("read only");
}
@Override
public BinaryHeaders set(AsciiString name, Iterable<AsciiString> values) {
throw new UnsupportedOperationException("read only");
}
@Override
public BinaryHeaders set(AsciiString name, AsciiString... values) {
throw new UnsupportedOperationException("read only");
}
@Override
public BinaryHeaders set(BinaryHeaders headers) {
throw new UnsupportedOperationException("read only");
}
@Override
public BinaryHeaders setAll(BinaryHeaders headers) {
throw new UnsupportedOperationException("read only");
}
@Override
public boolean remove(AsciiString name) {
return false;
}
@Override
public BinaryHeaders clear() {
return this;
}
@Override
public boolean contains(AsciiString name, AsciiString value) {
return false;
}
@Override
public Iterator<Entry<AsciiString, AsciiString>> iterator() {
return entries().iterator();
}
@Override
public BinaryHeaders forEachEntry(BinaryHeaderVisitor processor) {
return this;
}
@Override
public int hashCode() {
return BinaryHeaders.Utils.hashCode(this);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof BinaryHeaders)) {
return false;
}
return ((BinaryHeaders) obj).isEmpty();
}
@Override
public String toString() {
return BinaryHeaders.Utils.toStringUtf8(this);
}
}

View File

@ -0,0 +1,844 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeSet;
/**
* Basic map of header names to values. This is meant to be a central storage mechanism by all
* headers implementations. All keys and values are stored as {@link CharSequence}.
*/
public class HeaderMap implements Iterable<Entry<CharSequence, CharSequence>> {
private static final int BUCKET_SIZE = 17;
private static final int HASH_CODE_PRIME = 31;
public static final NameConverter IDENTITY_NAME_CONVERTER = new NameConverter() {
@Override
public CharSequence convertName(CharSequence name) {
return name;
}
};
public interface EntryVisitor {
boolean visit(Entry<CharSequence, CharSequence> entry);
}
public interface NameVisitor {
boolean visit(CharSequence name);
}
public interface NameConverter {
CharSequence convertName(CharSequence name);
}
public interface ValueMarshaller {
CharSequence marshal(Object value);
}
public interface ValueUnmarshaller<T> {
T unmarshal(CharSequence value);
}
private final HeaderEntry[] entries = new HeaderEntry[BUCKET_SIZE];
private final HeaderEntry head = new HeaderEntry();
private final NameConverter nameConverter;
private final boolean ignoreCase;
int size;
public HeaderMap() {
this(true);
}
public HeaderMap(boolean ignoreCase) {
this(ignoreCase, IDENTITY_NAME_CONVERTER);
}
public HeaderMap(boolean ignoreCase, NameConverter nameConverter) {
this.nameConverter = checkNotNull(nameConverter, "nameConverter");
head.before = head.after = head;
this.ignoreCase = ignoreCase;
}
public boolean isIgnoreCase() {
return ignoreCase;
}
public HeaderMap add(CharSequence name, CharSequence value) {
name = convertName(name);
checkNotNull(value, "value");
int h = hashCode(name);
int i = index(h);
add0(h, i, name, value);
return this;
}
public HeaderMap add(CharSequence name, Iterable<? extends CharSequence> values) {
name = convertName(name);
checkNotNull(values, "values");
int h = hashCode(name);
int i = index(h);
for (CharSequence v: values) {
if (v == null) {
break;
}
add0(h, i, name, v);
}
return this;
}
public HeaderMap add(CharSequence name, CharSequence... values) {
name = convertName(name);
checkNotNull(values, "values");
int h = hashCode(name);
int i = index(h);
for (CharSequence v: values) {
if (v == null) {
break;
}
add0(h, i, name, v);
}
return this;
}
public HeaderMap addConvertedValues(CharSequence name, ValueMarshaller converter, Iterable<?> values) {
name = convertName(name);
checkNotNull(values, "values");
checkNotNull(converter, "converter");
int h = hashCode(name);
int i = index(h);
for (Object v : values) {
if (v == null) {
break;
}
CharSequence convertedVal = converter.marshal(v);
add0(h, i, name, convertedVal);
}
return this;
}
public HeaderMap addConvertedValues(CharSequence name, ValueMarshaller converter, Object... values) {
name = convertName(name);
checkNotNull(values, "values");
checkNotNull(converter, "converter");
int h = hashCode(name);
int i = index(h);
for (Object v : values) {
if (v == null) {
break;
}
CharSequence convertedVal = converter.marshal(v);
add0(h, i, name, convertedVal);
}
return this;
}
private void add0(int h, int i, CharSequence name, CharSequence value) {
// Update the hash table.
HeaderEntry e = entries[i];
HeaderEntry newEntry;
entries[i] = newEntry = new HeaderEntry(h, name, value);
newEntry.next = e;
// Update the linked list.
newEntry.addBefore(head);
}
public HeaderMap add(HeaderMap headers) {
checkNotNull(headers, "headers");
add0(headers);
return this;
}
private void add0(HeaderMap headers) {
if (headers.isEmpty()) {
return;
}
HeaderMap m = (HeaderMap) headers;
HeaderEntry e = m.head.after;
while (e != m.head) {
add(e.name, e.value);
e = e.after;
}
}
public boolean remove(CharSequence name) {
checkNotNull(name, "name");
int h = hashCode(name);
int i = index(h);
return remove0(h, i, name);
}
private boolean remove0(int h, int i, CharSequence name) {
HeaderEntry e = entries[i];
if (e == null) {
return false;
}
boolean removed = false;
for (;;) {
if (e.hash == h && nameEquals(e.name, name)) {
e.remove();
HeaderEntry next = e.next;
if (next != null) {
entries[i] = next;
e = next;
} else {
entries[i] = null;
return true;
}
removed = true;
} else {
break;
}
}
for (;;) {
HeaderEntry next = e.next;
if (next == null) {
break;
}
if (next.hash == h && nameEquals(next.name, name)) {
e.next = next.next;
next.remove();
removed = true;
} else {
e = next;
}
}
return removed;
}
public HeaderMap set(CharSequence name, CharSequence value) {
name = convertName(name);
checkNotNull(value, "value");
int h = hashCode(name);
int i = index(h);
remove0(h, i, name);
add0(h, i, name, value);
return this;
}
public HeaderMap set(CharSequence name, Iterable<? extends CharSequence> values) {
name = convertName(name);
checkNotNull(values, "values");
int h = hashCode(name);
int i = index(h);
remove0(h, i, name);
for (CharSequence v: values) {
if (v == null) {
break;
}
add0(h, i, name, v);
}
return this;
}
public HeaderMap set(CharSequence name, CharSequence... values) {
name = convertName(name);
checkNotNull(values, "values");
int h = hashCode(name);
int i = index(h);
remove0(h, i, name);
for (CharSequence v: values) {
if (v == null) {
break;
}
add0(h, i, name, v);
}
return this;
}
public HeaderMap set(CharSequence name, ValueMarshaller converter, Iterable<?> values) {
name = convertName(name);
checkNotNull(converter, "converter");
checkNotNull(values, "values");
int h = hashCode(name);
int i = index(h);
remove0(h, i, name);
for (Object v: values) {
if (v == null) {
break;
}
CharSequence convertedVal = converter.marshal(v);
add0(h, i, name, convertedVal);
}
return this;
}
public HeaderMap set(CharSequence name, ValueMarshaller converter, Object... values) {
name = convertName(name);
checkNotNull(converter, "converter");
checkNotNull(values, "values");
int h = hashCode(name);
int i = index(h);
remove0(h, i, name);
for (Object v: values) {
if (v == null) {
break;
}
CharSequence convertedVal = converter.marshal(v);
add0(h, i, name, convertedVal);
}
return this;
}
public HeaderMap set(HeaderMap headers) {
checkNotNull(headers, "headers");
clear();
add0(headers);
return this;
}
public HeaderMap setAll(HeaderMap headers) {
checkNotNull(headers, "headers");
HeaderEntry e = headers.head.after;
while (e != headers.head) {
set(e.name, e.value);
e = e.after;
}
return this;
}
public HeaderMap clear() {
Arrays.fill(entries, null);
head.before = head.after = head;
size = 0;
return this;
}
public CharSequence get(CharSequence name) {
checkNotNull(name, "name");
int h = hashCode(name);
int i = index(h);
HeaderEntry e = entries[i];
CharSequence value = null;
// loop until the first header was found
while (e != null) {
if (e.hash == h && nameEquals(e.name, name)) {
value = e.value;
}
e = e.next;
}
return value;
}
public CharSequence get(CharSequence name, CharSequence defaultValue) {
CharSequence v = get(name);
if (v == null) {
return defaultValue;
}
return v;
}
public CharSequence getAndRemove(CharSequence name) {
checkNotNull(name, "name");
int h = hashCode(name);
int i = index(h);
HeaderEntry e = entries[i];
if (e == null) {
return null;
}
CharSequence value = null;
for (;;) {
if (e.hash == h && nameEquals(e.name, name)) {
value = e.value;
e.remove();
HeaderEntry next = e.next;
if (next != null) {
entries[i] = next;
e = next;
} else {
entries[i] = null;
return value;
}
} else {
break;
}
}
for (;;) {
HeaderEntry next = e.next;
if (next == null) {
break;
}
if (next.hash == h && nameEquals(next.name, name)) {
value = next.value;
e.next = next.next;
next.remove();
} else {
e = next;
}
}
return value;
}
public CharSequence getAndRemove(CharSequence name, CharSequence defaultValue) {
CharSequence v = getAndRemove(name);
if (v == null) {
return defaultValue;
}
return v;
}
public List<CharSequence> getAll(CharSequence name) {
checkNotNull(name, "name");
List<CharSequence> values = new ArrayList<CharSequence>(4);
int h = hashCode(name);
int i = index(h);
HeaderEntry e = entries[i];
while (e != null) {
if (e.hash == h && nameEquals(e.name, name)) {
values.add(e.getValue());
}
e = e.next;
}
Collections.reverse(values);
return values;
}
public List<CharSequence> getAllAndRemove(CharSequence name) {
checkNotNull(name, "name");
int h = hashCode(name);
int i = index(h);
HeaderEntry e = entries[i];
if (e == null) {
return null;
}
List<CharSequence> values = new ArrayList<CharSequence>(4);
for (;;) {
if (e.hash == h && nameEquals(e.name, name)) {
values.add(e.getValue());
e.remove();
HeaderEntry next = e.next;
if (next != null) {
entries[i] = next;
e = next;
} else {
entries[i] = null;
Collections.reverse(values);
return values;
}
} else {
break;
}
}
for (;;) {
HeaderEntry next = e.next;
if (next == null) {
break;
}
if (next.hash == h && nameEquals(next.name, name)) {
values.add(next.getValue());
e.next = next.next;
next.remove();
} else {
e = next;
}
}
Collections.reverse(values);
return values;
}
public <T> List<T> getAll(CharSequence name, ValueUnmarshaller<T> unmarshaller) {
checkNotNull(name, "name");
checkNotNull(unmarshaller, "unmarshaller");
List<T> values = new ArrayList<T>(4);
int h = hashCode(name);
int i = index(h);
HeaderEntry e = entries[i];
while (e != null) {
if (e.hash == h && nameEquals(e.name, name)) {
values.add(unmarshaller.unmarshal(e.value));
}
e = e.next;
}
Collections.reverse(values);
return values;
}
public <T> List<T> getAllAndRemove(CharSequence name, ValueUnmarshaller<T> unmarshaller) {
checkNotNull(name, "name");
checkNotNull(unmarshaller, "unmarshaller");
int h = hashCode(name);
int i = index(h);
HeaderEntry e = entries[i];
if (e == null) {
return null;
}
List<T> values = new ArrayList<T>(4);
for (;;) {
if (e.hash == h && nameEquals(e.name, name)) {
values.add(unmarshaller.unmarshal(e.value));
e.remove();
HeaderEntry next = e.next;
if (next != null) {
entries[i] = next;
e = next;
} else {
entries[i] = null;
Collections.reverse(values);
return values;
}
} else {
break;
}
}
for (;;) {
HeaderEntry next = e.next;
if (next == null) {
break;
}
if (next.hash == h && nameEquals(next.name, name)) {
values.add(unmarshaller.unmarshal(next.getValue()));
e.next = next.next;
next.remove();
} else {
e = next;
}
}
Collections.reverse(values);
return values;
}
public List<Map.Entry<CharSequence, CharSequence>> entries() {
int cnt = 0;
int size = size();
@SuppressWarnings("unchecked")
Map.Entry<CharSequence, CharSequence>[] all = new Map.Entry[size];
HeaderEntry e = head.after;
while (e != head) {
all[cnt ++] = e;
e = e.after;
}
assert size == cnt;
return Arrays.asList(all);
}
@Override
public Iterator<Entry<CharSequence, CharSequence>> iterator() {
return new HeaderIterator();
}
public boolean contains(CharSequence name) {
return get(name) != null;
}
public int size() {
return size;
}
public boolean isEmpty() {
return head == head.after;
}
public boolean contains(CharSequence name, CharSequence value) {
return contains(name, value, false);
}
public boolean contains(CharSequence name, CharSequence value, boolean ignoreCase) {
checkNotNull(name, "name");
checkNotNull(value, "value");
int h = hashCode(name);
int i = index(h);
HeaderEntry e = entries[i];
while (e != null) {
if (e.hash == h && nameEquals(e.name, name)) {
if (valueEquals(e.value, value, ignoreCase)) {
return true;
}
}
e = e.next;
}
return false;
}
public Set<CharSequence> names() {
return names(ignoreCase);
}
/**
* Get the set of names for all text headers
* @param caseInsensitive {@code true} if names should be added in a case insensitive
* @return The set of names for all text headers
*/
public Set<CharSequence> names(boolean caseInsensitive) {
final Set<CharSequence> names =
caseInsensitive ? new TreeSet<CharSequence>(
AsciiString.CHARSEQUENCE_CASE_INSENSITIVE_ORDER)
: new LinkedHashSet<CharSequence>(size());
forEachName(new NameVisitor() {
@Override
public boolean visit(CharSequence name) {
names.add(name);
return true;
}
});
return names;
}
public HeaderMap forEachEntry(EntryVisitor visitor) {
HeaderEntry e = head.after;
while (e != head) {
if (!visitor.visit(e)) {
break;
}
e = e.after;
}
return this;
}
public void forEachName(NameVisitor visitor) {
HeaderEntry e = head.after;
while (e != head) {
if (!visitor.visit(e.getKey())) {
return;
}
e = e.after;
}
}
@Override
public int hashCode() {
int result = 1;
for (CharSequence name : names()) {
result = HASH_CODE_PRIME * result + name.hashCode();
Set<CharSequence> values = new TreeSet<CharSequence>(getAll(name));
for (CharSequence value : values) {
result = HASH_CODE_PRIME * result + value.hashCode();
}
}
return result;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof HeaderMap)) {
return false;
}
// First, check that the set of names match.
HeaderMap h2 = (HeaderMap) o;
Set<CharSequence> names = names();
if (!names.equals(h2.names())) {
return false;
}
// Compare the values for each name.
for (CharSequence name : names) {
List<CharSequence> values = getAll(name);
List<CharSequence> otherValues = h2.getAll(name);
if (values.size() != otherValues.size()) {
return false;
}
// Convert the values to a set and remove values from the other object to see if
// they match.
Set<CharSequence> valueSet = new HashSet<CharSequence>(values);
valueSet.removeAll(otherValues);
if (!valueSet.isEmpty()) {
return false;
}
}
return true;
}
@Override
public String toString() {
StringBuilder builder =
new StringBuilder('[');
Set<CharSequence> names = names(true);
for (CharSequence name : names) {
Set<CharSequence> valueSet = new TreeSet<CharSequence>(getAll(name));
for (CharSequence value : valueSet) {
builder.append(name).append(": ").append(value).append(", ");
}
}
// Now remove the last ", " if there is one.
if (builder.length() >= 3) {
builder.setLength(builder.length() - 2);
}
return builder.append("]").toString();
}
private boolean nameEquals(CharSequence a, CharSequence b) {
return equals(a, b, ignoreCase);
}
private static boolean valueEquals(CharSequence a, CharSequence b, boolean ignoreCase) {
return equals(a, b, ignoreCase);
}
private static boolean equals(CharSequence a, CharSequence b, boolean ignoreCase) {
if (ignoreCase) {
return AsciiString.equalsIgnoreCase(a, b);
} else {
return AsciiString.equals(a, b);
}
}
private static int index(int hash) {
return Math.abs(hash % BUCKET_SIZE);
}
private CharSequence convertName(CharSequence name) {
return nameConverter.convertName(checkNotNull(name, "name"));
}
private static <T> T checkNotNull(T value, String name) {
if (value == null) {
throw new NullPointerException(name);
}
return value;
}
private static int hashCode(CharSequence name) {
return AsciiString.caseInsensitiveHashCode(name);
}
private final class HeaderEntry implements Map.Entry<CharSequence, CharSequence> {
final int hash;
final CharSequence name;
CharSequence value;
HeaderEntry next;
HeaderEntry before, after;
HeaderEntry(int hash, CharSequence name, CharSequence value) {
this.hash = hash;
this.name = name;
this.value = value;
}
HeaderEntry() {
hash = -1;
name = null;
value = null;
}
void remove() {
before.after = after;
after.before = before;
--size;
}
void addBefore(HeaderEntry e) {
after = e;
before = e.before;
before.after = this;
after.before = this;
++size;
}
@Override
public CharSequence getKey() {
return name;
}
@Override
public CharSequence getValue() {
return value;
}
@Override
public CharSequence setValue(CharSequence value) {
checkNotNull(value, "value");
checkNotNull(value, "value");
CharSequence oldValue = this.value;
this.value = value;
return oldValue;
}
@Override
public String toString() {
return new StringBuilder(name).append('=').append(value).toString();
}
}
protected final class HeaderIterator implements Iterator<Entry<CharSequence, CharSequence>> {
private HeaderEntry current = head;
@Override
public boolean hasNext() {
return current.after != head;
}
@Override
public Entry<CharSequence, CharSequence> next() {
current = current.after;
if (current == head) {
throw new NoSuchElementException();
}
return current;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
}

View File

@ -0,0 +1,314 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Random;
import java.util.Set;
import org.junit.Test;
/**
* Tests for {@link HeaderMap}.
*/
public class HeaderMapTest {
@Test
public void binaryHeadersWithSameValuesShouldBeEquivalent() {
byte[] key1 = randomBytes();
byte[] value1 = randomBytes();
byte[] key2 = randomBytes();
byte[] value2 = randomBytes();
HeaderMap h1 = new HeaderMap(false);
h1.set(as(key1), as(value1));
h1.set(as(key2), as(value2));
HeaderMap h2 = new HeaderMap(false);
h2.set(as(key1), as(value1));
h2.set(as(key2), as(value2));
assertTrue(h1.equals(h2));
assertTrue(h2.equals(h1));
assertTrue(h2.equals(h2));
assertTrue(h1.equals(h1));
}
@Test
public void binaryHeadersWithSameDuplicateValuesShouldBeEquivalent() {
byte[] k1 = randomBytes();
byte[] k2 = randomBytes();
byte[] v1 = randomBytes();
byte[] v2 = randomBytes();
byte[] v3 = randomBytes();
byte[] v4 = randomBytes();
HeaderMap h1 = new HeaderMap(false);
h1.set(as(k1), as(v1));
h1.set(as(k2), as(v2));
h1.add(as(k2), as(v3));
h1.add(as(k1), as(v4));
HeaderMap h2 = new HeaderMap(false);
h2.set(as(k1), as(v1));
h2.set(as(k2), as(v2));
h2.add(as(k1), as(v4));
h2.add(as(k2), as(v3));
assertTrue(h1.equals(h2));
assertTrue(h2.equals(h1));
assertTrue(h2.equals(h2));
assertTrue(h1.equals(h1));
}
@Test
public void binaryHeadersWithDifferentValuesShouldNotBeEquivalent() {
byte[] k1 = randomBytes();
byte[] k2 = randomBytes();
byte[] v1 = randomBytes();
byte[] v2 = randomBytes();
byte[] v3 = randomBytes();
byte[] v4 = randomBytes();
HeaderMap h1 = new HeaderMap(false);
h1.set(as(k1), as(v1));
h1.set(as(k2), as(v2));
h1.add(as(k2), as(v3));
h1.add(as(k1), as(v4));
HeaderMap h2 = new HeaderMap(false);
h2.set(as(k1), as(v1));
h2.set(as(k2), as(v2));
h2.add(as(k1), as(v4));
assertFalse(h1.equals(h2));
assertFalse(h2.equals(h1));
assertTrue(h2.equals(h2));
assertTrue(h1.equals(h1));
}
@Test
public void binarySetAllShouldMergeHeaders() {
byte[] k1 = randomBytes();
byte[] k2 = randomBytes();
byte[] v1 = randomBytes();
byte[] v2 = randomBytes();
byte[] v3 = randomBytes();
byte[] v4 = randomBytes();
HeaderMap h1 = new HeaderMap(false);
h1.set(as(k1), as(v1));
h1.set(as(k2), as(v2));
h1.add(as(k2), as(v3));
h1.add(as(k1), as(v4));
HeaderMap h2 = new HeaderMap(false);
h2.set(as(k1), as(v1));
h2.set(as(k2), as(v2));
h2.add(as(k1), as(v4));
HeaderMap expected = new HeaderMap(false);
expected.set(as(k1), as(v1));
expected.set(as(k2), as(v2));
expected.add(as(k2), as(v3));
expected.add(as(k1), as(v4));
expected.set(as(k1), as(v1));
expected.set(as(k2), as(v2));
expected.set(as(k1), as(v4));
h1.setAll(h2);
assertEquals(expected, h1);
}
@Test
public void binarySetShouldReplacePreviousValues() {
byte[] k1 = randomBytes();
byte[] v1 = randomBytes();
byte[] v2 = randomBytes();
byte[] v3 = randomBytes();
HeaderMap h1 = new HeaderMap(false);
h1.add(as(k1), as(v1));
h1.add(as(k1), as(v2));
assertEquals(2, h1.size());
h1.set(as(k1), as(v3));
assertEquals(1, h1.size());
List<CharSequence> list = h1.getAll(as(k1));
assertEquals(1, list.size());
assertEquals(as(v3), list.get(0));
}
@Test
public void headersWithSameValuesShouldBeEquivalent() {
HeaderMap h1 = new HeaderMap();
h1.set("foo", "goo");
h1.set("foo2", "goo2");
HeaderMap h2 = new HeaderMap();
h2.set("foo", "goo");
h2.set("foo2", "goo2");
assertTrue(h1.equals(h2));
assertTrue(h2.equals(h1));
assertTrue(h2.equals(h2));
assertTrue(h1.equals(h1));
}
@Test
public void headersWithSameDuplicateValuesShouldBeEquivalent() {
HeaderMap h1 = new HeaderMap();
h1.set("foo", "goo");
h1.set("foo2", "goo2");
h1.add("foo2", "goo3");
h1.add("foo", "goo4");
HeaderMap h2 = new HeaderMap();
h2.set("foo", "goo");
h2.set("foo2", "goo2");
h2.add("foo", "goo4");
h2.add("foo2", "goo3");
assertTrue(h1.equals(h2));
assertTrue(h2.equals(h1));
assertTrue(h2.equals(h2));
assertTrue(h1.equals(h1));
}
@Test
public void headersWithDifferentValuesShouldNotBeEquivalent() {
HeaderMap h1 = new HeaderMap();
h1.set("foo", "goo");
h1.set("foo2", "goo2");
h1.add("foo2", "goo3");
h1.add("foo", "goo4");
HeaderMap h2 = new HeaderMap();
h2.set("foo", "goo");
h2.set("foo2", "goo2");
h2.add("foo", "goo4");
assertFalse(h1.equals(h2));
assertFalse(h2.equals(h1));
assertTrue(h2.equals(h2));
assertTrue(h1.equals(h1));
}
@Test
public void setAllShouldMergeHeaders() {
HeaderMap h1 = new HeaderMap();
h1.set("foo", "goo");
h1.set("foo2", "goo2");
h1.add("foo2", "goo3");
h1.add("foo", "goo4");
HeaderMap h2 = new HeaderMap();
h2.set("foo", "goo");
h2.set("foo2", "goo2");
h2.add("foo", "goo4");
HeaderMap expected = new HeaderMap();
expected.set("foo", "goo");
expected.set("foo2", "goo2");
expected.add("foo2", "goo3");
expected.add("foo", "goo4");
expected.set("foo", "goo");
expected.set("foo2", "goo2");
expected.set("foo", "goo4");
h1.setAll(h2);
assertEquals(expected, h1);
}
@Test
public void setShouldReplacePreviousValues() {
HeaderMap h1 = new HeaderMap();
h1.add("foo", "goo");
h1.add("foo", "goo2");
assertEquals(2, h1.size());
h1.set("foo", "goo3");
assertEquals(1, h1.size());
List<CharSequence> list = h1.getAll("foo");
assertEquals(1, list.size());
assertEquals("goo3", list.get(0));
}
@Test(expected = NoSuchElementException.class)
public void iterateEmptyHeadersShouldThrow() {
Iterator<Map.Entry<CharSequence, CharSequence>> iterator =
new HeaderMap().iterator();
assertFalse(iterator.hasNext());
iterator.next();
}
@Test
public void iterateHeadersShouldReturnAllValues() {
Set<String> headers = new HashSet<String>();
headers.add("a:1");
headers.add("a:2");
headers.add("a:3");
headers.add("b:1");
headers.add("b:2");
headers.add("c:1");
// Build the headers from the input set.
HeaderMap h1 = new HeaderMap();
for (String header : headers) {
String[] parts = header.split(":");
h1.add(parts[0], parts[1]);
}
// Now iterate through the headers, removing them from the original set.
for (Map.Entry<CharSequence, CharSequence> entry : h1) {
assertTrue(headers
.remove(entry.getKey().toString() + ':' + entry.getValue().toString()));
}
// Make sure we removed them all.
assertTrue(headers.isEmpty());
}
@Test
public void getAndRemoveShouldReturnFirstEntry() {
HeaderMap h1 = new HeaderMap();
h1.add("foo", "goo");
h1.add("foo", "goo2");
assertEquals("goo", h1.getAndRemove("foo"));
assertEquals(0, h1.size());
List<CharSequence> values = h1.getAll("foo");
assertEquals(0, values.size());
}
private static byte[] randomBytes() {
byte[] data = new byte[100];
new Random().nextBytes(data);
return data;
}
private String as(byte[] bytes) {
return new String(bytes);
}
}

View File

@ -76,7 +76,7 @@ public class HttpResponseHandler extends SimpleChannelInboundHandler<FullHttpRes
@Override
protected void messageReceived(ChannelHandlerContext ctx, FullHttpResponse msg) throws Exception {
String streamIdText = msg.headers().get(HttpUtil.ExtensionHeaders.Names.STREAM_ID);
String streamIdText = msg.headers().get(HttpUtil.ExtensionHeaderNames.STREAM_ID.text());
if (streamIdText == null) {
System.err.println("HttpResponseHandler unexpected message received: " + msg);
return;

View File

@ -21,6 +21,7 @@ import static io.netty.example.http2.Http2ExampleUtil.UPGRADE_RESPONSE_HEADER;
import static io.netty.util.internal.logging.InternalLogLevel.INFO;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.http.HttpServerUpgradeHandler;
import io.netty.handler.codec.http2.AbstractHttp2ConnectionHandler;
import io.netty.handler.codec.http2.DefaultHttp2Connection;
@ -68,8 +69,8 @@ public class HelloWorldHttp2Handler extends AbstractHttp2ConnectionHandler {
if (evt instanceof HttpServerUpgradeHandler.UpgradeEvent) {
// Write an HTTP/2 response to the upgrade request
Http2Headers headers =
DefaultHttp2Headers.newBuilder().status("200")
.set(UPGRADE_RESPONSE_HEADER, "true").build();
new DefaultHttp2Headers().status(new AsciiString("200"))
.set(new AsciiString(UPGRADE_RESPONSE_HEADER), new AsciiString("true"));
writeHeaders(ctx, 1, headers, 0, true, ctx.newPromise());
}
super.userEventTriggered(ctx, evt);
@ -109,7 +110,7 @@ public class HelloWorldHttp2Handler extends AbstractHttp2ConnectionHandler {
*/
private void sendResponse(ChannelHandlerContext ctx, int streamId, ByteBuf payload) {
// Send a frame for the response status
Http2Headers headers = DefaultHttp2Headers.newBuilder().status("200").build();
Http2Headers headers = new DefaultHttp2Headers().status(new AsciiString("200"));
writeHeaders(ctx(), streamId, headers, 0, false, ctx().newPromise());
writeData(ctx(), streamId, payload, 0, true, ctx().newPromise());