Files
2018-04-11 09:31:39 -07:00

443 lines
21 KiB
C#

// 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.Multipart
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using DotNetty.Buffers;
using DotNetty.Codecs.Http.Multipart;
using DotNetty.Common.Utilities;
using Xunit;
public sealed class HttpPostRequestEncoderTest : IDisposable
{
readonly List<IDisposable> files = new List<IDisposable>();
[Fact]
public void AllowedMethods()
{
FileStream fileStream = File.Open("./Multipart/file-01.txt", FileMode.Open, FileAccess.Read);
this.files.Add(fileStream);
ShouldThrowExceptionIfNotAllowed(HttpMethod.Connect, fileStream);
ShouldThrowExceptionIfNotAllowed(HttpMethod.Put, fileStream);
ShouldThrowExceptionIfNotAllowed(HttpMethod.Post, fileStream);
ShouldThrowExceptionIfNotAllowed(HttpMethod.Patch, fileStream);
ShouldThrowExceptionIfNotAllowed(HttpMethod.Delete, fileStream);
ShouldThrowExceptionIfNotAllowed(HttpMethod.Get, fileStream);
ShouldThrowExceptionIfNotAllowed(HttpMethod.Head, fileStream);
ShouldThrowExceptionIfNotAllowed(HttpMethod.Options, fileStream);
Assert.Throws<ErrorDataEncoderException>(() => ShouldThrowExceptionIfNotAllowed(HttpMethod.Trace, fileStream));
}
static void ShouldThrowExceptionIfNotAllowed(HttpMethod method, FileStream fileStream)
{
fileStream.Position = 0; // Reset to the begining
var request = new DefaultFullHttpRequest(HttpVersion.Http11, method, "http://localhost");
var encoder = new HttpPostRequestEncoder(request, true);
encoder.AddBodyAttribute("foo", "bar");
encoder.AddBodyFileUpload("quux", fileStream, "text/plain", false);
string multipartDataBoundary = encoder.MultipartDataBoundary;
string content = GetRequestBody(encoder);
string expected = "--" + multipartDataBoundary + "\r\n" +
HttpHeaderNames.ContentDisposition + ": form-data; name=\"foo\"" + "\r\n" +
HttpHeaderNames.ContentLength + ": 3" + "\r\n" +
HttpHeaderNames.ContentType + ": text/plain; charset=utf-8" + "\r\n" +
"\r\n" +
"bar" +
"\r\n" +
"--" + multipartDataBoundary + "\r\n" +
HttpHeaderNames.ContentDisposition + ": form-data; name=\"quux\"; filename=\"file-01.txt\"" + "\r\n" +
HttpHeaderNames.ContentLength + ": " + fileStream.Length + "\r\n" +
HttpHeaderNames.ContentType + ": text/plain" + "\r\n" +
HttpHeaderNames.ContentTransferEncoding + ": binary" + "\r\n" +
"\r\n" +
"File 01" + StringUtil.Newline +
"\r\n" +
"--" + multipartDataBoundary + "--" + "\r\n";
Assert.Equal(expected, content);
}
[Fact]
public void SingleFileUploadNoName()
{
var request = new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Post, "http://localhost");
var encoder = new HttpPostRequestEncoder(request, true);
FileStream fileStream = File.Open("./Multipart/file-01.txt", FileMode.Open, FileAccess.Read);
this.files.Add(fileStream);
encoder.AddBodyAttribute("foo", "bar");
encoder.AddBodyFileUpload("quux", "", fileStream, "text/plain", false);
string multipartDataBoundary = encoder.MultipartDataBoundary;
string content = GetRequestBody(encoder);
string expected = "--" + multipartDataBoundary + "\r\n" +
HttpHeaderNames.ContentDisposition + ": form-data; name=\"foo\"" + "\r\n" +
HttpHeaderNames.ContentLength + ": 3" + "\r\n" +
HttpHeaderNames.ContentType + ": text/plain; charset=utf-8" + "\r\n" +
"\r\n" +
"bar" +
"\r\n" +
"--" + multipartDataBoundary + "\r\n" +
HttpHeaderNames.ContentDisposition + ": form-data; name=\"quux\"\r\n" +
HttpHeaderNames.ContentLength + ": " + fileStream.Length + "\r\n" +
HttpHeaderNames.ContentType + ": text/plain" + "\r\n" +
HttpHeaderNames.ContentTransferEncoding + ": binary" + "\r\n" +
"\r\n" +
"File 01" + StringUtil.Newline +
"\r\n" +
"--" + multipartDataBoundary + "--" + "\r\n";
Assert.Equal(expected, content);
}
[Fact]
public void MultiFileUploadInMixedMode()
{
var request = new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Post, "http://localhost");
var encoder = new HttpPostRequestEncoder(request, true);
FileStream fileStream1 = File.Open("./Multipart/file-01.txt", FileMode.Open, FileAccess.Read);
this.files.Add(fileStream1);
FileStream fileStream2 = File.Open("./Multipart/file-02.txt", FileMode.Open, FileAccess.Read);
this.files.Add(fileStream2);
encoder.AddBodyAttribute("foo", "bar");
encoder.AddBodyFileUpload("quux", fileStream1, "text/plain", false);
encoder.AddBodyFileUpload("quux", fileStream2, "text/plain", false);
// We have to query the value of these two fields before finalizing
// the request, which unsets one of them.
string multipartDataBoundary = encoder.MultipartDataBoundary;
string multipartMixedBoundary = encoder.MultipartMixedBoundary;
string content = GetRequestBody(encoder);
string expected = "--" + multipartDataBoundary + "\r\n" +
HttpHeaderNames.ContentDisposition + ": form-data; name=\"foo\"" + "\r\n" +
HttpHeaderNames.ContentLength + ": 3" + "\r\n" +
HttpHeaderNames.ContentType + ": text/plain; charset=utf-8" + "\r\n" +
"\r\n" +
"bar" + "\r\n" +
"--" + multipartDataBoundary + "\r\n" +
HttpHeaderNames.ContentDisposition + ": form-data; name=\"quux\"" + "\r\n" +
HttpHeaderNames.ContentType + ": multipart/mixed; boundary=" + multipartMixedBoundary + "\r\n" +
"\r\n" +
"--" + multipartMixedBoundary + "\r\n" +
HttpHeaderNames.ContentDisposition + ": attachment; filename=\"file-02.txt\"" + "\r\n" +
HttpHeaderNames.ContentLength + ": " + fileStream1.Length + "\r\n" +
HttpHeaderNames.ContentType + ": text/plain" + "\r\n" +
HttpHeaderNames.ContentTransferEncoding + ": binary" + "\r\n" +
"\r\n" +
"File 01" + StringUtil.Newline +
"\r\n" +
"--" + multipartMixedBoundary + "\r\n" +
HttpHeaderNames.ContentDisposition + ": attachment; filename=\"file-02.txt\"" + "\r\n" +
HttpHeaderNames.ContentLength + ": " + fileStream2.Length + "\r\n" +
HttpHeaderNames.ContentType + ": text/plain" + "\r\n" +
HttpHeaderNames.ContentTransferEncoding + ": binary" + "\r\n" +
"\r\n" +
"File 02" + StringUtil.Newline +
"\r\n" +
"--" + multipartMixedBoundary + "--" + "\r\n" +
"--" + multipartDataBoundary + "--" + "\r\n";
Assert.Equal(expected, content);
}
[Fact]
public void MultiFileUploadInMixedModeNoName()
{
var request = new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Post, "http://localhost");
var encoder = new HttpPostRequestEncoder(request, true);
FileStream fileStream1 = File.Open("./Multipart/file-01.txt", FileMode.Open, FileAccess.Read);
this.files.Add(fileStream1);
FileStream fileStream2 = File.Open("./Multipart/file-02.txt", FileMode.Open, FileAccess.Read);
this.files.Add(fileStream2);
encoder.AddBodyAttribute("foo", "bar");
encoder.AddBodyFileUpload("quux", "", fileStream1, "text/plain", false);
encoder.AddBodyFileUpload("quux", "", fileStream2, "text/plain", false);
// We have to query the value of these two fields before finalizing
// the request, which unsets one of them.
string multipartDataBoundary = encoder.MultipartDataBoundary;
string multipartMixedBoundary = encoder.MultipartMixedBoundary;
string content = GetRequestBody(encoder);
string expected = "--" + multipartDataBoundary + "\r\n" +
HttpHeaderNames.ContentDisposition + ": form-data; name=\"foo\"" + "\r\n" +
HttpHeaderNames.ContentLength + ": 3" + "\r\n" +
HttpHeaderNames.ContentType + ": text/plain; charset=utf-8" + "\r\n" +
"\r\n" +
"bar" + "\r\n" +
"--" + multipartDataBoundary + "\r\n" +
HttpHeaderNames.ContentDisposition + ": form-data; name=\"quux\"" + "\r\n" +
HttpHeaderNames.ContentType + ": multipart/mixed; boundary=" + multipartMixedBoundary + "\r\n" +
"\r\n" +
"--" + multipartMixedBoundary + "\r\n" +
HttpHeaderNames.ContentDisposition + ": attachment\r\n" +
HttpHeaderNames.ContentLength + ": " + fileStream1.Length + "\r\n" +
HttpHeaderNames.ContentType + ": text/plain" + "\r\n" +
HttpHeaderNames.ContentTransferEncoding + ": binary" + "\r\n" +
"\r\n" +
"File 01" + StringUtil.Newline +
"\r\n" +
"--" + multipartMixedBoundary + "\r\n" +
HttpHeaderNames.ContentDisposition + ": attachment\r\n" +
HttpHeaderNames.ContentLength + ": " + fileStream2.Length + "\r\n" +
HttpHeaderNames.ContentType + ": text/plain" + "\r\n" +
HttpHeaderNames.ContentTransferEncoding + ": binary" + "\r\n" +
"\r\n" +
"File 02" + StringUtil.Newline +
"\r\n" +
"--" + multipartMixedBoundary + "--" + "\r\n" +
"--" + multipartDataBoundary + "--" + "\r\n";
Assert.Equal(expected, content);
}
[Fact]
public void SingleFileUploadInHtml5Mode()
{
var request = new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Post, "http://localhost");
var factory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MinSize);
var encoder = new HttpPostRequestEncoder(
factory,
request,
true,
Encoding.UTF8,
HttpPostRequestEncoder.EncoderMode.HTML5);
FileStream fileStream1 = File.Open("./Multipart/file-01.txt", FileMode.Open, FileAccess.Read);
this.files.Add(fileStream1);
FileStream fileStream2 = File.Open("./Multipart/file-02.txt", FileMode.Open, FileAccess.Read);
this.files.Add(fileStream2);
encoder.AddBodyAttribute("foo", "bar");
encoder.AddBodyFileUpload("quux", fileStream1, "text/plain", false);
encoder.AddBodyFileUpload("quux", fileStream2, "text/plain", false);
string multipartDataBoundary = encoder.MultipartDataBoundary;
string content = GetRequestBody(encoder);
string expected = "--" + multipartDataBoundary + "\r\n" +
HttpHeaderNames.ContentDisposition + ": form-data; name=\"foo\"" + "\r\n" +
HttpHeaderNames.ContentLength + ": 3" + "\r\n" +
HttpHeaderNames.ContentType + ": text/plain; charset=utf-8" + "\r\n" +
"\r\n" +
"bar" + "\r\n" +
"--" + multipartDataBoundary + "\r\n" +
HttpHeaderNames.ContentDisposition + ": form-data; name=\"quux\"; filename=\"file-01.txt\"" + "\r\n" +
HttpHeaderNames.ContentLength + ": " + fileStream1.Length + "\r\n" +
HttpHeaderNames.ContentType + ": text/plain" + "\r\n" +
HttpHeaderNames.ContentTransferEncoding + ": binary" + "\r\n" +
"\r\n" +
"File 01" + StringUtil.Newline + "\r\n" +
"--" + multipartDataBoundary + "\r\n" +
HttpHeaderNames.ContentDisposition + ": form-data; name=\"quux\"; filename=\"file-02.txt\"" + "\r\n" +
HttpHeaderNames.ContentLength + ": " + fileStream2.Length + "\r\n" +
HttpHeaderNames.ContentType + ": text/plain" + "\r\n" +
HttpHeaderNames.ContentTransferEncoding + ": binary" + "\r\n" +
"\r\n" +
"File 02" + StringUtil.Newline +
"\r\n" +
"--" + multipartDataBoundary + "--" + "\r\n";
Assert.Equal(expected, content);
}
[Fact]
public void MultiFileUploadInHtml5Mode()
{
var request = new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Post, "http://localhost");
var factory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MinSize);
var encoder = new HttpPostRequestEncoder(
factory,
request,
true,
Encoding.UTF8,
HttpPostRequestEncoder.EncoderMode.HTML5);
FileStream fileStream1 = File.Open("./Multipart/file-01.txt", FileMode.Open, FileAccess.Read);
this.files.Add(fileStream1);
encoder.AddBodyAttribute("foo", "bar");
encoder.AddBodyFileUpload("quux", fileStream1, "text/plain", false);
string multipartDataBoundary = encoder.MultipartDataBoundary;
string content = GetRequestBody(encoder);
string expected = "--" + multipartDataBoundary + "\r\n" +
HttpHeaderNames.ContentDisposition + ": form-data; name=\"foo\"" + "\r\n" +
HttpHeaderNames.ContentLength + ": 3" + "\r\n" +
HttpHeaderNames.ContentType + ": text/plain; charset=utf-8" + "\r\n" +
"\r\n" +
"bar" +
"\r\n" +
"--" + multipartDataBoundary + "\r\n" +
HttpHeaderNames.ContentDisposition + ": form-data; name=\"quux\"; filename=\"file-01.txt\"" + "\r\n" +
HttpHeaderNames.ContentLength + ": " + fileStream1.Length + "\r\n" +
HttpHeaderNames.ContentType + ": text/plain" + "\r\n" +
HttpHeaderNames.ContentTransferEncoding + ": binary" + "\r\n" +
"\r\n" +
"File 01" + StringUtil.Newline +
"\r\n" +
"--" + multipartDataBoundary + "--" + "\r\n";
Assert.Equal(expected, content);
}
[Fact]
public void HttpPostRequestEncoderSlicedBuffer()
{
var request = new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Post, "http://localhost");
var encoder = new HttpPostRequestEncoder(request, true);
// add Form attribute
encoder.AddBodyAttribute("getform", "POST");
encoder.AddBodyAttribute("info", "first value");
encoder.AddBodyAttribute("secondinfo", "secondvalue a&");
encoder.AddBodyAttribute("thirdinfo", "short text");
const int Length = 100000;
var array = new char[Length];
array.Fill('a');
string longText = new string(array);
encoder.AddBodyAttribute("fourthinfo", longText.Substring(0, 7470));
FileStream fileStream1 = File.Open("./Multipart/file-01.txt", FileMode.Open, FileAccess.Read);
this.files.Add(fileStream1);
encoder.AddBodyFileUpload("myfile", fileStream1, "application/x-zip-compressed", false);
encoder.FinalizeRequest();
while (!encoder.IsEndOfInput)
{
IHttpContent httpContent = encoder.ReadChunk(null);
IByteBuffer content = httpContent.Content;
int refCnt = content.ReferenceCount;
Assert.True(
(ReferenceEquals(content.Unwrap(), content) || content.Unwrap() == null) && refCnt == 1
|| !ReferenceEquals(content.Unwrap(), content) && refCnt == 2,
"content: " + content + " content.unwrap(): " + content.Unwrap() + " refCnt: " + refCnt);
httpContent.Release();
}
encoder.CleanFiles();
encoder.Close();
}
static string GetRequestBody(HttpPostRequestEncoder encoder)
{
encoder.FinalizeRequest();
List<IInterfaceHttpData> chunks = encoder.MultipartHttpDatas;
var buffers = new IByteBuffer[chunks.Count];
for (int i = 0; i < buffers.Length; i++)
{
IInterfaceHttpData data = chunks[i];
if (data is InternalAttribute attribute)
{
buffers[i] = attribute.ToByteBuffer();
}
else if (data is IHttpData httpData)
{
buffers[i] = httpData.GetByteBuffer();
}
}
IByteBuffer content = Unpooled.WrappedBuffer(buffers);
string contentStr = content.ToString(Encoding.UTF8);
content.Release();
return contentStr;
}
[Fact]
public void DataIsMultipleOfChunkSize1()
{
var factory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MinSize);
var request = new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Post, "http://localhost");
var encoder = new HttpPostRequestEncoder(factory, request, true,
HttpConstants.DefaultEncoding, HttpPostRequestEncoder.EncoderMode.RFC1738);
var first = new MemoryFileUpload("resources", "", "application/json", null, Encoding.UTF8, -1);
first.MaxSize = -1;
first.SetContent(new MemoryStream(new byte[7955]));
encoder.AddBodyHttpData(first);
var second = new MemoryFileUpload("resources2", "", "application/json", null, Encoding.UTF8, -1);
second.MaxSize = -1;
second.SetContent(new MemoryStream(new byte[7928]));
encoder.AddBodyHttpData(second);
Assert.NotNull(encoder.FinalizeRequest());
CheckNextChunkSize(encoder, 8080);
CheckNextChunkSize(encoder, 8055);
IHttpContent httpContent = encoder.ReadChunk(default(IByteBufferAllocator));
Assert.True(httpContent is ILastHttpContent, "Expected LastHttpContent is not received");
httpContent.Release();
Assert.True(encoder.IsEndOfInput, "Expected end of input is not receive");
}
[Fact]
public void DataIsMultipleOfChunkSize2()
{
var request = new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Post, "http://localhost");
var encoder = new HttpPostRequestEncoder(request, true);
const int Length = 7943;
var array = new char[Length];
array.Fill('a');
string longText = new string(array);
encoder.AddBodyAttribute("foo", longText);
Assert.NotNull(encoder.FinalizeRequest());
// In Netty this is 8080 due to random long hex size difference
CheckNextChunkSize(encoder, 109 + Length + 8);
IHttpContent httpContent = encoder.ReadChunk(default(IByteBufferAllocator));
Assert.True(httpContent is ILastHttpContent, "Expected LastHttpContent is not received");
httpContent.Release();
Assert.True(encoder.IsEndOfInput, "Expected end of input is not receive");
}
static void CheckNextChunkSize(HttpPostRequestEncoder encoder, int sizeWithoutDelimiter)
{
// 16 bytes as HttpPostRequestEncoder uses Long.toHexString(...) to generate a hex-string which will be between
// 2 and 16 bytes.
// See https://github.com/netty/netty/blob/4.1/codec-http/src/main/java/io/netty/handler/
// codec/http/multipart/HttpPostRequestEncoder.java#L291
int expectedSizeMin = sizeWithoutDelimiter + (2 + 2); // Two multipar boundary strings
int expectedSizeMax = sizeWithoutDelimiter + (16 + 16); // Two multipar boundary strings
IHttpContent httpContent = encoder.ReadChunk(default(IByteBufferAllocator));
int readable = httpContent.Content.ReadableBytes;
bool expectedSize = readable >= expectedSizeMin && readable <= expectedSizeMax;
Assert.True(expectedSize, $"Chunk size is not in expected range ({expectedSizeMin} - {expectedSizeMax}), was: {readable}");
httpContent.Release();
}
public void Dispose()
{
foreach (IDisposable file in this.files)
{
file.Dispose();
}
}
}
}