Remove unnecessary code from HttpObjectDecoder and re-enable all HTTP tests
- Since Netty 4, HTTP decoder does not generate a full message at all. Therefore, there's no need to keep separate states for the content smaller than maxChunkSize. - maxChunkSize must be greater than 0. Setting it to 0 should not disable chunked encoding. We have a dedicated flag for that. - Uncommented the tests that were commented out for an unknown reason, with some fixes. - Added more tests for HTTP decoder. - Removed the Ignore annotation on some tests.
This commit is contained in:
parent
2b09d92c37
commit
0f76b3c357
@ -26,7 +26,7 @@ import io.netty.util.internal.AppendableCharSequence;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static io.netty.buffer.ByteBufUtil.readBytes;
|
import static io.netty.buffer.ByteBufUtil.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes {@link ByteBuf}s into {@link HttpMessage}s and
|
* Decodes {@link ByteBuf}s into {@link HttpMessage}s and
|
||||||
@ -107,11 +107,9 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder<HttpObjectDecod
|
|||||||
private final boolean chunkedSupported;
|
private final boolean chunkedSupported;
|
||||||
protected final boolean validateHeaders;
|
protected final boolean validateHeaders;
|
||||||
|
|
||||||
private ByteBuf content;
|
|
||||||
private HttpMessage message;
|
private HttpMessage message;
|
||||||
private long chunkSize;
|
private long chunkSize;
|
||||||
private int headerSize;
|
private int headerSize;
|
||||||
private int contentRead;
|
|
||||||
private final AppendableCharSequence sb = new AppendableCharSequence(128);
|
private final AppendableCharSequence sb = new AppendableCharSequence(128);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -123,12 +121,9 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder<HttpObjectDecod
|
|||||||
READ_INITIAL,
|
READ_INITIAL,
|
||||||
READ_HEADER,
|
READ_HEADER,
|
||||||
READ_VARIABLE_LENGTH_CONTENT,
|
READ_VARIABLE_LENGTH_CONTENT,
|
||||||
READ_VARIABLE_LENGTH_CONTENT_AS_CHUNKS,
|
|
||||||
READ_FIXED_LENGTH_CONTENT,
|
READ_FIXED_LENGTH_CONTENT,
|
||||||
READ_FIXED_LENGTH_CONTENT_AS_CHUNKS,
|
|
||||||
READ_CHUNK_SIZE,
|
READ_CHUNK_SIZE,
|
||||||
READ_CHUNKED_CONTENT,
|
READ_CHUNKED_CONTENT,
|
||||||
READ_CHUNKED_CONTENT_AS_CHUNKS,
|
|
||||||
READ_CHUNK_DELIMITER,
|
READ_CHUNK_DELIMITER,
|
||||||
READ_CHUNK_FOOTER,
|
READ_CHUNK_FOOTER,
|
||||||
BAD_MESSAGE
|
BAD_MESSAGE
|
||||||
@ -170,7 +165,7 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder<HttpObjectDecod
|
|||||||
"maxHeaderSize must be a positive integer: " +
|
"maxHeaderSize must be a positive integer: " +
|
||||||
maxHeaderSize);
|
maxHeaderSize);
|
||||||
}
|
}
|
||||||
if (maxChunkSize < 0) {
|
if (maxChunkSize <= 0) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"maxChunkSize must be a positive integer: " +
|
"maxChunkSize must be a positive integer: " +
|
||||||
maxChunkSize);
|
maxChunkSize);
|
||||||
@ -221,39 +216,28 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder<HttpObjectDecod
|
|||||||
}
|
}
|
||||||
if (nextState == State.SKIP_CONTROL_CHARS) {
|
if (nextState == State.SKIP_CONTROL_CHARS) {
|
||||||
// No content is expected.
|
// No content is expected.
|
||||||
reset(out);
|
out.add(message);
|
||||||
|
out.add(LastHttpContent.EMPTY_LAST_CONTENT);
|
||||||
|
reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
long contentLength = HttpHeaders.getContentLength(message, -1);
|
long contentLength = HttpHeaders.getContentLength(message, -1);
|
||||||
if (contentLength == 0 || contentLength == -1 && isDecodingRequest()) {
|
if (contentLength == 0 || contentLength == -1 && isDecodingRequest()) {
|
||||||
content = Unpooled.EMPTY_BUFFER;
|
out.add(message);
|
||||||
reset(out);
|
out.add(LastHttpContent.EMPTY_LAST_CONTENT);
|
||||||
|
reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (nextState) {
|
assert nextState == State.READ_FIXED_LENGTH_CONTENT || nextState == State.READ_VARIABLE_LENGTH_CONTENT;
|
||||||
case READ_FIXED_LENGTH_CONTENT:
|
|
||||||
if (contentLength > maxChunkSize || HttpHeaders.is100ContinueExpected(message)) {
|
out.add(message);
|
||||||
// Generate FullHttpMessage first. HttpChunks will follow.
|
|
||||||
checkpoint(State.READ_FIXED_LENGTH_CONTENT_AS_CHUNKS);
|
if (nextState == State.READ_FIXED_LENGTH_CONTENT) {
|
||||||
// chunkSize will be decreased as the READ_FIXED_LENGTH_CONTENT_AS_CHUNKS
|
// chunkSize will be decreased as the READ_FIXED_LENGTH_CONTENT state reads data chunk by chunk.
|
||||||
// state reads data chunk by chunk.
|
|
||||||
chunkSize = HttpHeaders.getContentLength(message, -1);
|
chunkSize = HttpHeaders.getContentLength(message, -1);
|
||||||
out.add(message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case READ_VARIABLE_LENGTH_CONTENT:
|
|
||||||
if (buffer.readableBytes() > maxChunkSize || HttpHeaders.is100ContinueExpected(message)) {
|
|
||||||
// Generate FullHttpMessage first. HttpChunks will follow.
|
|
||||||
checkpoint(State.READ_VARIABLE_LENGTH_CONTENT_AS_CHUNKS);
|
|
||||||
out.add(message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException("Unexpected state: " + nextState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We return here, this forces decode to be called again where we will decode the content
|
// We return here, this forces decode to be called again where we will decode the content
|
||||||
return;
|
return;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -261,35 +245,25 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder<HttpObjectDecod
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case READ_VARIABLE_LENGTH_CONTENT: {
|
case READ_VARIABLE_LENGTH_CONTENT: {
|
||||||
int toRead = actualReadableBytes();
|
|
||||||
if (toRead > maxChunkSize) {
|
|
||||||
toRead = maxChunkSize;
|
|
||||||
}
|
|
||||||
out.add(message);
|
|
||||||
out.add(new DefaultHttpContent(readBytes(ctx.alloc(), buffer, toRead)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case READ_VARIABLE_LENGTH_CONTENT_AS_CHUNKS: {
|
|
||||||
// Keep reading data as a chunk until the end of connection is reached.
|
// Keep reading data as a chunk until the end of connection is reached.
|
||||||
int toRead = actualReadableBytes();
|
int toRead = Math.min(actualReadableBytes(), maxChunkSize);
|
||||||
if (toRead > maxChunkSize) {
|
if (toRead > 0) {
|
||||||
toRead = maxChunkSize;
|
|
||||||
}
|
|
||||||
ByteBuf content = readBytes(ctx.alloc(), buffer, toRead);
|
ByteBuf content = readBytes(ctx.alloc(), buffer, toRead);
|
||||||
if (!buffer.isReadable()) {
|
if (buffer.isReadable()) {
|
||||||
reset();
|
|
||||||
out.add(new DefaultLastHttpContent(content, validateHeaders));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
out.add(new DefaultHttpContent(content));
|
out.add(new DefaultHttpContent(content));
|
||||||
|
} else {
|
||||||
|
// End of connection.
|
||||||
|
out.add(new DefaultLastHttpContent(content, validateHeaders));
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
} else if (!buffer.isReadable()) {
|
||||||
|
// End of connection.
|
||||||
|
out.add(LastHttpContent.EMPTY_LAST_CONTENT);
|
||||||
|
reset();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case READ_FIXED_LENGTH_CONTENT: {
|
case READ_FIXED_LENGTH_CONTENT: {
|
||||||
readFixedLengthContent(ctx, buffer, out);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case READ_FIXED_LENGTH_CONTENT_AS_CHUNKS: {
|
|
||||||
long chunkSize = this.chunkSize;
|
|
||||||
int readLimit = actualReadableBytes();
|
int readLimit = actualReadableBytes();
|
||||||
|
|
||||||
// Check if the buffer is readable first as we use the readable byte count
|
// Check if the buffer is readable first as we use the readable byte count
|
||||||
@ -302,28 +276,20 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder<HttpObjectDecod
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int toRead = readLimit;
|
int toRead = Math.min(readLimit, maxChunkSize);
|
||||||
if (toRead > maxChunkSize) {
|
|
||||||
toRead = maxChunkSize;
|
|
||||||
}
|
|
||||||
if (toRead > chunkSize) {
|
if (toRead > chunkSize) {
|
||||||
toRead = (int) chunkSize;
|
toRead = (int) chunkSize;
|
||||||
}
|
}
|
||||||
ByteBuf content = readBytes(ctx.alloc(), buffer, toRead);
|
ByteBuf content = readBytes(ctx.alloc(), buffer, toRead);
|
||||||
if (chunkSize > toRead) {
|
|
||||||
chunkSize -= toRead;
|
chunkSize -= toRead;
|
||||||
} else {
|
|
||||||
chunkSize = 0;
|
|
||||||
}
|
|
||||||
this.chunkSize = chunkSize;
|
|
||||||
|
|
||||||
if (chunkSize == 0) {
|
if (chunkSize == 0) {
|
||||||
// Read all content.
|
// Read all content.
|
||||||
reset();
|
|
||||||
out.add(new DefaultLastHttpContent(content, validateHeaders));
|
out.add(new DefaultLastHttpContent(content, validateHeaders));
|
||||||
return;
|
reset();
|
||||||
}
|
} else {
|
||||||
out.add(new DefaultHttpContent(content));
|
out.add(new DefaultHttpContent(content));
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -337,9 +303,6 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder<HttpObjectDecod
|
|||||||
if (chunkSize == 0) {
|
if (chunkSize == 0) {
|
||||||
checkpoint(State.READ_CHUNK_FOOTER);
|
checkpoint(State.READ_CHUNK_FOOTER);
|
||||||
return;
|
return;
|
||||||
} else if (chunkSize > maxChunkSize) {
|
|
||||||
// A chunk is too large. Split them into multiple chunks again.
|
|
||||||
checkpoint(State.READ_CHUNKED_CONTENT_AS_CHUNKS);
|
|
||||||
} else {
|
} else {
|
||||||
checkpoint(State.READ_CHUNKED_CONTENT);
|
checkpoint(State.READ_CHUNKED_CONTENT);
|
||||||
}
|
}
|
||||||
@ -349,49 +312,20 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder<HttpObjectDecod
|
|||||||
}
|
}
|
||||||
case READ_CHUNKED_CONTENT: {
|
case READ_CHUNKED_CONTENT: {
|
||||||
assert chunkSize <= Integer.MAX_VALUE;
|
assert chunkSize <= Integer.MAX_VALUE;
|
||||||
HttpContent chunk = new DefaultHttpContent(readBytes(ctx.alloc(), buffer, (int) chunkSize));
|
int toRead = Math.min((int) chunkSize, maxChunkSize);
|
||||||
checkpoint(State.READ_CHUNK_DELIMITER);
|
|
||||||
out.add(chunk);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case READ_CHUNKED_CONTENT_AS_CHUNKS: {
|
|
||||||
assert chunkSize <= Integer.MAX_VALUE;
|
|
||||||
int chunkSize = (int) this.chunkSize;
|
|
||||||
int readLimit = actualReadableBytes();
|
|
||||||
|
|
||||||
// Check if the buffer is readable first as we use the readable byte count
|
|
||||||
// to create the HttpChunk. This is needed as otherwise we may end up with
|
|
||||||
// create a HttpChunk instance that contains an empty buffer and so is
|
|
||||||
// handled like it is the last HttpChunk.
|
|
||||||
//
|
|
||||||
// See https://github.com/netty/netty/issues/433
|
|
||||||
if (readLimit == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int toRead = chunkSize;
|
|
||||||
if (toRead > maxChunkSize) {
|
|
||||||
toRead = maxChunkSize;
|
|
||||||
}
|
|
||||||
if (toRead > readLimit) {
|
|
||||||
toRead = readLimit;
|
|
||||||
}
|
|
||||||
HttpContent chunk = new DefaultHttpContent(readBytes(ctx.alloc(), buffer, toRead));
|
HttpContent chunk = new DefaultHttpContent(readBytes(ctx.alloc(), buffer, toRead));
|
||||||
if (chunkSize > toRead) {
|
|
||||||
chunkSize -= toRead;
|
chunkSize -= toRead;
|
||||||
} else {
|
|
||||||
chunkSize = 0;
|
out.add(chunk);
|
||||||
}
|
|
||||||
this.chunkSize = chunkSize;
|
|
||||||
|
|
||||||
if (chunkSize == 0) {
|
if (chunkSize == 0) {
|
||||||
// Read all content.
|
// Read all content.
|
||||||
checkpoint(State.READ_CHUNK_DELIMITER);
|
checkpoint(State.READ_CHUNK_DELIMITER);
|
||||||
}
|
} else {
|
||||||
|
|
||||||
out.add(chunk);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case READ_CHUNK_DELIMITER: {
|
case READ_CHUNK_DELIMITER: {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
byte next = buffer.readByte();
|
byte next = buffer.readByte();
|
||||||
@ -410,16 +344,9 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder<HttpObjectDecod
|
|||||||
}
|
}
|
||||||
case READ_CHUNK_FOOTER: try {
|
case READ_CHUNK_FOOTER: try {
|
||||||
LastHttpContent trailer = readTrailingHeaders(buffer);
|
LastHttpContent trailer = readTrailingHeaders(buffer);
|
||||||
if (maxChunkSize == 0) {
|
|
||||||
// Chunked encoding disabled.
|
|
||||||
reset(out);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
reset();
|
|
||||||
// The last chunk, which is empty
|
|
||||||
out.add(trailer);
|
out.add(trailer);
|
||||||
|
reset();
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
out.add(invalidChunk(e));
|
out.add(invalidChunk(e));
|
||||||
return;
|
return;
|
||||||
@ -427,10 +354,6 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder<HttpObjectDecod
|
|||||||
case BAD_MESSAGE: {
|
case BAD_MESSAGE: {
|
||||||
// Keep discarding until disconnection.
|
// Keep discarding until disconnection.
|
||||||
buffer.skipBytes(actualReadableBytes());
|
buffer.skipBytes(actualReadableBytes());
|
||||||
return;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
throw new Error("Shouldn't reach here.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -441,14 +364,8 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder<HttpObjectDecod
|
|||||||
|
|
||||||
// Handle the last unfinished message.
|
// Handle the last unfinished message.
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
// Get the length of the content received so far for the last message.
|
|
||||||
HttpMessage message = this.message;
|
HttpMessage message = this.message;
|
||||||
int actualContentLength;
|
this.message = null;
|
||||||
if (content != null) {
|
|
||||||
actualContentLength = content.readableBytes();
|
|
||||||
} else {
|
|
||||||
actualContentLength = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the closure of the connection signifies the end of the content.
|
// Check if the closure of the connection signifies the end of the content.
|
||||||
boolean prematureClosure;
|
boolean prematureClosure;
|
||||||
@ -460,15 +377,11 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder<HttpObjectDecod
|
|||||||
// If the 'Content-Length' header is absent, the length of the content is determined by the end of the
|
// If the 'Content-Length' header is absent, the length of the content is determined by the end of the
|
||||||
// connection, so it is perfectly fine.
|
// connection, so it is perfectly fine.
|
||||||
long expectedContentLength = HttpHeaders.getContentLength(message, -1);
|
long expectedContentLength = HttpHeaders.getContentLength(message, -1);
|
||||||
prematureClosure = expectedContentLength >= 0 && actualContentLength != expectedContentLength;
|
prematureClosure = expectedContentLength > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!prematureClosure) {
|
if (!prematureClosure) {
|
||||||
if (actualContentLength == 0) {
|
|
||||||
out.add(LastHttpContent.EMPTY_LAST_CONTENT);
|
out.add(LastHttpContent.EMPTY_LAST_CONTENT);
|
||||||
} else {
|
|
||||||
out.add(new DefaultLastHttpContent(content, validateHeaders));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -484,11 +397,8 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder<HttpObjectDecod
|
|||||||
// - http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html Section 4.4
|
// - http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html Section 4.4
|
||||||
// - https://github.com/netty/netty/issues/222
|
// - https://github.com/netty/netty/issues/222
|
||||||
if (code >= 100 && code < 200) {
|
if (code >= 100 && code < 200) {
|
||||||
if (code == 101 && !res.headers().contains(HttpHeaders.Names.SEC_WEBSOCKET_ACCEPT)) {
|
// One exception: Hixie 76 websocket handshake response
|
||||||
// It's Hixie 76 websocket handshake response
|
return !(code == 101 && !res.headers().contains(HttpHeaders.Names.SEC_WEBSOCKET_ACCEPT));
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (code) {
|
switch (code) {
|
||||||
@ -500,28 +410,7 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder<HttpObjectDecod
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void reset() {
|
private void reset() {
|
||||||
reset(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void reset(List<Object> out) {
|
|
||||||
if (out != null) {
|
|
||||||
HttpMessage message = this.message;
|
|
||||||
ByteBuf content = this.content;
|
|
||||||
LastHttpContent httpContent;
|
|
||||||
|
|
||||||
if (content == null || !content.isReadable()) {
|
|
||||||
httpContent = LastHttpContent.EMPTY_LAST_CONTENT;
|
|
||||||
} else {
|
|
||||||
httpContent = new DefaultLastHttpContent(content, validateHeaders);
|
|
||||||
}
|
|
||||||
|
|
||||||
out.add(message);
|
|
||||||
out.add(httpContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
content = null;
|
|
||||||
message = null;
|
message = null;
|
||||||
|
|
||||||
checkpoint(State.SKIP_CONTROL_CHARS);
|
checkpoint(State.SKIP_CONTROL_CHARS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -554,28 +443,6 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder<HttpObjectDecod
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void readFixedLengthContent(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) {
|
|
||||||
//we have a content-length so we just read the correct number of bytes
|
|
||||||
long length = HttpHeaders.getContentLength(message, -1);
|
|
||||||
assert length <= Integer.MAX_VALUE;
|
|
||||||
int toRead = (int) length - contentRead;
|
|
||||||
if (toRead > actualReadableBytes()) {
|
|
||||||
toRead = actualReadableBytes();
|
|
||||||
}
|
|
||||||
contentRead += toRead;
|
|
||||||
if (length < contentRead) {
|
|
||||||
out.add(message);
|
|
||||||
out.add(new DefaultHttpContent(readBytes(ctx.alloc(), buffer, toRead)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (content == null) {
|
|
||||||
content = readBytes(ctx.alloc(), buffer, (int) length);
|
|
||||||
} else {
|
|
||||||
content.writeBytes(buffer, (int) length);
|
|
||||||
}
|
|
||||||
reset(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
private State readHeaders(ByteBuf buffer) {
|
private State readHeaders(ByteBuf buffer) {
|
||||||
headerSize = 0;
|
headerSize = 0;
|
||||||
final HttpMessage message = this.message;
|
final HttpMessage message = this.message;
|
||||||
|
@ -0,0 +1,179 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 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.http;
|
||||||
|
|
||||||
|
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
|
import io.netty.util.CharsetUtil;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
public class HttpRequestDecoderTest {
|
||||||
|
private static final byte[] CONTENT_CRLF_DELIMITERS = createContent("\r\n");
|
||||||
|
private static final byte[] CONTENT_LF_DELIMITERS = createContent("\n");
|
||||||
|
private static final byte[] CONTENT_MIXED_DELIMITERS = createContent("\r\n", "\n");
|
||||||
|
private static final int CONTENT_LENGTH = 8;
|
||||||
|
|
||||||
|
private static byte[] createContent(String... lineDelimiters) {
|
||||||
|
String lineDelimiter;
|
||||||
|
String lineDelimiter2;
|
||||||
|
if (lineDelimiters.length == 2) {
|
||||||
|
lineDelimiter = lineDelimiters[0];
|
||||||
|
lineDelimiter2 = lineDelimiters[1];
|
||||||
|
} else {
|
||||||
|
lineDelimiter = lineDelimiters[0];
|
||||||
|
lineDelimiter2 = lineDelimiters[0];
|
||||||
|
}
|
||||||
|
return ("GET /some/path?foo=bar&wibble=eek HTTP/1.1" + "\r\n" +
|
||||||
|
"Upgrade: WebSocket" + lineDelimiter2 +
|
||||||
|
"Connection: Upgrade" + lineDelimiter +
|
||||||
|
"Host: localhost" + lineDelimiter2 +
|
||||||
|
"Origin: http://localhost:8080" + lineDelimiter +
|
||||||
|
"Sec-WebSocket-Key1: 10 28 8V7 8 48 0" + lineDelimiter2 +
|
||||||
|
"Sec-WebSocket-Key2: 8 Xt754O3Q3QW 0 _60" + lineDelimiter +
|
||||||
|
"Content-Length: " + CONTENT_LENGTH + lineDelimiter2 +
|
||||||
|
"\r\n" +
|
||||||
|
"12345678").getBytes(CharsetUtil.US_ASCII);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDecodeWholeRequestAtOnceCRLFDelimiters() {
|
||||||
|
testDecodeWholeRequestAtOnce(CONTENT_CRLF_DELIMITERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDecodeWholeRequestAtOnceLFDelimiters() {
|
||||||
|
testDecodeWholeRequestAtOnce(CONTENT_LF_DELIMITERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDecodeWholeRequestAtOnceMixedDelimiters() {
|
||||||
|
testDecodeWholeRequestAtOnce(CONTENT_MIXED_DELIMITERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testDecodeWholeRequestAtOnce(byte[] content) {
|
||||||
|
EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder());
|
||||||
|
assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(content)));
|
||||||
|
HttpRequest req = (HttpRequest) channel.readInbound();
|
||||||
|
assertNotNull(req);
|
||||||
|
checkHeaders(req.headers());
|
||||||
|
LastHttpContent c = (LastHttpContent) channel.readInbound();
|
||||||
|
assertEquals(CONTENT_LENGTH, c.content().readableBytes());
|
||||||
|
assertEquals(
|
||||||
|
Unpooled.wrappedBuffer(content, content.length - CONTENT_LENGTH, CONTENT_LENGTH),
|
||||||
|
c.content().readBytes(CONTENT_LENGTH));
|
||||||
|
c.release();
|
||||||
|
|
||||||
|
assertFalse(channel.finish());
|
||||||
|
assertNull(channel.readInbound());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkHeaders(HttpHeaders headers) {
|
||||||
|
assertEquals(7, headers.names().size());
|
||||||
|
checkHeader(headers, "Upgrade", "WebSocket");
|
||||||
|
checkHeader(headers, "Connection", "Upgrade");
|
||||||
|
checkHeader(headers, "Host", "localhost");
|
||||||
|
checkHeader(headers, "Origin", "http://localhost:8080");
|
||||||
|
checkHeader(headers, "Sec-WebSocket-Key1", "10 28 8V7 8 48 0");
|
||||||
|
checkHeader(headers, "Sec-WebSocket-Key2", "8 Xt754O3Q3QW 0 _60");
|
||||||
|
checkHeader(headers, "Content-Length", String.valueOf(CONTENT_LENGTH));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkHeader(HttpHeaders headers, String name, String value) {
|
||||||
|
List<String> header1 = headers.getAll(name);
|
||||||
|
assertEquals(1, header1.size());
|
||||||
|
assertEquals(value, header1.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDecodeWholeRequestInMultipleStepsCRLFDelimiters() {
|
||||||
|
testDecodeWholeRequestInMultipleSteps(CONTENT_CRLF_DELIMITERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDecodeWholeRequestInMultipleStepsLFDelimiters() {
|
||||||
|
testDecodeWholeRequestInMultipleSteps(CONTENT_LF_DELIMITERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDecodeWholeRequestInMultipleStepsMixedDelimiters() {
|
||||||
|
testDecodeWholeRequestInMultipleSteps(CONTENT_MIXED_DELIMITERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testDecodeWholeRequestInMultipleSteps(byte[] content) {
|
||||||
|
for (int i = 1; i < content.length; i++) {
|
||||||
|
testDecodeWholeRequestInMultipleSteps(content, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testDecodeWholeRequestInMultipleSteps(byte[] content, int fragmentSize) {
|
||||||
|
EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder());
|
||||||
|
int headerLength = content.length - CONTENT_LENGTH;
|
||||||
|
|
||||||
|
// split up the header
|
||||||
|
for (int a = 0; a < headerLength;) {
|
||||||
|
int amount = fragmentSize;
|
||||||
|
if (a + amount > headerLength) {
|
||||||
|
amount = headerLength - a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if header is done it should produce a HttpRequest
|
||||||
|
boolean headerDone = a + amount == headerLength;
|
||||||
|
channel.writeInbound(Unpooled.wrappedBuffer(content, a, amount));
|
||||||
|
a += amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = CONTENT_LENGTH; i > 0; i --) {
|
||||||
|
// Should produce HttpContent
|
||||||
|
channel.writeInbound(Unpooled.wrappedBuffer(content, content.length - i, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpRequest req = (HttpRequest) channel.readInbound();
|
||||||
|
assertNotNull(req);
|
||||||
|
checkHeaders(req.headers());
|
||||||
|
|
||||||
|
for (int i = CONTENT_LENGTH; i > 1; i --) {
|
||||||
|
HttpContent c = (HttpContent) channel.readInbound();
|
||||||
|
assertEquals(1, c.content().readableBytes());
|
||||||
|
assertEquals(content[content.length - i], c.content().readByte());
|
||||||
|
c.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
LastHttpContent c = (LastHttpContent) channel.readInbound();
|
||||||
|
assertEquals(1, c.content().readableBytes());
|
||||||
|
assertEquals(content[content.length - 1], c.content().readByte());
|
||||||
|
c.release();
|
||||||
|
|
||||||
|
assertFalse(channel.finish());
|
||||||
|
assertNull(channel.readInbound());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmptyHeaderValue() {
|
||||||
|
EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder());
|
||||||
|
String crlf = "\r\n";
|
||||||
|
String request = "GET /some/path HTTP/1.1" + crlf +
|
||||||
|
"Host: localhost" + crlf +
|
||||||
|
"EmptyHeader:" + crlf + crlf;
|
||||||
|
channel.writeInbound(Unpooled.wrappedBuffer(request.getBytes(CharsetUtil.US_ASCII)));
|
||||||
|
HttpRequest req = (HttpRequest) channel.readInbound();
|
||||||
|
assertEquals("", req.headers().get("EmptyHeader"));
|
||||||
|
}
|
||||||
|
}
|
@ -17,13 +17,211 @@ package io.netty.handler.codec.http;
|
|||||||
|
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
import io.netty.channel.embedded.EmbeddedChannel;
|
import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
|
import io.netty.handler.codec.http.HttpHeaders.Names;
|
||||||
import io.netty.util.CharsetUtil;
|
import io.netty.util.CharsetUtil;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.*;
|
import static org.hamcrest.CoreMatchers.*;
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
public class HttpResponseDecoderTest {
|
public class HttpResponseDecoderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testResponseChunked() {
|
||||||
|
EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder());
|
||||||
|
ch.writeInbound(Unpooled.copiedBuffer("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n",
|
||||||
|
CharsetUtil.US_ASCII));
|
||||||
|
|
||||||
|
HttpResponse res = (HttpResponse) ch.readInbound();
|
||||||
|
assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1));
|
||||||
|
assertThat(res.getStatus(), is(HttpResponseStatus.OK));
|
||||||
|
|
||||||
|
byte[] data = new byte[64];
|
||||||
|
for (int i = 0; i < data.length; i++) {
|
||||||
|
data[i] = (byte) i;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
assertFalse(ch.writeInbound(Unpooled.copiedBuffer(Integer.toHexString(data.length) + "\r\n",
|
||||||
|
CharsetUtil.US_ASCII)));
|
||||||
|
assertTrue(ch.writeInbound(Unpooled.wrappedBuffer(data)));
|
||||||
|
HttpContent content = (HttpContent) ch.readInbound();
|
||||||
|
assertEquals(data.length, content.content().readableBytes());
|
||||||
|
|
||||||
|
byte[] decodedData = new byte[data.length];
|
||||||
|
content.content().readBytes(decodedData);
|
||||||
|
assertArrayEquals(data, decodedData);
|
||||||
|
content.release();
|
||||||
|
|
||||||
|
assertFalse(ch.writeInbound(Unpooled.copiedBuffer("\r\n", CharsetUtil.US_ASCII)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the last chunk.
|
||||||
|
ch.writeInbound(Unpooled.copiedBuffer("0\r\n\r\n", CharsetUtil.US_ASCII));
|
||||||
|
|
||||||
|
// Ensure the last chunk was decoded.
|
||||||
|
LastHttpContent content = (LastHttpContent) ch.readInbound();
|
||||||
|
assertFalse(content.content().isReadable());
|
||||||
|
content.release();
|
||||||
|
|
||||||
|
ch.finish();
|
||||||
|
assertNull(ch.readInbound());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testResponseChunkedExceedMaxChunkSize() {
|
||||||
|
EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder(4096, 8192, 32));
|
||||||
|
ch.writeInbound(
|
||||||
|
Unpooled.copiedBuffer("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n", CharsetUtil.US_ASCII));
|
||||||
|
|
||||||
|
HttpResponse res = (HttpResponse) ch.readInbound();
|
||||||
|
assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1));
|
||||||
|
assertThat(res.getStatus(), is(HttpResponseStatus.OK));
|
||||||
|
|
||||||
|
byte[] data = new byte[64];
|
||||||
|
for (int i = 0; i < data.length; i++) {
|
||||||
|
data[i] = (byte) i;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
assertFalse(ch.writeInbound(Unpooled.copiedBuffer(Integer.toHexString(data.length) + "\r\n",
|
||||||
|
CharsetUtil.US_ASCII)));
|
||||||
|
assertTrue(ch.writeInbound(Unpooled.wrappedBuffer(data)));
|
||||||
|
|
||||||
|
byte[] decodedData = new byte[data.length];
|
||||||
|
HttpContent content = (HttpContent) ch.readInbound();
|
||||||
|
assertEquals(32, content.content().readableBytes());
|
||||||
|
content.content().readBytes(decodedData, 0, 32);
|
||||||
|
|
||||||
|
content = (HttpContent) ch.readInbound();
|
||||||
|
assertEquals(32, content.content().readableBytes());
|
||||||
|
|
||||||
|
content.content().readBytes(decodedData, 32, 32);
|
||||||
|
|
||||||
|
assertArrayEquals(data, decodedData);
|
||||||
|
content.release();
|
||||||
|
|
||||||
|
assertFalse(ch.writeInbound(Unpooled.copiedBuffer("\r\n", CharsetUtil.US_ASCII)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the last chunk.
|
||||||
|
ch.writeInbound(Unpooled.copiedBuffer("0\r\n\r\n", CharsetUtil.US_ASCII));
|
||||||
|
|
||||||
|
// Ensure the last chunk was decoded.
|
||||||
|
LastHttpContent content = (LastHttpContent) ch.readInbound();
|
||||||
|
assertFalse(content.content().isReadable());
|
||||||
|
content.release();
|
||||||
|
|
||||||
|
ch.finish();
|
||||||
|
assertNull(ch.readInbound());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testClosureWithoutContentLength1() throws Exception {
|
||||||
|
EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder());
|
||||||
|
ch.writeInbound(Unpooled.copiedBuffer("HTTP/1.1 200 OK\r\n\r\n", CharsetUtil.US_ASCII));
|
||||||
|
|
||||||
|
// Read the response headers.
|
||||||
|
HttpResponse res = (HttpResponse) ch.readInbound();
|
||||||
|
assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1));
|
||||||
|
assertThat(res.getStatus(), is(HttpResponseStatus.OK));
|
||||||
|
assertThat(ch.readInbound(), is(nullValue()));
|
||||||
|
|
||||||
|
// Close the connection without sending anything.
|
||||||
|
assertTrue(ch.finish());
|
||||||
|
|
||||||
|
// The decoder should still produce the last content.
|
||||||
|
LastHttpContent content = (LastHttpContent) ch.readInbound();
|
||||||
|
assertThat(content.content().isReadable(), is(false));
|
||||||
|
content.release();
|
||||||
|
|
||||||
|
// But nothing more.
|
||||||
|
assertThat(ch.readInbound(), is(nullValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testClosureWithoutContentLength2() throws Exception {
|
||||||
|
EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder());
|
||||||
|
|
||||||
|
// Write the partial response.
|
||||||
|
ch.writeInbound(Unpooled.copiedBuffer("HTTP/1.1 200 OK\r\n\r\n12345678", CharsetUtil.US_ASCII));
|
||||||
|
|
||||||
|
// Read the response headers.
|
||||||
|
HttpResponse res = (HttpResponse) ch.readInbound();
|
||||||
|
assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1));
|
||||||
|
assertThat(res.getStatus(), is(HttpResponseStatus.OK));
|
||||||
|
|
||||||
|
// Read the partial content.
|
||||||
|
HttpContent content = (HttpContent) ch.readInbound();
|
||||||
|
assertThat(content.content().toString(CharsetUtil.US_ASCII), is("12345678"));
|
||||||
|
assertThat(content, is(not(instanceOf(LastHttpContent.class))));
|
||||||
|
content.release();
|
||||||
|
|
||||||
|
assertThat(ch.readInbound(), is(nullValue()));
|
||||||
|
|
||||||
|
// Close the connection.
|
||||||
|
assertTrue(ch.finish());
|
||||||
|
|
||||||
|
// The decoder should still produce the last content.
|
||||||
|
LastHttpContent lastContent = (LastHttpContent) ch.readInbound();
|
||||||
|
assertThat(lastContent.content().isReadable(), is(false));
|
||||||
|
lastContent.release();
|
||||||
|
|
||||||
|
// But nothing more.
|
||||||
|
assertThat(ch.readInbound(), is(nullValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPrematureClosureWithChunkedEncoding1() throws Exception {
|
||||||
|
EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder());
|
||||||
|
ch.writeInbound(
|
||||||
|
Unpooled.copiedBuffer("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n", CharsetUtil.US_ASCII));
|
||||||
|
|
||||||
|
// Read the response headers.
|
||||||
|
HttpResponse res = (HttpResponse) ch.readInbound();
|
||||||
|
assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1));
|
||||||
|
assertThat(res.getStatus(), is(HttpResponseStatus.OK));
|
||||||
|
assertThat(res.headers().get(Names.TRANSFER_ENCODING), is("chunked"));
|
||||||
|
assertThat(ch.readInbound(), is(nullValue()));
|
||||||
|
|
||||||
|
// Close the connection without sending anything.
|
||||||
|
ch.finish();
|
||||||
|
|
||||||
|
// The decoder should not generate the last chunk because it's closed prematurely.
|
||||||
|
assertThat(ch.readInbound(), is(nullValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPrematureClosureWithChunkedEncoding2() throws Exception {
|
||||||
|
EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder());
|
||||||
|
|
||||||
|
// Write the partial response.
|
||||||
|
ch.writeInbound(Unpooled.copiedBuffer(
|
||||||
|
"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n8\r\n12345678", CharsetUtil.US_ASCII));
|
||||||
|
|
||||||
|
// Read the response headers.
|
||||||
|
HttpResponse res = (HttpResponse) ch.readInbound();
|
||||||
|
assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1));
|
||||||
|
assertThat(res.getStatus(), is(HttpResponseStatus.OK));
|
||||||
|
assertThat(res.headers().get(Names.TRANSFER_ENCODING), is("chunked"));
|
||||||
|
|
||||||
|
// Read the partial content.
|
||||||
|
HttpContent content = (HttpContent) ch.readInbound();
|
||||||
|
assertThat(content.content().toString(CharsetUtil.US_ASCII), is("12345678"));
|
||||||
|
assertThat(content, is(not(instanceOf(LastHttpContent.class))));
|
||||||
|
content.release();
|
||||||
|
|
||||||
|
assertThat(ch.readInbound(), is(nullValue()));
|
||||||
|
|
||||||
|
// Close the connection.
|
||||||
|
ch.finish();
|
||||||
|
|
||||||
|
// The decoder should not generate the last chunk because it's closed prematurely.
|
||||||
|
assertThat(ch.readInbound(), is(nullValue()));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testLastResponseWithEmptyHeaderAndEmptyContent() {
|
public void testLastResponseWithEmptyHeaderAndEmptyContent() {
|
||||||
EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder());
|
EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder());
|
||||||
@ -66,4 +264,190 @@ public class HttpResponseDecoderTest {
|
|||||||
|
|
||||||
assertThat(ch.readInbound(), is(nullValue()));
|
assertThat(ch.readInbound(), is(nullValue()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLastResponseWithTrailingHeader() {
|
||||||
|
EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder());
|
||||||
|
ch.writeInbound(Unpooled.copiedBuffer(
|
||||||
|
"HTTP/1.1 200 OK\r\n" +
|
||||||
|
"Transfer-Encoding: chunked\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
"0\r\n" +
|
||||||
|
"Set-Cookie: t1=t1v1\r\n" +
|
||||||
|
"Set-Cookie: t2=t2v2; Expires=Wed, 09-Jun-2021 10:18:14 GMT\r\n" +
|
||||||
|
"\r\n",
|
||||||
|
CharsetUtil.US_ASCII));
|
||||||
|
|
||||||
|
HttpResponse res = (HttpResponse) ch.readInbound();
|
||||||
|
assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1));
|
||||||
|
assertThat(res.getStatus(), is(HttpResponseStatus.OK));
|
||||||
|
|
||||||
|
LastHttpContent lastContent = (LastHttpContent) ch.readInbound();
|
||||||
|
assertThat(lastContent.content().isReadable(), is(false));
|
||||||
|
HttpHeaders headers = lastContent.trailingHeaders();
|
||||||
|
assertEquals(1, headers.names().size());
|
||||||
|
List<String> values = headers.getAll("Set-Cookie");
|
||||||
|
assertEquals(2, values.size());
|
||||||
|
assertTrue(values.contains("t1=t1v1"));
|
||||||
|
assertTrue(values.contains("t2=t2v2; Expires=Wed, 09-Jun-2021 10:18:14 GMT"));
|
||||||
|
lastContent.release();
|
||||||
|
|
||||||
|
assertThat(ch.finish(), is(false));
|
||||||
|
assertThat(ch.readInbound(), is(nullValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLastResponseWithTrailingHeaderFragmented() {
|
||||||
|
byte[] data = ("HTTP/1.1 200 OK\r\n" +
|
||||||
|
"Transfer-Encoding: chunked\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
"0\r\n" +
|
||||||
|
"Set-Cookie: t1=t1v1\r\n" +
|
||||||
|
"Set-Cookie: t2=t2v2; Expires=Wed, 09-Jun-2021 10:18:14 GMT\r\n" +
|
||||||
|
"\r\n").getBytes(CharsetUtil.US_ASCII);
|
||||||
|
|
||||||
|
for (int i = 1; i < data.length; i++) {
|
||||||
|
testLastResponseWithTrailingHeaderFragmented(data, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testLastResponseWithTrailingHeaderFragmented(byte[] content, int fragmentSize) {
|
||||||
|
EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder());
|
||||||
|
int headerLength = 47;
|
||||||
|
// split up the header
|
||||||
|
for (int a = 0; a < headerLength;) {
|
||||||
|
int amount = fragmentSize;
|
||||||
|
if (a + amount > headerLength) {
|
||||||
|
amount = headerLength - a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if header is done it should produce a HttpRequest
|
||||||
|
boolean headerDone = a + amount == headerLength;
|
||||||
|
assertEquals(headerDone, ch.writeInbound(Unpooled.wrappedBuffer(content, a, amount)));
|
||||||
|
a += amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.writeInbound(Unpooled.wrappedBuffer(content, headerLength, content.length - headerLength));
|
||||||
|
HttpResponse res = (HttpResponse) ch.readInbound();
|
||||||
|
assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1));
|
||||||
|
assertThat(res.getStatus(), is(HttpResponseStatus.OK));
|
||||||
|
|
||||||
|
LastHttpContent lastContent = (LastHttpContent) ch.readInbound();
|
||||||
|
assertThat(lastContent.content().isReadable(), is(false));
|
||||||
|
HttpHeaders headers = lastContent.trailingHeaders();
|
||||||
|
assertEquals(1, headers.names().size());
|
||||||
|
List<String> values = headers.getAll("Set-Cookie");
|
||||||
|
assertEquals(2, values.size());
|
||||||
|
assertTrue(values.contains("t1=t1v1"));
|
||||||
|
assertTrue(values.contains("t2=t2v2; Expires=Wed, 09-Jun-2021 10:18:14 GMT"));
|
||||||
|
lastContent.release();
|
||||||
|
|
||||||
|
assertThat(ch.finish(), is(false));
|
||||||
|
assertThat(ch.readInbound(), is(nullValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testResponseWithContentLength() {
|
||||||
|
EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder());
|
||||||
|
ch.writeInbound(Unpooled.copiedBuffer(
|
||||||
|
"HTTP/1.1 200 OK\r\n" +
|
||||||
|
"Content-Length: 10\r\n" +
|
||||||
|
"\r\n", CharsetUtil.US_ASCII));
|
||||||
|
|
||||||
|
byte[] data = new byte[10];
|
||||||
|
for (int i = 0; i < data.length; i++) {
|
||||||
|
data[i] = (byte) i;
|
||||||
|
}
|
||||||
|
ch.writeInbound(Unpooled.wrappedBuffer(data, 0, data.length / 2));
|
||||||
|
ch.writeInbound(Unpooled.wrappedBuffer(data, 5, data.length / 2));
|
||||||
|
|
||||||
|
HttpResponse res = (HttpResponse) ch.readInbound();
|
||||||
|
assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1));
|
||||||
|
assertThat(res.getStatus(), is(HttpResponseStatus.OK));
|
||||||
|
|
||||||
|
HttpContent firstContent = (HttpContent) ch.readInbound();
|
||||||
|
assertThat(firstContent.content().readableBytes(), is(5));
|
||||||
|
assertEquals(Unpooled.wrappedBuffer(data, 0, 5), firstContent.content());
|
||||||
|
firstContent.release();
|
||||||
|
|
||||||
|
LastHttpContent lastContent = (LastHttpContent) ch.readInbound();
|
||||||
|
assertEquals(5, lastContent.content().readableBytes());
|
||||||
|
assertEquals(Unpooled.wrappedBuffer(data, 5, 5), lastContent.content());
|
||||||
|
lastContent.release();
|
||||||
|
|
||||||
|
assertThat(ch.finish(), is(false));
|
||||||
|
assertThat(ch.readInbound(), is(nullValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testResponseWithContentLengthFragmented() {
|
||||||
|
byte[] data = ("HTTP/1.1 200 OK\r\n" +
|
||||||
|
"Content-Length: 10\r\n" +
|
||||||
|
"\r\n").getBytes(CharsetUtil.US_ASCII);
|
||||||
|
|
||||||
|
for (int i = 1; i < data.length; i++) {
|
||||||
|
testResponseWithContentLengthFragmented(data, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testResponseWithContentLengthFragmented(byte[] header, int fragmentSize) {
|
||||||
|
EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder());
|
||||||
|
// split up the header
|
||||||
|
for (int a = 0; a < header.length;) {
|
||||||
|
int amount = fragmentSize;
|
||||||
|
if (a + amount > header.length) {
|
||||||
|
amount = header.length - a;
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.writeInbound(Unpooled.wrappedBuffer(header, a, amount));
|
||||||
|
a += amount;
|
||||||
|
}
|
||||||
|
byte[] data = new byte[10];
|
||||||
|
for (int i = 0; i < data.length; i++) {
|
||||||
|
data[i] = (byte) i;
|
||||||
|
}
|
||||||
|
ch.writeInbound(Unpooled.wrappedBuffer(data, 0, data.length / 2));
|
||||||
|
ch.writeInbound(Unpooled.wrappedBuffer(data, 5, data.length / 2));
|
||||||
|
|
||||||
|
HttpResponse res = (HttpResponse) ch.readInbound();
|
||||||
|
assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1));
|
||||||
|
assertThat(res.getStatus(), is(HttpResponseStatus.OK));
|
||||||
|
|
||||||
|
HttpContent firstContent = (HttpContent) ch.readInbound();
|
||||||
|
assertThat(firstContent.content().readableBytes(), is(5));
|
||||||
|
assertEquals(Unpooled.wrappedBuffer(data, 0, 5), firstContent.content());
|
||||||
|
firstContent.release();
|
||||||
|
|
||||||
|
LastHttpContent lastContent = (LastHttpContent) ch.readInbound();
|
||||||
|
assertEquals(5, lastContent.content().readableBytes());
|
||||||
|
assertEquals(Unpooled.wrappedBuffer(data, 5, 5), lastContent.content());
|
||||||
|
lastContent.release();
|
||||||
|
|
||||||
|
assertThat(ch.finish(), is(false));
|
||||||
|
assertThat(ch.readInbound(), is(nullValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWebSocketResponse() {
|
||||||
|
byte[] data = ("HTTP/1.1 101 WebSocket Protocol Handshake\r\n" +
|
||||||
|
"Upgrade: WebSocket\r\n" +
|
||||||
|
"Connection: Upgrade\r\n" +
|
||||||
|
"Sec-WebSocket-Origin: http://localhost:8080\r\n" +
|
||||||
|
"Sec-WebSocket-Location: ws://localhost/some/path\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
"1234567812345678").getBytes();
|
||||||
|
EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder());
|
||||||
|
ch.writeInbound(Unpooled.wrappedBuffer(data));
|
||||||
|
|
||||||
|
HttpResponse res = (HttpResponse) ch.readInbound();
|
||||||
|
assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1));
|
||||||
|
assertThat(res.getStatus(), is(HttpResponseStatus.SWITCHING_PROTOCOLS));
|
||||||
|
HttpContent content = (HttpContent) ch.readInbound();
|
||||||
|
assertThat(content.content().readableBytes(), is(16));
|
||||||
|
content.release();
|
||||||
|
|
||||||
|
assertThat(ch.finish(), is(false));
|
||||||
|
|
||||||
|
assertThat(ch.readInbound(), is(nullValue()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user