// 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.Text; using System.Threading; using DotNetty.Buffers; using DotNetty.Common.Utilities; using DotNetty.Transport.Channels; using DotNetty.Transport.Channels.Embedded; using Xunit; public sealed class HttpContentEncoderTest { sealed class TestEncoder : HttpContentEncoder { protected override Result BeginEncode(IHttpResponse headers, ICharSequence acceptEncoding) => new Result(new StringCharSequence("test"), new EmbeddedChannel(new EmbeddedMessageEncoder())); } sealed class EmbeddedMessageEncoder : MessageToByteEncoder { protected override void Encode(IChannelHandlerContext context, IByteBuffer message, IByteBuffer output) { output.WriteBytes(Encoding.ASCII.GetBytes(Convert.ToString(message.ReadableBytes))); message.SkipBytes(message.ReadableBytes); } } [Fact] public void SplitContent() { var ch = new EmbeddedChannel(new TestEncoder()); ch.WriteInbound(new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Get, "/")); ch.WriteOutbound(new DefaultHttpResponse(HttpVersion.Http11, HttpResponseStatus.OK)); ch.WriteOutbound(new DefaultHttpContent(Unpooled.WrappedBuffer(new byte[3]))); ch.WriteOutbound(new DefaultHttpContent(Unpooled.WrappedBuffer(new byte[2]))); ch.WriteOutbound(new DefaultLastHttpContent(Unpooled.WrappedBuffer(new byte[1]))); AssertEncodedResponse(ch); var chunk = ch.ReadOutbound(); Assert.Equal("3", chunk.Content.ToString(Encoding.ASCII)); chunk.Release(); chunk = ch.ReadOutbound(); Assert.Equal("2", chunk.Content.ToString(Encoding.ASCII)); chunk.Release(); chunk = ch.ReadOutbound(); Assert.Equal("1", chunk.Content.ToString(Encoding.ASCII)); chunk.Release(); chunk = ch.ReadOutbound(); Assert.False(chunk.Content.IsReadable()); Assert.IsAssignableFrom(chunk); chunk.Release(); var next = ch.ReadOutbound(); Assert.Null(next); } [Fact] public void ChunkedContent() { var ch = new EmbeddedChannel(new TestEncoder()); ch.WriteInbound(new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Get, "/")); var res = new DefaultHttpResponse(HttpVersion.Http11, HttpResponseStatus.OK); res.Headers.Set(HttpHeaderNames.TransferEncoding, HttpHeaderValues.Chunked); ch.WriteOutbound(res); AssertEncodedResponse(ch); ch.WriteOutbound(new DefaultHttpContent(Unpooled.WrappedBuffer(new byte[3]))); ch.WriteOutbound(new DefaultHttpContent(Unpooled.WrappedBuffer(new byte[2]))); ch.WriteOutbound(new DefaultLastHttpContent(Unpooled.WrappedBuffer(new byte[1]))); var chunk = ch.ReadOutbound(); Assert.Equal("3", chunk.Content.ToString(Encoding.ASCII)); chunk.Release(); chunk = ch.ReadOutbound(); Assert.Equal("2", chunk.Content.ToString(Encoding.ASCII)); chunk.Release(); chunk = ch.ReadOutbound(); Assert.Equal("1", chunk.Content.ToString(Encoding.ASCII)); chunk.Release(); chunk = ch.ReadOutbound(); Assert.False(chunk.Content.IsReadable()); Assert.IsAssignableFrom(chunk); chunk.Release(); var next = ch.ReadOutbound(); Assert.Null(next); } [Fact] public void ChunkedContentWithTrailingHeader() { var ch = new EmbeddedChannel(new TestEncoder()); ch.WriteInbound(new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Get, "/")); var res = new DefaultHttpResponse(HttpVersion.Http11, HttpResponseStatus.OK); res.Headers.Set(HttpHeaderNames.TransferEncoding, HttpHeaderValues.Chunked); ch.WriteOutbound(res); AssertEncodedResponse(ch); ch.WriteOutbound(new DefaultHttpContent(Unpooled.WrappedBuffer(new byte[3]))); ch.WriteOutbound(new DefaultHttpContent(Unpooled.WrappedBuffer(new byte[2]))); var content = new DefaultLastHttpContent(Unpooled.WrappedBuffer(new byte[1])); content.TrailingHeaders.Set((AsciiString)"X-Test", (AsciiString)"Netty"); ch.WriteOutbound(content); var chunk = ch.ReadOutbound(); Assert.Equal("3", chunk.Content.ToString(Encoding.ASCII)); chunk.Release(); chunk = ch.ReadOutbound(); Assert.Equal("2", chunk.Content.ToString(Encoding.ASCII)); chunk.Release(); chunk = ch.ReadOutbound(); Assert.Equal("1", chunk.Content.ToString(Encoding.ASCII)); chunk.Release(); var last = ch.ReadOutbound(); Assert.NotNull(last); Assert.False(last.Content.IsReadable()); Assert.Equal("Netty", last.TrailingHeaders.Get((AsciiString)"X-Test", null).ToString()); last.Release(); var next = ch.ReadOutbound(); Assert.Null(next); } [Fact] public void FullContentWithContentLength() { var ch = new EmbeddedChannel(new TestEncoder()); ch.WriteInbound(new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Get, "/")); var fullRes = new DefaultFullHttpResponse( HttpVersion.Http11, HttpResponseStatus.OK, Unpooled.WrappedBuffer(new byte[42])); fullRes.Headers.Set(HttpHeaderNames.ContentLength, 42); ch.WriteOutbound(fullRes); var res = ch.ReadOutbound(); Assert.NotNull(res); Assert.False(res is IHttpContent, $"{res.GetType()}"); Assert.False(res.Headers.TryGet(HttpHeaderNames.TransferEncoding, out _)); Assert.Equal("2", res.Headers.Get(HttpHeaderNames.ContentLength, null).ToString()); Assert.Equal("test", res.Headers.Get(HttpHeaderNames.ContentEncoding, null).ToString()); var c = ch.ReadOutbound(); Assert.Equal(2, c.Content.ReadableBytes); Assert.Equal("42", c.Content.ToString(Encoding.ASCII)); c.Release(); var last = ch.ReadOutbound(); Assert.Equal(0, last.Content.ReadableBytes); last.Release(); var next = ch.ReadOutbound(); Assert.Null(next); } [Fact] public void FullContent() { var ch = new EmbeddedChannel(new TestEncoder()); ch.WriteInbound(new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Get, "/")); var res = new DefaultFullHttpResponse(HttpVersion.Http11, HttpResponseStatus.OK, Unpooled.WrappedBuffer(new byte[42])); ch.WriteOutbound(res); AssertEncodedResponse(ch); var c = ch.ReadOutbound(); Assert.NotNull(c); Assert.Equal(2, c.Content.ReadableBytes); Assert.Equal("42", c.Content.ToString(Encoding.ASCII)); c.Release(); var last = ch.ReadOutbound(); Assert.NotNull(last); Assert.Equal(0, last.Content.ReadableBytes); last.Release(); var next = ch.ReadOutbound(); Assert.Null(next); } // If the length of the content is unknown, {@link HttpContentEncoder} should not skip encoding the content // even if the actual length is turned out to be 0. [Fact] public void EmptySplitContent() { var ch = new EmbeddedChannel(new TestEncoder()); ch.WriteInbound(new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Get, "/")); ch.WriteOutbound(new DefaultHttpResponse(HttpVersion.Http11, HttpResponseStatus.OK)); AssertEncodedResponse(ch); ch.WriteOutbound(EmptyLastHttpContent.Default); var chunk = ch.ReadOutbound(); Assert.NotNull(chunk); Assert.Equal("0", chunk.Content.ToString(Encoding.ASCII)); chunk.Release(); var last = ch.ReadOutbound(); Assert.False(last.Content.IsReadable()); last.Release(); var next = ch.ReadOutbound(); Assert.Null(next); } // If the length of the content is 0 for sure, {@link HttpContentEncoder} should skip encoding. [Fact] public void EmptyFullContent() { var ch = new EmbeddedChannel(new TestEncoder()); ch.WriteInbound(new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Get, "/")); IFullHttpResponse res = new DefaultFullHttpResponse(HttpVersion.Http11, HttpResponseStatus.OK, Unpooled.Empty); ch.WriteOutbound(res); res = ch.ReadOutbound(); Assert.NotNull(res); Assert.False(res.Headers.TryGet(HttpHeaderNames.TransferEncoding, out _)); // Content encoding shouldn't be modified. Assert.False(res.Headers.TryGet(HttpHeaderNames.ContentEncoding, out _)); Assert.Equal(0, res.Content.ReadableBytes); Assert.Equal("", res.Content.ToString(Encoding.ASCII)); res.Release(); var next = ch.ReadOutbound(); Assert.Null(next); } [Fact] public void EmptyFullContentWithTrailer() { var ch = new EmbeddedChannel(new TestEncoder()); ch.WriteInbound(new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Get, "/")); IFullHttpResponse res = new DefaultFullHttpResponse( HttpVersion.Http11, HttpResponseStatus.OK, Unpooled.Empty); res.TrailingHeaders.Set((AsciiString)"X-Test", (StringCharSequence)"Netty"); ch.WriteOutbound(res); res = ch.ReadOutbound(); Assert.NotNull(res); Assert.False(res.Headers.TryGet(HttpHeaderNames.TransferEncoding, out _)); // Content encoding shouldn't be modified. Assert.False(res.Headers.TryGet(HttpHeaderNames.ContentEncoding, out _)); Assert.Equal(0, res.Content.ReadableBytes); Assert.Equal("", res.Content.ToString(Encoding.ASCII)); Assert.Equal("Netty", res.TrailingHeaders.Get((AsciiString)"X-Test", null)); var next = ch.ReadOutbound(); Assert.Null(next); } [Fact] public void EmptyHeadResponse() { var ch = new EmbeddedChannel(new TestEncoder()); var req = new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Head, "/"); ch.WriteInbound(req); var res = new DefaultHttpResponse(HttpVersion.Http11, HttpResponseStatus.OK); res.Headers.Set(HttpHeaderNames.TransferEncoding, HttpHeaderValues.Chunked); ch.WriteOutbound(res); ch.WriteOutbound(EmptyLastHttpContent.Default); AssertEmptyResponse(ch); } [Fact] public void Http304Response() { var ch = new EmbeddedChannel(new TestEncoder()); var req = new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Head, "/"); req.Headers.Set(HttpHeaderNames.AcceptEncoding, HttpHeaderValues.Gzip); ch.WriteInbound(req); var res = new DefaultHttpResponse(HttpVersion.Http11, HttpResponseStatus.NotModified); res.Headers.Set(HttpHeaderNames.TransferEncoding, HttpHeaderValues.Chunked); ch.WriteOutbound(res); ch.WriteOutbound(EmptyLastHttpContent.Default); AssertEmptyResponse(ch); } [Fact] public void Connect200Response() { var ch = new EmbeddedChannel(new TestEncoder()); var req = new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Connect, "google.com:80"); ch.WriteInbound(req); var res = new DefaultHttpResponse(HttpVersion.Http11, HttpResponseStatus.OK); res.Headers.Set(HttpHeaderNames.TransferEncoding, HttpHeaderValues.Chunked); ch.WriteOutbound(res); ch.WriteOutbound(EmptyLastHttpContent.Default); AssertEmptyResponse(ch); } [Fact] public void ConnectFailureResponse() { const string Content = "Not allowed by configuration"; var ch = new EmbeddedChannel(new TestEncoder()); var req = new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Connect, "google.com:80"); ch.WriteInbound(req); var res = new DefaultHttpResponse(HttpVersion.Http11, HttpResponseStatus.MethodNotAllowed); res.Headers.Set(HttpHeaderNames.TransferEncoding, HttpHeaderValues.Chunked); ch.WriteOutbound(res); ch.WriteOutbound(new DefaultHttpContent(Unpooled.WrappedBuffer(Encoding.ASCII.GetBytes(Content)))); ch.WriteOutbound(EmptyLastHttpContent.Default); AssertEncodedResponse(ch); var chunk = ch.ReadOutbound(); Assert.NotNull(chunk); Assert.Equal("28", chunk.Content.ToString(Encoding.ASCII)); chunk.Release(); chunk = ch.ReadOutbound(); Assert.True(chunk.Content.IsReadable()); Assert.Equal("0", chunk.Content.ToString(Encoding.ASCII)); chunk.Release(); var last = ch.ReadOutbound(); Assert.NotNull(last); last.Release(); var next = ch.ReadOutbound(); Assert.Null(next); } [Fact] public void Http10() { var ch = new EmbeddedChannel(new TestEncoder()); var req = new DefaultFullHttpRequest(HttpVersion.Http10, HttpMethod.Get, "/"); Assert.True(ch.WriteInbound(req)); var res = new DefaultHttpResponse(HttpVersion.Http10, HttpResponseStatus.OK); res.Headers.Set(HttpHeaderNames.ContentLength, HttpHeaderValues.Zero); Assert.True(ch.WriteOutbound(res)); Assert.True(ch.WriteOutbound(EmptyLastHttpContent.Default)); Assert.True(ch.Finish()); var request = ch.ReadInbound(); Assert.True(request.Release()); var next = ch.ReadInbound(); Assert.Null(next); var response = ch.ReadOutbound(); Assert.Same(res, response); var content = ch.ReadOutbound(); Assert.Same(content, EmptyLastHttpContent.Default); content.Release(); next = ch.ReadOutbound(); Assert.Null(next); } [Fact] public void CleanupThrows() { var encoder = new CleanupEncoder(); var inboundHandler = new InboundHandler(); var channel = new EmbeddedChannel(encoder, inboundHandler); Assert.True(channel.WriteInbound(new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Get, "/"))); Assert.True(channel.WriteOutbound(new DefaultHttpResponse(HttpVersion.Http11, HttpResponseStatus.OK))); var content = new DefaultHttpContent(Unpooled.Buffer().WriteZero(10)); Assert.True(channel.WriteOutbound(content)); Assert.Equal(1, content.ReferenceCount); Assert.Throws(() => channel.FinishAndReleaseAll()); Assert.Equal(1, inboundHandler.ChannelInactiveCalled); Assert.Equal(0, content.ReferenceCount); } sealed class CleanupEncoder : HttpContentEncoder { protected override Result BeginEncode(IHttpResponse headers, ICharSequence acceptEncoding) => new Result(new StringCharSequence("myencoding"), new EmbeddedChannel(new Handler())); sealed class Handler : ChannelHandlerAdapter { public override void ChannelInactive(IChannelHandlerContext context) { context.FireExceptionCaught(new EncoderException("CleanupThrows")); context.FireChannelInactive(); } } } sealed class InboundHandler : ChannelHandlerAdapter { public int ChannelInactiveCalled; public override void ChannelInactive(IChannelHandlerContext context) { Interlocked.CompareExchange(ref this.ChannelInactiveCalled, 1, 0); base.ChannelInactive(context); } } static void AssertEmptyResponse(EmbeddedChannel ch) { var res = ch.ReadOutbound(); Assert.NotNull(res); Assert.False(res is IHttpContent); Assert.Equal("chunked", res.Headers.Get(HttpHeaderNames.TransferEncoding, null).ToString()); Assert.False(res.Headers.TryGet(HttpHeaderNames.ContentLength, out _)); var chunk = ch.ReadOutbound(); Assert.NotNull(chunk); chunk.Release(); var next = ch.ReadOutbound(); Assert.Null(next); } static void AssertEncodedResponse(EmbeddedChannel ch) { var res = ch.ReadOutbound(); Assert.NotNull(res); Assert.False(res is IHttpContent, $"{res.GetType()}"); Assert.Equal("chunked", res.Headers.Get(HttpHeaderNames.TransferEncoding, null).ToString()); Assert.False(res.Headers.TryGet(HttpHeaderNames.ContentLength, out _)); Assert.Equal("test", res.Headers.Get(HttpHeaderNames.ContentEncoding, null).ToString()); } } }