// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace DotNetty.Codecs.Http.Tests
{
using System;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using DotNetty.Buffers;
using DotNetty.Common.Concurrency;
using DotNetty.Common.Utilities;
using DotNetty.Transport.Bootstrapping;
using DotNetty.Transport.Channels;
using DotNetty.Transport.Channels.Embedded;
using DotNetty.Transport.Channels.Sockets;
using Xunit;
using HttpVersion = DotNetty.Codecs.Http.HttpVersion;
public sealed class HttpClientCodecTest
{
const string EmptyResponse = "HTTP/1.0 200 OK\r\nContent-Length: 0\r\n\r\n";
const string Response = "HTTP/1.0 200 OK\r\n"
+ "Date: Fri, 31 Dec 1999 23:59:59 GMT\r\n"
+ "Content-Type: text/html\r\n" + "Content-Length: 28\r\n" + "\r\n"
+ "
\r\n";
const string IncompleteChunkedResponse = "HTTP/1.1 200 OK\r\n"
+ "Content-Type: text/plain\r\n"
+ "Transfer-Encoding: chunked\r\n" + "\r\n"
+ "5\r\n" + "first\r\n" + "6\r\n" + "second\r\n" + "0\r\n";
const string ChunkedResponse = IncompleteChunkedResponse + "\r\n";
[Fact]
public void ConnectWithResponseContent()
{
var codec = new HttpClientCodec(4096, 8192, 8192, true);
var ch = new EmbeddedChannel(codec);
SendRequestAndReadResponse(ch, HttpMethod.Connect, Response);
ch.Finish();
}
[Fact]
public void FailsNotOnRequestResponseChunked()
{
var codec = new HttpClientCodec(4096, 8192, 8192, true);
var ch = new EmbeddedChannel(codec);
SendRequestAndReadResponse(ch, HttpMethod.Get, ChunkedResponse);
ch.Finish();
}
[Fact]
public void FailsOnMissingResponse()
{
var codec = new HttpClientCodec(4096, 8192, 8192, true);
var ch = new EmbeddedChannel(codec);
Assert.True(ch.WriteOutbound(new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Get, "http://localhost/")));
var buffer = ch.ReadOutbound();
Assert.NotNull(buffer);
buffer.Release();
Assert.Throws(() => ch.Finish());
}
[Fact]
public void FailsOnIncompleteChunkedResponse()
{
var codec = new HttpClientCodec(4096, 8192, 8192, true);
var ch = new EmbeddedChannel(codec);
ch.WriteOutbound(new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Get, "http://localhost/"));
var buffer = ch.ReadOutbound();
Assert.NotNull(buffer);
buffer.Release();
Assert.Null(ch.ReadInbound());
ch.WriteInbound(Unpooled.CopiedBuffer(Encoding.UTF8.GetBytes(IncompleteChunkedResponse)));
var response = ch.ReadInbound();
Assert.NotNull(response);
var content = ch.ReadInbound();
Assert.NotNull(content); // Chunk 'first'
content.Release();
content = ch.ReadInbound();
Assert.NotNull(content); // Chunk 'second'
content.Release();
content = ch.ReadInbound();
Assert.Null(content);
Assert.Throws(() => ch.Finish());
}
[Fact]
public void ServerCloseSocketInputProvidesData()
{
var clientGroup = new MultithreadEventLoopGroup(1);
var serverGroup = new MultithreadEventLoopGroup(1);
try
{
var serverCompletion = new TaskCompletionSource();
var serverHandler = new ServerHandler();
ServerBootstrap sb = new ServerBootstrap()
.Group(serverGroup)
.Channel()
.ChildHandler(
new ActionChannelInitializer(
ch =>
{
// Don't use the HttpServerCodec, because we don't want to have content-length or anything added.
ch.Pipeline.AddLast(new HttpRequestDecoder(4096, 8192, 8192, true));
ch.Pipeline.AddLast(new HttpObjectAggregator(4096));
ch.Pipeline.AddLast(serverHandler);
serverCompletion.TryComplete();
}));
var clientHandler = new ClientHandler();
Bootstrap cb = new Bootstrap()
.Group(clientGroup)
.Channel()
.Handler(
new ActionChannelInitializer(
ch =>
{
ch.Pipeline.AddLast(new HttpClientCodec(4096, 8192, 8192, true));
ch.Pipeline.AddLast(new HttpObjectAggregator(4096));
ch.Pipeline.AddLast(clientHandler);
}));
Task task = sb.BindAsync(IPAddress.Loopback, IPEndPoint.MinPort);
task.Wait(TimeSpan.FromSeconds(5));
Assert.True(task.Status == TaskStatus.RanToCompletion);
IChannel serverChannel = task.Result;
int port = ((IPEndPoint)serverChannel.LocalAddress).Port;
task = cb.ConnectAsync(IPAddress.Loopback, port);
task.Wait(TimeSpan.FromSeconds(5));
Assert.True(task.Status == TaskStatus.RanToCompletion);
IChannel clientChannel = task.Result;
serverCompletion.Task.Wait(TimeSpan.FromSeconds(5));
clientChannel.WriteAndFlushAsync(new DefaultHttpRequest(HttpVersion.Http11, HttpMethod.Get, "/")).Wait(TimeSpan.FromSeconds(1));
Assert.True(serverHandler.WaitForCompletion());
Assert.True(clientHandler.WaitForCompletion());
}
finally
{
Task.WaitAll(
clientGroup.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1)),
serverGroup.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1)));
}
}
class ClientHandler : SimpleChannelInboundHandler
{
readonly TaskCompletionSource completion = new TaskCompletionSource();
public bool WaitForCompletion()
{
this.completion.Task.Wait(TimeSpan.FromSeconds(5));
return this.completion.Task.Status == TaskStatus.RanToCompletion;
}
protected override void ChannelRead0(IChannelHandlerContext ctx, IFullHttpResponse msg) =>
this.completion.TryComplete();
public override void ExceptionCaught(IChannelHandlerContext context, Exception exception) =>
this.completion.TrySetException(exception);
}
class ServerHandler : SimpleChannelInboundHandler
{
readonly TaskCompletionSource completion = new TaskCompletionSource();
public bool WaitForCompletion()
{
this.completion.Task.Wait(TimeSpan.FromSeconds(5));
return this.completion.Task.Status == TaskStatus.RanToCompletion;
}
protected override void ChannelRead0(IChannelHandlerContext ctx, IFullHttpRequest msg)
{
// This is just a simple demo...don't block in IO
Assert.IsAssignableFrom(ctx.Channel);
var sChannel = (ISocketChannel)ctx.Channel;
/**
* The point of this test is to not add any content-length or content-encoding headers
* and the client should still handle this.
* See RFC 7230, 3.3.3.
*/
sChannel.WriteAndFlushAsync(Unpooled.WrappedBuffer(Encoding.UTF8.GetBytes("HTTP/1.0 200 OK\r\n" + "Date: Fri, 31 Dec 1999 23:59:59 GMT\r\n" + "Content-Type: text/html\r\n\r\n")));
sChannel.WriteAndFlushAsync(Unpooled.WrappedBuffer(Encoding.UTF8.GetBytes("hello half closed!\r\n")));
sChannel.CloseAsync();
sChannel.CloseCompletion.LinkOutcome(this.completion);
}
public override void ExceptionCaught(IChannelHandlerContext context, Exception exception) => this.completion.TrySetException(exception);
}
[Fact]
public void ContinueParsingAfterConnect() => AfterConnect(true);
[Fact]
public void PassThroughAfterConnect() => AfterConnect(false);
static void AfterConnect(bool parseAfterConnect)
{
var ch = new EmbeddedChannel(new HttpClientCodec(4096, 8192, 8192, true, true, parseAfterConnect));
var connectResponseConsumer = new Consumer();
SendRequestAndReadResponse(ch, HttpMethod.Connect, EmptyResponse, connectResponseConsumer);
Assert.True(connectResponseConsumer.ReceivedCount > 0, "No connect response messages received.");
void Handler(object m)
{
if (parseAfterConnect)
{
Assert.True(m is IHttpObject, "Unexpected response message type.");
}
else
{
Assert.False(m is IHttpObject, "Unexpected response message type.");
}
}
var responseConsumer = new Consumer(Handler);
SendRequestAndReadResponse(ch, HttpMethod.Get, Response, responseConsumer);
Assert.True(responseConsumer.ReceivedCount > 0, "No response messages received.");
Assert.False(ch.Finish(), "Channel finish failed.");
}
static void SendRequestAndReadResponse(EmbeddedChannel ch, HttpMethod httpMethod, string response) =>
SendRequestAndReadResponse(ch, httpMethod, response, new Consumer());
static void SendRequestAndReadResponse(
EmbeddedChannel ch,
HttpMethod httpMethod,
string response,
Consumer responseConsumer)
{
Assert.True(
ch.WriteOutbound(new DefaultFullHttpRequest(HttpVersion.Http11, httpMethod, "http://localhost/")),
"Channel outbound write failed.");
Assert.True(
ch.WriteInbound(Unpooled.CopiedBuffer(Encoding.UTF8.GetBytes(response))),
"Channel inbound write failed.");
for (;;)
{
var msg = ch.ReadOutbound