зеркало из https://github.com/Azure/DotNetty.git
Родитель
997e60f19b
Коммит
f227f95966
|
@ -42,9 +42,10 @@
|
|||
*.fs text=auto
|
||||
*.fsx text=auto
|
||||
*.hs text=auto
|
||||
*.txt eol=crlf
|
||||
|
||||
*.csproj text=auto
|
||||
*.vbproj text=auto
|
||||
*.fsproj text=auto
|
||||
*.dbproj text=auto
|
||||
*.sln text=auto eol=crlf
|
||||
*.sln text=auto eol=crlf
|
||||
|
|
27
DotNetty.sln
27
DotNetty.sln
|
@ -1,7 +1,7 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.27004.2010
|
||||
VisualStudioVersion = 15.0.27130.2024
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{F5B1CA65-5852-41C6-9D6F-184A3889237B}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
|
@ -95,7 +95,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetty.Microbench", "test
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetty.Transport.Libuv", "src\DotNetty.Transport.Libuv\DotNetty.Transport.Libuv.csproj", "{9FE6A783-C20D-4097-9988-4178E2C4CE75}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetty.Transport.Libuv.Tests", "test\DotNetty.Transport.Libuv.Tests\DotNetty.Transport.Libuv.Tests.csproj", "{1012C962-7F6D-4EC5-A0EC-0741A95BAD6B}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetty.Transport.Libuv.Tests", "test\DotNetty.Transport.Libuv.Tests\DotNetty.Transport.Libuv.Tests.csproj", "{1012C962-7F6D-4EC5-A0EC-0741A95BAD6B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpServer", "examples\HttpServer\HttpServer.csproj", "{A7CACAE7-66E7-43DA-948B-28EB0DDDB582}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetty.Codecs.Http", "src\DotNetty.Codecs.Http\DotNetty.Codecs.Http.csproj", "{5F68A5B1-7907-4B16-8AFE-326E9DD7D65B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetty.Codecs.Http.Tests", "test\DotNetty.Codecs.Http.Tests\DotNetty.Codecs.Http.Tests.csproj", "{16C89E7C-1575-4685-8DFA-8E7E2C6101BF}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
@ -255,6 +261,18 @@ Global
|
|||
{1012C962-7F6D-4EC5-A0EC-0741A95BAD6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1012C962-7F6D-4EC5-A0EC-0741A95BAD6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1012C962-7F6D-4EC5-A0EC-0741A95BAD6B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A7CACAE7-66E7-43DA-948B-28EB0DDDB582}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A7CACAE7-66E7-43DA-948B-28EB0DDDB582}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A7CACAE7-66E7-43DA-948B-28EB0DDDB582}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A7CACAE7-66E7-43DA-948B-28EB0DDDB582}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5F68A5B1-7907-4B16-8AFE-326E9DD7D65B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5F68A5B1-7907-4B16-8AFE-326E9DD7D65B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5F68A5B1-7907-4B16-8AFE-326E9DD7D65B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5F68A5B1-7907-4B16-8AFE-326E9DD7D65B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{16C89E7C-1575-4685-8DFA-8E7E2C6101BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{16C89E7C-1575-4685-8DFA-8E7E2C6101BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{16C89E7C-1575-4685-8DFA-8E7E2C6101BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{16C89E7C-1575-4685-8DFA-8E7E2C6101BF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -298,9 +316,12 @@ Global
|
|||
{7155D1E6-00CE-4081-B922-E6C5524EE600} = {541093F6-616E-43D9-B671-FCD1F9C0A181}
|
||||
{9FE6A783-C20D-4097-9988-4178E2C4CE75} = {126EA539-4B28-4B07-8B5D-D1D7F794D189}
|
||||
{1012C962-7F6D-4EC5-A0EC-0741A95BAD6B} = {541093F6-616E-43D9-B671-FCD1F9C0A181}
|
||||
{A7CACAE7-66E7-43DA-948B-28EB0DDDB582} = {F716F1EF-81EF-4020-914A-5422A13A9E13}
|
||||
{5F68A5B1-7907-4B16-8AFE-326E9DD7D65B} = {126EA539-4B28-4B07-8B5D-D1D7F794D189}
|
||||
{16C89E7C-1575-4685-8DFA-8E7E2C6101BF} = {541093F6-616E-43D9-B671-FCD1F9C0A181}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {1FBD8DF1-D90A-4F21-8EB6-DA17B9431FE3}
|
||||
{9FE6A783-C20D-4097-9988-4178E2C4CE75} = {126EA539-4B28-4B07-8B5D-D1D7F794D189}
|
||||
SolutionGuid = {1FBD8DF1-D90A-4F21-8EB6-DA17B9431FE3}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace HttpServer
|
||||
{
|
||||
using System.Text;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Codecs.Http;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
using System;
|
||||
using DotNetty.Common;
|
||||
|
||||
sealed class HelloServerHandler : ChannelHandlerAdapter
|
||||
{
|
||||
static readonly ThreadLocalCache Cache = new ThreadLocalCache();
|
||||
|
||||
sealed class ThreadLocalCache : FastThreadLocal<AsciiString>
|
||||
{
|
||||
protected override AsciiString GetInitialValue()
|
||||
{
|
||||
DateTime dateTime = DateTime.UtcNow;
|
||||
return AsciiString.Cached($"{dateTime.DayOfWeek}, {dateTime:dd MMM yyyy HH:mm:ss z}");
|
||||
}
|
||||
}
|
||||
|
||||
static readonly byte[] StaticPlaintext = Encoding.UTF8.GetBytes("Hello, World!");
|
||||
static readonly int StaticPlaintextLen = StaticPlaintext.Length;
|
||||
static readonly IByteBuffer PlaintextContentBuffer = Unpooled.UnreleasableBuffer(Unpooled.DirectBuffer().WriteBytes(StaticPlaintext));
|
||||
static readonly AsciiString PlaintextClheaderValue = AsciiString.Cached($"{StaticPlaintextLen}");
|
||||
static readonly AsciiString JsonClheaderValue = AsciiString.Cached($"{JsonLen()}");
|
||||
|
||||
static readonly AsciiString TypePlain = AsciiString.Cached("text/plain");
|
||||
static readonly AsciiString TypeJson = AsciiString.Cached("application/json");
|
||||
static readonly AsciiString ServerName = AsciiString.Cached("Netty");
|
||||
static readonly AsciiString ContentTypeEntity = HttpHeaderNames.ContentType;
|
||||
static readonly AsciiString DateEntity = HttpHeaderNames.Date;
|
||||
static readonly AsciiString ContentLengthEntity = HttpHeaderNames.ContentLength;
|
||||
static readonly AsciiString ServerEntity = HttpHeaderNames.Server;
|
||||
|
||||
volatile ICharSequence date = Cache.Value;
|
||||
|
||||
static int JsonLen() => Encoding.UTF8.GetBytes(NewMessage().ToJsonFormat()).Length;
|
||||
|
||||
static MessageBody NewMessage() => new MessageBody("Hello, World!");
|
||||
|
||||
public override void ChannelRead(IChannelHandlerContext ctx, object message)
|
||||
{
|
||||
if (message is IHttpRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Process(ctx, request);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ReferenceCountUtil.Release(message);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx.FireChannelRead(message);
|
||||
}
|
||||
}
|
||||
|
||||
void Process(IChannelHandlerContext ctx, IHttpRequest request)
|
||||
{
|
||||
string uri = request.Uri;
|
||||
switch (uri)
|
||||
{
|
||||
case "/plaintext":
|
||||
this.WriteResponse(ctx, PlaintextContentBuffer.Duplicate(), TypePlain, PlaintextClheaderValue);
|
||||
break;
|
||||
case "/json":
|
||||
byte[] json = Encoding.UTF8.GetBytes(NewMessage().ToJsonFormat());
|
||||
this.WriteResponse(ctx, Unpooled.WrappedBuffer(json), TypeJson, JsonClheaderValue);
|
||||
break;
|
||||
default:
|
||||
var response = new DefaultFullHttpResponse(HttpVersion.Http11, HttpResponseStatus.NotFound, Unpooled.Empty, false);
|
||||
ctx.WriteAndFlushAsync(response);
|
||||
ctx.CloseAsync();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void WriteResponse(IChannelHandlerContext ctx, IByteBuffer buf, ICharSequence contentType, ICharSequence contentLength)
|
||||
{
|
||||
// Build the response object.
|
||||
var response = new DefaultFullHttpResponse(HttpVersion.Http11, HttpResponseStatus.OK, buf, false);
|
||||
HttpHeaders headers = response.Headers;
|
||||
headers.Set(ContentTypeEntity, contentType);
|
||||
headers.Set(ServerEntity, ServerName);
|
||||
headers.Set(DateEntity, this.date);
|
||||
headers.Set(ContentLengthEntity, contentLength);
|
||||
|
||||
// Close the non-keep-alive connection after the write operation is done.
|
||||
ctx.WriteAsync(response);
|
||||
}
|
||||
|
||||
public override void ExceptionCaught(IChannelHandlerContext context, Exception exception) => context.CloseAsync();
|
||||
|
||||
public override void ChannelReadComplete(IChannelHandlerContext context) => context.Flush();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>netcoreapp1.1;net451</TargetFrameworks>
|
||||
<NetStandardImplicitPackageVersion Condition=" '$(TargetFramework)' == 'netcoreapp1.1' ">1.6.1</NetStandardImplicitPackageVersion>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(TargetFramework)' == 'net451' ">
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\..\shared\dotnetty.com.pfx" Link="dotnetty.com.pfx">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\DotNetty.Buffers\DotNetty.Buffers.csproj" />
|
||||
<ProjectReference Include="..\..\src\DotNetty.Codecs.Http\DotNetty.Codecs.Http.csproj" />
|
||||
<ProjectReference Include="..\..\src\DotNetty.Codecs\DotNetty.Codecs.csproj" />
|
||||
<ProjectReference Include="..\..\src\DotNetty.Common\DotNetty.Common.csproj" />
|
||||
<ProjectReference Include="..\..\src\DotNetty.Handlers\DotNetty.Handlers.csproj" />
|
||||
<ProjectReference Include="..\..\src\DotNetty.Transport.Libuv\DotNetty.Transport.Libuv.csproj" />
|
||||
<ProjectReference Include="..\..\src\DotNetty.Transport\DotNetty.Transport.csproj" />
|
||||
<ProjectReference Include="..\Examples.Common\Examples.Common.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace HttpServer
|
||||
{
|
||||
sealed class MessageBody
|
||||
{
|
||||
public MessageBody(string message)
|
||||
{
|
||||
this.Message = message;
|
||||
}
|
||||
|
||||
public string Message { get; }
|
||||
|
||||
public string ToJsonFormat() => "{" + $"\"{nameof(MessageBody)}\" :" + "{" + $"\"{nameof(this.Message)}\"" + " :\"" + this.Message + "\"}" +"}";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace HttpServer
|
||||
{
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Runtime;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Codecs.Http;
|
||||
using DotNetty.Common;
|
||||
using DotNetty.Handlers.Tls;
|
||||
using DotNetty.Transport.Bootstrapping;
|
||||
using DotNetty.Transport.Channels;
|
||||
using DotNetty.Transport.Channels.Sockets;
|
||||
using DotNetty.Transport.Libuv;
|
||||
using Examples.Common;
|
||||
|
||||
class Program
|
||||
{
|
||||
static Program()
|
||||
{
|
||||
ResourceLeakDetector.Level = ResourceLeakDetector.DetectionLevel.Disabled;
|
||||
}
|
||||
|
||||
static async Task RunServerAsync()
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"\n{RuntimeInformation.OSArchitecture} {RuntimeInformation.OSDescription}"
|
||||
+ $"\n{RuntimeInformation.ProcessArchitecture} {RuntimeInformation.FrameworkDescription}"
|
||||
+ $"\nProcessor Count : {Environment.ProcessorCount}\n");
|
||||
|
||||
bool useLibuv = ServerSettings.UseLibuv;
|
||||
Console.WriteLine("Transport type : " + (useLibuv ? "Libuv" : "Socket"));
|
||||
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Server garbage collection: {GCSettings.IsServerGC}");
|
||||
Console.WriteLine($"Current latency mode for garbage collection: {GCSettings.LatencyMode}");
|
||||
|
||||
IEventLoopGroup group;
|
||||
IEventLoopGroup workGroup;
|
||||
if (useLibuv)
|
||||
{
|
||||
var dispatcher = new DispatcherEventLoopGroup();
|
||||
group = dispatcher;
|
||||
workGroup = new WorkerEventLoopGroup(dispatcher);
|
||||
}
|
||||
else
|
||||
{
|
||||
group = new MultithreadEventLoopGroup(1);
|
||||
workGroup = new MultithreadEventLoopGroup();
|
||||
}
|
||||
|
||||
X509Certificate2 tlsCertificate = null;
|
||||
if (ServerSettings.IsSsl)
|
||||
{
|
||||
tlsCertificate = new X509Certificate2(Path.Combine(ExampleHelper.ProcessDirectory, "dotnetty.com.pfx"), "password");
|
||||
}
|
||||
try
|
||||
{
|
||||
var bootstrap = new ServerBootstrap();
|
||||
bootstrap.Group(group, workGroup);
|
||||
|
||||
if (useLibuv)
|
||||
{
|
||||
bootstrap.Channel<TcpServerChannel>();
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
||||
|| RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
bootstrap
|
||||
.Option(ChannelOption.SoReuseport, true)
|
||||
.ChildOption(ChannelOption.SoReuseaddr, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bootstrap.Channel<TcpServerSocketChannel>();
|
||||
}
|
||||
|
||||
bootstrap
|
||||
.Option(ChannelOption.SoBacklog, 8192)
|
||||
.ChildHandler(new ActionChannelInitializer<IChannel>(channel =>
|
||||
{
|
||||
IChannelPipeline pipeline = channel.Pipeline;
|
||||
if (tlsCertificate != null)
|
||||
{
|
||||
pipeline.AddLast(TlsHandler.Server(tlsCertificate));
|
||||
}
|
||||
pipeline.AddLast("encoder", new HttpResponseEncoder());
|
||||
pipeline.AddLast("decoder", new HttpRequestDecoder(4096, 8192, 8192, false));
|
||||
pipeline.AddLast("handler", new HelloServerHandler());
|
||||
}));
|
||||
|
||||
IChannel bootstrapChannel = await bootstrap.BindAsync(IPAddress.IPv6Any, ServerSettings.Port);
|
||||
|
||||
Console.WriteLine($"Httpd started. Listening on {bootstrapChannel.LocalAddress}");
|
||||
Console.ReadLine();
|
||||
|
||||
await bootstrapChannel.CloseAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
group.ShutdownGracefullyAsync().Wait();
|
||||
}
|
||||
}
|
||||
|
||||
static void Main() => RunServerAsync().Wait();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("HttpWebServer")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("d4efc310-c3a7-42a2-bc9c-aa9cce3d1c63")]
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"port": "7686",
|
||||
"libuv": "true"
|
||||
}
|
|
@ -491,6 +491,42 @@ namespace DotNetty.Buffers
|
|||
return value;
|
||||
}
|
||||
|
||||
public virtual unsafe ICharSequence GetCharSequence(int index, int length, Encoding encoding)
|
||||
{
|
||||
this.CheckIndex0(index, length);
|
||||
if (length == 0)
|
||||
{
|
||||
return StringCharSequence.Empty;
|
||||
}
|
||||
|
||||
if (this.HasMemoryAddress)
|
||||
{
|
||||
IntPtr ptr = this.AddressOfPinnedMemory();
|
||||
if (ptr != IntPtr.Zero)
|
||||
{
|
||||
return new StringCharSequence(UnsafeByteBufferUtil.GetString((byte*)(ptr + index), length, encoding));
|
||||
}
|
||||
else
|
||||
{
|
||||
fixed (byte* p = &this.GetPinnableMemoryAddress())
|
||||
return new StringCharSequence(UnsafeByteBufferUtil.GetString(p + index, length, encoding));
|
||||
}
|
||||
}
|
||||
if (this.HasArray)
|
||||
{
|
||||
return new StringCharSequence(encoding.GetString(this.Array, this.ArrayOffset + index, length));
|
||||
}
|
||||
|
||||
return new StringCharSequence(this.ToString(index, length, encoding));
|
||||
}
|
||||
|
||||
public virtual ICharSequence ReadCharSequence(int length, Encoding encoding)
|
||||
{
|
||||
ICharSequence sequence = this.GetCharSequence(this.readerIndex, length, encoding);
|
||||
this.readerIndex += length;
|
||||
return sequence;
|
||||
}
|
||||
|
||||
public virtual IByteBuffer SetByte(int index, int value)
|
||||
{
|
||||
this.CheckIndex(index);
|
||||
|
@ -633,6 +669,7 @@ namespace DotNetty.Buffers
|
|||
this.SetBytes(index, src, 0, src.Length);
|
||||
return this;
|
||||
}
|
||||
|
||||
public abstract IByteBuffer SetBytes(int index, byte[] src, int srcIndex, int length);
|
||||
|
||||
public virtual IByteBuffer SetBytes(int index, IByteBuffer src)
|
||||
|
@ -744,6 +781,48 @@ namespace DotNetty.Buffers
|
|||
return bytes.Length;
|
||||
}
|
||||
|
||||
public virtual int SetCharSequence(int index, ICharSequence sequence, Encoding encoding) => this.SetCharSequence0(index, sequence, encoding, false);
|
||||
|
||||
int SetCharSequence0(int index, ICharSequence sequence, Encoding encoding, bool expand)
|
||||
{
|
||||
if (ReferenceEquals(encoding, Encoding.UTF8))
|
||||
{
|
||||
int length = ByteBufferUtil.Utf8MaxBytes(sequence);
|
||||
if (expand)
|
||||
{
|
||||
this.EnsureWritable0(length);
|
||||
this.CheckIndex0(index, length);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.CheckIndex(index, length);
|
||||
}
|
||||
return ByteBufferUtil.WriteUtf8(this, index, sequence, sequence.Count);
|
||||
}
|
||||
if (ReferenceEquals(encoding, Encoding.ASCII))
|
||||
{
|
||||
int length = sequence.Count;
|
||||
if (expand)
|
||||
{
|
||||
this.EnsureWritable0(length);
|
||||
this.CheckIndex0(index, length);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.CheckIndex(index, length);
|
||||
}
|
||||
return ByteBufferUtil.WriteAscii(this, index, sequence, length);
|
||||
}
|
||||
byte[] bytes = encoding.GetBytes(sequence.ToString());
|
||||
if (expand)
|
||||
{
|
||||
this.EnsureWritable0(bytes.Length);
|
||||
// setBytes(...) will take care of checking the indices.
|
||||
}
|
||||
this.SetBytes(index, bytes);
|
||||
return bytes.Length;
|
||||
}
|
||||
|
||||
public virtual byte ReadByte()
|
||||
{
|
||||
this.CheckReadableBytes0(1);
|
||||
|
@ -1184,6 +1263,13 @@ namespace DotNetty.Buffers
|
|||
return this;
|
||||
}
|
||||
|
||||
public virtual int WriteCharSequence(ICharSequence sequence, Encoding encoding)
|
||||
{
|
||||
int written = this.SetCharSequence0(this.writerIndex, sequence, encoding, true);
|
||||
this.writerIndex += written;
|
||||
return written;
|
||||
}
|
||||
|
||||
public virtual int WriteString(string value, Encoding encoding)
|
||||
{
|
||||
int written = this.SetString0(this.writerIndex, value, encoding, true);
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
namespace DotNetty.Buffers
|
||||
{
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using DotNetty.Common;
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace DotNetty.Buffers
|
|||
{
|
||||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using DotNetty.Common.Internal;
|
||||
using DotNetty.Common.Internal.Logging;
|
||||
|
@ -68,8 +69,8 @@ namespace DotNetty.Buffers
|
|||
/// </summary>
|
||||
public static string HexDump(byte[] array, int fromIndex, int length) => HexUtil.DoHexDump(array, fromIndex, length);
|
||||
|
||||
public static bool EnsureWritableSuccess(int ensureWritableResult) => ensureWritableResult == 0 || ensureWritableResult == 2;
|
||||
|
||||
public static bool EnsureWritableSuccess(int ensureWritableResult) => ensureWritableResult == 0 || ensureWritableResult == 2;
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the hash code of the specified buffer. This method is
|
||||
/// useful when implementing a new buffer type.
|
||||
|
@ -298,6 +299,104 @@ namespace DotNetty.Buffers
|
|||
return buffer.ForEachByteDesc(toIndex, fromIndex - toIndex, new IndexOfProcessor(value));
|
||||
}
|
||||
|
||||
public static IByteBuffer WriteUtf8(IByteBufferAllocator alloc, ICharSequence seq)
|
||||
{
|
||||
// UTF-8 uses max. 3 bytes per char, so calculate the worst case.
|
||||
IByteBuffer buf = alloc.Buffer(Utf8MaxBytes(seq));
|
||||
WriteUtf8(buf, seq);
|
||||
return buf;
|
||||
}
|
||||
|
||||
public static int WriteUtf8(IByteBuffer buf, ICharSequence seq) => ReserveAndWriteUtf8(buf, seq, Utf8MaxBytes(seq));
|
||||
|
||||
public static int ReserveAndWriteUtf8(IByteBuffer buf, ICharSequence seq, int reserveBytes)
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
if (buf is AbstractByteBuffer byteBuf)
|
||||
{
|
||||
byteBuf.EnsureWritable0(reserveBytes);
|
||||
int written = WriteUtf8(byteBuf, byteBuf.WriterIndex, seq, seq.Count);
|
||||
byteBuf.SetWriterIndex(byteBuf.WriterIndex + written);
|
||||
return written;
|
||||
}
|
||||
else if (buf is WrappedByteBuffer)
|
||||
{
|
||||
// Unwrap as the wrapped buffer may be an AbstractByteBuf and so we can use fast-path.
|
||||
buf = buf.Unwrap();
|
||||
}
|
||||
else
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(seq.ToString());
|
||||
buf.WriteBytes(bytes);
|
||||
return bytes.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fast-Path implementation
|
||||
internal static int WriteUtf8(AbstractByteBuffer buffer, int writerIndex, ICharSequence value, int len)
|
||||
{
|
||||
int oldWriterIndex = writerIndex;
|
||||
|
||||
// We can use the _set methods as these not need to do any index checks and reference checks.
|
||||
// This is possible as we called ensureWritable(...) before.
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
char c = value[i];
|
||||
if (c < 0x80)
|
||||
{
|
||||
buffer._SetByte(writerIndex++, (byte)c);
|
||||
}
|
||||
else if (c < 0x800)
|
||||
{
|
||||
buffer._SetByte(writerIndex++, (byte)(0xc0 | (c >> 6)));
|
||||
buffer._SetByte(writerIndex++, (byte)(0x80 | (c & 0x3f)));
|
||||
}
|
||||
else if (char.IsSurrogate(c))
|
||||
{
|
||||
if (!char.IsHighSurrogate(c))
|
||||
{
|
||||
buffer._SetByte(writerIndex++, WriteUtfUnknown);
|
||||
continue;
|
||||
}
|
||||
char c2;
|
||||
try
|
||||
{
|
||||
// Surrogate Pair consumes 2 characters. Optimistically try to get the next character to avoid
|
||||
// duplicate bounds checking with charAt. If an IndexOutOfBoundsException is thrown we will
|
||||
// re-throw a more informative exception describing the problem.
|
||||
c2 = value[++i];
|
||||
}
|
||||
catch (IndexOutOfRangeException)
|
||||
{
|
||||
buffer._SetByte(writerIndex++, WriteUtfUnknown);
|
||||
break;
|
||||
}
|
||||
if (!char.IsLowSurrogate(c2))
|
||||
{
|
||||
buffer._SetByte(writerIndex++, WriteUtfUnknown);
|
||||
buffer._SetByte(writerIndex++, char.IsHighSurrogate(c2) ? WriteUtfUnknown : c2);
|
||||
continue;
|
||||
}
|
||||
int codePoint = CharUtil.ToCodePoint(c, c2);
|
||||
// See http://www.unicode.org/versions/Unicode7.0.0/ch03.pdf#G2630.
|
||||
buffer._SetByte(writerIndex++, (byte)(0xf0 | (codePoint >> 18)));
|
||||
buffer._SetByte(writerIndex++, (byte)(0x80 | ((codePoint >> 12) & 0x3f)));
|
||||
buffer._SetByte(writerIndex++, (byte)(0x80 | ((codePoint >> 6) & 0x3f)));
|
||||
buffer._SetByte(writerIndex++, (byte)(0x80 | (codePoint & 0x3f)));
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer._SetByte(writerIndex++, (byte)(0xe0 | (c >> 12)));
|
||||
buffer._SetByte(writerIndex++, (byte)(0x80 | ((c >> 6) & 0x3f)));
|
||||
buffer._SetByte(writerIndex++, (byte)(0x80 | (c & 0x3f)));
|
||||
}
|
||||
}
|
||||
|
||||
return writerIndex - oldWriterIndex;
|
||||
}
|
||||
|
||||
public static IByteBuffer WriteUtf8(IByteBufferAllocator alloc, string value)
|
||||
{
|
||||
// UTF-8 uses max. 3 bytes per char, so calculate the worst case.
|
||||
|
@ -403,6 +502,8 @@ namespace DotNetty.Buffers
|
|||
return writerIndex - oldWriterIndex;
|
||||
}
|
||||
|
||||
internal static int Utf8MaxBytes(ICharSequence seq) => Utf8MaxBytes(seq.Count);
|
||||
|
||||
internal static int Utf8MaxBytes(string seq) => Utf8MaxBytes(seq.Length);
|
||||
|
||||
internal static int Utf8MaxBytes(int seqLength) => seqLength * MaxBytesPerCharUtf8;
|
||||
|
@ -470,6 +571,61 @@ namespace DotNetty.Buffers
|
|||
return encodedLength;
|
||||
}
|
||||
|
||||
public static IByteBuffer WriteAscii(IByteBufferAllocator alloc, ICharSequence seq)
|
||||
{
|
||||
// ASCII uses 1 byte per char
|
||||
IByteBuffer buf = alloc.Buffer(seq.Count);
|
||||
WriteAscii(buf, seq);
|
||||
return buf;
|
||||
}
|
||||
|
||||
public static int WriteAscii(IByteBuffer buf, ICharSequence seq)
|
||||
{
|
||||
// ASCII uses 1 byte per char
|
||||
int len = seq.Count;
|
||||
if (seq is AsciiString asciiString)
|
||||
{
|
||||
buf.WriteBytes(asciiString.Array, asciiString.Offset, len);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
if (buf is AbstractByteBuffer byteBuf)
|
||||
{
|
||||
byteBuf.EnsureWritable0(len);
|
||||
int written = WriteAscii(byteBuf, byteBuf.WriterIndex, seq, len);
|
||||
byteBuf.SetWriterIndex(byteBuf.WriterIndex + written);
|
||||
return written;
|
||||
}
|
||||
else if (buf is WrappedByteBuffer)
|
||||
{
|
||||
// Unwrap as the wrapped buffer may be an AbstractByteBuf and so we can use fast-path.
|
||||
buf = buf.Unwrap();
|
||||
}
|
||||
else
|
||||
{
|
||||
byte[] bytes = Encoding.ASCII.GetBytes(seq.ToString());
|
||||
buf.WriteBytes(bytes);
|
||||
return bytes.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
// Fast-Path implementation
|
||||
internal static int WriteAscii(AbstractByteBuffer buffer, int writerIndex, ICharSequence seq, int len)
|
||||
{
|
||||
// We can use the _set methods as these not need to do any index checks and reference checks.
|
||||
// This is possible as we called ensureWritable(...) before.
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
buffer._SetByte(writerIndex++, AsciiString.CharToByte(seq[i]));
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
public static IByteBuffer WriteAscii(IByteBufferAllocator alloc, string value)
|
||||
{
|
||||
// ASCII uses 1 byte per char
|
||||
|
@ -592,6 +748,38 @@ namespace DotNetty.Buffers
|
|||
}
|
||||
}
|
||||
|
||||
public static void Copy(AsciiString src, IByteBuffer dst) => Copy(src, 0, dst, src.Count);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Copy(AsciiString src, int srcIdx, IByteBuffer dst, int dstIdx, int length)
|
||||
{
|
||||
if (MathUtil.IsOutOfBounds(srcIdx, length, src.Count))
|
||||
{
|
||||
ThrowHelper.ThrowIndexOutOfRangeException_Src(srcIdx, length, src.Count);
|
||||
}
|
||||
if (dst == null)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentNullException_Dst();
|
||||
}
|
||||
// ReSharper disable once PossibleNullReferenceException
|
||||
dst.SetBytes(dstIdx, src.Array, srcIdx + src.Offset, length);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Copy(AsciiString src, int srcIdx, IByteBuffer dst, int length)
|
||||
{
|
||||
if (MathUtil.IsOutOfBounds(srcIdx, length, src.Count))
|
||||
{
|
||||
ThrowHelper.ThrowIndexOutOfRangeException_Src(srcIdx, length, src.Count);
|
||||
}
|
||||
if (dst == null)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentNullException_Dst();
|
||||
}
|
||||
// ReSharper disable once PossibleNullReferenceException
|
||||
dst.WriteBytes(src.Array, srcIdx + src.Offset, length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a multi-line hexadecimal dump of the specified {@link ByteBuf} that is easy to read by humans.
|
||||
/// </summary>
|
||||
|
|
|
@ -156,6 +156,12 @@ namespace DotNetty.Buffers
|
|||
|
||||
public IByteBuffer GetBytes(int index, Stream destination, int length) => this.CheckIndex(index, length);
|
||||
|
||||
public ICharSequence GetCharSequence(int index, int length, Encoding encoding)
|
||||
{
|
||||
this.CheckIndex(index, length);
|
||||
return null;
|
||||
}
|
||||
|
||||
public string GetString(int index, int length, Encoding encoding)
|
||||
{
|
||||
this.CheckIndex(index, length);
|
||||
|
@ -218,6 +224,8 @@ namespace DotNetty.Buffers
|
|||
|
||||
public IByteBuffer SetZero(int index, int length) => this.CheckIndex(index, length);
|
||||
|
||||
public int SetCharSequence(int index, ICharSequence sequence, Encoding encoding) => throw new IndexOutOfRangeException();
|
||||
|
||||
public int SetString(int index, string value, Encoding encoding) => throw new IndexOutOfRangeException();
|
||||
|
||||
public bool ReadBoolean() => throw new IndexOutOfRangeException();
|
||||
|
@ -276,6 +284,12 @@ namespace DotNetty.Buffers
|
|||
|
||||
public IByteBuffer ReadBytes(Stream destination, int length) => this.CheckLength(length);
|
||||
|
||||
public ICharSequence ReadCharSequence(int length, Encoding encoding)
|
||||
{
|
||||
this.CheckLength(length);
|
||||
return null;
|
||||
}
|
||||
|
||||
public string ReadString(int length, Encoding encoding)
|
||||
{
|
||||
this.CheckLength(length);
|
||||
|
@ -338,6 +352,8 @@ namespace DotNetty.Buffers
|
|||
|
||||
public IByteBuffer WriteZero(int length) => this.CheckLength(length);
|
||||
|
||||
public int WriteCharSequence(ICharSequence sequence, Encoding encoding) => throw new IndexOutOfRangeException();
|
||||
|
||||
public int WriteString(string value, Encoding encoding) => throw new IndexOutOfRangeException();
|
||||
|
||||
public int IndexOf(int fromIndex, int toIndex, byte value)
|
||||
|
|
|
@ -483,6 +483,9 @@ namespace DotNetty.Buffers
|
|||
/// </exception>
|
||||
IByteBuffer GetBytes(int index, Stream destination, int length);
|
||||
|
||||
|
||||
ICharSequence GetCharSequence(int index, int length, Encoding encoding);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string with the given length at the given index.
|
||||
/// </summary>
|
||||
|
@ -781,6 +784,8 @@ namespace DotNetty.Buffers
|
|||
/// </exception>
|
||||
IByteBuffer SetZero(int index, int length);
|
||||
|
||||
int SetCharSequence(int index, ICharSequence sequence, Encoding encoding);
|
||||
|
||||
/// <summary>
|
||||
/// Writes the specified string at the current writer index and increases
|
||||
/// the writer index by the written bytes.
|
||||
|
@ -973,6 +978,8 @@ namespace DotNetty.Buffers
|
|||
|
||||
IByteBuffer ReadBytes(Stream destination, int length);
|
||||
|
||||
ICharSequence ReadCharSequence(int length, Encoding encoding);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string with the given length at the current reader index
|
||||
/// and increases the reader index by the given length.
|
||||
|
@ -1186,6 +1193,8 @@ namespace DotNetty.Buffers
|
|||
|
||||
IByteBuffer WriteZero(int length);
|
||||
|
||||
int WriteCharSequence(ICharSequence sequence, Encoding encoding);
|
||||
|
||||
int WriteString(string value, Encoding encoding);
|
||||
|
||||
int IndexOf(int fromIndex, int toIndex, byte value);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
namespace DotNetty.Buffers
|
||||
{
|
||||
/// <summary>
|
||||
/// Thread-safe interface for allocating <see cref="IByteBuffer" /> instances for use inside Helios reactive I/O
|
||||
/// Thread-safe interface for allocating <see cref="IByteBuffer" />/.
|
||||
/// </summary>
|
||||
public interface IByteBufferAllocator
|
||||
{
|
||||
|
|
|
@ -648,7 +648,7 @@ namespace DotNetty.Buffers
|
|||
.Append(i)
|
||||
.Append(": ");
|
||||
PoolSubpage<T> s = head.Next;
|
||||
for (; ;)
|
||||
for (; ; )
|
||||
{
|
||||
buf.Append(s);
|
||||
s = s.Next;
|
||||
|
@ -706,7 +706,7 @@ namespace DotNetty.Buffers
|
|||
// Rely on GC.
|
||||
}
|
||||
|
||||
protected override PooledByteBuffer<byte[]> NewByteBuf(int maxCapacity) =>
|
||||
protected override PooledByteBuffer<byte[]> NewByteBuf(int maxCapacity) =>
|
||||
PooledHeapByteBuffer.NewInstance(maxCapacity);
|
||||
|
||||
protected override void MemoryCopy(byte[] src, int srcOffset, byte[] dst, int dstOffset, int length)
|
||||
|
@ -751,7 +751,7 @@ namespace DotNetty.Buffers
|
|||
return chunk;
|
||||
}
|
||||
|
||||
protected override PooledByteBuffer<byte[]> NewByteBuf(int maxCapacity) =>
|
||||
protected override PooledByteBuffer<byte[]> NewByteBuf(int maxCapacity) =>
|
||||
PooledUnsafeDirectByteBuffer.NewInstance(maxCapacity);
|
||||
|
||||
protected override unsafe void MemoryCopy(byte[] src, int srcOffset, byte[] dst, int dstOffset, int length) =>
|
||||
|
|
|
@ -59,7 +59,11 @@ namespace DotNetty.Buffers
|
|||
this.DiscardMarks();
|
||||
}
|
||||
|
||||
public override int Capacity => this.Length;
|
||||
public override int Capacity
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => this.Length;
|
||||
}
|
||||
|
||||
public sealed override IByteBuffer AdjustCapacity(int newCapacity)
|
||||
{
|
||||
|
|
|
@ -36,7 +36,7 @@ namespace DotNetty.Buffers
|
|||
return this;
|
||||
}
|
||||
|
||||
public override int ArrayOffset => this.Unwrap().ArrayOffset;
|
||||
public override int ArrayOffset => this.Unwrap().ArrayOffset;
|
||||
|
||||
public override ref byte GetPinnableMemoryAddress() => ref this.Unwrap().GetPinnableMemoryAddress();
|
||||
|
||||
|
@ -103,4 +103,4 @@ namespace DotNetty.Buffers
|
|||
protected internal override void _SetLongLE(int index, long value) => this.UnwrapCore()._SetLongLE(index, value);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -41,7 +41,7 @@ namespace DotNetty.Buffers
|
|||
|
||||
public override int Capacity => this.MaxCapacity;
|
||||
|
||||
public override IByteBuffer AdjustCapacity(int newCapacity) =>throw new NotSupportedException("sliced buffer");
|
||||
public override IByteBuffer AdjustCapacity(int newCapacity) => throw new NotSupportedException("sliced buffer");
|
||||
|
||||
public override int ArrayOffset => this.Idx(this.Unwrap().ArrayOffset);
|
||||
|
||||
|
@ -308,4 +308,4 @@ namespace DotNetty.Buffers
|
|||
|
||||
int Idx(int index) => index + this.adjustment;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -143,6 +143,17 @@ namespace DotNetty.Buffers
|
|||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
internal static void ThrowIndexOutOfRangeException_Src(int srcIndex, int length, int count)
|
||||
{
|
||||
throw GetIndexOutOfRangeException();
|
||||
|
||||
IndexOutOfRangeException GetIndexOutOfRangeException()
|
||||
{
|
||||
return new IndexOutOfRangeException(string.Format("expected: 0 <= srcIdx({0}) <= srcIdx + length({1}) <= srcLen({2})", srcIndex, length, count));
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
internal static void ThrowIllegalReferenceCountException(int count)
|
||||
{
|
||||
|
@ -230,5 +241,16 @@ namespace DotNetty.Buffers
|
|||
return new ArgumentOutOfRangeException("newCapacity", string.Format($"newCapacity: {0} (expected: 0-{1})", newCapacity, maxCapacity));
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
internal static void ThrowArgumentNullException_Dst()
|
||||
{
|
||||
throw GetArgumentOutOfRangeException();
|
||||
|
||||
ArgumentNullException GetArgumentOutOfRangeException()
|
||||
{
|
||||
return new ArgumentNullException("dst");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -356,4 +356,4 @@ namespace DotNetty.Buffers
|
|||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,28 +24,28 @@ namespace DotNetty.Buffers
|
|||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static int GetUnsignedMedium(byte* bytes) =>
|
||||
*bytes << 16 |
|
||||
*(bytes + 1) << 8 |
|
||||
*bytes << 16 |
|
||||
*(bytes + 1) << 8 |
|
||||
*(bytes + 2);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static int GetUnsignedMediumLE(byte* bytes) =>
|
||||
*bytes |
|
||||
*(bytes + 1) << 8 |
|
||||
*bytes |
|
||||
*(bytes + 1) << 8 |
|
||||
*(bytes + 2) << 16;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static int GetInt(byte* bytes) =>
|
||||
(*bytes << 24) |
|
||||
(*(bytes + 1) << 16) |
|
||||
(*(bytes + 2) << 8) |
|
||||
(*bytes << 24) |
|
||||
(*(bytes + 1) << 16) |
|
||||
(*(bytes + 2) << 8) |
|
||||
(*(bytes + 3));
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static int GetIntLE(byte* bytes) =>
|
||||
*bytes |
|
||||
*bytes |
|
||||
(*(bytes + 1) << 8) |
|
||||
(*(bytes + 2) << 16) |
|
||||
(*(bytes + 2) << 16) |
|
||||
(*(bytes + 3) << 24);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
@ -365,4 +365,4 @@ namespace DotNetty.Buffers
|
|||
internal static UnpooledUnsafeDirectByteBuffer NewUnsafeDirectByteBuffer(IByteBufferAllocator alloc, int initialCapacity, int maxCapacity) =>
|
||||
new UnpooledUnsafeDirectByteBuffer(alloc, initialCapacity, maxCapacity);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -210,6 +210,8 @@ namespace DotNetty.Buffers
|
|||
return this;
|
||||
}
|
||||
|
||||
public ICharSequence GetCharSequence(int index, int length, Encoding encoding) => this.Buf.GetCharSequence(index, length, encoding);
|
||||
|
||||
public string GetString(int index, int length, Encoding encoding) => this.Buf.GetString(index, length, encoding);
|
||||
|
||||
public virtual IByteBuffer SetBoolean(int index, bool value)
|
||||
|
@ -350,6 +352,8 @@ namespace DotNetty.Buffers
|
|||
return this;
|
||||
}
|
||||
|
||||
public int SetCharSequence(int index, ICharSequence sequence, Encoding encoding) => this.Buf.SetCharSequence(index, sequence, encoding);
|
||||
|
||||
public virtual bool ReadBoolean() => this.Buf.ReadBoolean();
|
||||
|
||||
public virtual byte ReadByte() => this.Buf.ReadByte();
|
||||
|
@ -436,6 +440,8 @@ namespace DotNetty.Buffers
|
|||
return this;
|
||||
}
|
||||
|
||||
public ICharSequence ReadCharSequence(int length, Encoding encoding) => this.Buf.ReadCharSequence(length, encoding);
|
||||
|
||||
public string ReadString(int length, Encoding encoding) => this.Buf.ReadString(length, encoding);
|
||||
|
||||
public virtual IByteBuffer SkipBytes(int length)
|
||||
|
@ -576,6 +582,8 @@ namespace DotNetty.Buffers
|
|||
return this;
|
||||
}
|
||||
|
||||
public int WriteCharSequence(ICharSequence sequence, Encoding encoding) => this.Buf.WriteCharSequence(sequence, encoding);
|
||||
|
||||
public int WriteString(string value, Encoding encoding) => this.Buf.WriteString(value, encoding);
|
||||
|
||||
public virtual int IndexOf(int fromIndex, int toIndex, byte value) => this.Buf.IndexOf(fromIndex, toIndex, value);
|
||||
|
|
|
@ -522,6 +522,12 @@ namespace DotNetty.Buffers
|
|||
return this;
|
||||
}
|
||||
|
||||
public override ICharSequence GetCharSequence(int index, int length, Encoding encoding) => this.wrapped.GetCharSequence(index, length, encoding);
|
||||
|
||||
public override ICharSequence ReadCharSequence(int length, Encoding encoding) => this.wrapped.ReadCharSequence(length, encoding);
|
||||
|
||||
public override int SetCharSequence(int index, ICharSequence sequence, Encoding encoding) => this.wrapped.SetCharSequence(index, sequence, encoding);
|
||||
|
||||
public override string GetString(int index, int length, Encoding encoding) => this.wrapped.GetString(index, length, encoding);
|
||||
|
||||
public override string ReadString(int length, Encoding encoding) => this.wrapped.ReadString(length, encoding);
|
||||
|
@ -530,6 +536,8 @@ namespace DotNetty.Buffers
|
|||
|
||||
public override IByteBuffer ReadBytes(Stream destination, int length) => this.wrapped.ReadBytes(destination, length);
|
||||
|
||||
public override int WriteCharSequence(ICharSequence sequence, Encoding encoding) => this.wrapped.WriteCharSequence(sequence, encoding);
|
||||
|
||||
public override int WriteString(string value, Encoding encoding) => this.wrapped.WriteString(value, encoding);
|
||||
|
||||
public override IByteBuffer SkipBytes(int length)
|
||||
|
|
|
@ -0,0 +1,221 @@
|
|||
// 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
|
||||
{
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
using static Common.Utilities.StringUtil;
|
||||
|
||||
public sealed class CombinedHttpHeaders : DefaultHttpHeaders
|
||||
{
|
||||
public CombinedHttpHeaders(bool validate)
|
||||
: base(new CombinedHttpHeadersImpl(AsciiString.CaseSensitiveHasher, ValueConverter(validate), NameValidator(validate)))
|
||||
{
|
||||
}
|
||||
|
||||
public override bool ContainsValue(AsciiString name, ICharSequence value, bool ignoreCase) =>
|
||||
base.ContainsValue(name, TrimOws(value), ignoreCase);
|
||||
|
||||
sealed class CombinedHttpHeadersImpl : DefaultHeaders<AsciiString, ICharSequence>
|
||||
{
|
||||
// An estimate of the size of a header value.
|
||||
const int ValueLengthEstimate = 10;
|
||||
|
||||
public CombinedHttpHeadersImpl(IHashingStrategy<AsciiString> nameHashingStrategy,
|
||||
IValueConverter<ICharSequence> valueConverter, INameValidator<ICharSequence> nameValidator)
|
||||
: base(nameHashingStrategy, valueConverter, nameValidator)
|
||||
{
|
||||
}
|
||||
|
||||
public override IEnumerable<ICharSequence> ValueIterator(AsciiString name)
|
||||
{
|
||||
ICharSequence value = null;
|
||||
foreach (ICharSequence v in base.ValueIterator(name))
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(CombinedHttpHeaders)} should only have one value");
|
||||
}
|
||||
value = v;
|
||||
}
|
||||
return value != null ? UnescapeCsvFields(value) : Enumerable.Empty<ICharSequence>();
|
||||
}
|
||||
|
||||
public override IList<ICharSequence> GetAll(AsciiString name)
|
||||
{
|
||||
IList<ICharSequence> values = base.GetAll(name);
|
||||
if (values.Count == 0)
|
||||
{
|
||||
return values;
|
||||
}
|
||||
if (values.Count != 1)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(CombinedHttpHeaders)} should only have one value");
|
||||
}
|
||||
|
||||
return UnescapeCsvFields(values[0]);
|
||||
}
|
||||
|
||||
public override IHeaders<AsciiString, ICharSequence> Add(IHeaders<AsciiString, ICharSequence> headers)
|
||||
{
|
||||
// Override the fast-copy mechanism used by DefaultHeaders
|
||||
if (ReferenceEquals(headers, this))
|
||||
{
|
||||
throw new ArgumentException("can't add to itself.");
|
||||
}
|
||||
|
||||
if (headers is CombinedHttpHeadersImpl)
|
||||
{
|
||||
if (this.IsEmpty)
|
||||
{
|
||||
// Can use the fast underlying copy
|
||||
this.AddImpl(headers);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Values are already escaped so don't escape again
|
||||
foreach (HeaderEntry<AsciiString, ICharSequence> header in headers)
|
||||
{
|
||||
this.AddEscapedValue(header.Key, header.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (HeaderEntry<AsciiString, ICharSequence> header in headers)
|
||||
{
|
||||
this.Add(header.Key, header.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public override IHeaders<AsciiString, ICharSequence> Set(IHeaders<AsciiString, ICharSequence> headers)
|
||||
{
|
||||
if (ReferenceEquals(headers, this))
|
||||
{
|
||||
return this;
|
||||
}
|
||||
this.Clear();
|
||||
return this.Add(headers);
|
||||
}
|
||||
|
||||
public override IHeaders<AsciiString, ICharSequence> SetAll(IHeaders<AsciiString, ICharSequence> headers)
|
||||
{
|
||||
if (ReferenceEquals(headers, this))
|
||||
{
|
||||
return this;
|
||||
}
|
||||
foreach (AsciiString key in headers.Names())
|
||||
{
|
||||
this.Remove(key);
|
||||
}
|
||||
return this.Add(headers);
|
||||
}
|
||||
|
||||
public override IHeaders<AsciiString, ICharSequence> Add(AsciiString name, ICharSequence value) =>
|
||||
this.AddEscapedValue(name, EscapeCsv(value));
|
||||
|
||||
public override IHeaders<AsciiString, ICharSequence> Add(AsciiString name, IEnumerable<ICharSequence> values) =>
|
||||
this.AddEscapedValue(name, CommaSeparate(values));
|
||||
|
||||
public override IHeaders<AsciiString, ICharSequence> AddObject(AsciiString name, object value) =>
|
||||
this.AddEscapedValue(name, EscapeCsv(this.ValueConverter.ConvertObject(value)));
|
||||
|
||||
public override IHeaders<AsciiString, ICharSequence> AddObject(AsciiString name, IEnumerable<object> values) =>
|
||||
this.AddEscapedValue(name, this.CommaSeparate(values));
|
||||
|
||||
public override IHeaders<AsciiString, ICharSequence> AddObject(AsciiString name, params object[] values) =>
|
||||
this.AddEscapedValue(name, this.CommaSeparate(values));
|
||||
|
||||
public override IHeaders<AsciiString, ICharSequence> Set(AsciiString name, IEnumerable<ICharSequence> values)
|
||||
{
|
||||
base.Set(name, CommaSeparate(values));
|
||||
return this;
|
||||
}
|
||||
|
||||
public override IHeaders<AsciiString, ICharSequence> SetObject(AsciiString name, object value)
|
||||
{
|
||||
ICharSequence charSequence = EscapeCsv(this.ValueConverter.ConvertObject(value));
|
||||
base.Set(name, charSequence);
|
||||
return this;
|
||||
}
|
||||
|
||||
public override IHeaders<AsciiString, ICharSequence> SetObject(AsciiString name, IEnumerable<object> values)
|
||||
{
|
||||
base.Set(name, this.CommaSeparate(values));
|
||||
return this;
|
||||
}
|
||||
|
||||
CombinedHttpHeadersImpl AddEscapedValue(AsciiString name, ICharSequence escapedValue)
|
||||
{
|
||||
if (!this.TryGet(name, out ICharSequence currentValue))
|
||||
{
|
||||
base.Add(name, escapedValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
base.Set(name, CommaSeparateEscapedValues(currentValue, escapedValue));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
ICharSequence CommaSeparate(IEnumerable<object> values)
|
||||
{
|
||||
StringBuilderCharSequence sb = values is ICollection collection
|
||||
? new StringBuilderCharSequence(collection.Count * ValueLengthEstimate)
|
||||
: new StringBuilderCharSequence();
|
||||
|
||||
foreach (object value in values)
|
||||
{
|
||||
if (sb.Count > 0)
|
||||
{
|
||||
sb.Append(Comma);
|
||||
}
|
||||
|
||||
sb.Append(EscapeCsv(this.ValueConverter.ConvertObject(value)));
|
||||
}
|
||||
|
||||
return sb;
|
||||
}
|
||||
|
||||
static ICharSequence CommaSeparate(IEnumerable<ICharSequence> values)
|
||||
{
|
||||
StringBuilderCharSequence sb = values is ICollection collection
|
||||
? new StringBuilderCharSequence(collection.Count * ValueLengthEstimate)
|
||||
: new StringBuilderCharSequence();
|
||||
|
||||
foreach (ICharSequence value in values)
|
||||
{
|
||||
if (sb.Count > 0)
|
||||
{
|
||||
sb.Append(Comma);
|
||||
}
|
||||
|
||||
sb.Append(EscapeCsv(value));
|
||||
}
|
||||
|
||||
return sb;
|
||||
}
|
||||
|
||||
static ICharSequence CommaSeparateEscapedValues(ICharSequence currentValue, ICharSequence value)
|
||||
{
|
||||
var builder = new StringBuilderCharSequence(currentValue.Count + 1 + value.Count);
|
||||
builder.Append(currentValue);
|
||||
builder.Append(Comma);
|
||||
builder.Append(value);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
static ICharSequence EscapeCsv(ICharSequence value) => StringUtil.EscapeCsv(value, true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ConvertToAutoPropertyWhenPossible
|
||||
// ReSharper disable ConvertToAutoProperty
|
||||
namespace DotNetty.Codecs.Http
|
||||
{
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common;
|
||||
|
||||
public sealed class ComposedLastHttpContent : ILastHttpContent
|
||||
{
|
||||
readonly HttpHeaders trailingHeaders;
|
||||
DecoderResult result;
|
||||
|
||||
internal ComposedLastHttpContent(HttpHeaders trailingHeaders)
|
||||
{
|
||||
this.trailingHeaders = trailingHeaders;
|
||||
}
|
||||
|
||||
public HttpHeaders TrailingHeaders => this.trailingHeaders;
|
||||
|
||||
public IByteBufferHolder Copy()
|
||||
{
|
||||
var content = new DefaultLastHttpContent(Unpooled.Empty);
|
||||
content.TrailingHeaders.Set(this.trailingHeaders);
|
||||
return content;
|
||||
}
|
||||
|
||||
public IByteBufferHolder Duplicate() => this.Copy();
|
||||
|
||||
public IByteBufferHolder RetainedDuplicate() => this.Copy();
|
||||
|
||||
public IByteBufferHolder Replace(IByteBuffer content)
|
||||
{
|
||||
var dup = new DefaultLastHttpContent(content);
|
||||
dup.TrailingHeaders.SetAll(this.trailingHeaders);
|
||||
return dup;
|
||||
}
|
||||
|
||||
public IReferenceCounted Retain() => this;
|
||||
|
||||
public IReferenceCounted Retain(int increment) => this;
|
||||
|
||||
public IReferenceCounted Touch() => this;
|
||||
|
||||
public IReferenceCounted Touch(object hint) => this;
|
||||
|
||||
public IByteBuffer Content => Unpooled.Empty;
|
||||
|
||||
public DecoderResult Result
|
||||
{
|
||||
get => this.result;
|
||||
set => this.result = value;
|
||||
}
|
||||
|
||||
public int ReferenceCount => 1;
|
||||
|
||||
public bool Release() => false;
|
||||
|
||||
public bool Release(int decrement) => false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,266 @@
|
|||
// 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.Cookies
|
||||
{
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Contracts;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public sealed class ClientCookieDecoder : CookieDecoder
|
||||
{
|
||||
// Strict encoder that validates that name and value chars are in the valid scope
|
||||
// defined in RFC6265
|
||||
public static readonly ClientCookieDecoder StrictDecoder = new ClientCookieDecoder(true);
|
||||
|
||||
// Lax instance that doesn't validate name and value
|
||||
public static readonly ClientCookieDecoder LaxDecoder = new ClientCookieDecoder(false);
|
||||
|
||||
ClientCookieDecoder(bool strict) : base(strict)
|
||||
{
|
||||
}
|
||||
|
||||
public ICookie Decode(string header)
|
||||
{
|
||||
Contract.Requires(header != null);
|
||||
|
||||
int headerLen = header.Length;
|
||||
if (headerLen == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
CookieBuilder cookieBuilder = null;
|
||||
//loop:
|
||||
for (int i = 0;;)
|
||||
{
|
||||
|
||||
// Skip spaces and separators.
|
||||
for (;;)
|
||||
{
|
||||
if (i == headerLen)
|
||||
{
|
||||
goto loop;
|
||||
}
|
||||
char c = header[i];
|
||||
if (c == ',')
|
||||
{
|
||||
// Having multiple cookies in a single Set-Cookie header is
|
||||
// deprecated, modern browsers only parse the first one
|
||||
goto loop;
|
||||
|
||||
}
|
||||
else if (c == '\t' || c == '\n' || c == 0x0b || c == '\f'
|
||||
|| c == '\r' || c == ' ' || c == ';')
|
||||
{
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
int nameBegin = i;
|
||||
int nameEnd;
|
||||
int valueBegin;
|
||||
int valueEnd;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
char curChar = header[i];
|
||||
if (curChar == ';')
|
||||
{
|
||||
// NAME; (no value till ';')
|
||||
nameEnd = i;
|
||||
valueBegin = valueEnd = -1;
|
||||
break;
|
||||
|
||||
}
|
||||
else if (curChar == '=')
|
||||
{
|
||||
// NAME=VALUE
|
||||
nameEnd = i;
|
||||
i++;
|
||||
if (i == headerLen)
|
||||
{
|
||||
// NAME= (empty value, i.e. nothing after '=')
|
||||
valueBegin = valueEnd = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
valueBegin = i;
|
||||
// NAME=VALUE;
|
||||
int semiPos = header.IndexOf(';', i);
|
||||
valueEnd = i = semiPos > 0 ? semiPos : headerLen;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
if (i == headerLen)
|
||||
{
|
||||
// NAME (no value till the end of string)
|
||||
nameEnd = headerLen;
|
||||
valueBegin = valueEnd = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (valueEnd > 0 && header[valueEnd - 1] == ',')
|
||||
{
|
||||
// old multiple cookies separator, skipping it
|
||||
valueEnd--;
|
||||
}
|
||||
|
||||
if (cookieBuilder == null)
|
||||
{
|
||||
// cookie name-value pair
|
||||
DefaultCookie cookie = this.InitCookie(header, nameBegin, nameEnd, valueBegin, valueEnd);
|
||||
|
||||
if (cookie == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
cookieBuilder = new CookieBuilder(cookie, header);
|
||||
}
|
||||
else
|
||||
{
|
||||
// cookie attribute
|
||||
cookieBuilder.AppendAttribute(nameBegin, nameEnd, valueBegin, valueEnd);
|
||||
}
|
||||
}
|
||||
|
||||
loop:
|
||||
Debug.Assert(cookieBuilder != null);
|
||||
return cookieBuilder.Cookie();
|
||||
}
|
||||
|
||||
sealed class CookieBuilder
|
||||
{
|
||||
readonly string header;
|
||||
readonly DefaultCookie cookie;
|
||||
string domain;
|
||||
string path;
|
||||
long maxAge = long.MinValue;
|
||||
int expiresStart;
|
||||
int expiresEnd;
|
||||
bool secure;
|
||||
bool httpOnly;
|
||||
|
||||
internal CookieBuilder(DefaultCookie cookie, string header)
|
||||
{
|
||||
this.cookie = cookie;
|
||||
this.header = header;
|
||||
}
|
||||
|
||||
long MergeMaxAgeAndExpires()
|
||||
{
|
||||
// max age has precedence over expires
|
||||
if (this.maxAge != long.MinValue)
|
||||
{
|
||||
return this.maxAge;
|
||||
}
|
||||
else if (IsValueDefined(this.expiresStart, this.expiresEnd))
|
||||
{
|
||||
DateTime? expiresDate = DateFormatter.ParseHttpDate(this.header, this.expiresStart, this.expiresEnd);
|
||||
if (expiresDate != null)
|
||||
{
|
||||
return (expiresDate.Value.Ticks - DateTime.UtcNow.Ticks) / TimeSpan.TicksPerSecond;
|
||||
}
|
||||
}
|
||||
return long.MinValue;
|
||||
}
|
||||
|
||||
internal ICookie Cookie()
|
||||
{
|
||||
this.cookie.Domain = this.domain;
|
||||
this.cookie.Path = this.path;
|
||||
this.cookie.MaxAge = this.MergeMaxAgeAndExpires();
|
||||
this.cookie.IsSecure = this.secure;
|
||||
this.cookie.IsHttpOnly = this.httpOnly;
|
||||
|
||||
return this.cookie;
|
||||
}
|
||||
|
||||
public void AppendAttribute(int keyStart, int keyEnd, int valueStart, int valueEnd)
|
||||
{
|
||||
int length = keyEnd - keyStart;
|
||||
|
||||
if (length == 4)
|
||||
{
|
||||
this.Parse4(keyStart, valueStart, valueEnd);
|
||||
}
|
||||
else if (length == 6)
|
||||
{
|
||||
this.Parse6(keyStart, valueStart, valueEnd);
|
||||
}
|
||||
else if (length == 7)
|
||||
{
|
||||
this.Parse7(keyStart, valueStart, valueEnd);
|
||||
}
|
||||
else if (length == 8)
|
||||
{
|
||||
this.Parse8(keyStart);
|
||||
}
|
||||
}
|
||||
|
||||
void Parse4(int nameStart, int valueStart, int valueEnd)
|
||||
{
|
||||
if (CharUtil.RegionMatchesIgnoreCase(this.header, nameStart, CookieHeaderNames.Path, 0, 4))
|
||||
{
|
||||
this.path = this.ComputeValue(valueStart, valueEnd);
|
||||
}
|
||||
}
|
||||
|
||||
void Parse6(int nameStart, int valueStart, int valueEnd)
|
||||
{
|
||||
if (CharUtil.RegionMatchesIgnoreCase(this.header, nameStart, CookieHeaderNames.Domain, 0, 5))
|
||||
{
|
||||
this.domain = this.ComputeValue(valueStart, valueEnd);
|
||||
}
|
||||
else if (CharUtil.RegionMatchesIgnoreCase(this.header, nameStart, CookieHeaderNames.Secure, 0, 5))
|
||||
{
|
||||
this.secure = true;
|
||||
}
|
||||
}
|
||||
|
||||
void SetMaxAge(string value)
|
||||
{
|
||||
if (long.TryParse(value, out long v))
|
||||
{
|
||||
this.maxAge = Math.Max(v, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void Parse7(int nameStart, int valueStart, int valueEnd)
|
||||
{
|
||||
if (CharUtil.RegionMatchesIgnoreCase(this.header, nameStart, CookieHeaderNames.Expires, 0, 7))
|
||||
{
|
||||
this.expiresStart = valueStart;
|
||||
this.expiresEnd = valueEnd;
|
||||
}
|
||||
else if (CharUtil.RegionMatchesIgnoreCase(this.header, nameStart, CookieHeaderNames.MaxAge, 0, 7))
|
||||
{
|
||||
this.SetMaxAge(this.ComputeValue(valueStart, valueEnd));
|
||||
}
|
||||
}
|
||||
|
||||
void Parse8(int nameStart)
|
||||
{
|
||||
if (CharUtil.RegionMatchesIgnoreCase(this.header, nameStart, CookieHeaderNames.HttpOnly, 0, 8))
|
||||
{
|
||||
this.httpOnly = true;
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsValueDefined(int valueStart, int valueEnd) => valueStart != -1 && valueStart != valueEnd;
|
||||
|
||||
string ComputeValue(int valueStart, int valueEnd) => IsValueDefined(valueStart, valueEnd)
|
||||
? this.header.Substring(valueStart, valueEnd - valueStart)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
// 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.Cookies
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Text;
|
||||
|
||||
using static CookieUtil;
|
||||
|
||||
public sealed class ClientCookieEncoder : CookieEncoder
|
||||
{
|
||||
// Strict encoder that validates that name and value chars are in the valid scope and (for methods that accept
|
||||
// multiple cookies) sorts cookies into order of decreasing path length, as specified in RFC6265.
|
||||
public static readonly ClientCookieEncoder StrictEncoder = new ClientCookieEncoder(true);
|
||||
|
||||
// Lax instance that doesn't validate name and value, and (for methods that accept multiple cookies) keeps
|
||||
// cookies in the order in which they were given.
|
||||
public static readonly ClientCookieEncoder LaxEncoder = new ClientCookieEncoder(false);
|
||||
|
||||
static readonly CookieComparer Comparer = new CookieComparer();
|
||||
|
||||
ClientCookieEncoder(bool strict) : base(strict)
|
||||
{
|
||||
}
|
||||
|
||||
public string Encode(string name, string value) => this.Encode(new DefaultCookie(name, value));
|
||||
|
||||
public string Encode(ICookie cookie)
|
||||
{
|
||||
Contract.Requires(cookie != null);
|
||||
|
||||
StringBuilder buf = StringBuilder();
|
||||
this.Encode(buf, cookie);
|
||||
return StripTrailingSeparator(buf);
|
||||
}
|
||||
|
||||
sealed class CookieComparer : IComparer<ICookie>
|
||||
{
|
||||
public int Compare(ICookie c1, ICookie c2)
|
||||
{
|
||||
Debug.Assert(c1 != null && c2 != null);
|
||||
|
||||
string path1 = c1.Path;
|
||||
string path2 = c2.Path;
|
||||
// Cookies with unspecified path default to the path of the request. We don't
|
||||
// know the request path here, but we assume that the length of an unspecified
|
||||
// path is longer than any specified path (i.e. pathless cookies come first),
|
||||
// because setting cookies with a path longer than the request path is of
|
||||
// limited use.
|
||||
int len1 = path1?.Length ?? int.MaxValue;
|
||||
int len2 = path2?.Length ?? int.MaxValue;
|
||||
int diff = len2 - len1;
|
||||
if (diff != 0)
|
||||
{
|
||||
return diff;
|
||||
}
|
||||
// Rely on Java's sort stability to retain creation order in cases where
|
||||
// cookies have same path length.
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public string Encode(params ICookie[] cookies)
|
||||
{
|
||||
if (cookies == null || cookies.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder buf = StringBuilder();
|
||||
if (this.Strict)
|
||||
{
|
||||
if (cookies.Length == 1)
|
||||
{
|
||||
this.Encode(buf, cookies[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
var cookiesSorted = new ICookie[cookies.Length];
|
||||
Array.Copy(cookies, cookiesSorted, cookies.Length);
|
||||
Array.Sort(cookiesSorted, Comparer);
|
||||
foreach(ICookie c in cookiesSorted)
|
||||
{
|
||||
this.Encode(buf, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (ICookie c in cookies)
|
||||
{
|
||||
this.Encode(buf, c);
|
||||
}
|
||||
}
|
||||
return StripTrailingSeparatorOrNull(buf);
|
||||
}
|
||||
|
||||
public string Encode(IEnumerable<ICookie> cookies)
|
||||
{
|
||||
Contract.Requires(cookies != null);
|
||||
|
||||
StringBuilder buf = StringBuilder();
|
||||
if (this.Strict)
|
||||
{
|
||||
var cookiesList = new List<ICookie>();
|
||||
foreach (ICookie cookie in cookies)
|
||||
{
|
||||
cookiesList.Add(cookie);
|
||||
}
|
||||
cookiesList.Sort(Comparer);
|
||||
foreach (ICookie c in cookiesList)
|
||||
{
|
||||
this.Encode(buf, c);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (ICookie cookie in cookies)
|
||||
{
|
||||
this.Encode(buf, cookie);
|
||||
}
|
||||
}
|
||||
return StripTrailingSeparatorOrNull(buf);
|
||||
}
|
||||
|
||||
void Encode(StringBuilder buf, ICookie c)
|
||||
{
|
||||
string name = c.Name;
|
||||
string value = c.Value?? "";
|
||||
|
||||
this.ValidateCookie(name, value);
|
||||
|
||||
if (c.Wrap)
|
||||
{
|
||||
AddQuoted(buf, name, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Add(buf, name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
// 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.Cookies
|
||||
{
|
||||
using DotNetty.Common.Internal.Logging;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
using static CookieUtil;
|
||||
|
||||
public abstract class CookieDecoder
|
||||
{
|
||||
static readonly IInternalLogger Logger = InternalLoggerFactory.GetInstance<CookieDecoder>();
|
||||
protected readonly bool Strict;
|
||||
|
||||
protected CookieDecoder(bool strict)
|
||||
{
|
||||
this.Strict = strict;
|
||||
}
|
||||
|
||||
protected DefaultCookie InitCookie(string header, int nameBegin, int nameEnd, int valueBegin, int valueEnd)
|
||||
{
|
||||
if (nameBegin == -1 || nameBegin == nameEnd)
|
||||
{
|
||||
Logger.Debug("Skipping cookie with null name");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (valueBegin == -1)
|
||||
{
|
||||
Logger.Debug("Skipping cookie with null value");
|
||||
return null;
|
||||
}
|
||||
|
||||
var sequence = new StringCharSequence(header, valueBegin, valueEnd - valueBegin);
|
||||
ICharSequence unwrappedValue = UnwrapValue(sequence);
|
||||
if (unwrappedValue == null)
|
||||
{
|
||||
Logger.Debug("Skipping cookie because starting quotes are not properly balanced in '{}'", sequence);
|
||||
return null;
|
||||
}
|
||||
|
||||
string name = header.Substring(nameBegin, nameEnd - nameBegin);
|
||||
|
||||
int invalidOctetPos;
|
||||
if (this.Strict && (invalidOctetPos = FirstInvalidCookieNameOctet(name)) >= 0)
|
||||
{
|
||||
if (Logger.DebugEnabled)
|
||||
{
|
||||
Logger.Debug("Skipping cookie because name '{}' contains invalid char '{}'",
|
||||
name, name[invalidOctetPos]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool wrap = unwrappedValue.Count != valueEnd - valueBegin;
|
||||
|
||||
if (this.Strict && (invalidOctetPos = FirstInvalidCookieValueOctet(unwrappedValue)) >= 0)
|
||||
{
|
||||
if (Logger.DebugEnabled)
|
||||
{
|
||||
Logger.Debug("Skipping cookie because value '{}' contains invalid char '{}'",
|
||||
unwrappedValue, unwrappedValue[invalidOctetPos]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var cookie = new DefaultCookie(name, unwrappedValue.ToString());
|
||||
cookie.Wrap = wrap;
|
||||
return cookie;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// 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.Cookies
|
||||
{
|
||||
using System;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
using static CookieUtil;
|
||||
|
||||
public abstract class CookieEncoder
|
||||
{
|
||||
protected readonly bool Strict;
|
||||
|
||||
protected CookieEncoder(bool strict)
|
||||
{
|
||||
this.Strict = strict;
|
||||
}
|
||||
|
||||
protected void ValidateCookie(string name, string value)
|
||||
{
|
||||
if (!this.Strict)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int pos;
|
||||
if ((pos = FirstInvalidCookieNameOctet(name)) >= 0)
|
||||
{
|
||||
throw new ArgumentException($"Cookie name contains an invalid char: {name[pos]}");
|
||||
}
|
||||
|
||||
var sequnce = new StringCharSequence(value);
|
||||
ICharSequence unwrappedValue = UnwrapValue(sequnce);
|
||||
if (unwrappedValue == null)
|
||||
{
|
||||
throw new ArgumentException($"Cookie value wrapping quotes are not balanced: {value}");
|
||||
}
|
||||
|
||||
if ((pos = FirstInvalidCookieValueOctet(unwrappedValue)) >= 0)
|
||||
{
|
||||
throw new ArgumentException($"Cookie value contains an invalid char: {value[pos]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
// 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.Cookies
|
||||
{
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public static class CookieHeaderNames
|
||||
{
|
||||
public static readonly AsciiString Path = AsciiString.Cached("Path");
|
||||
|
||||
public static readonly AsciiString Expires = AsciiString.Cached("Expires");
|
||||
|
||||
public static readonly AsciiString MaxAge = AsciiString.Cached("Max-Age");
|
||||
|
||||
public static readonly AsciiString Domain = AsciiString.Cached("Domain");
|
||||
|
||||
public static readonly AsciiString Secure = AsciiString.Cached("Secure");
|
||||
|
||||
public static readonly AsciiString HttpOnly = AsciiString.Cached("HTTPOnly");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,203 @@
|
|||
// 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.Cookies
|
||||
{
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
using DotNetty.Common;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
static class CookieUtil
|
||||
{
|
||||
static readonly BitArray ValidCookieNameOctets = GetValidCookieNameOctets();
|
||||
static readonly BitArray ValidCookieValueOctects = GetValidCookieValueOctets();
|
||||
static readonly BitArray ValidCookieAttributeOctets = GetValidCookieAttributeValueOctets();
|
||||
|
||||
// token = 1*<any CHAR except CTLs or separators>
|
||||
// separators =
|
||||
//"(" | ")" | "<" | ">" | "@"
|
||||
// | "," | ";" | ":" | "\" | <">
|
||||
// | "/" | "[" | "]" | "?" | "="
|
||||
// | "{" | "}" | SP | HT
|
||||
static BitArray GetValidCookieNameOctets()
|
||||
{
|
||||
var bitArray = new BitArray(128, false);
|
||||
for (int i = 32; i < 127; i++)
|
||||
{
|
||||
bitArray[i] = true;
|
||||
}
|
||||
|
||||
var separators = new int[]
|
||||
{ '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t' };
|
||||
foreach (int separator in separators)
|
||||
{
|
||||
bitArray[separator] = false;
|
||||
}
|
||||
|
||||
return bitArray;
|
||||
}
|
||||
|
||||
// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
|
||||
// US-ASCII characters excluding CTLs, whitespace, DQUOTE, comma, semicolon, and backslash
|
||||
static BitArray GetValidCookieValueOctets()
|
||||
{
|
||||
var bitArray = new BitArray(128, false);
|
||||
bitArray[0x21] = true;
|
||||
for (int i = 0x23; i <= 0x2B; i++)
|
||||
{
|
||||
bitArray[i] = true;
|
||||
}
|
||||
for (int i = 0x2D; i <= 0x3A; i++)
|
||||
{
|
||||
bitArray[i] = true;
|
||||
}
|
||||
for (int i = 0x3C; i <= 0x5B; i++)
|
||||
{
|
||||
bitArray[i] = true;
|
||||
}
|
||||
for (int i = 0x5D; i <= 0x7E; i++)
|
||||
{
|
||||
bitArray[i] = true;
|
||||
}
|
||||
|
||||
return bitArray;
|
||||
}
|
||||
|
||||
// path-value = <any CHAR except CTLs or ";">
|
||||
static BitArray GetValidCookieAttributeValueOctets()
|
||||
{
|
||||
var bitArray = new BitArray(128, false);
|
||||
for (int i = 32; i < 127; i++)
|
||||
{
|
||||
bitArray[i] = true;
|
||||
}
|
||||
bitArray[';'] = false;
|
||||
|
||||
return bitArray;
|
||||
}
|
||||
|
||||
internal static StringBuilder StringBuilder() => InternalThreadLocalMap.Get().StringBuilder;
|
||||
|
||||
internal static string StripTrailingSeparatorOrNull(StringBuilder buf) =>
|
||||
buf.Length == 0 ? null : StripTrailingSeparator(buf);
|
||||
|
||||
internal static string StripTrailingSeparator(StringBuilder buf)
|
||||
{
|
||||
if (buf.Length > 0)
|
||||
{
|
||||
buf.Length = buf.Length - 2;
|
||||
}
|
||||
|
||||
return buf.ToString();
|
||||
}
|
||||
|
||||
internal static void Add(StringBuilder sb, string name, long val)
|
||||
{
|
||||
sb.Append(name);
|
||||
sb.Append((char)HttpConstants.EqualsSign);
|
||||
sb.Append(val);
|
||||
sb.Append((char)HttpConstants.Semicolon);
|
||||
sb.Append((char)HttpConstants.HorizontalSpace);
|
||||
}
|
||||
|
||||
internal static void Add(StringBuilder sb, string name, string val)
|
||||
{
|
||||
sb.Append(name);
|
||||
sb.Append((char)HttpConstants.EqualsSign);
|
||||
sb.Append(val);
|
||||
sb.Append((char)HttpConstants.Semicolon);
|
||||
sb.Append((char)HttpConstants.HorizontalSpace);
|
||||
}
|
||||
|
||||
internal static void Add(StringBuilder sb, string name)
|
||||
{
|
||||
sb.Append(name);
|
||||
sb.Append((char)HttpConstants.Semicolon);
|
||||
sb.Append((char)HttpConstants.HorizontalSpace);
|
||||
}
|
||||
|
||||
internal static void AddQuoted(StringBuilder sb, string name, string val)
|
||||
{
|
||||
if (val == null)
|
||||
{
|
||||
val = "";
|
||||
}
|
||||
|
||||
sb.Append(name);
|
||||
sb.Append((char)HttpConstants.EqualsSign);
|
||||
sb.Append((char)HttpConstants.DoubleQuote);
|
||||
sb.Append(val);
|
||||
sb.Append((char)HttpConstants.DoubleQuote);
|
||||
sb.Append((char)HttpConstants.Semicolon);
|
||||
sb.Append((char)HttpConstants.HorizontalSpace);
|
||||
}
|
||||
|
||||
internal static int FirstInvalidCookieNameOctet(string cs) => FirstInvalidOctet(cs, ValidCookieNameOctets);
|
||||
|
||||
internal static int FirstInvalidCookieValueOctet(ICharSequence cs) => FirstInvalidOctet(cs, ValidCookieValueOctects);
|
||||
|
||||
static int FirstInvalidOctet(string cs, BitArray bits)
|
||||
{
|
||||
for (int i = 0; i < cs.Length; i++)
|
||||
{
|
||||
char c = cs[i];
|
||||
if (!bits[c])
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int FirstInvalidOctet(ICharSequence cs, BitArray bits)
|
||||
{
|
||||
for (int i = 0; i < cs.Count; i++)
|
||||
{
|
||||
char c = cs[i];
|
||||
if (!bits[c])
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
internal static ICharSequence UnwrapValue(ICharSequence cs)
|
||||
{
|
||||
int len = cs.Count;
|
||||
if (len > 0 && cs[0] == '"')
|
||||
{
|
||||
if (len >= 2 && cs[len - 1] == '"')
|
||||
{
|
||||
// properly balanced
|
||||
return len == 2 ? StringCharSequence.Empty : cs.SubSequence(1, len - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return cs;
|
||||
}
|
||||
|
||||
internal static string ValidateAttributeValue(string name, string value)
|
||||
{
|
||||
value = value?.Trim();
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
int i = FirstInvalidOctet(value, ValidCookieAttributeOctets);
|
||||
if (i != -1)
|
||||
{
|
||||
throw new ArgumentException($"{name} contains the prohibited characters: ${value[i]}");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,227 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ConvertToAutoProperty
|
||||
// ReSharper disable ConvertToAutoPropertyWhenPossible
|
||||
namespace DotNetty.Codecs.Http.Cookies
|
||||
{
|
||||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Text;
|
||||
|
||||
using static CookieUtil;
|
||||
|
||||
public sealed class DefaultCookie : ICookie
|
||||
{
|
||||
// Constant for undefined MaxAge attribute value.
|
||||
const long UndefinedMaxAge = long.MinValue;
|
||||
|
||||
readonly string name;
|
||||
string value;
|
||||
bool wrap;
|
||||
string domain;
|
||||
string path;
|
||||
long maxAge = UndefinedMaxAge;
|
||||
bool secure;
|
||||
bool httpOnly;
|
||||
|
||||
public DefaultCookie(string name, string value)
|
||||
{
|
||||
Contract.Requires(!string.IsNullOrEmpty(name?.Trim()));
|
||||
Contract.Requires(value != null);
|
||||
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public string Name => this.name;
|
||||
|
||||
public string Value
|
||||
{
|
||||
get => this.value;
|
||||
set
|
||||
{
|
||||
Contract.Requires(value != null);
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Wrap
|
||||
{
|
||||
get => this.wrap;
|
||||
set => this.wrap = value;
|
||||
}
|
||||
|
||||
public string Domain
|
||||
{
|
||||
get => this.domain;
|
||||
set => this.domain = ValidateAttributeValue(nameof(this.domain), value);
|
||||
}
|
||||
|
||||
public string Path
|
||||
{
|
||||
get => this.path;
|
||||
set => this.path = ValidateAttributeValue(nameof(this.path), value);
|
||||
}
|
||||
|
||||
public long MaxAge
|
||||
{
|
||||
get => this.maxAge;
|
||||
set => this.maxAge = value;
|
||||
}
|
||||
|
||||
public bool IsSecure
|
||||
{
|
||||
get => this.secure;
|
||||
set => this.secure = value;
|
||||
}
|
||||
|
||||
public bool IsHttpOnly
|
||||
{
|
||||
get => this.httpOnly;
|
||||
set => this.httpOnly = value;
|
||||
}
|
||||
|
||||
public override int GetHashCode() => this.name.GetHashCode();
|
||||
|
||||
public override bool Equals(object obj) => obj is DefaultCookie cookie && this.Equals(cookie);
|
||||
|
||||
public bool Equals(ICookie other)
|
||||
{
|
||||
if (ReferenceEquals(null, other))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.name.Equals(other.Name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.path == null)
|
||||
{
|
||||
if (other.Path != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (other.Path == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (!this.path.Equals(other.Path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.domain == null)
|
||||
{
|
||||
if (other.Domain != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return this.domain.Equals(other.Domain, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public int CompareTo(ICookie other)
|
||||
{
|
||||
int v = string.Compare(this.name, other.Name, StringComparison.Ordinal);
|
||||
if (v != 0)
|
||||
{
|
||||
return v;
|
||||
}
|
||||
|
||||
if (this.path == null)
|
||||
{
|
||||
if (other.Path != null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else if (other.Path == null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
v = string.Compare(this.path, other.Path, StringComparison.Ordinal);
|
||||
if (v != 0)
|
||||
{
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.domain == null)
|
||||
{
|
||||
if (other.Domain != null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else if (other.Domain == null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
v = string.Compare(this.domain, other.Domain, StringComparison.OrdinalIgnoreCase);
|
||||
return v;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int CompareTo(object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!(obj is ICookie cookie))
|
||||
{
|
||||
throw new ArgumentException($"{nameof(obj)} must be of {nameof(ICookie)} type");
|
||||
}
|
||||
|
||||
return this.CompareTo(cookie);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder buf = StringBuilder();
|
||||
buf.Append($"{this.name}={this.Value}");
|
||||
if (this.domain != null)
|
||||
{
|
||||
buf.Append($", domain={this.domain}");
|
||||
}
|
||||
if (this.path != null)
|
||||
{
|
||||
buf.Append($", path={this.path}");
|
||||
}
|
||||
if (this.maxAge >= 0)
|
||||
{
|
||||
buf.Append($", maxAge={this.maxAge}s");
|
||||
}
|
||||
if (this.secure)
|
||||
{
|
||||
buf.Append(", secure");
|
||||
}
|
||||
if (this.httpOnly)
|
||||
{
|
||||
buf.Append(", HTTPOnly");
|
||||
}
|
||||
|
||||
return buf.ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
// 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.Cookies
|
||||
{
|
||||
using System;
|
||||
|
||||
// http://en.wikipedia.org/wiki/HTTP_cookie
|
||||
public interface ICookie : IEquatable<ICookie>, IComparable<ICookie>, IComparable
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
string Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the raw value of this {@link Cookie},
|
||||
/// was wrapped with double quotes in original Set-Cookie header.
|
||||
/// </summary>
|
||||
bool Wrap { get; set; }
|
||||
|
||||
string Domain { get; set; }
|
||||
|
||||
string Path { get; set; }
|
||||
|
||||
long MaxAge { get; set; }
|
||||
|
||||
bool IsSecure { get; set; }
|
||||
|
||||
///<summary>
|
||||
/// Checks to see if this Cookie can only be accessed via HTTP.
|
||||
/// If this returns true, the Cookie cannot be accessed through
|
||||
/// client side script - But only if the browser supports it.
|
||||
/// For more information, please look "http://www.owasp.org/index.php/HTTPOnly".
|
||||
///</summary>
|
||||
bool IsHttpOnly { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
// 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.Cookies
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.Contracts;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
// http://tools.ietf.org/html/rfc6265
|
||||
// compliant cookie decoder to be used server side.
|
||||
//
|
||||
// http://tools.ietf.org/html/rfc2965
|
||||
// cookies are still supported,old fields will simply be ignored.
|
||||
public sealed class ServerCookieDecoder : CookieDecoder
|
||||
{
|
||||
static readonly AsciiString RFC2965Version = new AsciiString("$Version");
|
||||
static readonly AsciiString RFC2965Path = new AsciiString($"${CookieHeaderNames.Path}");
|
||||
static readonly AsciiString RFC2965Domain = new AsciiString($"${CookieHeaderNames.Domain}");
|
||||
static readonly AsciiString RFC2965Port = new AsciiString("$Port");
|
||||
|
||||
//
|
||||
// Strict encoder that validates that name and value chars are in the valid scope
|
||||
// defined in RFC6265
|
||||
//
|
||||
public static readonly ServerCookieDecoder StrictDecoder = new ServerCookieDecoder(true);
|
||||
|
||||
//
|
||||
// Lax instance that doesn't validate name and value
|
||||
//
|
||||
public static readonly ServerCookieDecoder LaxDecoder = new ServerCookieDecoder(false);
|
||||
|
||||
ServerCookieDecoder(bool strict) : base(strict)
|
||||
{
|
||||
}
|
||||
|
||||
public ISet<ICookie> Decode(string header)
|
||||
{
|
||||
Contract.Requires(header != null);
|
||||
|
||||
int headerLen = header.Length;
|
||||
if (headerLen == 0)
|
||||
{
|
||||
return ImmutableHashSet<ICookie>.Empty;
|
||||
}
|
||||
|
||||
var cookies = new SortedSet<ICookie>();
|
||||
|
||||
int i = 0;
|
||||
|
||||
bool rfc2965Style = false;
|
||||
if (CharUtil.RegionMatchesIgnoreCase(header, 0, RFC2965Version, 0, RFC2965Version.Count))
|
||||
{
|
||||
// RFC 2965 style cookie, move to after version value
|
||||
i = header.IndexOf(';') + 1;
|
||||
rfc2965Style = true;
|
||||
}
|
||||
|
||||
// loop
|
||||
for (;;)
|
||||
{
|
||||
// Skip spaces and separators.
|
||||
for (;;)
|
||||
{
|
||||
if (i == headerLen)
|
||||
{
|
||||
goto loop;
|
||||
}
|
||||
char c = header[i];
|
||||
if (c == '\t' || c == '\n' || c == 0x0b || c == '\f'
|
||||
|| c == '\r' || c == ' ' || c == ',' || c == ';')
|
||||
{
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
int nameBegin = i;
|
||||
int nameEnd;
|
||||
int valueBegin;
|
||||
int valueEnd;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
char curChar = header[i];
|
||||
if (curChar == ';')
|
||||
{
|
||||
// NAME; (no value till ';')
|
||||
nameEnd = i;
|
||||
valueBegin = valueEnd = -1;
|
||||
break;
|
||||
}
|
||||
else if (curChar == '=')
|
||||
{
|
||||
// NAME=VALUE
|
||||
nameEnd = i;
|
||||
i++;
|
||||
if (i == headerLen)
|
||||
{
|
||||
// NAME= (empty value, i.e. nothing after '=')
|
||||
valueBegin = valueEnd = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
valueBegin = i;
|
||||
// NAME=VALUE;
|
||||
int semiPos = header.IndexOf(';', i);
|
||||
valueEnd = i = semiPos > 0 ? semiPos : headerLen;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
if (i == headerLen)
|
||||
{
|
||||
// NAME (no value till the end of string)
|
||||
nameEnd = headerLen;
|
||||
valueBegin = valueEnd = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (rfc2965Style && (CharUtil.RegionMatches(header, nameBegin, RFC2965Path, 0, RFC2965Path.Count)
|
||||
|| CharUtil.RegionMatches(header, nameBegin, RFC2965Domain, 0, RFC2965Domain.Count)
|
||||
|| CharUtil.RegionMatches(header, nameBegin, RFC2965Port, 0, RFC2965Port.Count)))
|
||||
{
|
||||
// skip obsolete RFC2965 fields
|
||||
continue;
|
||||
}
|
||||
|
||||
DefaultCookie cookie = this.InitCookie(header, nameBegin, nameEnd, valueBegin, valueEnd);
|
||||
if (cookie != null)
|
||||
{
|
||||
cookies.Add(cookie);
|
||||
}
|
||||
}
|
||||
|
||||
loop:
|
||||
return cookies;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
// 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.Cookies
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Text;
|
||||
|
||||
using static CookieUtil;
|
||||
|
||||
// http://tools.ietf.org/html/rfc6265 compliant cookie encoder to be used server side,
|
||||
// so some fields are sent (Version is typically ignored).
|
||||
//
|
||||
// As Netty's Cookie merges Expires and MaxAge into one single field, only Max-Age field is sent.
|
||||
// Note that multiple cookies must be sent as separate "Set-Cookie" headers.
|
||||
public sealed class ServerCookieEncoder : CookieEncoder
|
||||
{
|
||||
//
|
||||
// Strict encoder that validates that name and value chars are in the valid scope
|
||||
// defined in RFC6265, and(for methods that accept multiple cookies) that only
|
||||
// one cookie is encoded with any given name. (If multiple cookies have the same
|
||||
// name, the last one is the one that is encoded.)
|
||||
//
|
||||
public static readonly ServerCookieEncoder StrictEncoder = new ServerCookieEncoder(true);
|
||||
|
||||
//
|
||||
// Lax instance that doesn't validate name and value, and that allows multiple
|
||||
// cookies with the same name.
|
||||
//
|
||||
public static readonly ServerCookieEncoder LaxEncoder = new ServerCookieEncoder(false);
|
||||
|
||||
ServerCookieEncoder(bool strict) : base(strict)
|
||||
{
|
||||
}
|
||||
|
||||
public string Encode(string name, string value) => this.Encode(new DefaultCookie(name, value));
|
||||
|
||||
public string Encode(ICookie cookie)
|
||||
{
|
||||
Contract.Requires(cookie != null);
|
||||
|
||||
string name = cookie.Name ?? nameof(cookie);
|
||||
string value = cookie.Value ?? "";
|
||||
|
||||
this.ValidateCookie(name, value);
|
||||
|
||||
StringBuilder buf = StringBuilder();
|
||||
|
||||
if (cookie.Wrap)
|
||||
{
|
||||
AddQuoted(buf, name, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Add(buf, name, value);
|
||||
}
|
||||
|
||||
if (cookie.MaxAge != long.MinValue)
|
||||
{
|
||||
Add(buf, (string)CookieHeaderNames.MaxAge, cookie.MaxAge);
|
||||
DateTime expires = DateTime.UtcNow.AddMilliseconds(cookie.MaxAge * 1000);
|
||||
buf.Append(CookieHeaderNames.Expires);
|
||||
buf.Append((char)HttpConstants.EqualsSign);
|
||||
DateFormatter.Append(expires, buf);
|
||||
buf.Append((char)HttpConstants.Semicolon);
|
||||
buf.Append((char)HttpConstants.HorizontalSpace);
|
||||
}
|
||||
|
||||
if (cookie.Path != null)
|
||||
{
|
||||
Add(buf, (string)CookieHeaderNames.Path, cookie.Path);
|
||||
}
|
||||
|
||||
if (cookie.Domain != null)
|
||||
{
|
||||
Add(buf, (string)CookieHeaderNames.Domain, cookie.Domain);
|
||||
}
|
||||
|
||||
if (cookie.IsSecure)
|
||||
{
|
||||
Add(buf, (string)CookieHeaderNames.Secure);
|
||||
}
|
||||
|
||||
if (cookie.IsHttpOnly)
|
||||
{
|
||||
Add(buf, (string)CookieHeaderNames.HttpOnly);
|
||||
}
|
||||
|
||||
return StripTrailingSeparator(buf);
|
||||
}
|
||||
|
||||
static List<string> Dedup(IReadOnlyList<string> encoded, IDictionary<string, int> nameToLastIndex)
|
||||
{
|
||||
var isLastInstance = new bool[encoded.Count];
|
||||
foreach (int idx in nameToLastIndex.Values)
|
||||
{
|
||||
isLastInstance[idx] = true;
|
||||
}
|
||||
|
||||
var dedupd = new List<string>(nameToLastIndex.Count);
|
||||
for (int i = 0, n = encoded.Count; i < n; i++)
|
||||
{
|
||||
if (isLastInstance[i])
|
||||
{
|
||||
dedupd.Add(encoded[i]);
|
||||
}
|
||||
}
|
||||
return dedupd;
|
||||
}
|
||||
|
||||
public IList<string> Encode(params ICookie[] cookies)
|
||||
{
|
||||
if (cookies == null || cookies.Length == 0)
|
||||
{
|
||||
return ImmutableList<string>.Empty;
|
||||
}
|
||||
|
||||
var encoded = new List<string>(cookies.Length);
|
||||
Dictionary<string, int> nameToIndex = this.Strict && cookies.Length > 1 ? new Dictionary<string, int>() : null;
|
||||
bool hasDupdName = false;
|
||||
for (int i = 0; i < cookies.Length; i++)
|
||||
{
|
||||
ICookie c = cookies[i];
|
||||
encoded.Add(this.Encode(c));
|
||||
if (nameToIndex != null)
|
||||
{
|
||||
if (nameToIndex.ContainsKey(c.Name))
|
||||
{
|
||||
nameToIndex[c.Name] = i;
|
||||
hasDupdName = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
nameToIndex.Add(c.Name, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasDupdName ? Dedup(encoded, nameToIndex) : encoded;
|
||||
}
|
||||
|
||||
public IList<string> Encode(ICollection<ICookie> cookies)
|
||||
{
|
||||
Contract.Requires(cookies != null);
|
||||
if (cookies.Count == 0)
|
||||
{
|
||||
return ImmutableList<string>.Empty;
|
||||
}
|
||||
|
||||
var encoded = new List<string>();
|
||||
var nameToIndex = new Dictionary<string, int>();
|
||||
bool hasDupdName = false;
|
||||
int i = 0;
|
||||
foreach (ICookie c in cookies)
|
||||
{
|
||||
encoded.Add(this.Encode(c));
|
||||
if (nameToIndex.ContainsKey(c.Name))
|
||||
{
|
||||
nameToIndex[c.Name] = i;
|
||||
hasDupdName = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
nameToIndex.Add(c.Name, i);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return hasDupdName ? Dedup(encoded, nameToIndex) : encoded;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ConvertToAutoProperty
|
||||
// ReSharper disable ConvertToAutoPropertyWhenPossible
|
||||
namespace DotNetty.Codecs.Http.Cors
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using DotNetty.Common.Concurrency;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
// Configuration for Cross-Origin Resource Sharing (CORS).
|
||||
public sealed class CorsConfig
|
||||
{
|
||||
readonly ISet<ICharSequence> origins;
|
||||
readonly bool anyOrigin;
|
||||
readonly bool enabled;
|
||||
readonly ISet<ICharSequence> exposeHeaders;
|
||||
readonly bool allowCredentials;
|
||||
readonly long maxAge;
|
||||
readonly ISet<HttpMethod> allowedRequestMethods;
|
||||
readonly ISet<AsciiString> allowedRequestHeaders;
|
||||
readonly bool allowNullOrigin;
|
||||
readonly IDictionary<AsciiString, ICallable<object>> preflightHeaders;
|
||||
readonly bool shortCircuit;
|
||||
|
||||
internal CorsConfig(CorsConfigBuilder builder)
|
||||
{
|
||||
this.origins = new HashSet<ICharSequence>(builder.origins, AsciiString.CaseSensitiveHasher);
|
||||
this.anyOrigin = builder.anyOrigin;
|
||||
this.enabled = builder.enabled;
|
||||
this.exposeHeaders = builder.exposeHeaders;
|
||||
this.allowCredentials = builder.allowCredentials;
|
||||
this.maxAge = builder.maxAge;
|
||||
this.allowedRequestMethods = builder.requestMethods;
|
||||
this.allowedRequestHeaders = builder.requestHeaders;
|
||||
this.allowNullOrigin = builder.allowNullOrigin;
|
||||
this.preflightHeaders = builder.preflightHeaders;
|
||||
this.shortCircuit = builder.shortCircuit;
|
||||
}
|
||||
|
||||
public bool IsCorsSupportEnabled => this.enabled;
|
||||
|
||||
public bool IsAnyOriginSupported => this.anyOrigin;
|
||||
|
||||
public ICharSequence Origin => this.origins.Count == 0 ? CorsHandler.AnyOrigin : this.origins.First();
|
||||
|
||||
public ISet<ICharSequence> Origins => this.origins;
|
||||
|
||||
public bool IsNullOriginAllowed => this.allowNullOrigin;
|
||||
|
||||
public ISet<ICharSequence> ExposedHeaders() => this.exposeHeaders.ToImmutableHashSet();
|
||||
|
||||
public bool IsCredentialsAllowed => this.allowCredentials;
|
||||
|
||||
public long MaxAge => this.maxAge;
|
||||
|
||||
public ISet<HttpMethod> AllowedRequestMethods() => this.allowedRequestMethods.ToImmutableHashSet();
|
||||
|
||||
public ISet<AsciiString> AllowedRequestHeaders() => this.allowedRequestHeaders.ToImmutableHashSet();
|
||||
|
||||
public HttpHeaders PreflightResponseHeaders()
|
||||
{
|
||||
if (this.preflightHeaders.Count == 0)
|
||||
{
|
||||
return EmptyHttpHeaders.Default;
|
||||
}
|
||||
HttpHeaders headers = new DefaultHttpHeaders();
|
||||
foreach (KeyValuePair<AsciiString, ICallable<object>> entry in this.preflightHeaders)
|
||||
{
|
||||
object value = GetValue(entry.Value);
|
||||
if (value is IEnumerable<object> values)
|
||||
{
|
||||
headers.Add(entry.Key, values);
|
||||
}
|
||||
else
|
||||
{
|
||||
headers.Add(entry.Key, value);
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
public bool IsShortCircuit => this.shortCircuit;
|
||||
|
||||
static object GetValue(ICallable<object> callable)
|
||||
{
|
||||
try
|
||||
{
|
||||
return callable.Call();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new InvalidOperationException($"Could not generate value for callable [{callable}]", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append($"{StringUtil.SimpleClassName(this)}")
|
||||
.Append($"[enabled = {this.enabled}");
|
||||
|
||||
builder.Append(", origins=");
|
||||
if (this.Origins.Count == 0)
|
||||
{
|
||||
builder.Append("*");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append("(");
|
||||
foreach (ICharSequence value in this.Origins)
|
||||
{
|
||||
builder.Append($"'{value}'");
|
||||
}
|
||||
builder.Append(")");
|
||||
}
|
||||
|
||||
builder.Append(", exposedHeaders=");
|
||||
if (this.exposeHeaders.Count == 0)
|
||||
{
|
||||
builder.Append("*");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append("(");
|
||||
foreach (ICharSequence value in this.exposeHeaders)
|
||||
{
|
||||
builder.Append($"'{value}'");
|
||||
}
|
||||
builder.Append(")");
|
||||
}
|
||||
|
||||
builder.Append($", isCredentialsAllowed={this.allowCredentials}");
|
||||
builder.Append($", maxAge={this.maxAge}");
|
||||
|
||||
builder.Append(", allowedRequestMethods=");
|
||||
if (this.allowedRequestMethods.Count == 0)
|
||||
{
|
||||
builder.Append("*");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append("(");
|
||||
foreach (HttpMethod value in this.allowedRequestMethods)
|
||||
{
|
||||
builder.Append($"'{value}'");
|
||||
}
|
||||
builder.Append(")");
|
||||
}
|
||||
|
||||
builder.Append(", allowedRequestHeaders=");
|
||||
if (this.allowedRequestHeaders.Count == 0)
|
||||
{
|
||||
builder.Append("*");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append("(");
|
||||
foreach(AsciiString value in this.allowedRequestHeaders)
|
||||
{
|
||||
builder.Append($"'{value}'");
|
||||
}
|
||||
builder.Append(")");
|
||||
}
|
||||
|
||||
builder.Append(", preflightHeaders=");
|
||||
if (this.preflightHeaders.Count == 0)
|
||||
{
|
||||
builder.Append("*");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append("(");
|
||||
foreach (AsciiString value in this.preflightHeaders.Keys)
|
||||
{
|
||||
builder.Append($"'{value}'");
|
||||
}
|
||||
builder.Append(")");
|
||||
}
|
||||
|
||||
builder.Append("]");
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace DotNetty.Codecs.Http.Cors
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.Contracts;
|
||||
using DotNetty.Common.Concurrency;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public sealed class CorsConfigBuilder
|
||||
{
|
||||
public static CorsConfigBuilder ForAnyOrigin() => new CorsConfigBuilder();
|
||||
|
||||
public static CorsConfigBuilder ForOrigin(ICharSequence origin)
|
||||
{
|
||||
return CorsHandler.AnyOrigin.ContentEquals(origin) // * AnyOrigin
|
||||
? new CorsConfigBuilder()
|
||||
: new CorsConfigBuilder(origin);
|
||||
}
|
||||
|
||||
public static CorsConfigBuilder ForOrigins(params ICharSequence[] origins) => new CorsConfigBuilder(origins);
|
||||
|
||||
internal readonly ISet<ICharSequence> origins;
|
||||
internal readonly bool anyOrigin;
|
||||
internal bool allowNullOrigin;
|
||||
internal bool enabled = true;
|
||||
internal bool allowCredentials;
|
||||
internal readonly HashSet<ICharSequence> exposeHeaders = new HashSet<ICharSequence>(AsciiString.CaseSensitiveHasher);
|
||||
internal long maxAge;
|
||||
internal readonly ISet<HttpMethod> requestMethods = new HashSet<HttpMethod>();
|
||||
internal readonly ISet<AsciiString> requestHeaders = new HashSet<AsciiString>();
|
||||
internal readonly Dictionary<AsciiString, ICallable<object>> preflightHeaders = new Dictionary<AsciiString, ICallable<object>>();
|
||||
internal bool noPreflightHeaders;
|
||||
internal bool shortCircuit;
|
||||
|
||||
CorsConfigBuilder(params ICharSequence[] origins)
|
||||
{
|
||||
this.origins = new HashSet<ICharSequence>(origins);
|
||||
this.anyOrigin = false;
|
||||
}
|
||||
|
||||
CorsConfigBuilder()
|
||||
{
|
||||
this.anyOrigin = true;
|
||||
this.origins = ImmutableHashSet<ICharSequence>.Empty;
|
||||
}
|
||||
|
||||
public CorsConfigBuilder AllowNullOrigin()
|
||||
{
|
||||
this.allowNullOrigin = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CorsConfigBuilder Disable()
|
||||
{
|
||||
this.enabled = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CorsConfigBuilder ExposeHeaders(params ICharSequence[] headers)
|
||||
{
|
||||
foreach (ICharSequence header in headers)
|
||||
{
|
||||
this.exposeHeaders.Add(header);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public CorsConfigBuilder ExposeHeaders(params string[] headers)
|
||||
{
|
||||
foreach (string header in headers)
|
||||
{
|
||||
this.exposeHeaders.Add(new StringCharSequence(header));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public CorsConfigBuilder AllowCredentials()
|
||||
{
|
||||
this.allowCredentials = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CorsConfigBuilder MaxAge(long max)
|
||||
{
|
||||
this.maxAge = max;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CorsConfigBuilder AllowedRequestMethods(params HttpMethod[] methods)
|
||||
{
|
||||
this.requestMethods.UnionWith(methods);
|
||||
return this;
|
||||
}
|
||||
|
||||
public CorsConfigBuilder AllowedRequestHeaders(params AsciiString[] headers)
|
||||
{
|
||||
this.requestHeaders.UnionWith(headers);
|
||||
return this;
|
||||
}
|
||||
|
||||
public CorsConfigBuilder AllowedRequestHeaders(params ICharSequence[] headers)
|
||||
{
|
||||
foreach (ICharSequence header in headers)
|
||||
{
|
||||
this.requestHeaders.Add(new AsciiString(header));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public CorsConfigBuilder PreflightResponseHeader(AsciiString name, params object[] values)
|
||||
{
|
||||
Contract.Requires(values != null);
|
||||
|
||||
if (values.Length == 1)
|
||||
{
|
||||
this.preflightHeaders.Add(name, new ConstantValueGenerator(values[0]));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.PreflightResponseHeader(name, new List<object>(values));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public CorsConfigBuilder PreflightResponseHeader(AsciiString name, ICollection<object> value)
|
||||
{
|
||||
this.preflightHeaders.Add(name, new ConstantValueGenerator(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public CorsConfigBuilder PreflightResponseHeader(AsciiString name, ICallable<object> valueGenerator)
|
||||
{
|
||||
this.preflightHeaders.Add(name, valueGenerator);
|
||||
return this;
|
||||
}
|
||||
|
||||
public CorsConfigBuilder NoPreflightResponseHeaders()
|
||||
{
|
||||
this.noPreflightHeaders = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CorsConfigBuilder ShortCircuit()
|
||||
{
|
||||
this.shortCircuit = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CorsConfig Build()
|
||||
{
|
||||
if (this.preflightHeaders.Count == 0 && !this.noPreflightHeaders)
|
||||
{
|
||||
this.preflightHeaders.Add(HttpHeaderNames.Date, DateValueGenerator.Default);
|
||||
this.preflightHeaders.Add(HttpHeaderNames.ContentLength, new ConstantValueGenerator(new AsciiString("0")));
|
||||
}
|
||||
return new CorsConfig(this);
|
||||
}
|
||||
|
||||
// This class is used for preflight HTTP response values that do not need to be
|
||||
// generated, but instead the value is "static" in that the same value will be returned
|
||||
// for each call.
|
||||
sealed class ConstantValueGenerator : ICallable<object>
|
||||
{
|
||||
readonly object value;
|
||||
|
||||
internal ConstantValueGenerator(object value)
|
||||
{
|
||||
Contract.Requires(value != null);
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public object Call() => this.value;
|
||||
}
|
||||
|
||||
// This callable is used for the DATE preflight HTTP response HTTP header.
|
||||
// It's value must be generated when the response is generated, hence will be
|
||||
// different for every call.
|
||||
sealed class DateValueGenerator : ICallable<object>
|
||||
{
|
||||
internal static readonly DateValueGenerator Default = new DateValueGenerator();
|
||||
|
||||
public object Call() => new DateTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
// 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.Cors
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Common.Internal.Logging;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
using static Common.Utilities.ReferenceCountUtil;
|
||||
|
||||
public class CorsHandler : ChannelDuplexHandler
|
||||
{
|
||||
static readonly IInternalLogger Logger = InternalLoggerFactory.GetInstance<CorsHandler>();
|
||||
|
||||
internal static readonly AsciiString AnyOrigin = new AsciiString("*");
|
||||
internal static readonly AsciiString NullOrigin = new AsciiString("null");
|
||||
|
||||
readonly CorsConfig config;
|
||||
IHttpRequest request;
|
||||
|
||||
public CorsHandler(CorsConfig config)
|
||||
{
|
||||
Contract.Requires(config != null);
|
||||
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public override void ChannelRead(IChannelHandlerContext context, object message)
|
||||
{
|
||||
if (this.config.IsCorsSupportEnabled && message is IHttpRequest)
|
||||
{
|
||||
this.request = (IHttpRequest)message;
|
||||
if (IsPreflightRequest(this.request))
|
||||
{
|
||||
this.HandlePreflight(context, this.request);
|
||||
return;
|
||||
}
|
||||
if (this.config.IsShortCircuit && !this.ValidateOrigin())
|
||||
{
|
||||
Forbidden(context, this.request);
|
||||
return;
|
||||
}
|
||||
}
|
||||
context.FireChannelRead(message);
|
||||
}
|
||||
|
||||
void HandlePreflight(IChannelHandlerContext ctx, IHttpRequest req)
|
||||
{
|
||||
var response = new DefaultFullHttpResponse(req.ProtocolVersion, HttpResponseStatus.OK, true, true);
|
||||
if (this.SetOrigin(response))
|
||||
{
|
||||
this.SetAllowMethods(response);
|
||||
this.SetAllowHeaders(response);
|
||||
this.SetAllowCredentials(response);
|
||||
this.SetMaxAge(response);
|
||||
this.SetPreflightHeaders(response);
|
||||
}
|
||||
if (!response.Headers.Contains(HttpHeaderNames.ContentLength))
|
||||
{
|
||||
response.Headers.Set(HttpHeaderNames.ContentLength, HttpHeaderValues.Zero);
|
||||
}
|
||||
|
||||
Release(req);
|
||||
Respond(ctx, req, response);
|
||||
}
|
||||
|
||||
void SetPreflightHeaders(IHttpResponse response) => response.Headers.Add(this.config.PreflightResponseHeaders());
|
||||
|
||||
bool SetOrigin(IHttpResponse response)
|
||||
{
|
||||
if (!this.request.Headers.TryGet(HttpHeaderNames.Origin, out ICharSequence origin))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (NullOrigin.ContentEquals(origin) && this.config.IsNullOriginAllowed)
|
||||
{
|
||||
SetNullOrigin(response);
|
||||
return true;
|
||||
}
|
||||
if (this.config.IsAnyOriginSupported)
|
||||
{
|
||||
if (this.config.IsCredentialsAllowed)
|
||||
{
|
||||
this.EchoRequestOrigin(response);
|
||||
SetVaryHeader(response);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetAnyOrigin(response);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (this.config.Origins.Contains(origin))
|
||||
{
|
||||
SetOrigin(response, origin);
|
||||
SetVaryHeader(response);
|
||||
return true;
|
||||
}
|
||||
Logger.Debug("Request origin [{}]] was not among the configured origins [{}]", origin, this.config.Origins);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ValidateOrigin()
|
||||
{
|
||||
if (this.config.IsAnyOriginSupported)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.request.Headers.TryGet(HttpHeaderNames.Origin, out ICharSequence origin))
|
||||
{
|
||||
// Not a CORS request so we cannot validate it. It may be a non CORS request.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (NullOrigin.ContentEquals(origin) && this.config.IsNullOriginAllowed)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return this.config.Origins.Contains(origin);
|
||||
}
|
||||
|
||||
void EchoRequestOrigin(IHttpResponse response) => SetOrigin(response, this.request.Headers.Get(HttpHeaderNames.Origin, null));
|
||||
|
||||
static void SetVaryHeader(IHttpResponse response) => response.Headers.Set(HttpHeaderNames.Vary, HttpHeaderNames.Origin);
|
||||
|
||||
static void SetAnyOrigin(IHttpResponse response) => SetOrigin(response, AnyOrigin);
|
||||
|
||||
static void SetNullOrigin(IHttpResponse response) => SetOrigin(response, NullOrigin);
|
||||
|
||||
static void SetOrigin(IHttpResponse response, ICharSequence origin) => response.Headers.Set(HttpHeaderNames.AccessControlAllowOrigin, origin);
|
||||
|
||||
void SetAllowCredentials(IHttpResponse response)
|
||||
{
|
||||
if (this.config.IsCredentialsAllowed
|
||||
&& !AsciiString.ContentEquals(response.Headers.Get(HttpHeaderNames.AccessControlAllowOrigin, null), AnyOrigin))
|
||||
{
|
||||
response.Headers.Set(HttpHeaderNames.AccessControlAllowCredentials, new AsciiString("true"));
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsPreflightRequest(IHttpRequest request)
|
||||
{
|
||||
HttpHeaders headers = request.Headers;
|
||||
return request.Method.Equals(HttpMethod.Options)
|
||||
&& headers.Contains(HttpHeaderNames.Origin)
|
||||
&& headers.Contains(HttpHeaderNames.AccessControlRequestMethod);
|
||||
}
|
||||
|
||||
void SetExposeHeaders(IHttpResponse response)
|
||||
{
|
||||
ISet<ICharSequence> headers = this.config.ExposedHeaders();
|
||||
if (headers.Count > 0)
|
||||
{
|
||||
response.Headers.Set(HttpHeaderNames.AccessControlExposeHeaders, headers);
|
||||
}
|
||||
}
|
||||
|
||||
void SetAllowMethods(IHttpResponse response) => response.Headers.Set(HttpHeaderNames.AccessControlAllowMethods, this.config.AllowedRequestMethods());
|
||||
|
||||
void SetAllowHeaders(IHttpResponse response) => response.Headers.Set(HttpHeaderNames.AccessControlAllowHeaders, this.config.AllowedRequestHeaders());
|
||||
|
||||
void SetMaxAge(IHttpResponse response) => response.Headers.Set(HttpHeaderNames.AccessControlMaxAge, this.config.MaxAge);
|
||||
|
||||
public override Task WriteAsync(IChannelHandlerContext context, object message)
|
||||
{
|
||||
if (this.config.IsCorsSupportEnabled && message is IHttpResponse response)
|
||||
{
|
||||
if (this.SetOrigin(response))
|
||||
{
|
||||
this.SetAllowCredentials(response);
|
||||
this.SetExposeHeaders(response);
|
||||
}
|
||||
}
|
||||
return context.WriteAndFlushAsync(message);
|
||||
}
|
||||
|
||||
static void Forbidden(IChannelHandlerContext ctx, IHttpRequest request)
|
||||
{
|
||||
var response = new DefaultFullHttpResponse(request.ProtocolVersion, HttpResponseStatus.Forbidden);
|
||||
response.Headers.Set(HttpHeaderNames.ContentLength, HttpHeaderValues.Zero);
|
||||
Release(request);
|
||||
Respond(ctx, request, response);
|
||||
}
|
||||
|
||||
static void Respond(IChannelHandlerContext ctx, IHttpRequest request, IHttpResponse response)
|
||||
{
|
||||
bool keepAlive = HttpUtil.IsKeepAlive(request);
|
||||
|
||||
HttpUtil.SetKeepAlive(response, keepAlive);
|
||||
|
||||
Task task = ctx.WriteAndFlushAsync(response);
|
||||
if (!keepAlive)
|
||||
{
|
||||
task.ContinueWith(CloseOnComplete, ctx,
|
||||
TaskContinuationOptions.ExecuteSynchronously);
|
||||
}
|
||||
}
|
||||
|
||||
static void CloseOnComplete(Task task, object state)
|
||||
{
|
||||
var ctx = (IChannelHandlerContext)state;
|
||||
ctx.CloseAsync();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ConvertToAutoPropertyWhenPossible
|
||||
// ReSharper disable ConvertToAutoProperty
|
||||
namespace DotNetty.Codecs.Http
|
||||
{
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Text;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common;
|
||||
|
||||
public class DefaultFullHttpRequest : DefaultHttpRequest, IFullHttpRequest
|
||||
{
|
||||
readonly IByteBuffer content;
|
||||
readonly HttpHeaders trailingHeader;
|
||||
// Used to cache the value of the hash code and avoid {@link IllegalReferenceCountException}.
|
||||
int hash;
|
||||
|
||||
public DefaultFullHttpRequest(HttpVersion httpVersion, HttpMethod method, string uri)
|
||||
: this(httpVersion, method, uri, Unpooled.Buffer(0))
|
||||
{
|
||||
}
|
||||
|
||||
public DefaultFullHttpRequest(HttpVersion httpVersion, HttpMethod method, string uri, IByteBuffer content)
|
||||
: this(httpVersion, method, uri, content, true)
|
||||
{
|
||||
}
|
||||
|
||||
public DefaultFullHttpRequest(HttpVersion httpVersion, HttpMethod method, string uri, bool validateHeaders)
|
||||
: this(httpVersion, method, uri, Unpooled.Buffer(0), validateHeaders)
|
||||
{
|
||||
}
|
||||
|
||||
public DefaultFullHttpRequest(HttpVersion httpVersion, HttpMethod method, string uri,
|
||||
IByteBuffer content, bool validateHeaders)
|
||||
: base(httpVersion, method, uri, validateHeaders)
|
||||
{
|
||||
Contract.Requires(content != null);
|
||||
|
||||
this.content = content;
|
||||
this.trailingHeader = new DefaultHttpHeaders(validateHeaders);
|
||||
}
|
||||
|
||||
public DefaultFullHttpRequest(HttpVersion httpVersion, HttpMethod method, string uri,
|
||||
IByteBuffer content, HttpHeaders headers, HttpHeaders trailingHeader)
|
||||
: base(httpVersion, method, uri, headers)
|
||||
{
|
||||
Contract.Requires(content != null);
|
||||
Contract.Requires(trailingHeader != null);
|
||||
|
||||
this.content = content;
|
||||
this.trailingHeader = trailingHeader;
|
||||
}
|
||||
|
||||
public HttpHeaders TrailingHeaders => this.trailingHeader;
|
||||
|
||||
public IByteBuffer Content => this.content;
|
||||
|
||||
public int ReferenceCount => this.content.ReferenceCount;
|
||||
|
||||
public IReferenceCounted Retain()
|
||||
{
|
||||
this.content.Retain();
|
||||
return this;
|
||||
}
|
||||
|
||||
public IReferenceCounted Retain(int increment)
|
||||
{
|
||||
this.content.Retain(increment);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IReferenceCounted Touch()
|
||||
{
|
||||
this.content.Touch();
|
||||
return this;
|
||||
}
|
||||
|
||||
public IReferenceCounted Touch(object hint)
|
||||
{
|
||||
this.content.Touch(hint);
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Release() => this.content.Release();
|
||||
|
||||
public bool Release(int decrement) => this.content.Release(decrement);
|
||||
|
||||
public IByteBufferHolder Copy() => this.Replace(this.content.Copy());
|
||||
|
||||
public IByteBufferHolder Duplicate() => this.Replace(this.content.Duplicate());
|
||||
|
||||
public IByteBufferHolder RetainedDuplicate() => this.Replace(this.content.RetainedDuplicate());
|
||||
|
||||
public IByteBufferHolder Replace(IByteBuffer newContent) =>
|
||||
new DefaultFullHttpRequest(this.ProtocolVersion, this.Method, this.Uri, newContent, this.Headers, this.trailingHeader);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
// ReSharper disable NonReadonlyMemberInGetHashCode
|
||||
int hashCode = this.hash;
|
||||
if (hashCode == 0)
|
||||
{
|
||||
if (this.content.ReferenceCount != 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
hashCode = 31 + this.content.GetHashCode();
|
||||
}
|
||||
catch (IllegalReferenceCountException)
|
||||
{
|
||||
// Handle race condition between checking refCnt() == 0 and using the object.
|
||||
hashCode = 31;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
hashCode = 31;
|
||||
}
|
||||
hashCode = 31 * hashCode + this.trailingHeader.GetHashCode();
|
||||
hashCode = 31 * hashCode + base.GetHashCode();
|
||||
|
||||
this.hash = hashCode;
|
||||
}
|
||||
// ReSharper restore NonReadonlyMemberInGetHashCode
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (!(obj is DefaultFullHttpRequest other))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return base.Equals(other)
|
||||
&& this.content.Equals(other.content)
|
||||
&& this.trailingHeader.Equals(other.trailingHeader);
|
||||
}
|
||||
|
||||
public override string ToString() => HttpMessageUtil.AppendFullRequest(new StringBuilder(256), this).ToString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ConvertToAutoPropertyWhenPossible
|
||||
// ReSharper disable ConvertToAutoProperty
|
||||
namespace DotNetty.Codecs.Http
|
||||
{
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Text;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common;
|
||||
|
||||
public class DefaultFullHttpResponse : DefaultHttpResponse, IFullHttpResponse
|
||||
{
|
||||
readonly IByteBuffer content;
|
||||
readonly HttpHeaders trailingHeaders;
|
||||
|
||||
// Used to cache the value of the hash code and avoid {@link IllegalReferenceCountException}.
|
||||
int hash;
|
||||
|
||||
public DefaultFullHttpResponse(HttpVersion version, HttpResponseStatus status)
|
||||
: this(version, status, Unpooled.Buffer(0))
|
||||
{
|
||||
}
|
||||
|
||||
public DefaultFullHttpResponse(HttpVersion version, HttpResponseStatus status, IByteBuffer content)
|
||||
: this(version, status, content, true)
|
||||
{
|
||||
}
|
||||
|
||||
public DefaultFullHttpResponse(HttpVersion version, HttpResponseStatus status, bool validateHeaders)
|
||||
: this(version, status, Unpooled.Buffer(0), validateHeaders, false)
|
||||
{
|
||||
}
|
||||
|
||||
public DefaultFullHttpResponse(HttpVersion version, HttpResponseStatus status, bool validateHeaders,
|
||||
bool singleFieldHeaders)
|
||||
: this(version, status, Unpooled.Buffer(0), validateHeaders, singleFieldHeaders)
|
||||
{
|
||||
}
|
||||
|
||||
public DefaultFullHttpResponse(HttpVersion version, HttpResponseStatus status,
|
||||
IByteBuffer content, bool validateHeaders)
|
||||
: this(version, status, content, validateHeaders, false)
|
||||
{
|
||||
}
|
||||
|
||||
public DefaultFullHttpResponse(HttpVersion version, HttpResponseStatus status,
|
||||
IByteBuffer content, bool validateHeaders, bool singleFieldHeaders)
|
||||
: base(version, status, validateHeaders, singleFieldHeaders)
|
||||
{
|
||||
Contract.Requires(content != null);
|
||||
|
||||
this.content = content;
|
||||
this.trailingHeaders = singleFieldHeaders
|
||||
? new CombinedHttpHeaders(validateHeaders)
|
||||
: new DefaultHttpHeaders(validateHeaders);
|
||||
}
|
||||
|
||||
public DefaultFullHttpResponse(HttpVersion version, HttpResponseStatus status, IByteBuffer content, HttpHeaders headers, HttpHeaders trailingHeaders)
|
||||
: base(version, status, headers)
|
||||
{
|
||||
Contract.Requires(content != null);
|
||||
Contract.Requires(trailingHeaders != null);
|
||||
|
||||
this.content = content;
|
||||
this.trailingHeaders = trailingHeaders;
|
||||
}
|
||||
|
||||
public HttpHeaders TrailingHeaders => this.trailingHeaders;
|
||||
|
||||
public IByteBuffer Content => this.content;
|
||||
|
||||
public int ReferenceCount => this.content.ReferenceCount;
|
||||
|
||||
public IReferenceCounted Retain()
|
||||
{
|
||||
this.content.Retain();
|
||||
return this;
|
||||
}
|
||||
|
||||
public IReferenceCounted Retain(int increment)
|
||||
{
|
||||
this.content.Retain(increment);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IReferenceCounted Touch()
|
||||
{
|
||||
this.content.Touch();
|
||||
return this;
|
||||
}
|
||||
|
||||
public IReferenceCounted Touch(object hint)
|
||||
{
|
||||
this.content.Touch(hint);
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Release() => this.content.Release();
|
||||
|
||||
public bool Release(int decrement) => this.content.Release(decrement);
|
||||
|
||||
public IByteBufferHolder Copy() => this.Replace(this.content.Copy());
|
||||
|
||||
public IByteBufferHolder Duplicate() => this.Replace(this.content.Duplicate());
|
||||
|
||||
public IByteBufferHolder RetainedDuplicate() => this.Replace(this.content.RetainedDuplicate());
|
||||
|
||||
public IByteBufferHolder Replace(IByteBuffer newContent) =>
|
||||
new DefaultFullHttpResponse(this.ProtocolVersion, this.Status, newContent, this.Headers, this.trailingHeaders);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
// ReSharper disable NonReadonlyMemberInGetHashCode
|
||||
int hashCode = this.hash;
|
||||
if (hashCode == 0)
|
||||
{
|
||||
if (this.content.ReferenceCount != 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
hashCode = 31 + this.content.GetHashCode();
|
||||
}
|
||||
catch (IllegalReferenceCountException)
|
||||
{
|
||||
// Handle race condition between checking refCnt() == 0 and using the object.
|
||||
hashCode = 31;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
hashCode = 31;
|
||||
}
|
||||
hashCode = 31 * hashCode + this.trailingHeaders.GetHashCode();
|
||||
hashCode = 31 * hashCode + base.GetHashCode();
|
||||
this.hash = hashCode;
|
||||
}
|
||||
// ReSharper restore NonReadonlyMemberInGetHashCode
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (!(obj is DefaultFullHttpResponse other))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return base.Equals(other)
|
||||
&& this.content.Equals(other.content)
|
||||
&& this.trailingHeaders.Equals(other.trailingHeaders);
|
||||
}
|
||||
|
||||
public override string ToString() => HttpMessageUtil.AppendFullResponse(new StringBuilder(256), this).ToString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ConvertToAutoPropertyWhenPossible
|
||||
// ReSharper disable ConvertToAutoProperty
|
||||
namespace DotNetty.Codecs.Http
|
||||
{
|
||||
using System.Diagnostics.Contracts;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public class DefaultHttpContent : DefaultHttpObject, IHttpContent
|
||||
{
|
||||
readonly IByteBuffer content;
|
||||
|
||||
public DefaultHttpContent(IByteBuffer content)
|
||||
{
|
||||
Contract.Requires(content != null);
|
||||
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public IByteBuffer Content => this.content;
|
||||
|
||||
public IByteBufferHolder Copy() => this.Replace(this.content.Copy());
|
||||
|
||||
public IByteBufferHolder Duplicate() => this.Replace(this.content.Duplicate());
|
||||
|
||||
public IByteBufferHolder RetainedDuplicate() => this.Replace(this.content.RetainedDuplicate());
|
||||
|
||||
public virtual IByteBufferHolder Replace(IByteBuffer buffer) => new DefaultHttpContent(buffer);
|
||||
|
||||
public int ReferenceCount => this.content.ReferenceCount;
|
||||
|
||||
public IReferenceCounted Retain()
|
||||
{
|
||||
this.content.Retain();
|
||||
return this;
|
||||
}
|
||||
|
||||
public IReferenceCounted Retain(int increment)
|
||||
{
|
||||
this.content.Retain(increment);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IReferenceCounted Touch()
|
||||
{
|
||||
this.content.Touch();
|
||||
return this;
|
||||
}
|
||||
|
||||
public IReferenceCounted Touch(object hint)
|
||||
{
|
||||
this.content.Touch(hint);
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Release() => this.content.Release();
|
||||
|
||||
public bool Release(int decrement) => this.content.Release(decrement);
|
||||
|
||||
public override string ToString() => $"{StringUtil.SimpleClassName(this)} (data: {this.content}, decoderResult: {this.Result})";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,372 @@
|
|||
// 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
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using DotNetty.Codecs;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public class DefaultHttpHeaders : HttpHeaders
|
||||
{
|
||||
const int HighestInvalidValueCharMask = ~15;
|
||||
internal static readonly INameValidator<ICharSequence> HttpNameValidator = new HeaderNameValidator();
|
||||
internal static readonly INameValidator<ICharSequence> NotNullValidator = new NullNameValidator<ICharSequence>();
|
||||
|
||||
sealed class NameProcessor : IByteProcessor
|
||||
{
|
||||
public bool Process(byte value)
|
||||
{
|
||||
ValidateHeaderNameElement(value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
sealed class HeaderNameValidator : INameValidator<ICharSequence>
|
||||
{
|
||||
static readonly NameProcessor ByteProcessor = new NameProcessor();
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ValidateName(ICharSequence name)
|
||||
{
|
||||
if (name == null || name.Count == 0)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentException_HeaderName();
|
||||
}
|
||||
if (name is AsciiString asciiString)
|
||||
{
|
||||
asciiString.ForEachByte(ByteProcessor);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Go through each character in the name
|
||||
Debug.Assert(name != null);
|
||||
// ReSharper disable once ForCanBeConvertedToForeach
|
||||
// Avoid new enumerator instance
|
||||
for (int index = 0; index < name.Count; ++index)
|
||||
{
|
||||
ValidateHeaderNameElement(name[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly DefaultHeaders<AsciiString, ICharSequence> headers;
|
||||
|
||||
public DefaultHttpHeaders() : this(true)
|
||||
{
|
||||
}
|
||||
|
||||
public DefaultHttpHeaders(bool validate) : this(validate, NameValidator(validate))
|
||||
{
|
||||
}
|
||||
|
||||
protected DefaultHttpHeaders(bool validate, INameValidator<ICharSequence> nameValidator)
|
||||
: this(new DefaultHeaders<AsciiString, ICharSequence>(AsciiString.CaseInsensitiveHasher,
|
||||
ValueConverter(validate), nameValidator))
|
||||
{
|
||||
}
|
||||
|
||||
protected DefaultHttpHeaders(DefaultHeaders<AsciiString, ICharSequence> headers)
|
||||
{
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
public override HttpHeaders Add(HttpHeaders httpHeaders)
|
||||
{
|
||||
if (httpHeaders is DefaultHttpHeaders defaultHttpHeaders)
|
||||
{
|
||||
this.headers.Add(defaultHttpHeaders.headers);
|
||||
return this;
|
||||
}
|
||||
return base.Add(httpHeaders);
|
||||
}
|
||||
|
||||
public override HttpHeaders Set(HttpHeaders httpHeaders)
|
||||
{
|
||||
if (httpHeaders is DefaultHttpHeaders defaultHttpHeaders)
|
||||
{
|
||||
this.headers.Set(defaultHttpHeaders.headers);
|
||||
return this;
|
||||
}
|
||||
return base.Set(httpHeaders);
|
||||
}
|
||||
|
||||
public override HttpHeaders Add(AsciiString name, object value)
|
||||
{
|
||||
this.headers.AddObject(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public override HttpHeaders AddInt(AsciiString name, int value)
|
||||
{
|
||||
this.headers.AddInt(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public override HttpHeaders AddShort(AsciiString name, short value)
|
||||
{
|
||||
this.headers.AddShort(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public override HttpHeaders Remove(AsciiString name)
|
||||
{
|
||||
this.headers.Remove(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
public override HttpHeaders Set(AsciiString name, object value)
|
||||
{
|
||||
this.headers.SetObject(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public override HttpHeaders Set(AsciiString name, IEnumerable<object> values)
|
||||
{
|
||||
this.headers.SetObject(name, values);
|
||||
return this;
|
||||
}
|
||||
|
||||
public override HttpHeaders SetInt(AsciiString name, int value)
|
||||
{
|
||||
this.headers.SetInt(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public override HttpHeaders SetShort(AsciiString name, short value)
|
||||
{
|
||||
this.headers.SetShort(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public override HttpHeaders Clear()
|
||||
{
|
||||
this.headers.Clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
public override bool TryGet(AsciiString name, out ICharSequence value) => this.headers.TryGet(name, out value);
|
||||
|
||||
public override bool TryGetInt(AsciiString name, out int value) => this.headers.TryGetInt(name, out value);
|
||||
|
||||
public override int GetInt(AsciiString name, int defaultValue) => this.headers.GetInt(name, defaultValue);
|
||||
|
||||
public override bool TryGetShort(AsciiString name, out short value) => this.headers.TryGetShort(name, out value);
|
||||
|
||||
public override short GetShort(AsciiString name, short defaultValue) => this.headers.GetShort(name, defaultValue);
|
||||
|
||||
public override bool TryGetTimeMillis(AsciiString name, out long value) => this.headers.TryGetTimeMillis(name, out value);
|
||||
|
||||
public override long GetTimeMillis(AsciiString name, long defaultValue) => this.headers.GetTimeMillis(name, defaultValue);
|
||||
|
||||
public override IList<ICharSequence> GetAll(AsciiString name) => this.headers.GetAll(name);
|
||||
|
||||
public override IEnumerable<ICharSequence> ValueCharSequenceIterator(AsciiString name) => this.headers.ValueIterator(name);
|
||||
|
||||
public override IList<HeaderEntry<AsciiString, ICharSequence>> Entries()
|
||||
{
|
||||
if (this.IsEmpty)
|
||||
{
|
||||
return ImmutableList<HeaderEntry<AsciiString, ICharSequence>>.Empty;
|
||||
}
|
||||
var entriesConverted = new List<HeaderEntry<AsciiString, ICharSequence>>(this.headers.Size);
|
||||
foreach(HeaderEntry<AsciiString, ICharSequence> entry in this)
|
||||
{
|
||||
entriesConverted.Add(entry);
|
||||
}
|
||||
return entriesConverted;
|
||||
}
|
||||
|
||||
public override IEnumerator<HeaderEntry<AsciiString, ICharSequence>> GetEnumerator() => this.headers.GetEnumerator();
|
||||
|
||||
public override bool Contains(AsciiString name) => this.headers.Contains(name);
|
||||
|
||||
public override bool IsEmpty => this.headers.IsEmpty;
|
||||
|
||||
public override int Size => this.headers.Size;
|
||||
|
||||
public override bool Contains(AsciiString name, ICharSequence value, bool ignoreCase) =>
|
||||
this.headers.Contains(name, value,
|
||||
ignoreCase ? AsciiString.CaseInsensitiveHasher : AsciiString.CaseSensitiveHasher);
|
||||
|
||||
public override ISet<AsciiString> Names() => this.headers.Names();
|
||||
|
||||
public override bool Equals(object obj) => obj is DefaultHttpHeaders other
|
||||
&& this.headers.Equals(other.headers, AsciiString.CaseSensitiveHasher);
|
||||
|
||||
public override int GetHashCode() => this.headers.HashCode(AsciiString.CaseSensitiveHasher);
|
||||
|
||||
public override HttpHeaders Copy() => new DefaultHttpHeaders(this.headers.Copy());
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static void ValidateHeaderNameElement(byte value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case 0x00:
|
||||
case 0x09: //'\t':
|
||||
case 0x0a: //'\n':
|
||||
case 0x0b:
|
||||
case 0x0c: //'\f':
|
||||
case 0x0d: //'\r':
|
||||
case 0x20: //' ':
|
||||
case 0x2c: //',':
|
||||
case 0x3a: //':':
|
||||
case 0x3b: //';':
|
||||
case 0x3d: //'=':
|
||||
ThrowHelper.ThrowArgumentException_HeaderValue(value);
|
||||
break;
|
||||
default:
|
||||
// Check to see if the character is not an ASCII character, or invalid
|
||||
if (value > 127)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentException_HeaderValueNonAscii(value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static void ValidateHeaderNameElement(char value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case '\x00':
|
||||
case '\t':
|
||||
case '\n':
|
||||
case '\x0b':
|
||||
case '\f':
|
||||
case '\r':
|
||||
case ' ':
|
||||
case ',':
|
||||
case ':':
|
||||
case ';':
|
||||
case '=':
|
||||
ThrowHelper.ThrowArgumentException_HeaderValue(value);
|
||||
break;
|
||||
default:
|
||||
// Check to see if the character is not an ASCII character, or invalid
|
||||
if (value > 127)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentException_HeaderValueNonAscii(value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected static IValueConverter<ICharSequence> ValueConverter(bool validate) =>
|
||||
validate ? DefaultHeaderValueConverterAndValidator : DefaultHeaderValueConverter;
|
||||
|
||||
protected static INameValidator<ICharSequence> NameValidator(bool validate) =>
|
||||
validate ? HttpNameValidator : NotNullValidator;
|
||||
|
||||
static readonly HeaderValueConverter DefaultHeaderValueConverter = new HeaderValueConverter();
|
||||
|
||||
class HeaderValueConverter : CharSequenceValueConverter
|
||||
{
|
||||
public override ICharSequence ConvertObject(object value)
|
||||
{
|
||||
if (value is ICharSequence seq)
|
||||
{
|
||||
return seq;
|
||||
}
|
||||
if (value is DateTime time)
|
||||
{
|
||||
return new StringCharSequence(DateFormatter.Format(time));
|
||||
}
|
||||
return new StringCharSequence(value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
static readonly HeaderValueConverterAndValidator DefaultHeaderValueConverterAndValidator = new HeaderValueConverterAndValidator();
|
||||
|
||||
sealed class HeaderValueConverterAndValidator : HeaderValueConverter
|
||||
{
|
||||
public override ICharSequence ConvertObject(object value)
|
||||
{
|
||||
ICharSequence seq = base.ConvertObject(value);
|
||||
int state = 0;
|
||||
// Start looping through each of the character
|
||||
// ReSharper disable once ForCanBeConvertedToForeach
|
||||
// Avoid enumerator allocation
|
||||
for (int index = 0; index < seq.Count; index++)
|
||||
{
|
||||
state = ValidateValueChar(state, seq[index]);
|
||||
}
|
||||
|
||||
if (state != 0)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentException_HeaderValueEnd(seq);
|
||||
}
|
||||
return seq;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static int ValidateValueChar(int state, char character)
|
||||
{
|
||||
// State:
|
||||
// 0: Previous character was neither CR nor LF
|
||||
// 1: The previous character was CR
|
||||
// 2: The previous character was LF
|
||||
if ((character & HighestInvalidValueCharMask) == 0)
|
||||
{
|
||||
// Check the absolutely prohibited characters.
|
||||
switch (character)
|
||||
{
|
||||
case '\x00': // NULL
|
||||
ThrowHelper.ThrowArgumentException_HeaderValueNullChar();
|
||||
break;
|
||||
case '\x0b': // Vertical tab
|
||||
ThrowHelper.ThrowArgumentException_HeaderValueVerticalTabChar();
|
||||
break;
|
||||
case '\f':
|
||||
ThrowHelper.ThrowArgumentException_HeaderValueFormFeed();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check the CRLF (HT | SP) pattern
|
||||
switch (state)
|
||||
{
|
||||
case 0:
|
||||
switch (character)
|
||||
{
|
||||
case '\r':
|
||||
return 1;
|
||||
case '\n':
|
||||
return 2;
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
switch (character)
|
||||
{
|
||||
case '\n':
|
||||
return 2;
|
||||
default:
|
||||
ThrowHelper.ThrowArgumentException_NewLineAfterLineFeed();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
switch (character)
|
||||
{
|
||||
case '\t':
|
||||
case ' ':
|
||||
return 0;
|
||||
default:
|
||||
ThrowHelper.ThrowArgumentException_TabAndSpaceAfterLineFeed();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ConvertToAutoPropertyWhenPossible
|
||||
// ReSharper disable ConvertToAutoProperty
|
||||
// ReSharper disable ConvertToAutoPropertyWithPrivateSetter
|
||||
namespace DotNetty.Codecs.Http
|
||||
{
|
||||
using System.Diagnostics.Contracts;
|
||||
|
||||
public abstract class DefaultHttpMessage : DefaultHttpObject, IHttpMessage
|
||||
{
|
||||
const int HashCodePrime = 31;
|
||||
HttpVersion version;
|
||||
readonly HttpHeaders headers;
|
||||
|
||||
protected DefaultHttpMessage(HttpVersion version) : this(version, true, false)
|
||||
{
|
||||
}
|
||||
|
||||
protected DefaultHttpMessage(HttpVersion version, bool validateHeaders, bool singleFieldHeaders)
|
||||
{
|
||||
Contract.Requires(version != null);
|
||||
|
||||
this.version = version;
|
||||
this.headers = singleFieldHeaders
|
||||
? new CombinedHttpHeaders(validateHeaders)
|
||||
: new DefaultHttpHeaders(validateHeaders);
|
||||
}
|
||||
|
||||
protected DefaultHttpMessage(HttpVersion version, HttpHeaders headers)
|
||||
{
|
||||
Contract.Requires(version != null);
|
||||
Contract.Requires(headers != null);
|
||||
|
||||
this.version = version;
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
public HttpHeaders Headers => this.headers;
|
||||
|
||||
public HttpVersion ProtocolVersion => this.version;
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
int result = 1;
|
||||
result = HashCodePrime * result + this.headers.GetHashCode();
|
||||
// ReSharper disable once NonReadonlyMemberInGetHashCode
|
||||
result = HashCodePrime * result + this.version.GetHashCode();
|
||||
result = HashCodePrime * result + base.GetHashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (!(obj is DefaultHttpMessage other))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.headers.Equals(other.headers)
|
||||
&& this.version.Equals(other.version)
|
||||
&& base.Equals(obj);
|
||||
}
|
||||
|
||||
public IHttpMessage SetProtocolVersion(HttpVersion value)
|
||||
{
|
||||
Contract.Requires(value != null);
|
||||
this.version = value;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// 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
|
||||
{
|
||||
using System.Diagnostics.Contracts;
|
||||
|
||||
public class DefaultHttpObject : IHttpObject
|
||||
{
|
||||
const int HashCodePrime = 31;
|
||||
DecoderResult decoderResult = DecoderResult.Success;
|
||||
|
||||
protected DefaultHttpObject()
|
||||
{
|
||||
}
|
||||
|
||||
public DecoderResult Result
|
||||
{
|
||||
get => this.decoderResult;
|
||||
set
|
||||
{
|
||||
Contract.Requires(value != null);
|
||||
this.decoderResult = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
int result = 1;
|
||||
// ReSharper disable once NonReadonlyMemberInGetHashCode
|
||||
result = HashCodePrime * result + this.decoderResult.GetHashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (!(obj is DefaultHttpObject other))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return this.decoderResult.Equals(other.decoderResult);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ConvertToAutoPropertyWhenPossible
|
||||
// ReSharper disable ConvertToAutoPropertyWithPrivateSetter
|
||||
namespace DotNetty.Codecs.Http
|
||||
{
|
||||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Text;
|
||||
|
||||
public class DefaultHttpRequest : DefaultHttpMessage, IHttpRequest
|
||||
{
|
||||
const int HashCodePrime = 31;
|
||||
|
||||
HttpMethod method;
|
||||
string uri;
|
||||
|
||||
public DefaultHttpRequest(HttpVersion httpVersion, HttpMethod method, string uri)
|
||||
: this(httpVersion, method, uri, true)
|
||||
{
|
||||
}
|
||||
|
||||
public DefaultHttpRequest(HttpVersion version, HttpMethod method, string uri, bool validateHeaders)
|
||||
: base(version, validateHeaders, false)
|
||||
{
|
||||
Contract.Requires(method != null);
|
||||
Contract.Requires(uri != null);
|
||||
|
||||
this.method = method;
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public DefaultHttpRequest(HttpVersion version, HttpMethod method, string uri, HttpHeaders headers)
|
||||
: base(version, headers)
|
||||
{
|
||||
Contract.Requires(method != null);
|
||||
Contract.Requires(uri != null);
|
||||
|
||||
this.method = method;
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public HttpMethod Method => this.method;
|
||||
|
||||
public string Uri => this.uri;
|
||||
|
||||
public IHttpRequest SetMethod(HttpMethod value)
|
||||
{
|
||||
Contract.Requires(value != null);
|
||||
this.method = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IHttpRequest SetUri(string value)
|
||||
{
|
||||
Contract.Requires(value != null);
|
||||
this.uri = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
// ReSharper disable NonReadonlyMemberInGetHashCode
|
||||
public override int GetHashCode()
|
||||
{
|
||||
int result = 1;
|
||||
result = HashCodePrime * result + this.method.GetHashCode();
|
||||
result = HashCodePrime * result + this.uri.GetHashCode();
|
||||
result = HashCodePrime * result + base.GetHashCode();
|
||||
|
||||
return result;
|
||||
}
|
||||
// ReSharper restore NonReadonlyMemberInGetHashCode
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (!(obj is DefaultHttpRequest other))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.method.Equals(other.method)
|
||||
&& this.uri.Equals(other.uri, StringComparison.OrdinalIgnoreCase)
|
||||
&& base.Equals(obj);
|
||||
}
|
||||
|
||||
public override string ToString() => HttpMessageUtil.AppendRequest(new StringBuilder(256), this).ToString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ConvertToAutoPropertyWithPrivateSetter
|
||||
namespace DotNetty.Codecs.Http
|
||||
{
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Text;
|
||||
|
||||
public class DefaultHttpResponse : DefaultHttpMessage, IHttpResponse
|
||||
{
|
||||
HttpResponseStatus status;
|
||||
|
||||
public DefaultHttpResponse(HttpVersion version, HttpResponseStatus status, bool validateHeaders = true, bool singleFieldHeaders = false)
|
||||
: base(version, validateHeaders, singleFieldHeaders)
|
||||
{
|
||||
Contract.Requires(status != null);
|
||||
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public DefaultHttpResponse(HttpVersion version, HttpResponseStatus status, HttpHeaders headers)
|
||||
: base(version, headers)
|
||||
{
|
||||
Contract.Requires(status != null);
|
||||
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public HttpResponseStatus Status => this.status;
|
||||
|
||||
public IHttpResponse SetStatus(HttpResponseStatus value)
|
||||
{
|
||||
Contract.Requires(value != null);
|
||||
this.status = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public override string ToString() => HttpMessageUtil.AppendResponse(new StringBuilder(256), this).ToString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ConvertToAutoPropertyWhenPossible
|
||||
// ReSharper disable ConvertToAutoProperty
|
||||
namespace DotNetty.Codecs.Http
|
||||
{
|
||||
using System.Text;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public class DefaultLastHttpContent : DefaultHttpContent, ILastHttpContent
|
||||
{
|
||||
readonly HttpHeaders trailingHeaders;
|
||||
readonly bool validateHeaders;
|
||||
|
||||
public DefaultLastHttpContent() : this(Unpooled.Buffer(0), true)
|
||||
{
|
||||
}
|
||||
|
||||
public DefaultLastHttpContent(IByteBuffer content) : this(content, true)
|
||||
{
|
||||
}
|
||||
|
||||
public DefaultLastHttpContent(IByteBuffer content, bool validateHeaders)
|
||||
: base(content)
|
||||
{
|
||||
this.trailingHeaders = new TrailingHttpHeaders(validateHeaders);
|
||||
this.validateHeaders = validateHeaders;
|
||||
}
|
||||
|
||||
public HttpHeaders TrailingHeaders => this.trailingHeaders;
|
||||
|
||||
public override IByteBufferHolder Replace(IByteBuffer buffer)
|
||||
{
|
||||
var dup = new DefaultLastHttpContent(this.Content, this.validateHeaders);
|
||||
dup.TrailingHeaders.Set(this.trailingHeaders);
|
||||
return dup;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var buf = new StringBuilder(base.ToString());
|
||||
buf.Append(StringUtil.Newline);
|
||||
this.AppendHeaders(buf);
|
||||
|
||||
// Remove the last newline.
|
||||
buf.Length = buf.Length - StringUtil.Newline.Length;
|
||||
return buf.ToString();
|
||||
}
|
||||
|
||||
void AppendHeaders(StringBuilder buf)
|
||||
{
|
||||
foreach (HeaderEntry<AsciiString, ICharSequence> e in this.trailingHeaders)
|
||||
{
|
||||
buf.Append($"{e.Key}: {e.Value}{StringUtil.Newline}");
|
||||
}
|
||||
}
|
||||
|
||||
sealed class TrailerNameValidator : INameValidator<ICharSequence>
|
||||
{
|
||||
public void ValidateName(ICharSequence name)
|
||||
{
|
||||
DefaultHttpHeaders.HttpNameValidator.ValidateName(name);
|
||||
if (HttpHeaderNames.ContentLength.ContentEqualsIgnoreCase(name)
|
||||
|| HttpHeaderNames.TransferEncoding.ContentEqualsIgnoreCase(name)
|
||||
|| HttpHeaderNames.Trailer.ContentEqualsIgnoreCase(name))
|
||||
{
|
||||
ThrowHelper.ThrowArgumentException_TrailingHeaderName(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class TrailingHttpHeaders : DefaultHttpHeaders
|
||||
{
|
||||
static readonly TrailerNameValidator TrailerNameValidator = new TrailerNameValidator();
|
||||
|
||||
public TrailingHttpHeaders(bool validate)
|
||||
: base(validate, validate ? TrailerNameValidator : NotNullValidator)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup Label="NuGet">
|
||||
<TargetFrameworks>netstandard1.3;net45</TargetFrameworks>
|
||||
<IsPackable>true</IsPackable>
|
||||
<PackageId>DotNetty.Codecs.Http</PackageId>
|
||||
<Description>Http codec for DotNetty</Description>
|
||||
<Copyright>Copyright © Microsoft Corporation</Copyright>
|
||||
<AssemblyTitle>DotNetty: Http codec</AssemblyTitle>
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<VersionPrefix>0.4.7</VersionPrefix>
|
||||
<Authors>Microsoft Azure</Authors>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<AssemblyName>DotNetty.Codecs.Http</AssemblyName>
|
||||
<AssemblyOriginatorKeyFile>../../DotNetty.snk</AssemblyOriginatorKeyFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
|
||||
<PackageTags>socket;tcp;protocol;netty;dotnetty;network;http</PackageTags>
|
||||
<PackageProjectUrl>https://github.com/Azure/DotNetty/</PackageProjectUrl>
|
||||
<PackageLicenseUrl>https://github.com/Azure/DotNetty/blob/master/LICENSE.txt</PackageLicenseUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/Azure/DotNetty/</RepositoryUrl>
|
||||
<NetStandardImplicitPackageVersion Condition=" '$(TargetFramework)' == 'netstandard1.3' ">1.6.1</NetStandardImplicitPackageVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\shared\SharedAssemblyInfo.cs" Link="Properties\SharedAssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.4.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DotNetty.Common\DotNetty.Common.csproj" />
|
||||
<ProjectReference Include="..\DotNetty.Buffers\DotNetty.Buffers.csproj" />
|
||||
<ProjectReference Include="..\DotNetty.Codecs\DotNetty.Codecs.csproj" />
|
||||
<ProjectReference Include="..\DotNetty.Handlers\DotNetty.Handlers.csproj" />
|
||||
<ProjectReference Include="..\DotNetty.Transport\DotNetty.Transport.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.3' ">
|
||||
<PackageReference Include="System.Diagnostics.Contracts" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
|
||||
<Reference Include="System" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,85 @@
|
|||
// 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
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public class EmptyHttpHeaders : HttpHeaders
|
||||
{
|
||||
static readonly IEnumerator<HeaderEntry<AsciiString, ICharSequence>> EmptryEnumerator =
|
||||
Enumerable.Empty<HeaderEntry<AsciiString, ICharSequence>>().GetEnumerator();
|
||||
|
||||
public static readonly EmptyHttpHeaders Default = new EmptyHttpHeaders();
|
||||
|
||||
protected EmptyHttpHeaders()
|
||||
{
|
||||
}
|
||||
|
||||
public override bool TryGet(AsciiString name, out ICharSequence value)
|
||||
{
|
||||
value = default(ICharSequence);
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool TryGetInt(AsciiString name, out int value)
|
||||
{
|
||||
value = default(int);
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int GetInt(AsciiString name, int defaultValue) => defaultValue;
|
||||
|
||||
public override bool TryGetShort(AsciiString name, out short value)
|
||||
{
|
||||
value = default(short);
|
||||
return false;
|
||||
}
|
||||
|
||||
public override short GetShort(AsciiString name, short defaultValue) => defaultValue;
|
||||
|
||||
public override bool TryGetTimeMillis(AsciiString name, out long value)
|
||||
{
|
||||
value = default(long);
|
||||
return false;
|
||||
}
|
||||
|
||||
public override long GetTimeMillis(AsciiString name, long defaultValue) => defaultValue;
|
||||
|
||||
public override IList<ICharSequence> GetAll(AsciiString name) => ImmutableList<ICharSequence>.Empty;
|
||||
|
||||
public override IList<HeaderEntry<AsciiString, ICharSequence>> Entries() => ImmutableList<HeaderEntry<AsciiString, ICharSequence>>.Empty;
|
||||
|
||||
public override bool Contains(AsciiString name) => false;
|
||||
|
||||
public override bool IsEmpty => true;
|
||||
|
||||
public override int Size => 0;
|
||||
|
||||
public override ISet<AsciiString> Names() => ImmutableHashSet<AsciiString>.Empty;
|
||||
|
||||
public override HttpHeaders AddInt(AsciiString name, int value) => throw new NotSupportedException("read only");
|
||||
|
||||
public override HttpHeaders AddShort(AsciiString name, short value) => throw new NotSupportedException("read only");
|
||||
|
||||
public override HttpHeaders Set(AsciiString name, object value) => throw new NotSupportedException("read only");
|
||||
|
||||
public override HttpHeaders Set(AsciiString name, IEnumerable<object> values) => throw new NotSupportedException("read only");
|
||||
|
||||
public override HttpHeaders SetInt(AsciiString name, int value) => throw new NotSupportedException("read only");
|
||||
|
||||
public override HttpHeaders SetShort(AsciiString name, short value) => throw new NotSupportedException("read only");
|
||||
|
||||
public override HttpHeaders Remove(AsciiString name) => throw new NotSupportedException("read only");
|
||||
|
||||
public override HttpHeaders Clear() => throw new NotSupportedException("read only");
|
||||
|
||||
public override HttpHeaders Add(AsciiString name, object value) => throw new NotSupportedException("read only");
|
||||
|
||||
public override IEnumerator<HeaderEntry<AsciiString, ICharSequence>> GetEnumerator() => EmptryEnumerator;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
// 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
|
||||
{
|
||||
using System;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common;
|
||||
|
||||
public sealed class EmptyLastHttpContent : ILastHttpContent
|
||||
{
|
||||
public static readonly EmptyLastHttpContent Default = new EmptyLastHttpContent();
|
||||
|
||||
EmptyLastHttpContent()
|
||||
{
|
||||
this.Content = Unpooled.Empty;
|
||||
}
|
||||
|
||||
public DecoderResult Result
|
||||
{
|
||||
get => DecoderResult.Success;
|
||||
set => throw new NotSupportedException("read only");
|
||||
}
|
||||
|
||||
public int ReferenceCount => 1;
|
||||
|
||||
public IReferenceCounted Retain() => this;
|
||||
|
||||
public IReferenceCounted Retain(int increment) => this;
|
||||
|
||||
public IReferenceCounted Touch() => this;
|
||||
|
||||
public IReferenceCounted Touch(object hint) => this;
|
||||
|
||||
public bool Release() => false;
|
||||
|
||||
public bool Release(int decrement) => false;
|
||||
|
||||
public IByteBuffer Content { get; }
|
||||
|
||||
public IByteBufferHolder Copy() => this;
|
||||
|
||||
public IByteBufferHolder Duplicate() => this;
|
||||
|
||||
public IByteBufferHolder RetainedDuplicate() => this;
|
||||
|
||||
public IByteBufferHolder Replace(IByteBuffer content) => new DefaultLastHttpContent(content);
|
||||
|
||||
public HttpHeaders TrailingHeaders => EmptyHttpHeaders.Default;
|
||||
|
||||
public override string ToString() => nameof(EmptyLastHttpContent);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
// 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
|
||||
{
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Handlers.Streams;
|
||||
|
||||
public class HttpChunkedInput : IChunkedInput<IHttpContent>
|
||||
{
|
||||
readonly IChunkedInput<IByteBuffer> input;
|
||||
readonly ILastHttpContent lastHttpContent;
|
||||
bool sentLastChunk;
|
||||
|
||||
public HttpChunkedInput(IChunkedInput<IByteBuffer> input)
|
||||
{
|
||||
this.input = input;
|
||||
this.lastHttpContent = EmptyLastHttpContent.Default;
|
||||
}
|
||||
|
||||
public HttpChunkedInput(IChunkedInput<IByteBuffer> input, ILastHttpContent lastHttpContent)
|
||||
{
|
||||
this.input = input;
|
||||
this.lastHttpContent = lastHttpContent;
|
||||
}
|
||||
|
||||
public bool IsEndOfInput => this.input.IsEndOfInput && this.sentLastChunk;
|
||||
|
||||
public void Close() => this.input.Close();
|
||||
|
||||
public IHttpContent ReadChunk(IByteBufferAllocator allocator)
|
||||
{
|
||||
if (this.input.IsEndOfInput)
|
||||
{
|
||||
if (this.sentLastChunk)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// Send last chunk for this input
|
||||
this.sentLastChunk = true;
|
||||
return this.lastHttpContent;
|
||||
}
|
||||
else
|
||||
{
|
||||
IByteBuffer buf = this.input.ReadChunk(allocator);
|
||||
return buf == null ? null : new DefaultHttpContent(buf);
|
||||
}
|
||||
}
|
||||
|
||||
public long Length => this.input.Length;
|
||||
|
||||
public long Progress => this.input.Progress;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,259 @@
|
|||
// 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
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public class HttpClientCodec : CombinedChannelDuplexHandler<HttpResponseDecoder, HttpRequestEncoder>,
|
||||
HttpClientUpgradeHandler.ISourceCodec
|
||||
{
|
||||
// A queue that is used for correlating a request and a response.
|
||||
readonly Queue<HttpMethod> queue = new Queue<HttpMethod>();
|
||||
readonly bool parseHttpAfterConnectRequest;
|
||||
|
||||
// If true, decoding stops (i.e. pass-through)
|
||||
bool done;
|
||||
|
||||
long requestResponseCounter;
|
||||
readonly bool failOnMissingResponse;
|
||||
|
||||
public HttpClientCodec() : this(4096, 8192, 8192, false)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpClientCodec(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize)
|
||||
: this(maxInitialLineLength, maxHeaderSize, maxChunkSize, false)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpClientCodec(
|
||||
int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, bool failOnMissingResponse)
|
||||
: this(maxInitialLineLength, maxHeaderSize, maxChunkSize, failOnMissingResponse, true)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpClientCodec(
|
||||
int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, bool failOnMissingResponse,
|
||||
bool validateHeaders)
|
||||
: this(maxInitialLineLength, maxHeaderSize, maxChunkSize, failOnMissingResponse, validateHeaders, false)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpClientCodec(
|
||||
int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, bool failOnMissingResponse,
|
||||
bool validateHeaders, bool parseHttpAfterConnectRequest)
|
||||
{
|
||||
this.Init(new Decoder(this, maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders), new Encoder(this));
|
||||
this.failOnMissingResponse = failOnMissingResponse;
|
||||
this.parseHttpAfterConnectRequest = parseHttpAfterConnectRequest;
|
||||
}
|
||||
|
||||
public HttpClientCodec(
|
||||
int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, bool failOnMissingResponse,
|
||||
bool validateHeaders, int initialBufferSize)
|
||||
: this(maxInitialLineLength, maxHeaderSize, maxChunkSize, failOnMissingResponse, validateHeaders, initialBufferSize, false)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpClientCodec(
|
||||
int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, bool failOnMissingResponse,
|
||||
bool validateHeaders, int initialBufferSize, bool parseHttpAfterConnectRequest)
|
||||
{
|
||||
this.Init(new Decoder(this, maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders, initialBufferSize), new Encoder(this));
|
||||
this.parseHttpAfterConnectRequest = parseHttpAfterConnectRequest;
|
||||
this.failOnMissingResponse = failOnMissingResponse;
|
||||
}
|
||||
|
||||
public void PrepareUpgradeFrom(IChannelHandlerContext ctx) => ((Encoder)this.OutboundHandler).Upgraded = true;
|
||||
|
||||
public void UpgradeFrom(IChannelHandlerContext ctx)
|
||||
{
|
||||
IChannelPipeline p = ctx.Channel.Pipeline;
|
||||
p.Remove(this);
|
||||
}
|
||||
|
||||
public bool SingleDecode
|
||||
{
|
||||
get => this.InboundHandler.SingleDecode;
|
||||
set => this.InboundHandler.SingleDecode = value;
|
||||
}
|
||||
|
||||
sealed class Encoder : HttpRequestEncoder
|
||||
{
|
||||
readonly HttpClientCodec clientCodec;
|
||||
internal bool Upgraded;
|
||||
|
||||
public Encoder(HttpClientCodec clientCodec)
|
||||
{
|
||||
this.clientCodec = clientCodec;
|
||||
}
|
||||
|
||||
protected override void Encode(IChannelHandlerContext context, object message, List<object> output)
|
||||
{
|
||||
if (this.Upgraded)
|
||||
{
|
||||
output.Add(ReferenceCountUtil.Retain(message));
|
||||
return;
|
||||
}
|
||||
|
||||
if (message is IHttpRequest request && !this.clientCodec.done)
|
||||
{
|
||||
this.clientCodec.queue.Enqueue(request.Method);
|
||||
}
|
||||
|
||||
base.Encode(context, message, output);
|
||||
|
||||
if (this.clientCodec.failOnMissingResponse && !this.clientCodec.done)
|
||||
{
|
||||
// check if the request is chunked if so do not increment
|
||||
if (message is ILastHttpContent)
|
||||
{
|
||||
// increment as its the last chunk
|
||||
Interlocked.Increment(ref this.clientCodec.requestResponseCounter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Decoder : HttpResponseDecoder
|
||||
{
|
||||
readonly HttpClientCodec clientCodec;
|
||||
|
||||
internal Decoder(HttpClientCodec clientCodec, int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, bool validateHeaders)
|
||||
: base(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders)
|
||||
{
|
||||
this.clientCodec = clientCodec;
|
||||
}
|
||||
|
||||
internal Decoder(HttpClientCodec clientCodec, int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, bool validateHeaders, int initialBufferSize)
|
||||
: base(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders, initialBufferSize)
|
||||
{
|
||||
this.clientCodec = clientCodec;
|
||||
}
|
||||
|
||||
protected override void Decode(IChannelHandlerContext context, IByteBuffer buffer, List<object> output)
|
||||
{
|
||||
if (this.clientCodec.done)
|
||||
{
|
||||
int readable = this.ActualReadableBytes;
|
||||
if (readable == 0)
|
||||
{
|
||||
// if non is readable just return null
|
||||
// https://github.com/netty/netty/issues/1159
|
||||
return;
|
||||
}
|
||||
output.Add(buffer.ReadBytes(readable));
|
||||
}
|
||||
else
|
||||
{
|
||||
int oldSize = output.Count;
|
||||
base.Decode(context, buffer, output);
|
||||
if (this.clientCodec.failOnMissingResponse)
|
||||
{
|
||||
int size = output.Count;
|
||||
for (int i = oldSize; i < size; i++)
|
||||
{
|
||||
this.Decrement(output[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Decrement(object msg)
|
||||
{
|
||||
if (ReferenceEquals(null, msg))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// check if it's an Header and its transfer encoding is not chunked.
|
||||
if (msg is ILastHttpContent)
|
||||
{
|
||||
Interlocked.Decrement(ref this.clientCodec.requestResponseCounter);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool IsContentAlwaysEmpty(IHttpMessage msg)
|
||||
{
|
||||
int statusCode = ((IHttpResponse)msg).Status.Code;
|
||||
if (statusCode == 100 || statusCode == 101)
|
||||
{
|
||||
// 100-continue and 101 switching protocols response should be excluded from paired comparison.
|
||||
// Just delegate to super method which has all the needed handling.
|
||||
return base.IsContentAlwaysEmpty(msg);
|
||||
}
|
||||
|
||||
// Get the getMethod of the HTTP request that corresponds to the
|
||||
// current response.
|
||||
HttpMethod method = this.clientCodec.queue.Dequeue();
|
||||
|
||||
char firstChar = method.AsciiName[0];
|
||||
switch (firstChar)
|
||||
{
|
||||
case 'H':
|
||||
// According to 4.3, RFC2616:
|
||||
// All responses to the HEAD request getMethod MUST NOT include a
|
||||
// message-body, even though the presence of entity-header fields
|
||||
// might lead one to believe they do.
|
||||
if (HttpMethod.Head.Equals(method))
|
||||
{
|
||||
return true;
|
||||
|
||||
// The following code was inserted to work around the servers
|
||||
// that behave incorrectly. It has been commented out
|
||||
// because it does not work with well behaving servers.
|
||||
// Please note, even if the 'Transfer-Encoding: chunked'
|
||||
// header exists in the HEAD response, the response should
|
||||
// have absolutely no content.
|
||||
//
|
||||
// Interesting edge case:
|
||||
// Some poorly implemented servers will send a zero-byte
|
||||
// chunk if Transfer-Encoding of the response is 'chunked'.
|
||||
//
|
||||
// return !msg.isChunked();
|
||||
}
|
||||
break;
|
||||
case 'C':
|
||||
// Successful CONNECT request results in a response with empty body.
|
||||
if (statusCode == 200)
|
||||
{
|
||||
if (HttpMethod.Connect.Equals(method))
|
||||
{
|
||||
// Proxy connection established - Parse HTTP only if configured by parseHttpAfterConnectRequest,
|
||||
// else pass through.
|
||||
if (!this.clientCodec.parseHttpAfterConnectRequest)
|
||||
{
|
||||
this.clientCodec.done = true;
|
||||
this.clientCodec.queue.Clear();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return base.IsContentAlwaysEmpty(msg);
|
||||
}
|
||||
|
||||
public override void ChannelInactive(IChannelHandlerContext ctx)
|
||||
{
|
||||
base.ChannelInactive(ctx);
|
||||
|
||||
if (this.clientCodec.failOnMissingResponse)
|
||||
{
|
||||
long missingResponses = Interlocked.Read(ref this.clientCodec.requestResponseCounter);
|
||||
if (missingResponses > 0)
|
||||
{
|
||||
ctx.FireExceptionCaught(new PrematureChannelClosureException(
|
||||
$"channel gone inactive with {missingResponses} missing response(s)"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
// 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
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
// Note HttpObjectAggregator already implements IChannelHandler
|
||||
public class HttpClientUpgradeHandler : HttpObjectAggregator
|
||||
{
|
||||
// User events that are fired to notify about upgrade status.
|
||||
public enum UpgradeEvent
|
||||
{
|
||||
// The Upgrade request was sent to the server.
|
||||
UpgradeIssued,
|
||||
|
||||
// The Upgrade to the new protocol was successful.
|
||||
UpgradeSuccessful,
|
||||
|
||||
// The Upgrade was unsuccessful due to the server not issuing
|
||||
// with a 101 Switching Protocols response.
|
||||
UpgradeRejected
|
||||
}
|
||||
|
||||
public interface ISourceCodec
|
||||
{
|
||||
// Removes or disables the encoder of this codec so that the {@link UpgradeCodec} can send an initial greeting
|
||||
// (if any).
|
||||
void PrepareUpgradeFrom(IChannelHandlerContext ctx);
|
||||
|
||||
// Removes this codec (i.e. all associated handlers) from the pipeline.
|
||||
void UpgradeFrom(IChannelHandlerContext ctx);
|
||||
}
|
||||
|
||||
public interface IUpgradeCodec
|
||||
{
|
||||
// Returns the name of the protocol supported by this codec, as indicated by the {@code 'UPGRADE'} header.
|
||||
ICharSequence Protocol { get; }
|
||||
|
||||
// Sets any protocol-specific headers required to the upgrade request. Returns the names of
|
||||
// all headers that were added. These headers will be used to populate the CONNECTION header.
|
||||
ICollection<ICharSequence> SetUpgradeHeaders(IChannelHandlerContext ctx, IHttpRequest upgradeRequest);
|
||||
|
||||
///
|
||||
// Performs an HTTP protocol upgrade from the source codec. This method is responsible for
|
||||
// adding all handlers required for the new protocol.
|
||||
//
|
||||
// ctx the context for the current handler.
|
||||
// upgradeResponse the 101 Switching Protocols response that indicates that the server
|
||||
// has switched to this protocol.
|
||||
void UpgradeTo(IChannelHandlerContext ctx, IFullHttpResponse upgradeResponse);
|
||||
}
|
||||
|
||||
readonly ISourceCodec sourceCodec;
|
||||
readonly IUpgradeCodec upgradeCodec;
|
||||
bool upgradeRequested;
|
||||
|
||||
public HttpClientUpgradeHandler(ISourceCodec sourceCodec, IUpgradeCodec upgradeCodec, int maxContentLength)
|
||||
: base(maxContentLength)
|
||||
{
|
||||
Contract.Requires(sourceCodec != null);
|
||||
Contract.Requires(upgradeCodec != null);
|
||||
|
||||
this.sourceCodec = sourceCodec;
|
||||
this.upgradeCodec = upgradeCodec;
|
||||
}
|
||||
|
||||
public override Task WriteAsync(IChannelHandlerContext context, object message)
|
||||
{
|
||||
if (!(message is IHttpRequest))
|
||||
{
|
||||
return context.WriteAsync(message);
|
||||
}
|
||||
|
||||
if (this.upgradeRequested)
|
||||
{
|
||||
return TaskEx.FromException(new InvalidOperationException("Attempting to write HTTP request with upgrade in progress"));
|
||||
}
|
||||
|
||||
this.upgradeRequested = true;
|
||||
this.SetUpgradeRequestHeaders(context, (IHttpRequest)message);
|
||||
|
||||
// Continue writing the request.
|
||||
Task task = context.WriteAsync(message);
|
||||
|
||||
// Notify that the upgrade request was issued.
|
||||
context.FireUserEventTriggered(UpgradeEvent.UpgradeIssued);
|
||||
// Now we wait for the next HTTP response to see if we switch protocols.
|
||||
return task;
|
||||
}
|
||||
|
||||
protected override void Decode(IChannelHandlerContext context, IHttpObject message, List<object> output)
|
||||
{
|
||||
IFullHttpResponse response = null;
|
||||
try
|
||||
{
|
||||
if (!this.upgradeRequested)
|
||||
{
|
||||
throw new InvalidOperationException("Read HTTP response without requesting protocol switch");
|
||||
}
|
||||
|
||||
if (message is IHttpResponse rep)
|
||||
{
|
||||
if (!HttpResponseStatus.SwitchingProtocols.Equals(rep.Status))
|
||||
{
|
||||
// The server does not support the requested protocol, just remove this handler
|
||||
// and continue processing HTTP.
|
||||
// NOTE: not releasing the response since we're letting it propagate to the
|
||||
// next handler.
|
||||
context.FireUserEventTriggered(UpgradeEvent.UpgradeRejected);
|
||||
RemoveThisHandler(context);
|
||||
context.FireChannelRead(rep);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (message is IFullHttpResponse fullRep)
|
||||
{
|
||||
response = fullRep;
|
||||
// Need to retain since the base class will release after returning from this method.
|
||||
response.Retain();
|
||||
output.Add(response);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Call the base class to handle the aggregation of the full request.
|
||||
base.Decode(context, message, output);
|
||||
if (output.Count == 0)
|
||||
{
|
||||
// The full request hasn't been created yet, still awaiting more data.
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Assert(output.Count == 1);
|
||||
response = (IFullHttpResponse)output[0];
|
||||
}
|
||||
|
||||
if (response.Headers.TryGet(HttpHeaderNames.Upgrade, out ICharSequence upgradeHeader) && !AsciiString.ContentEqualsIgnoreCase(this.upgradeCodec.Protocol, upgradeHeader))
|
||||
{
|
||||
throw new InvalidOperationException($"Switching Protocols response with unexpected UPGRADE protocol: {upgradeHeader}");
|
||||
}
|
||||
|
||||
// Upgrade to the new protocol.
|
||||
this.sourceCodec.PrepareUpgradeFrom(context);
|
||||
this.upgradeCodec.UpgradeTo(context, response);
|
||||
|
||||
// Notify that the upgrade to the new protocol completed successfully.
|
||||
context.FireUserEventTriggered(UpgradeEvent.UpgradeSuccessful);
|
||||
|
||||
// We guarantee UPGRADE_SUCCESSFUL event will be arrived at the next handler
|
||||
// before http2 setting frame and http response.
|
||||
this.sourceCodec.UpgradeFrom(context);
|
||||
|
||||
// We switched protocols, so we're done with the upgrade response.
|
||||
// Release it and clear it from the output.
|
||||
response.Release();
|
||||
output.Clear();
|
||||
RemoveThisHandler(context);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ReferenceCountUtil.Release(response);
|
||||
context.FireExceptionCaught(exception);
|
||||
RemoveThisHandler(context);
|
||||
}
|
||||
}
|
||||
|
||||
static void RemoveThisHandler(IChannelHandlerContext ctx) => ctx.Channel.Pipeline.Remove(ctx.Name);
|
||||
|
||||
void SetUpgradeRequestHeaders(IChannelHandlerContext ctx, IHttpRequest request)
|
||||
{
|
||||
// Set the UPGRADE header on the request.
|
||||
request.Headers.Set(HttpHeaderNames.Upgrade, this.upgradeCodec.Protocol);
|
||||
|
||||
// Add all protocol-specific headers to the request.
|
||||
var connectionParts = new List<ICharSequence>(2);
|
||||
connectionParts.AddRange(this.upgradeCodec.SetUpgradeHeaders(ctx, request));
|
||||
|
||||
// Set the CONNECTION header from the set of all protocol-specific headers that were added.
|
||||
var builder = new StringBuilder();
|
||||
foreach (ICharSequence part in connectionParts)
|
||||
{
|
||||
builder.Append(part);
|
||||
builder.Append(',');
|
||||
}
|
||||
builder.Append(HttpHeaderValues.Upgrade);
|
||||
request.Headers.Set(HttpHeaderNames.Connection, new StringCharSequence(builder.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
// 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
|
||||
{
|
||||
using System.Text;
|
||||
using DotNetty.Buffers;
|
||||
|
||||
public static class HttpConstants
|
||||
{
|
||||
// Horizontal space
|
||||
public const byte HorizontalSpace = 32;
|
||||
|
||||
// Horizontal tab
|
||||
public const byte HorizontalTab = 9;
|
||||
|
||||
// Carriage return
|
||||
public const byte CarriageReturn = 13;
|
||||
|
||||
// Equals '='
|
||||
public const byte EqualsSign = 61;
|
||||
|
||||
// Line feed character
|
||||
public const byte LineFeed = 10;
|
||||
|
||||
// Colon ':'
|
||||
public const byte Colon = 58;
|
||||
|
||||
// Semicolon ';'
|
||||
public const byte Semicolon = 59;
|
||||
|
||||
// Comma ','
|
||||
public const byte Comma = 44;
|
||||
|
||||
// Double quote '"'
|
||||
public const byte DoubleQuote = (byte)'"';
|
||||
|
||||
// Default character set (UTF-8)
|
||||
public static readonly Encoding DefaultEncoding = Encoding.UTF8;
|
||||
|
||||
// Horizontal space in char
|
||||
public static readonly char HorizontalSpaceChar = (char)HorizontalSpace;
|
||||
|
||||
// For HttpObjectEncoder
|
||||
internal static readonly int CrlfShort = (CarriageReturn << 8) | LineFeed;
|
||||
|
||||
internal static readonly int ZeroCrlfMedium = ('0' << 16) | CrlfShort;
|
||||
|
||||
internal static readonly byte[] ZeroCrlfCrlf = { (byte)'0', CarriageReturn, LineFeed, CarriageReturn, LineFeed };
|
||||
|
||||
internal static readonly IByteBuffer CrlfBuf = Unpooled.UnreleasableBuffer(Unpooled.WrappedBuffer(new[] { CarriageReturn, LineFeed }));
|
||||
|
||||
internal static readonly IByteBuffer ZeroCrlfCrlfBuf = Unpooled.UnreleasableBuffer(Unpooled.WrappedBuffer(ZeroCrlfCrlf));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
// 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
|
||||
{
|
||||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using DotNetty.Codecs.Compression;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
using DotNetty.Transport.Channels.Embedded;
|
||||
|
||||
public class HttpContentCompressor : HttpContentEncoder
|
||||
{
|
||||
static readonly AsciiString GZipString = AsciiString.Cached("gzip");
|
||||
static readonly AsciiString DeflateString = AsciiString.Cached("deflate");
|
||||
|
||||
readonly int compressionLevel;
|
||||
readonly int windowBits;
|
||||
readonly int memLevel;
|
||||
|
||||
IChannelHandlerContext handlerContext;
|
||||
|
||||
public HttpContentCompressor() : this(6)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpContentCompressor(int compressionLevel) : this(compressionLevel, 15, 8)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel)
|
||||
{
|
||||
Contract.Requires(compressionLevel >= 0 && compressionLevel <= 9);
|
||||
Contract.Requires(windowBits >= 9 && windowBits <= 15);
|
||||
Contract.Requires(memLevel >= 1 && memLevel <= 9);
|
||||
|
||||
this.compressionLevel = compressionLevel;
|
||||
this.windowBits = windowBits;
|
||||
this.memLevel = memLevel;
|
||||
}
|
||||
|
||||
public override void HandlerAdded(IChannelHandlerContext context) => this.handlerContext = context;
|
||||
|
||||
protected override Result BeginEncode(IHttpResponse headers, ICharSequence acceptEncoding)
|
||||
{
|
||||
if (headers.Headers.Contains(HttpHeaderNames.ContentEncoding))
|
||||
{
|
||||
// Content-Encoding was set, either as something specific or as the IDENTITY encoding
|
||||
// Therefore, we should NOT encode here
|
||||
return null;
|
||||
}
|
||||
|
||||
ZlibWrapper? wrapper = this.DetermineWrapper(acceptEncoding);
|
||||
if (wrapper == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
ICharSequence targetContentEncoding;
|
||||
switch (wrapper.Value)
|
||||
{
|
||||
case ZlibWrapper.Gzip:
|
||||
targetContentEncoding = GZipString;
|
||||
break;
|
||||
case ZlibWrapper.Zlib:
|
||||
targetContentEncoding = DeflateString;
|
||||
break;
|
||||
default:
|
||||
throw new CodecException($"{wrapper.Value} not supported, only Gzip and Zlib are allowed.");
|
||||
}
|
||||
|
||||
return new Result(targetContentEncoding,
|
||||
new EmbeddedChannel(
|
||||
this.handlerContext.Channel.Id,
|
||||
this.handlerContext.Channel.Metadata.HasDisconnect,
|
||||
this.handlerContext.Channel.Configuration,
|
||||
ZlibCodecFactory.NewZlibEncoder(
|
||||
wrapper.Value, this.compressionLevel, this.windowBits, this.memLevel)));
|
||||
}
|
||||
|
||||
protected internal ZlibWrapper? DetermineWrapper(ICharSequence acceptEncoding)
|
||||
{
|
||||
float starQ = -1.0f;
|
||||
float gzipQ = -1.0f;
|
||||
float deflateQ = -1.0f;
|
||||
ICharSequence[] parts = CharUtil.Split(acceptEncoding, ',');
|
||||
foreach (ICharSequence encoding in parts)
|
||||
{
|
||||
float q = 1.0f;
|
||||
int equalsPos = encoding.IndexOf('=');
|
||||
if (equalsPos != -1)
|
||||
{
|
||||
try
|
||||
{
|
||||
q = float.Parse(encoding.ToString(equalsPos + 1));
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// Ignore encoding
|
||||
q = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
if (CharUtil.Contains(encoding, '*'))
|
||||
{
|
||||
starQ = q;
|
||||
}
|
||||
else if (AsciiString.Contains(encoding, GZipString) && q > gzipQ)
|
||||
{
|
||||
gzipQ = q;
|
||||
}
|
||||
else if (AsciiString.Contains(encoding, DeflateString) && q > deflateQ)
|
||||
{
|
||||
deflateQ = q;
|
||||
}
|
||||
}
|
||||
if (gzipQ > 0.0f || deflateQ > 0.0f)
|
||||
{
|
||||
return gzipQ >= deflateQ ? ZlibWrapper.Gzip : ZlibWrapper.Zlib;
|
||||
}
|
||||
if (starQ > 0.0f)
|
||||
{
|
||||
// ReSharper disable CompareOfFloatsByEqualityOperator
|
||||
if (gzipQ == -1.0f)
|
||||
{
|
||||
return ZlibWrapper.Gzip;
|
||||
}
|
||||
if (deflateQ == -1.0f)
|
||||
{
|
||||
return ZlibWrapper.Zlib;
|
||||
}
|
||||
// ReSharper restore CompareOfFloatsByEqualityOperator
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
// 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
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
using DotNetty.Transport.Channels.Embedded;
|
||||
|
||||
public abstract class HttpContentDecoder : MessageToMessageDecoder<IHttpObject>
|
||||
{
|
||||
internal static readonly AsciiString Identity = HttpHeaderValues.Identity;
|
||||
|
||||
protected IChannelHandlerContext HandlerContext;
|
||||
EmbeddedChannel decoder;
|
||||
bool continueResponse;
|
||||
|
||||
protected override void Decode(IChannelHandlerContext context, IHttpObject message, List<object> output)
|
||||
{
|
||||
if (message is IHttpResponse response && response.Status.Code == 100)
|
||||
{
|
||||
if (!(response is ILastHttpContent))
|
||||
{
|
||||
this.continueResponse = true;
|
||||
}
|
||||
// 100-continue response must be passed through.
|
||||
output.Add(ReferenceCountUtil.Retain(message));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.continueResponse)
|
||||
{
|
||||
if (message is ILastHttpContent)
|
||||
{
|
||||
this.continueResponse = false;
|
||||
}
|
||||
// 100-continue response must be passed through.
|
||||
output.Add(ReferenceCountUtil.Retain(message));
|
||||
return;
|
||||
}
|
||||
|
||||
if (message is IHttpMessage httpMessage)
|
||||
{
|
||||
this.Cleanup();
|
||||
HttpHeaders headers = httpMessage.Headers;
|
||||
|
||||
// Determine the content encoding.
|
||||
if (headers.TryGet(HttpHeaderNames.ContentEncoding, out ICharSequence contentEncoding))
|
||||
{
|
||||
contentEncoding = AsciiString.Trim(contentEncoding);
|
||||
}
|
||||
else
|
||||
{
|
||||
contentEncoding = Identity;
|
||||
}
|
||||
this.decoder = this.NewContentDecoder(contentEncoding);
|
||||
|
||||
if (this.decoder == null)
|
||||
{
|
||||
if (httpMessage is IHttpContent httpContent)
|
||||
{
|
||||
httpContent.Retain();
|
||||
}
|
||||
output.Add(httpMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove content-length header:
|
||||
// the correct value can be set only after all chunks are processed/decoded.
|
||||
// If buffering is not an issue, add HttpObjectAggregator down the chain, it will set the header.
|
||||
// Otherwise, rely on LastHttpContent message.
|
||||
if (headers.Contains(HttpHeaderNames.ContentLength))
|
||||
{
|
||||
headers.Remove(HttpHeaderNames.ContentLength);
|
||||
headers.Set(HttpHeaderNames.TransferEncoding, HttpHeaderValues.Chunked);
|
||||
}
|
||||
// Either it is already chunked or EOF terminated.
|
||||
// See https://github.com/netty/netty/issues/5892
|
||||
|
||||
// set new content encoding,
|
||||
ICharSequence targetContentEncoding = this.GetTargetContentEncoding(contentEncoding);
|
||||
if (HttpHeaderValues.Identity.ContentEquals(targetContentEncoding))
|
||||
{
|
||||
// Do NOT set the 'Content-Encoding' header if the target encoding is 'identity'
|
||||
// as per: http://tools.ietf.org/html/rfc2616#section-14.11
|
||||
headers.Remove(HttpHeaderNames.ContentEncoding);
|
||||
}
|
||||
else
|
||||
{
|
||||
headers.Set(HttpHeaderNames.ContentEncoding, targetContentEncoding);
|
||||
}
|
||||
|
||||
if (httpMessage is IHttpContent)
|
||||
{
|
||||
// If message is a full request or response object (headers + data), don't copy data part into out.
|
||||
// Output headers only; data part will be decoded below.
|
||||
// Note: "copy" object must not be an instance of LastHttpContent class,
|
||||
// as this would (erroneously) indicate the end of the HttpMessage to other handlers.
|
||||
IHttpMessage copy;
|
||||
if (httpMessage is IHttpRequest req)
|
||||
{
|
||||
// HttpRequest or FullHttpRequest
|
||||
copy = new DefaultHttpRequest(req.ProtocolVersion, req.Method, req.Uri);
|
||||
}
|
||||
else if (httpMessage is IHttpResponse res)
|
||||
{
|
||||
// HttpResponse or FullHttpResponse
|
||||
copy = new DefaultHttpResponse(res.ProtocolVersion, res.Status);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new CodecException($"Object of class {StringUtil.SimpleClassName(httpMessage.GetType())} is not a HttpRequest or HttpResponse");
|
||||
}
|
||||
copy.Headers.Set(httpMessage.Headers);
|
||||
copy.Result = httpMessage.Result;
|
||||
output.Add(copy);
|
||||
}
|
||||
else
|
||||
{
|
||||
output.Add(httpMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (message is IHttpContent c)
|
||||
{
|
||||
if (this.decoder == null)
|
||||
{
|
||||
output.Add(c.Retain());
|
||||
}
|
||||
else
|
||||
{
|
||||
this.DecodeContent(c, output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DecodeContent(IHttpContent c, IList<object> output)
|
||||
{
|
||||
IByteBuffer content = c.Content;
|
||||
|
||||
this.Decode(content, output);
|
||||
|
||||
if (c is ILastHttpContent last)
|
||||
{
|
||||
this.FinishDecode(output);
|
||||
|
||||
// Generate an additional chunk if the decoder produced
|
||||
// the last product on closure,
|
||||
HttpHeaders headers = last.TrailingHeaders;
|
||||
if (headers.IsEmpty)
|
||||
{
|
||||
output.Add(EmptyLastHttpContent.Default);
|
||||
}
|
||||
else
|
||||
{
|
||||
output.Add(new ComposedLastHttpContent(headers));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract EmbeddedChannel NewContentDecoder(ICharSequence contentEncoding);
|
||||
|
||||
protected ICharSequence GetTargetContentEncoding(ICharSequence contentEncoding) => Identity;
|
||||
|
||||
public override void HandlerRemoved(IChannelHandlerContext context)
|
||||
{
|
||||
this.CleanupSafely(context);
|
||||
base.HandlerRemoved(context);
|
||||
}
|
||||
|
||||
public override void ChannelInactive(IChannelHandlerContext context)
|
||||
{
|
||||
this.CleanupSafely(context);
|
||||
base.ChannelInactive(context);
|
||||
}
|
||||
|
||||
public override void HandlerAdded(IChannelHandlerContext context)
|
||||
{
|
||||
this.HandlerContext = context;
|
||||
base.HandlerAdded(context);
|
||||
}
|
||||
|
||||
void Cleanup()
|
||||
{
|
||||
if (this.decoder != null)
|
||||
{
|
||||
this.decoder.FinishAndReleaseAll();
|
||||
this.decoder = null;
|
||||
}
|
||||
}
|
||||
|
||||
void CleanupSafely(IChannelHandlerContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Cleanup();
|
||||
}
|
||||
catch (Exception cause)
|
||||
{
|
||||
// If cleanup throws any error we need to propagate it through the pipeline
|
||||
// so we don't fail to propagate pipeline events.
|
||||
context.FireExceptionCaught(cause);
|
||||
}
|
||||
}
|
||||
|
||||
void Decode(IByteBuffer buf, IList<object> output)
|
||||
{
|
||||
// call retain here as it will call release after its written to the channel
|
||||
this.decoder.WriteInbound(buf.Retain());
|
||||
this.FetchDecoderOutput(output);
|
||||
}
|
||||
|
||||
void FinishDecode(ICollection<object> output)
|
||||
{
|
||||
if (this.decoder.Finish())
|
||||
{
|
||||
this.FetchDecoderOutput(output);
|
||||
}
|
||||
this.decoder = null;
|
||||
}
|
||||
|
||||
void FetchDecoderOutput(ICollection<object> output)
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
var buf = this.decoder.ReadInbound<IByteBuffer>();
|
||||
if (buf == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (!buf.IsReadable())
|
||||
{
|
||||
buf.Release();
|
||||
continue;
|
||||
}
|
||||
output.Add(new DefaultHttpContent(buf));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
// 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
|
||||
{
|
||||
using DotNetty.Codecs.Compression;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels.Embedded;
|
||||
|
||||
public class HttpContentDecompressor : HttpContentDecoder
|
||||
{
|
||||
readonly bool strict;
|
||||
|
||||
public HttpContentDecompressor() : this(false)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpContentDecompressor(bool strict)
|
||||
{
|
||||
this.strict = strict;
|
||||
}
|
||||
|
||||
protected override EmbeddedChannel NewContentDecoder(ICharSequence contentEncoding)
|
||||
{
|
||||
if (HttpHeaderValues.Gzip.ContentEqualsIgnoreCase(contentEncoding)
|
||||
|| HttpHeaderValues.XGzip.ContentEqualsIgnoreCase(contentEncoding))
|
||||
{
|
||||
return new EmbeddedChannel(
|
||||
this.HandlerContext.Channel.Id,
|
||||
this.HandlerContext.Channel.Metadata.HasDisconnect,
|
||||
this.HandlerContext.Channel.Configuration,
|
||||
ZlibCodecFactory.NewZlibDecoder(ZlibWrapper.Gzip));
|
||||
}
|
||||
|
||||
if (HttpHeaderValues.Deflate.ContentEqualsIgnoreCase(contentEncoding)
|
||||
|| HttpHeaderValues.XDeflate.ContentEqualsIgnoreCase(contentEncoding))
|
||||
{
|
||||
ZlibWrapper wrapper = this.strict ? ZlibWrapper.Zlib : ZlibWrapper.ZlibOrNone;
|
||||
return new EmbeddedChannel(
|
||||
this.HandlerContext.Channel.Id,
|
||||
this.HandlerContext.Channel.Metadata.HasDisconnect,
|
||||
this.HandlerContext.Channel.Configuration,
|
||||
ZlibCodecFactory.NewZlibDecoder(wrapper));
|
||||
}
|
||||
|
||||
// 'identity' or unsupported
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,357 @@
|
|||
// 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
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Contracts;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
using DotNetty.Transport.Channels.Embedded;
|
||||
|
||||
public abstract class HttpContentEncoder : MessageToMessageCodec<IHttpRequest, IHttpObject>
|
||||
{
|
||||
enum State
|
||||
{
|
||||
PassThrough,
|
||||
AwaitHeaders,
|
||||
AwaitContent
|
||||
}
|
||||
|
||||
static readonly AsciiString ZeroLengthHead = AsciiString.Cached("HEAD");
|
||||
static readonly AsciiString ZeroLengthConnect = AsciiString.Cached("CONNECT");
|
||||
static readonly int ContinueCode = HttpResponseStatus.Continue.Code;
|
||||
|
||||
readonly Queue<ICharSequence> acceptEncodingQueue = new Queue<ICharSequence>();
|
||||
EmbeddedChannel encoder;
|
||||
State state = State.AwaitHeaders;
|
||||
|
||||
public override bool AcceptOutboundMessage(object msg) => msg is IHttpContent || msg is IHttpResponse;
|
||||
|
||||
protected override void Decode(IChannelHandlerContext ctx, IHttpRequest msg, List<object> output)
|
||||
{
|
||||
ICharSequence acceptedEncoding = msg.Headers.Get(HttpHeaderNames.AcceptEncoding, HttpContentDecoder.Identity);
|
||||
|
||||
HttpMethod meth = msg.Method;
|
||||
if (ReferenceEquals(meth, HttpMethod.Head))
|
||||
{
|
||||
acceptedEncoding = ZeroLengthHead;
|
||||
}
|
||||
else if (ReferenceEquals(meth, HttpMethod.Connect))
|
||||
{
|
||||
acceptedEncoding = ZeroLengthConnect;
|
||||
}
|
||||
|
||||
this.acceptEncodingQueue.Enqueue(acceptedEncoding);
|
||||
output.Add(ReferenceCountUtil.Retain(msg));
|
||||
}
|
||||
|
||||
protected override void Encode(IChannelHandlerContext ctx, IHttpObject msg, List<object> output)
|
||||
{
|
||||
bool isFull = msg is IHttpResponse && msg is ILastHttpContent;
|
||||
switch (this.state)
|
||||
{
|
||||
case State.AwaitHeaders:
|
||||
{
|
||||
EnsureHeaders(msg);
|
||||
Debug.Assert(this.encoder == null);
|
||||
|
||||
var res = (IHttpResponse)msg;
|
||||
int code = res.Status.Code;
|
||||
ICharSequence acceptEncoding;
|
||||
if (code == ContinueCode)
|
||||
{
|
||||
// We need to not poll the encoding when response with CONTINUE as another response will follow
|
||||
// for the issued request. See https://github.com/netty/netty/issues/4079
|
||||
acceptEncoding = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get the list of encodings accepted by the peer.
|
||||
acceptEncoding = this.acceptEncodingQueue.Count > 0 ? this.acceptEncodingQueue.Dequeue() : null;
|
||||
if (acceptEncoding == null)
|
||||
{
|
||||
throw new InvalidOperationException("cannot send more responses than requests");
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// per rfc2616 4.3 Message Body
|
||||
// All 1xx (informational), 204 (no content), and 304 (not modified) responses MUST NOT include a
|
||||
// message-body. All other responses do include a message-body, although it MAY be of zero length.
|
||||
//
|
||||
// 9.4 HEAD
|
||||
// The HEAD method is identical to GET except that the server MUST NOT return a message-body
|
||||
// in the response.
|
||||
//
|
||||
// Also we should pass through HTTP/1.0 as transfer-encoding: chunked is not supported.
|
||||
//
|
||||
// See https://github.com/netty/netty/issues/5382
|
||||
//
|
||||
if (IsPassthru(res.ProtocolVersion, code, acceptEncoding))
|
||||
{
|
||||
if (isFull)
|
||||
{
|
||||
output.Add(ReferenceCountUtil.Retain(res));
|
||||
}
|
||||
else
|
||||
{
|
||||
output.Add(res);
|
||||
// Pass through all following contents.
|
||||
this.state = State.PassThrough;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (isFull)
|
||||
{
|
||||
// Pass through the full response with empty content and continue waiting for the the next resp.
|
||||
if (!((IByteBufferHolder)res).Content.IsReadable())
|
||||
{
|
||||
output.Add(ReferenceCountUtil.Retain(res));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare to encode the content.
|
||||
Result result = this.BeginEncode(res, acceptEncoding);
|
||||
|
||||
// If unable to encode, pass through.
|
||||
if (result == null)
|
||||
{
|
||||
if (isFull)
|
||||
{
|
||||
output.Add(ReferenceCountUtil.Retain(res));
|
||||
}
|
||||
else
|
||||
{
|
||||
output.Add(res);
|
||||
// Pass through all following contents.
|
||||
this.state = State.PassThrough;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this.encoder = result.ContentEncoder;
|
||||
|
||||
// Encode the content and remove or replace the existing headers
|
||||
// so that the message looks like a decoded message.
|
||||
res.Headers.Set(HttpHeaderNames.ContentEncoding, result.TargetContentEncoding);
|
||||
|
||||
// Output the rewritten response.
|
||||
if (isFull)
|
||||
{
|
||||
// Convert full message into unfull one.
|
||||
var newRes = new DefaultHttpResponse(res.ProtocolVersion, res.Status);
|
||||
newRes.Headers.Set(res.Headers);
|
||||
output.Add(newRes);
|
||||
|
||||
EnsureContent(res);
|
||||
this.EncodeFullResponse(newRes, (IHttpContent)res, output);
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Make the response chunked to simplify content transformation.
|
||||
res.Headers.Remove(HttpHeaderNames.ContentLength);
|
||||
res.Headers.Set(HttpHeaderNames.TransferEncoding, HttpHeaderValues.Chunked);
|
||||
|
||||
output.Add(res);
|
||||
this.state = State.AwaitContent;
|
||||
|
||||
if (!(msg is IHttpContent))
|
||||
{
|
||||
// only break out the switch statement if we have not content to process
|
||||
// See https://github.com/netty/netty/issues/2006
|
||||
break;
|
||||
}
|
||||
// Fall through to encode the content
|
||||
goto case State.AwaitContent;
|
||||
}
|
||||
}
|
||||
case State.AwaitContent:
|
||||
{
|
||||
EnsureContent(msg);
|
||||
if (this.EncodeContent((IHttpContent)msg, output))
|
||||
{
|
||||
this.state = State.AwaitHeaders;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case State.PassThrough:
|
||||
{
|
||||
EnsureContent(msg);
|
||||
output.Add(ReferenceCountUtil.Retain(msg));
|
||||
// Passed through all following contents of the current response.
|
||||
if (msg is ILastHttpContent)
|
||||
{
|
||||
this.state = State.AwaitHeaders;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EncodeFullResponse(IHttpResponse newRes, IHttpContent content, IList<object> output)
|
||||
{
|
||||
int existingMessages = output.Count;
|
||||
this.EncodeContent(content, output);
|
||||
|
||||
if (HttpUtil.IsContentLengthSet(newRes))
|
||||
{
|
||||
// adjust the content-length header
|
||||
int messageSize = 0;
|
||||
for (int i = existingMessages; i < output.Count; i++)
|
||||
{
|
||||
if (output[i] is IHttpContent httpContent)
|
||||
{
|
||||
messageSize += httpContent.Content.ReadableBytes;
|
||||
}
|
||||
}
|
||||
HttpUtil.SetContentLength(newRes, messageSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
newRes.Headers.Set(HttpHeaderNames.TransferEncoding, HttpHeaderValues.Chunked);
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsPassthru(HttpVersion version, int code, ICharSequence httpMethod) =>
|
||||
code < 200 || code == 204 || code == 304
|
||||
|| (ReferenceEquals(httpMethod, ZeroLengthHead) || ReferenceEquals(httpMethod, ZeroLengthConnect) && code == 200)
|
||||
|| ReferenceEquals(version, HttpVersion.Http10);
|
||||
|
||||
static void EnsureHeaders(IHttpObject msg)
|
||||
{
|
||||
if (!(msg is IHttpResponse))
|
||||
{
|
||||
throw new CodecException($"unexpected message type: {msg.GetType().Name} (expected: {StringUtil.SimpleClassName<IHttpResponse>()})");
|
||||
}
|
||||
}
|
||||
|
||||
static void EnsureContent(IHttpObject msg)
|
||||
{
|
||||
if (!(msg is IHttpContent))
|
||||
{
|
||||
throw new CodecException($"unexpected message type: {msg.GetType().Name} (expected: {StringUtil.SimpleClassName<IHttpContent>()})");
|
||||
}
|
||||
}
|
||||
|
||||
bool EncodeContent(IHttpContent c, IList<object> output)
|
||||
{
|
||||
IByteBuffer content = c.Content;
|
||||
|
||||
this.Encode(content, output);
|
||||
|
||||
if (c is ILastHttpContent last)
|
||||
{
|
||||
this.FinishEncode(output);
|
||||
|
||||
// Generate an additional chunk if the decoder produced
|
||||
// the last product on closure,
|
||||
HttpHeaders headers = last.TrailingHeaders;
|
||||
if (headers.IsEmpty)
|
||||
{
|
||||
output.Add(EmptyLastHttpContent.Default);
|
||||
}
|
||||
else
|
||||
{
|
||||
output.Add(new ComposedLastHttpContent(headers));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected abstract Result BeginEncode(IHttpResponse headers, ICharSequence acceptEncoding);
|
||||
|
||||
public override void HandlerRemoved(IChannelHandlerContext context)
|
||||
{
|
||||
this.CleanupSafely(context);
|
||||
base.HandlerRemoved(context);
|
||||
}
|
||||
|
||||
public override void ChannelInactive(IChannelHandlerContext context)
|
||||
{
|
||||
this.CleanupSafely(context);
|
||||
base.ChannelInactive(context);
|
||||
}
|
||||
|
||||
void Cleanup()
|
||||
{
|
||||
if (this.encoder != null)
|
||||
{
|
||||
// Clean-up the previous encoder if not cleaned up correctly.
|
||||
this.encoder.FinishAndReleaseAll();
|
||||
this.encoder = null;
|
||||
}
|
||||
}
|
||||
|
||||
void CleanupSafely(IChannelHandlerContext ctx)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Cleanup();
|
||||
}
|
||||
catch (Exception cause)
|
||||
{
|
||||
// If cleanup throws any error we need to propagate it through the pipeline
|
||||
// so we don't fail to propagate pipeline events.
|
||||
ctx.FireExceptionCaught(cause);
|
||||
}
|
||||
}
|
||||
|
||||
void Encode(IByteBuffer buf, IList<object> output)
|
||||
{
|
||||
// call retain here as it will call release after its written to the channel
|
||||
this.encoder.WriteOutbound(buf.Retain());
|
||||
this.FetchEncoderOutput(output);
|
||||
}
|
||||
|
||||
void FinishEncode(IList<object> output)
|
||||
{
|
||||
if (this.encoder.Finish())
|
||||
{
|
||||
this.FetchEncoderOutput(output);
|
||||
}
|
||||
this.encoder = null;
|
||||
}
|
||||
|
||||
void FetchEncoderOutput(ICollection<object> output)
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
var buf = this.encoder.ReadOutbound<IByteBuffer>();
|
||||
if (buf == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (!buf.IsReadable())
|
||||
{
|
||||
buf.Release();
|
||||
continue;
|
||||
}
|
||||
output.Add(new DefaultHttpContent(buf));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class Result
|
||||
{
|
||||
public Result(ICharSequence targetContentEncoding, EmbeddedChannel contentEncoder)
|
||||
{
|
||||
Contract.Requires(targetContentEncoding != null);
|
||||
Contract.Requires(contentEncoder != null);
|
||||
|
||||
this.TargetContentEncoding = targetContentEncoding;
|
||||
this.ContentEncoder = contentEncoder;
|
||||
}
|
||||
|
||||
public ICharSequence TargetContentEncoding { get; }
|
||||
|
||||
public EmbeddedChannel ContentEncoder { get; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// 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
|
||||
{
|
||||
// A user event designed to communicate that a expectation has failed and there should be no expectation that a
|
||||
// body will follow.
|
||||
public sealed class HttpExpectationFailedEvent
|
||||
{
|
||||
public static readonly HttpExpectationFailedEvent Default = new HttpExpectationFailedEvent();
|
||||
|
||||
HttpExpectationFailedEvent()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
// 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
|
||||
{
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
///
|
||||
/// Standard HTTP header names.
|
||||
///
|
||||
/// These are all defined as lowercase to support HTTP/2 requirements while also not
|
||||
/// violating HTTP/1.x requirements.New header names should always be lowercase.
|
||||
///
|
||||
public static class HttpHeaderNames
|
||||
{
|
||||
public static readonly AsciiString Accept = AsciiString.Cached("accept");
|
||||
|
||||
public static readonly AsciiString AcceptCharset = AsciiString.Cached("accept-charset");
|
||||
|
||||
public static readonly AsciiString AcceptEncoding = AsciiString.Cached("accept-encoding");
|
||||
|
||||
public static readonly AsciiString AcceptLanguage = AsciiString.Cached("accept-language");
|
||||
|
||||
public static readonly AsciiString AcceptRanges = AsciiString.Cached("accept-ranges");
|
||||
|
||||
public static readonly AsciiString AcceptPatch = AsciiString.Cached("accept-patch");
|
||||
|
||||
public static readonly AsciiString AccessControlAllowCredentials = AsciiString.Cached("access-control-allow-credentials");
|
||||
|
||||
public static readonly AsciiString AccessControlAllowHeaders = AsciiString.Cached("access-control-allow-headers");
|
||||
|
||||
public static readonly AsciiString AccessControlAllowMethods = AsciiString.Cached("access-control-allow-methods");
|
||||
|
||||
public static readonly AsciiString AccessControlAllowOrigin = AsciiString.Cached("access-control-allow-origin");
|
||||
|
||||
public static readonly AsciiString AccessControlExposeHeaders = AsciiString.Cached("access-control-expose-headers");
|
||||
|
||||
public static readonly AsciiString AccessControlMaxAge = AsciiString.Cached("access-control-max-age");
|
||||
|
||||
public static readonly AsciiString AccessControlRequestHeaders = AsciiString.Cached("access-control-request-headers");
|
||||
|
||||
public static readonly AsciiString AccessControlRequestMethod = AsciiString.Cached("access-control-request-method");
|
||||
|
||||
public static readonly AsciiString Age = AsciiString.Cached("age");
|
||||
|
||||
public static readonly AsciiString Allow = AsciiString.Cached("allow");
|
||||
|
||||
public static readonly AsciiString Authorization = AsciiString.Cached("authorization");
|
||||
|
||||
public static readonly AsciiString CacheControl = AsciiString.Cached("cache-control");
|
||||
|
||||
public static readonly AsciiString Connection = AsciiString.Cached("connection");
|
||||
|
||||
public static readonly AsciiString ContentBase = AsciiString.Cached("content-base");
|
||||
|
||||
public static readonly AsciiString ContentEncoding = AsciiString.Cached("content-encoding");
|
||||
|
||||
public static readonly AsciiString ContentLanguage = AsciiString.Cached("content-language");
|
||||
|
||||
public static readonly AsciiString ContentLength = AsciiString.Cached("content-length");
|
||||
|
||||
public static readonly AsciiString ContentLocation = AsciiString.Cached("content-location");
|
||||
|
||||
public static readonly AsciiString ContentTransferEncoding = AsciiString.Cached("content-transfer-encoding");
|
||||
|
||||
public static readonly AsciiString ContentDisposition = AsciiString.Cached("content-disposition");
|
||||
|
||||
public static readonly AsciiString ContentMD5 = AsciiString.Cached("content-md5");
|
||||
|
||||
public static readonly AsciiString ContentRange = AsciiString.Cached("content-range");
|
||||
|
||||
public static readonly AsciiString ContentSecurityPolicy = AsciiString.Cached("content-security-policy");
|
||||
|
||||
public static readonly AsciiString ContentType = AsciiString.Cached("content-type");
|
||||
|
||||
public static readonly AsciiString Cookie = AsciiString.Cached("cookie");
|
||||
|
||||
public static readonly AsciiString Date = AsciiString.Cached("date");
|
||||
|
||||
public static readonly AsciiString Etag = AsciiString.Cached("etag");
|
||||
|
||||
public static readonly AsciiString Expect = AsciiString.Cached("expect");
|
||||
|
||||
public static readonly AsciiString Expires = AsciiString.Cached("expires");
|
||||
|
||||
public static readonly AsciiString From = AsciiString.Cached("from");
|
||||
|
||||
public static readonly AsciiString Host = AsciiString.Cached("host");
|
||||
|
||||
public static readonly AsciiString IfMatch = AsciiString.Cached("if-match");
|
||||
|
||||
public static readonly AsciiString IfModifiedSince = AsciiString.Cached("if-modified-since");
|
||||
|
||||
public static readonly AsciiString IfNoneMatch = AsciiString.Cached("if-none-match");
|
||||
|
||||
public static readonly AsciiString IfRange = AsciiString.Cached("if-range");
|
||||
|
||||
public static readonly AsciiString IfUnmodifiedSince = AsciiString.Cached("if-unmodified-since");
|
||||
|
||||
public static readonly AsciiString LastModified = AsciiString.Cached("last-modified");
|
||||
|
||||
public static readonly AsciiString Location = AsciiString.Cached("location");
|
||||
|
||||
public static readonly AsciiString MaxForwards = AsciiString.Cached("max-forwards");
|
||||
|
||||
public static readonly AsciiString Origin = AsciiString.Cached("origin");
|
||||
|
||||
public static readonly AsciiString Pragma = AsciiString.Cached("pragma");
|
||||
|
||||
public static readonly AsciiString ProxyAuthenticate = AsciiString.Cached("proxy-authenticate");
|
||||
|
||||
public static readonly AsciiString ProxyAuthorization = AsciiString.Cached("proxy-authorization");
|
||||
|
||||
public static readonly AsciiString Range = AsciiString.Cached("range");
|
||||
|
||||
public static readonly AsciiString Referer = AsciiString.Cached("referer");
|
||||
|
||||
public static readonly AsciiString RetryAfter = AsciiString.Cached("retry-after");
|
||||
|
||||
public static readonly AsciiString SecWebsocketKey1 = AsciiString.Cached("sec-websocket-key1");
|
||||
|
||||
public static readonly AsciiString SecWebsocketKey2 = AsciiString.Cached("sec-websocket-key2");
|
||||
|
||||
public static readonly AsciiString SecWebsocketLocation = AsciiString.Cached("sec-websocket-location");
|
||||
|
||||
public static readonly AsciiString SecWebsocketOrigin = AsciiString.Cached("sec-websocket-origin");
|
||||
|
||||
public static readonly AsciiString SecWebsocketProtocol = AsciiString.Cached("sec-websocket-protocol");
|
||||
|
||||
public static readonly AsciiString SecWebsocketVersion = AsciiString.Cached("sec-websocket-version");
|
||||
|
||||
public static readonly AsciiString SecWebsocketKey = AsciiString.Cached("sec-websocket-key");
|
||||
|
||||
public static readonly AsciiString SecWebsocketAccept = AsciiString.Cached("sec-websocket-accept");
|
||||
|
||||
public static readonly AsciiString SecWebsocketExtensions = AsciiString.Cached("sec-websocket-extensions");
|
||||
|
||||
public static readonly AsciiString Server = AsciiString.Cached("server");
|
||||
|
||||
public static readonly AsciiString SetCookie = AsciiString.Cached("set-cookie");
|
||||
|
||||
public static readonly AsciiString SetCookie2 = AsciiString.Cached("set-cookie2");
|
||||
|
||||
public static readonly AsciiString Te = AsciiString.Cached("te");
|
||||
|
||||
public static readonly AsciiString Trailer = AsciiString.Cached("trailer");
|
||||
|
||||
public static readonly AsciiString TransferEncoding = AsciiString.Cached("transfer-encoding");
|
||||
|
||||
public static readonly AsciiString Upgrade = AsciiString.Cached("upgrade");
|
||||
|
||||
public static readonly AsciiString UserAgent = AsciiString.Cached("user-agent");
|
||||
|
||||
public static readonly AsciiString Vary = AsciiString.Cached("vary");
|
||||
|
||||
public static readonly AsciiString Via = AsciiString.Cached("via");
|
||||
|
||||
public static readonly AsciiString Warning = AsciiString.Cached("warning");
|
||||
|
||||
public static readonly AsciiString WebsocketLocation = AsciiString.Cached("websocket-location");
|
||||
|
||||
public static readonly AsciiString WebsocketOrigin = AsciiString.Cached("websocket-origin");
|
||||
|
||||
public static readonly AsciiString WebsocketProtocol = AsciiString.Cached("websocket-protocol");
|
||||
|
||||
public static readonly AsciiString WwwAuthenticate = AsciiString.Cached("www-authenticate");
|
||||
|
||||
public static readonly AsciiString XFrameOptions = AsciiString.Cached("x-frame-options");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
// 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
|
||||
{
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public static class HttpHeaderValues
|
||||
{
|
||||
public static readonly AsciiString ApplicationJson = AsciiString.Cached("application/json");
|
||||
|
||||
public static readonly AsciiString ApplicationXWwwFormUrlencoded = AsciiString.Cached("application/x-www-form-urlencoded");
|
||||
|
||||
public static readonly AsciiString ApplicationOctetStream = AsciiString.Cached("application/octet-stream");
|
||||
|
||||
public static readonly AsciiString Attachment = AsciiString.Cached("attachment");
|
||||
|
||||
public static readonly AsciiString Base64 = AsciiString.Cached("base64");
|
||||
|
||||
public static readonly AsciiString Binary = AsciiString.Cached("binary");
|
||||
|
||||
public static readonly AsciiString Boundary = AsciiString.Cached("boundary");
|
||||
|
||||
public static readonly AsciiString Bytes = AsciiString.Cached("bytes");
|
||||
|
||||
public static readonly AsciiString Charset = AsciiString.Cached("charset");
|
||||
|
||||
public static readonly AsciiString Chunked = AsciiString.Cached("chunked");
|
||||
|
||||
public static readonly AsciiString Close = AsciiString.Cached("close");
|
||||
|
||||
public static readonly AsciiString Compress = AsciiString.Cached("compress");
|
||||
|
||||
public static readonly AsciiString Continue = AsciiString.Cached("100-continue");
|
||||
|
||||
public static readonly AsciiString Deflate = AsciiString.Cached("deflate");
|
||||
|
||||
public static readonly AsciiString XDeflate = AsciiString.Cached("x-deflate");
|
||||
|
||||
public static readonly AsciiString File = AsciiString.Cached("file");
|
||||
|
||||
public static readonly AsciiString FileName = AsciiString.Cached("filename");
|
||||
|
||||
public static readonly AsciiString FormData = AsciiString.Cached("form-data");
|
||||
|
||||
public static readonly AsciiString Gzip = AsciiString.Cached("gzip");
|
||||
|
||||
public static readonly AsciiString GzipDeflate = AsciiString.Cached("gzip,deflate");
|
||||
|
||||
public static readonly AsciiString XGzip = AsciiString.Cached("x-gzip");
|
||||
|
||||
public static readonly AsciiString Identity = AsciiString.Cached("identity");
|
||||
|
||||
public static readonly AsciiString KeepAlive = AsciiString.Cached("keep-alive");
|
||||
|
||||
public static readonly AsciiString MaxAge = AsciiString.Cached("max-age");
|
||||
|
||||
public static readonly AsciiString MaxStale = AsciiString.Cached("max-stale");
|
||||
|
||||
public static readonly AsciiString MinFresh = AsciiString.Cached("min-fresh");
|
||||
|
||||
public static readonly AsciiString MultipartFormData = AsciiString.Cached("multipart/form-data");
|
||||
|
||||
public static readonly AsciiString MultipartMixed = AsciiString.Cached("multipart/mixed");
|
||||
|
||||
public static readonly AsciiString MustRevalidate = AsciiString.Cached("must-revalidate");
|
||||
|
||||
public static readonly AsciiString Name = AsciiString.Cached("name");
|
||||
|
||||
public static readonly AsciiString NoCache = AsciiString.Cached("no-cache");
|
||||
|
||||
public static readonly AsciiString NoStore = AsciiString.Cached("no-store");
|
||||
|
||||
public static readonly AsciiString NoTransform = AsciiString.Cached("no-transform");
|
||||
|
||||
public static readonly AsciiString None = AsciiString.Cached("none");
|
||||
|
||||
public static readonly AsciiString Zero = AsciiString.Cached("0");
|
||||
|
||||
public static readonly AsciiString OnlyIfCached = AsciiString.Cached("only-if-cached");
|
||||
|
||||
public static readonly AsciiString Private = AsciiString.Cached("private");
|
||||
|
||||
public static readonly AsciiString ProxyRevalidate = AsciiString.Cached("proxy-revalidate");
|
||||
|
||||
public static readonly AsciiString Public = AsciiString.Cached("public");
|
||||
|
||||
public static readonly AsciiString QuotedPrintable = AsciiString.Cached("quoted-printable");
|
||||
|
||||
public static readonly AsciiString SMaxage = AsciiString.Cached("s-maxage");
|
||||
|
||||
public static readonly AsciiString TextPlain = AsciiString.Cached("text/plain");
|
||||
|
||||
public static readonly AsciiString Trailers = AsciiString.Cached("trailers");
|
||||
|
||||
public static readonly AsciiString Upgrade = AsciiString.Cached("upgrade");
|
||||
|
||||
public static readonly AsciiString Websocket = AsciiString.Cached("websocket");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,267 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ForCanBeConvertedToForeach
|
||||
namespace DotNetty.Codecs.Http
|
||||
{
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
using static Common.Utilities.AsciiString;
|
||||
|
||||
public abstract class HttpHeaders : IEnumerable<HeaderEntry<AsciiString, ICharSequence>>
|
||||
{
|
||||
public abstract bool TryGet(AsciiString name, out ICharSequence value);
|
||||
|
||||
public ICharSequence Get(AsciiString name, ICharSequence defaultValue) => this.TryGet(name, out ICharSequence value) ? value : defaultValue;
|
||||
|
||||
public abstract bool TryGetInt(AsciiString name, out int value);
|
||||
|
||||
public abstract int GetInt(AsciiString name, int defaultValue);
|
||||
|
||||
public abstract bool TryGetShort(AsciiString name, out short value);
|
||||
|
||||
public abstract short GetShort(AsciiString name, short defaultValue);
|
||||
|
||||
public abstract bool TryGetTimeMillis(AsciiString name, out long value);
|
||||
|
||||
public abstract long GetTimeMillis(AsciiString name, long defaultValue);
|
||||
|
||||
public abstract IList<ICharSequence> GetAll(AsciiString name);
|
||||
|
||||
public abstract IList<HeaderEntry<AsciiString, ICharSequence>> Entries();
|
||||
|
||||
public virtual IEnumerable<ICharSequence> ValueCharSequenceIterator(AsciiString name) => this.GetAll(name);
|
||||
|
||||
public abstract bool Contains(AsciiString name);
|
||||
|
||||
public abstract bool IsEmpty { get; }
|
||||
|
||||
public abstract int Size { get; }
|
||||
|
||||
public abstract ISet<AsciiString> Names();
|
||||
|
||||
public abstract HttpHeaders Add(AsciiString name, object value);
|
||||
|
||||
public HttpHeaders Add(AsciiString name, IEnumerable<object> values)
|
||||
{
|
||||
foreach (object value in values)
|
||||
{
|
||||
this.Add(name, value);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public virtual HttpHeaders Add(HttpHeaders headers)
|
||||
{
|
||||
Contract.Requires(headers != null);
|
||||
|
||||
foreach (HeaderEntry<AsciiString, ICharSequence> pair in headers)
|
||||
{
|
||||
this.Add(pair.Key, pair.Value);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public abstract HttpHeaders AddInt(AsciiString name, int value);
|
||||
|
||||
public abstract HttpHeaders AddShort(AsciiString name, short value);
|
||||
|
||||
public abstract HttpHeaders Set(AsciiString name, object value);
|
||||
|
||||
public abstract HttpHeaders Set(AsciiString name, IEnumerable<object> values);
|
||||
|
||||
public virtual HttpHeaders Set(HttpHeaders headers)
|
||||
{
|
||||
Contract.Requires(headers != null);
|
||||
|
||||
this.Clear();
|
||||
|
||||
if (headers.IsEmpty)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
foreach(HeaderEntry<AsciiString, ICharSequence> pair in headers)
|
||||
{
|
||||
this.Add(pair.Key, pair.Value);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpHeaders SetAll(HttpHeaders headers)
|
||||
{
|
||||
Contract.Requires(headers != null);
|
||||
|
||||
if (headers.IsEmpty)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
foreach (HeaderEntry<AsciiString, ICharSequence> pair in headers)
|
||||
{
|
||||
this.Add(pair.Key, pair.Value);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public abstract HttpHeaders SetInt(AsciiString name, int value);
|
||||
|
||||
public abstract HttpHeaders SetShort(AsciiString name, short value);
|
||||
|
||||
public abstract HttpHeaders Remove(AsciiString name);
|
||||
|
||||
public abstract HttpHeaders Clear();
|
||||
|
||||
public virtual bool Contains(AsciiString name, ICharSequence value, bool ignoreCase)
|
||||
{
|
||||
IEnumerable<ICharSequence> values = this.ValueCharSequenceIterator(name);
|
||||
if (ignoreCase)
|
||||
{
|
||||
foreach (ICharSequence v in values)
|
||||
{
|
||||
if (v.ContentEqualsIgnoreCase(value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (ICharSequence v in this.ValueCharSequenceIterator(name))
|
||||
{
|
||||
if (v.ContentEquals(value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual bool ContainsValue(AsciiString name, ICharSequence value, bool ignoreCase)
|
||||
{
|
||||
foreach (ICharSequence v in this.ValueCharSequenceIterator(name))
|
||||
{
|
||||
if (ContainsCommaSeparatedTrimmed(v, value, ignoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool ContainsCommaSeparatedTrimmed(ICharSequence rawNext, ICharSequence expected, bool ignoreCase)
|
||||
{
|
||||
int begin = 0;
|
||||
int end;
|
||||
if (ignoreCase)
|
||||
{
|
||||
if ((end = IndexOf(rawNext, ',', begin)) == -1)
|
||||
{
|
||||
if (ContentEqualsIgnoreCase(Trim(rawNext), expected))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
do
|
||||
{
|
||||
if (ContentEqualsIgnoreCase(Trim(rawNext.SubSequence(begin, end)), expected))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
begin = end + 1;
|
||||
}
|
||||
while ((end = IndexOf(rawNext, ',', begin)) != -1);
|
||||
|
||||
if (begin < rawNext.Count)
|
||||
{
|
||||
if (ContentEqualsIgnoreCase(Trim(rawNext.SubSequence(begin, rawNext.Count)), expected))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((end = IndexOf(rawNext, ',', begin)) == -1)
|
||||
{
|
||||
if (ContentEquals(Trim(rawNext), expected))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
do
|
||||
{
|
||||
if (ContentEquals(Trim(rawNext.SubSequence(begin, end)), expected))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
begin = end + 1;
|
||||
}
|
||||
while ((end = IndexOf(rawNext, ',', begin)) != -1);
|
||||
|
||||
if (begin < rawNext.Count)
|
||||
{
|
||||
if (ContentEquals(Trim(rawNext.SubSequence(begin, rawNext.Count)), expected))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetAsString(AsciiString name, out string value)
|
||||
{
|
||||
if (this.TryGet(name, out ICharSequence v))
|
||||
{
|
||||
value = v.ToString();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = default(string);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public IList<string> GetAllAsString(AsciiString name)
|
||||
{
|
||||
var values = new List<string>();
|
||||
IList<ICharSequence> list = this.GetAll(name);
|
||||
foreach (ICharSequence value in list)
|
||||
{
|
||||
values.Add(value.ToString());
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
public abstract IEnumerator<HeaderEntry<AsciiString, ICharSequence>> GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
public override string ToString() => HeadersUtils.ToString(this, this.Size);
|
||||
|
||||
/// <summary>
|
||||
/// Deep copy of the headers.
|
||||
/// </summary>
|
||||
/// <returns>A deap copy of this.</returns>
|
||||
public virtual HttpHeaders Copy()
|
||||
{
|
||||
var copy = new DefaultHttpHeaders();
|
||||
copy.Set(this);
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// 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
|
||||
{
|
||||
using System.Text;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
using static HttpConstants;
|
||||
|
||||
static class HttpHeadersEncoder
|
||||
{
|
||||
const int ColonAndSpaceShort = (Colon << 8) | HorizontalSpace;
|
||||
|
||||
public static void EncoderHeader(AsciiString name, ICharSequence value, IByteBuffer buf)
|
||||
{
|
||||
int nameLen = name.Count;
|
||||
int valueLen = value.Count;
|
||||
int entryLen = nameLen + valueLen + 4;
|
||||
buf.EnsureWritable(entryLen);
|
||||
int offset = buf.WriterIndex;
|
||||
WriteAscii(buf, offset, name);
|
||||
offset += nameLen;
|
||||
buf.SetShort(offset, ColonAndSpaceShort);
|
||||
offset += 2;
|
||||
WriteAscii(buf, offset, value);
|
||||
offset += valueLen;
|
||||
buf.SetShort(offset, CrlfShort);
|
||||
offset += 2;
|
||||
buf.SetWriterIndex(offset);
|
||||
}
|
||||
|
||||
static void WriteAscii(IByteBuffer buf, int offset, ICharSequence value)
|
||||
{
|
||||
if (value is AsciiString asciiString)
|
||||
{
|
||||
ByteBufferUtil.Copy(asciiString, 0, buf, offset, value.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
buf.SetCharSequence(offset, value, Encoding.ASCII);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
// 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
|
||||
{
|
||||
using System.Text;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
static class HttpMessageUtil
|
||||
{
|
||||
internal static StringBuilder AppendRequest(StringBuilder buf, IHttpRequest req)
|
||||
{
|
||||
AppendCommon(buf, req);
|
||||
AppendInitialLine(buf, req);
|
||||
AppendHeaders(buf, req.Headers);
|
||||
RemoveLastNewLine(buf);
|
||||
return buf;
|
||||
}
|
||||
|
||||
internal static StringBuilder AppendResponse(StringBuilder buf, IHttpResponse res)
|
||||
{
|
||||
AppendCommon(buf, res);
|
||||
AppendInitialLine(buf, res);
|
||||
AppendHeaders(buf, res.Headers);
|
||||
RemoveLastNewLine(buf);
|
||||
return buf;
|
||||
}
|
||||
|
||||
static void AppendCommon(StringBuilder buf, IHttpMessage msg)
|
||||
{
|
||||
buf.Append($"{StringUtil.SimpleClassName(msg)}");
|
||||
buf.Append("(decodeResult: ");
|
||||
buf.Append(msg.Result);
|
||||
buf.Append(", version: ");
|
||||
buf.Append(msg.ProtocolVersion);
|
||||
buf.Append($"){StringUtil.Newline}");
|
||||
}
|
||||
|
||||
internal static StringBuilder AppendFullRequest(StringBuilder buf, IFullHttpRequest req)
|
||||
{
|
||||
AppendFullCommon(buf, req);
|
||||
AppendInitialLine(buf, req);
|
||||
AppendHeaders(buf, req.Headers);
|
||||
AppendHeaders(buf, req.TrailingHeaders);
|
||||
RemoveLastNewLine(buf);
|
||||
return buf;
|
||||
}
|
||||
|
||||
internal static StringBuilder AppendFullResponse(StringBuilder buf, IFullHttpResponse res)
|
||||
{
|
||||
AppendFullCommon(buf, res);
|
||||
AppendInitialLine(buf, res);
|
||||
AppendHeaders(buf, res.Headers);
|
||||
AppendHeaders(buf, res.TrailingHeaders);
|
||||
RemoveLastNewLine(buf);
|
||||
return buf;
|
||||
}
|
||||
|
||||
static void AppendFullCommon(StringBuilder buf, IFullHttpMessage msg)
|
||||
{
|
||||
buf.Append(StringUtil.SimpleClassName(msg));
|
||||
buf.Append("(decodeResult: ");
|
||||
buf.Append(msg.Result);
|
||||
buf.Append(", version: ");
|
||||
buf.Append(msg.ProtocolVersion);
|
||||
buf.Append(", content: ");
|
||||
buf.Append(msg.Content);
|
||||
buf.Append(')');
|
||||
buf.Append(StringUtil.Newline);
|
||||
}
|
||||
|
||||
static void AppendInitialLine(StringBuilder buf, IHttpRequest req) =>
|
||||
buf.Append($"{req.Method} {req.Uri} {req.ProtocolVersion}{StringUtil.Newline}");
|
||||
|
||||
static void AppendInitialLine(StringBuilder buf, IHttpResponse res) =>
|
||||
buf.Append($"{res.ProtocolVersion} {res.Status}{StringUtil.Newline}");
|
||||
|
||||
static void AppendHeaders(StringBuilder buf, HttpHeaders headers)
|
||||
{
|
||||
foreach(HeaderEntry<AsciiString, ICharSequence> e in headers)
|
||||
{
|
||||
buf.Append($"{e.Key}:{e.Value}{StringUtil.Newline}");
|
||||
}
|
||||
}
|
||||
|
||||
static void RemoveLastNewLine(StringBuilder buf) => buf.Length = buf.Length - StringUtil.Newline.Length;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,234 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ForCanBeConvertedToForeach
|
||||
// ReSharper disable ConvertToAutoPropertyWhenPossible
|
||||
namespace DotNetty.Codecs.Http
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Runtime.CompilerServices;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public sealed class HttpMethod : IComparable<HttpMethod>, IComparable
|
||||
{
|
||||
/**
|
||||
* The OPTIONS method represents a request for information about the communication options
|
||||
* available on the request/response chain identified by the Request-URI. This method allows
|
||||
* the client to determine the options and/or requirements associated with a resource, or the
|
||||
* capabilities of a server, without implying a resource action or initiating a resource
|
||||
* retrieval.
|
||||
*/
|
||||
public static readonly HttpMethod Options = new HttpMethod("OPTIONS");
|
||||
|
||||
/**
|
||||
* The GET method means retrieve whatever information (in the form of an entity) is identified
|
||||
* by the Request-URI. If the Request-URI refers to a data-producing process, it is the
|
||||
* produced data which shall be returned as the entity in the response and not the source text
|
||||
* of the process, unless that text happens to be the output of the process.
|
||||
*/
|
||||
public static readonly HttpMethod Get = new HttpMethod("GET");
|
||||
|
||||
/**
|
||||
* The HEAD method is identical to GET except that the server MUST NOT return a message-body
|
||||
* in the response.
|
||||
*/
|
||||
public static readonly HttpMethod Head = new HttpMethod("HEAD");
|
||||
|
||||
/**
|
||||
* The POST method is used to request that the origin server accept the entity enclosed in the
|
||||
* request as a new subordinate of the resource identified by the Request-URI in the
|
||||
* Request-Line.
|
||||
*/
|
||||
public static readonly HttpMethod Post = new HttpMethod("POST");
|
||||
|
||||
/**
|
||||
* The PUT method requests that the enclosed entity be stored under the supplied Request-URI.
|
||||
*/
|
||||
public static readonly HttpMethod Put = new HttpMethod("PUT");
|
||||
|
||||
/**
|
||||
* The PATCH method requests that a set of changes described in the
|
||||
* request entity be applied to the resource identified by the Request-URI.
|
||||
*/
|
||||
public static readonly HttpMethod Patch = new HttpMethod("PATCH");
|
||||
|
||||
/**
|
||||
* The DELETE method requests that the origin server delete the resource identified by the
|
||||
* Request-URI.
|
||||
*/
|
||||
public static readonly HttpMethod Delete = new HttpMethod("DELETE");
|
||||
|
||||
/**
|
||||
* The TRACE method is used to invoke a remote, application-layer loop- back of the request
|
||||
* message.
|
||||
*/
|
||||
public static readonly HttpMethod Trace = new HttpMethod("TRACE");
|
||||
|
||||
/**
|
||||
* This specification reserves the method name CONNECT for use with a proxy that can dynamically
|
||||
* switch to being a tunnel
|
||||
*/
|
||||
public static readonly HttpMethod Connect = new HttpMethod("CONNECT");
|
||||
|
||||
// HashMap
|
||||
static readonly Dictionary<string, HttpMethod> MethodMap;
|
||||
|
||||
static HttpMethod()
|
||||
{
|
||||
MethodMap = new Dictionary<string, HttpMethod>
|
||||
{
|
||||
{ Options.ToString(), Options },
|
||||
{ Get.ToString(), Get },
|
||||
{ Head.ToString(), Head },
|
||||
{ Post.ToString(), Post },
|
||||
{ Put.ToString(), Put },
|
||||
{ Patch.ToString(), Patch },
|
||||
{ Delete.ToString(), Delete },
|
||||
{ Trace.ToString(), Trace },
|
||||
{ Connect.ToString(), Connect },
|
||||
};
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static HttpMethod ValueOf(AsciiString name)
|
||||
{
|
||||
if (name != null)
|
||||
{
|
||||
HttpMethod result = ValueOfInline(name.Array);
|
||||
if (result != null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
// Fall back to slow path
|
||||
if (MethodMap.TryGetValue(name.ToString(), out result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
// Really slow path and error handling
|
||||
return new HttpMethod(name?.ToString());
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static HttpMethod ValueOfInline(byte[] bytes)
|
||||
{
|
||||
if (bytes.Length <= 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
HttpMethod match = null;
|
||||
int i = 0;
|
||||
switch (bytes[i++])
|
||||
{
|
||||
case (byte)'C':
|
||||
match = Connect;
|
||||
break;
|
||||
case (byte)'D':
|
||||
match = Delete;
|
||||
break;
|
||||
case (byte)'G':
|
||||
match = Get;
|
||||
break;
|
||||
case (byte)'H':
|
||||
match = Head;
|
||||
break;
|
||||
case (byte)'O':
|
||||
match = Options;
|
||||
break;
|
||||
case (byte)'P':
|
||||
switch (bytes[i++])
|
||||
{
|
||||
case (byte)'O':
|
||||
match = Post;
|
||||
break;
|
||||
case (byte)'U':
|
||||
match = Put;
|
||||
break;
|
||||
case (byte)'A':
|
||||
match = Patch;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case (byte)'T':
|
||||
match = Trace;
|
||||
break;
|
||||
}
|
||||
if (match != null)
|
||||
{
|
||||
byte[] array = match.name.Array;
|
||||
if (bytes.Length == array.Length)
|
||||
{
|
||||
for (; i < bytes.Length; i++)
|
||||
{
|
||||
if (bytes[i] != array[i])
|
||||
{
|
||||
match = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
match = null;
|
||||
}
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
readonly AsciiString name;
|
||||
|
||||
// Creates a new HTTP method with the specified name. You will not need to
|
||||
// create a new method unless you are implementing a protocol derived from
|
||||
// HTTP, such as
|
||||
// http://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol and
|
||||
// http://en.wikipedia.org/wiki/Internet_Content_Adaptation_Protocol
|
||||
//
|
||||
public HttpMethod(string name)
|
||||
{
|
||||
Contract.Requires(name != null);
|
||||
|
||||
name = name.Trim();
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
throw new ArgumentException(nameof(name));
|
||||
}
|
||||
|
||||
for (int i=0; i<name.Length;i++)
|
||||
{
|
||||
char c = name[i];
|
||||
if (CharUtil.IsISOControl(c) || char.IsWhiteSpace(c))
|
||||
{
|
||||
throw new ArgumentException($"Invalid character '{c}' in {nameof(name)}");
|
||||
}
|
||||
}
|
||||
|
||||
this.name = AsciiString.Cached(name);
|
||||
}
|
||||
|
||||
public string Name => this.name.ToString();
|
||||
|
||||
public AsciiString AsciiName => this.name;
|
||||
|
||||
public override int GetHashCode() => this.name.GetHashCode();
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (!(obj is HttpMethod method))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.name.Equals(method.name);
|
||||
}
|
||||
|
||||
public override string ToString() => this.name.ToString();
|
||||
|
||||
public int CompareTo(object obj) => this.CompareTo(obj as HttpMethod);
|
||||
|
||||
public int CompareTo(HttpMethod other) => this.name.CompareTo(other.name);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,354 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ConvertToAutoPropertyWhenPossible
|
||||
// ReSharper disable ConvertToAutoProperty
|
||||
namespace DotNetty.Codecs.Http
|
||||
{
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common;
|
||||
using DotNetty.Common.Internal.Logging;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public class HttpObjectAggregator : MessageAggregator<IHttpObject, IHttpMessage, IHttpContent, IFullHttpMessage>
|
||||
{
|
||||
static readonly IInternalLogger Logger = InternalLoggerFactory.GetInstance<HttpObjectAggregator>();
|
||||
static readonly IFullHttpResponse Continue = new DefaultFullHttpResponse(HttpVersion.Http11, HttpResponseStatus.Continue, Unpooled.Empty);
|
||||
static readonly IFullHttpResponse ExpectationFailed = new DefaultFullHttpResponse(HttpVersion.Http11, HttpResponseStatus.ExpectationFailed, Unpooled.Empty);
|
||||
static readonly IFullHttpResponse TooLargeClose = new DefaultFullHttpResponse(HttpVersion.Http11, HttpResponseStatus.RequestEntityTooLarge, Unpooled.Empty);
|
||||
static readonly IFullHttpResponse TooLarge = new DefaultFullHttpResponse(HttpVersion.Http11, HttpResponseStatus.RequestEntityTooLarge, Unpooled.Empty);
|
||||
|
||||
static HttpObjectAggregator()
|
||||
{
|
||||
ExpectationFailed.Headers.Set(HttpHeaderNames.ContentLength, HttpHeaderValues.Zero);
|
||||
TooLarge.Headers.Set(HttpHeaderNames.ContentLength, HttpHeaderValues.Zero);
|
||||
|
||||
TooLargeClose.Headers.Set(HttpHeaderNames.ContentLength, HttpHeaderValues.Zero);
|
||||
TooLargeClose.Headers.Set(HttpHeaderNames.Connection, HttpHeaderValues.Close);
|
||||
}
|
||||
|
||||
readonly bool closeOnExpectationFailed;
|
||||
|
||||
public HttpObjectAggregator(int maxContentLength)
|
||||
: this(maxContentLength, false)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpObjectAggregator(int maxContentLength, bool closeOnExpectationFailed)
|
||||
: base(maxContentLength)
|
||||
{
|
||||
this.closeOnExpectationFailed = closeOnExpectationFailed;
|
||||
}
|
||||
|
||||
protected override bool IsStartMessage(IHttpObject msg) => msg is IHttpMessage;
|
||||
|
||||
protected override bool IsContentMessage(IHttpObject msg) => msg is IHttpContent;
|
||||
|
||||
protected override bool IsLastContentMessage(IHttpContent msg) => msg is ILastHttpContent;
|
||||
|
||||
protected override bool IsAggregated(IHttpObject msg) => msg is IFullHttpMessage;
|
||||
|
||||
protected override bool IsContentLengthInvalid(IHttpMessage start, int maxContentLength)
|
||||
{
|
||||
try
|
||||
{
|
||||
return HttpUtil.GetContentLength(start, -1) > maxContentLength;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static object ContinueResponse(IHttpMessage start, int maxContentLength, IChannelPipeline pipeline)
|
||||
{
|
||||
if (HttpUtil.IsUnsupportedExpectation(start))
|
||||
{
|
||||
// if the request contains an unsupported expectation, we return 417
|
||||
pipeline.FireUserEventTriggered(HttpExpectationFailedEvent.Default);
|
||||
return ExpectationFailed.RetainedDuplicate();
|
||||
}
|
||||
else if (HttpUtil.Is100ContinueExpected(start))
|
||||
{
|
||||
// if the request contains 100-continue but the content-length is too large, we return 413
|
||||
if (HttpUtil.GetContentLength(start, -1L) <= maxContentLength)
|
||||
{
|
||||
return Continue.RetainedDuplicate();
|
||||
}
|
||||
pipeline.FireUserEventTriggered(HttpExpectationFailedEvent.Default);
|
||||
return TooLarge.RetainedDuplicate();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override object NewContinueResponse(IHttpMessage start, int maxContentLength, IChannelPipeline pipeline)
|
||||
{
|
||||
object response = ContinueResponse(start, maxContentLength, pipeline);
|
||||
// we're going to respond based on the request expectation so there's no
|
||||
// need to propagate the expectation further.
|
||||
if (response != null)
|
||||
{
|
||||
start.Headers.Remove(HttpHeaderNames.Expect);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
protected override bool CloseAfterContinueResponse(object msg) =>
|
||||
this.closeOnExpectationFailed && this.IgnoreContentAfterContinueResponse(msg);
|
||||
|
||||
protected override bool IgnoreContentAfterContinueResponse(object msg) =>
|
||||
msg is IHttpResponse response && response.Status.CodeClass.Equals(HttpStatusClass.ClientError);
|
||||
|
||||
protected override IFullHttpMessage BeginAggregation(IHttpMessage start, IByteBuffer content)
|
||||
{
|
||||
Debug.Assert(!(start is IFullHttpMessage));
|
||||
|
||||
HttpUtil.SetTransferEncodingChunked(start, false);
|
||||
|
||||
if (start is IHttpRequest request)
|
||||
{
|
||||
return new AggregatedFullHttpRequest(request, content, null);
|
||||
}
|
||||
else if (start is IHttpResponse response)
|
||||
{
|
||||
return new AggregatedFullHttpResponse(response, content, null);
|
||||
}
|
||||
|
||||
throw new CodecException($"Invalid type {StringUtil.SimpleClassName(start)} expecting {nameof(IHttpRequest)} or {nameof(IHttpResponse)}");
|
||||
}
|
||||
|
||||
protected override void Aggregate(IFullHttpMessage aggregated, IHttpContent content)
|
||||
{
|
||||
if (content is ILastHttpContent httpContent)
|
||||
{
|
||||
// Merge trailing headers into the message.
|
||||
((AggregatedFullHttpMessage)aggregated).TrailingHeaders = httpContent.TrailingHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void FinishAggregation(IFullHttpMessage aggregated)
|
||||
{
|
||||
// Set the 'Content-Length' header. If one isn't already set.
|
||||
// This is important as HEAD responses will use a 'Content-Length' header which
|
||||
// does not match the actual body, but the number of bytes that would be
|
||||
// transmitted if a GET would have been used.
|
||||
//
|
||||
// See rfc2616 14.13 Content-Length
|
||||
if (!HttpUtil.IsContentLengthSet(aggregated))
|
||||
{
|
||||
aggregated.Headers.Set(
|
||||
HttpHeaderNames.ContentLength,
|
||||
new AsciiString(aggregated.Content.ReadableBytes.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void HandleOversizedMessage(IChannelHandlerContext ctx, IHttpMessage oversized)
|
||||
{
|
||||
if (oversized is IHttpRequest)
|
||||
{
|
||||
// send back a 413 and close the connection
|
||||
|
||||
// If the client started to send data already, close because it's impossible to recover.
|
||||
// If keep-alive is off and 'Expect: 100-continue' is missing, no need to leave the connection open.
|
||||
if (oversized is IFullHttpMessage ||
|
||||
!HttpUtil.Is100ContinueExpected(oversized) && !HttpUtil.IsKeepAlive(oversized))
|
||||
{
|
||||
ctx.WriteAndFlushAsync(TooLargeClose.RetainedDuplicate()).ContinueWith((t, s) =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
Logger.Debug("Failed to send a 413 Request Entity Too Large.", t.Exception);
|
||||
}
|
||||
((IChannelHandlerContext)s).CloseAsync();
|
||||
},
|
||||
ctx,
|
||||
TaskContinuationOptions.ExecuteSynchronously);
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx.WriteAndFlushAsync(TooLarge.RetainedDuplicate()).ContinueWith((t, s) =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
Logger.Debug("Failed to send a 413 Request Entity Too Large.", t.Exception);
|
||||
((IChannelHandlerContext)s).CloseAsync();
|
||||
}
|
||||
},
|
||||
ctx,
|
||||
TaskContinuationOptions.ExecuteSynchronously);
|
||||
}
|
||||
// If an oversized request was handled properly and the connection is still alive
|
||||
// (i.e. rejected 100-continue). the decoder should prepare to handle a new message.
|
||||
var decoder = ctx.Channel.Pipeline.Get<HttpObjectDecoder>();
|
||||
decoder?.Reset();
|
||||
}
|
||||
else if (oversized is IHttpResponse)
|
||||
{
|
||||
ctx.CloseAsync();
|
||||
throw new TooLongFrameException($"Response entity too large: {oversized}");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid type {StringUtil.SimpleClassName(oversized)}, expecting {nameof(IHttpRequest)} or {nameof(IHttpResponse)}");
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AggregatedFullHttpMessage : IFullHttpMessage
|
||||
{
|
||||
protected readonly IHttpMessage Message;
|
||||
readonly IByteBuffer content;
|
||||
HttpHeaders trailingHeaders;
|
||||
|
||||
protected AggregatedFullHttpMessage(IHttpMessage message, IByteBuffer content, HttpHeaders trailingHeaders)
|
||||
{
|
||||
this.Message = message;
|
||||
this.content = content;
|
||||
this.trailingHeaders = trailingHeaders;
|
||||
}
|
||||
|
||||
public HttpHeaders TrailingHeaders
|
||||
{
|
||||
get
|
||||
{
|
||||
HttpHeaders headers = this.trailingHeaders;
|
||||
return headers ?? EmptyHttpHeaders.Default;
|
||||
}
|
||||
internal set => this.trailingHeaders = value;
|
||||
}
|
||||
|
||||
public HttpVersion ProtocolVersion => this.Message.ProtocolVersion;
|
||||
|
||||
public IHttpMessage SetProtocolVersion(HttpVersion version)
|
||||
{
|
||||
this.Message.SetProtocolVersion(version);
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpHeaders Headers => this.Message.Headers;
|
||||
|
||||
public DecoderResult Result
|
||||
{
|
||||
get => this.Message.Result;
|
||||
set => this.Message.Result = value;
|
||||
}
|
||||
|
||||
public IByteBuffer Content => this.content;
|
||||
|
||||
public int ReferenceCount => this.content.ReferenceCount;
|
||||
|
||||
public IReferenceCounted Retain()
|
||||
{
|
||||
this.content.Retain();
|
||||
return this;
|
||||
}
|
||||
|
||||
public IReferenceCounted Retain(int increment)
|
||||
{
|
||||
this.content.Retain(increment);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IReferenceCounted Touch()
|
||||
{
|
||||
this.content.Touch();
|
||||
return this;
|
||||
}
|
||||
|
||||
public IReferenceCounted Touch(object hint)
|
||||
{
|
||||
this.content.Touch(hint);
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Release() => this.content.Release();
|
||||
|
||||
public bool Release(int decrement) => this.content.Release(decrement);
|
||||
|
||||
public abstract IByteBufferHolder Copy();
|
||||
|
||||
public abstract IByteBufferHolder Duplicate();
|
||||
|
||||
public abstract IByteBufferHolder RetainedDuplicate();
|
||||
|
||||
public abstract IByteBufferHolder Replace(IByteBuffer content);
|
||||
}
|
||||
|
||||
sealed class AggregatedFullHttpRequest : AggregatedFullHttpMessage, IFullHttpRequest
|
||||
{
|
||||
internal AggregatedFullHttpRequest(IHttpRequest message, IByteBuffer content, HttpHeaders trailingHeaders)
|
||||
: base(message, content, trailingHeaders)
|
||||
{
|
||||
}
|
||||
|
||||
public override IByteBufferHolder Copy() => this.Replace(this.Content.Copy());
|
||||
|
||||
public override IByteBufferHolder Duplicate() => this.Replace(this.Content.Duplicate());
|
||||
|
||||
public override IByteBufferHolder RetainedDuplicate() => this.Replace(this.Content.RetainedDuplicate());
|
||||
|
||||
public override IByteBufferHolder Replace(IByteBuffer content)
|
||||
{
|
||||
var dup = new DefaultFullHttpRequest(this.ProtocolVersion, this.Method, this.Uri, content,
|
||||
this.Headers.Copy(), this.TrailingHeaders.Copy());
|
||||
dup.Result = this.Result;
|
||||
return dup;
|
||||
}
|
||||
|
||||
public HttpMethod Method => ((IHttpRequest)this.Message).Method;
|
||||
|
||||
public IHttpRequest SetMethod(HttpMethod method)
|
||||
{
|
||||
((IHttpRequest)this.Message).SetMethod(method);
|
||||
return this;
|
||||
}
|
||||
|
||||
public string Uri => ((IHttpRequest)this.Message).Uri;
|
||||
|
||||
public IHttpRequest SetUri(string uri)
|
||||
{
|
||||
((IHttpRequest)this.Message).SetUri(uri);
|
||||
return this;
|
||||
}
|
||||
|
||||
public override string ToString() => HttpMessageUtil.AppendFullRequest(new StringBuilder(256), this).ToString();
|
||||
}
|
||||
|
||||
sealed class AggregatedFullHttpResponse : AggregatedFullHttpMessage, IFullHttpResponse
|
||||
{
|
||||
public AggregatedFullHttpResponse(IHttpResponse message, IByteBuffer content, HttpHeaders trailingHeaders)
|
||||
: base(message, content, trailingHeaders)
|
||||
{
|
||||
}
|
||||
|
||||
public override IByteBufferHolder Copy() => this.Replace(this.Content.Copy());
|
||||
|
||||
public override IByteBufferHolder Duplicate() => this.Replace(this.Content.Duplicate());
|
||||
|
||||
public override IByteBufferHolder RetainedDuplicate() => this.Replace(this.Content.RetainedDuplicate());
|
||||
|
||||
public override IByteBufferHolder Replace(IByteBuffer content)
|
||||
{
|
||||
var dup = new DefaultFullHttpResponse(this.ProtocolVersion, this.Status, content,
|
||||
this.Headers.Copy(), this.TrailingHeaders.Copy());
|
||||
dup.Result = this.Result;
|
||||
return dup;
|
||||
}
|
||||
|
||||
public HttpResponseStatus Status => ((IHttpResponse)this.Message).Status;
|
||||
|
||||
public IHttpResponse SetStatus(HttpResponseStatus status)
|
||||
{
|
||||
((IHttpResponse)this.Message).SetStatus(status);
|
||||
return this;
|
||||
}
|
||||
|
||||
public override string ToString() => HttpMessageUtil.AppendFullResponse(new StringBuilder(256), this).ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,898 @@
|
|||
// 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
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Runtime.CompilerServices;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Internal;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public abstract class HttpObjectDecoder : ByteToMessageDecoder
|
||||
{
|
||||
readonly int maxChunkSize;
|
||||
readonly bool chunkedSupported;
|
||||
protected readonly bool ValidateHeaders;
|
||||
readonly HeaderParser headerParser;
|
||||
readonly LineParser lineParser;
|
||||
|
||||
IHttpMessage message;
|
||||
long chunkSize;
|
||||
long contentLength = long.MinValue;
|
||||
volatile bool resetRequested;
|
||||
|
||||
// These will be updated by splitHeader(...)
|
||||
AsciiString name;
|
||||
AsciiString value;
|
||||
|
||||
ILastHttpContent trailer;
|
||||
|
||||
enum State
|
||||
{
|
||||
SkipControlChars,
|
||||
ReadInitial,
|
||||
ReadHeader,
|
||||
ReadVariableLengthContent,
|
||||
ReadFixedLengthContent,
|
||||
ReadChunkSize,
|
||||
ReadChunkedContent,
|
||||
ReadChunkDelimiter,
|
||||
ReadChunkFooter,
|
||||
BadMessage,
|
||||
Upgraded
|
||||
}
|
||||
|
||||
State currentState = State.SkipControlChars;
|
||||
|
||||
protected HttpObjectDecoder() : this(4096, 8192, 8192, true)
|
||||
{
|
||||
}
|
||||
|
||||
protected HttpObjectDecoder(
|
||||
int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, bool chunkedSupported)
|
||||
: this(maxInitialLineLength, maxHeaderSize, maxChunkSize, chunkedSupported, true)
|
||||
{
|
||||
}
|
||||
|
||||
protected HttpObjectDecoder(
|
||||
int maxInitialLineLength, int maxHeaderSize, int maxChunkSize,
|
||||
bool chunkedSupported, bool validateHeaders)
|
||||
: this(maxInitialLineLength, maxHeaderSize, maxChunkSize, chunkedSupported, validateHeaders, 128)
|
||||
{
|
||||
}
|
||||
|
||||
protected HttpObjectDecoder(
|
||||
int maxInitialLineLength, int maxHeaderSize, int maxChunkSize,
|
||||
bool chunkedSupported, bool validateHeaders, int initialBufferSize)
|
||||
{
|
||||
Contract.Requires(maxInitialLineLength > 0);
|
||||
Contract.Requires(maxHeaderSize > 0);
|
||||
Contract.Requires(maxChunkSize > 0);
|
||||
|
||||
var seq = new AppendableCharSequence(initialBufferSize);
|
||||
this.lineParser = new LineParser(seq, maxInitialLineLength);
|
||||
this.headerParser = new HeaderParser(seq, maxHeaderSize);
|
||||
this.maxChunkSize = maxChunkSize;
|
||||
this.chunkedSupported = chunkedSupported;
|
||||
this.ValidateHeaders = validateHeaders;
|
||||
}
|
||||
|
||||
protected override void Decode(IChannelHandlerContext context, IByteBuffer buffer, List<object> output)
|
||||
{
|
||||
if (this.resetRequested)
|
||||
{
|
||||
this.ResetNow();
|
||||
}
|
||||
|
||||
switch (this.currentState)
|
||||
{
|
||||
case State.SkipControlChars:
|
||||
{
|
||||
if (!SkipControlCharacters(buffer))
|
||||
{
|
||||
return;
|
||||
}
|
||||
this.currentState = State.ReadInitial;
|
||||
goto case State.ReadInitial; // Fall through
|
||||
}
|
||||
case State.ReadInitial:
|
||||
{
|
||||
try
|
||||
{
|
||||
AppendableCharSequence line = this.lineParser.Parse(buffer);
|
||||
if (line == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
AsciiString[] initialLine = SplitInitialLine(line);
|
||||
if (initialLine.Length < 3)
|
||||
{
|
||||
// Invalid initial line - ignore.
|
||||
this.currentState = State.SkipControlChars;
|
||||
return;
|
||||
}
|
||||
|
||||
this.message = this.CreateMessage(initialLine);
|
||||
this.currentState = State.ReadHeader;
|
||||
goto case State.ReadHeader; // Fall through
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
output.Add(this.InvalidMessage(buffer, e));
|
||||
return;
|
||||
}
|
||||
}
|
||||
case State.ReadHeader:
|
||||
{
|
||||
try
|
||||
{
|
||||
State? nextState = this.ReadHeaders(buffer);
|
||||
if (nextState == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this.currentState = nextState.Value;
|
||||
switch (nextState.Value)
|
||||
{
|
||||
case State.SkipControlChars:
|
||||
{
|
||||
// fast-path
|
||||
// No content is expected.
|
||||
output.Add(this.message);
|
||||
output.Add(EmptyLastHttpContent.Default);
|
||||
this.ResetNow();
|
||||
return;
|
||||
}
|
||||
case State.ReadChunkSize:
|
||||
{
|
||||
if (!this.chunkedSupported)
|
||||
{
|
||||
throw new ArgumentException("Chunked messages not supported");
|
||||
}
|
||||
// Chunked encoding - generate HttpMessage first. HttpChunks will follow.
|
||||
output.Add(this.message);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
{
|
||||
// <a href="https://tools.ietf.org/html/rfc7230#section-3.3.3">RFC 7230, 3.3.3</a> states that if a
|
||||
// request does not have either a transfer-encoding or a content-length header then the message body
|
||||
// length is 0. However for a response the body length is the number of octets received prior to the
|
||||
// server closing the connection. So we treat this as variable length chunked encoding.
|
||||
long length = this.ContentLength();
|
||||
if (length == 0 || length == -1 && this.IsDecodingRequest())
|
||||
{
|
||||
output.Add(this.message);
|
||||
output.Add(EmptyLastHttpContent.Default);
|
||||
this.ResetNow();
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Assert(nextState.Value == State.ReadFixedLengthContent
|
||||
|| nextState.Value == State.ReadVariableLengthContent);
|
||||
|
||||
output.Add(this.message);
|
||||
|
||||
if (nextState == State.ReadFixedLengthContent)
|
||||
{
|
||||
// chunkSize will be decreased as the READ_FIXED_LENGTH_CONTENT state reads data chunk by chunk.
|
||||
this.chunkSize = length;
|
||||
}
|
||||
|
||||
// We return here, this forces decode to be called again where we will decode the content
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
output.Add(this.InvalidMessage(buffer, exception));
|
||||
return;
|
||||
}
|
||||
}
|
||||
case State.ReadVariableLengthContent:
|
||||
{
|
||||
// Keep reading data as a chunk until the end of connection is reached.
|
||||
int toRead = Math.Min(buffer.ReadableBytes, this.maxChunkSize);
|
||||
if (toRead > 0)
|
||||
{
|
||||
IByteBuffer content = buffer.ReadRetainedSlice(toRead);
|
||||
output.Add(new DefaultHttpContent(content));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case State.ReadFixedLengthContent:
|
||||
{
|
||||
int readLimit = buffer.ReadableBytes;
|
||||
|
||||
// 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 = Math.Min(readLimit, this.maxChunkSize);
|
||||
if (toRead > this.chunkSize)
|
||||
{
|
||||
toRead = (int)this.chunkSize;
|
||||
}
|
||||
IByteBuffer content = buffer.ReadRetainedSlice(toRead);
|
||||
this.chunkSize -= toRead;
|
||||
|
||||
if (this.chunkSize == 0)
|
||||
{
|
||||
// Read all content.
|
||||
output.Add(new DefaultLastHttpContent(content, this.ValidateHeaders));
|
||||
this.ResetNow();
|
||||
}
|
||||
else
|
||||
{
|
||||
output.Add(new DefaultHttpContent(content));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// everything else after this point takes care of reading chunked content. basically, read chunk size,
|
||||
// read chunk, read and ignore the CRLF and repeat until 0
|
||||
case State.ReadChunkSize:
|
||||
{
|
||||
try
|
||||
{
|
||||
AppendableCharSequence line = this.lineParser.Parse(buffer);
|
||||
if (line == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
int size = GetChunkSize(line.ToAsciiString());
|
||||
this.chunkSize = size;
|
||||
if (size == 0)
|
||||
{
|
||||
this.currentState = State.ReadChunkFooter;
|
||||
return;
|
||||
}
|
||||
this.currentState = State.ReadChunkedContent;
|
||||
goto case State.ReadChunkedContent; // fall-through
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
output.Add(this.InvalidChunk(buffer, e));
|
||||
return;
|
||||
}
|
||||
}
|
||||
case State.ReadChunkedContent:
|
||||
{
|
||||
Debug.Assert(this.chunkSize <= int.MaxValue);
|
||||
|
||||
int toRead = Math.Min((int)this.chunkSize, this.maxChunkSize);
|
||||
toRead = Math.Min(toRead, buffer.ReadableBytes);
|
||||
if (toRead == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
IHttpContent chunk = new DefaultHttpContent(buffer.ReadRetainedSlice(toRead));
|
||||
this.chunkSize -= toRead;
|
||||
|
||||
output.Add(chunk);
|
||||
|
||||
if (this.chunkSize != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this.currentState = State.ReadChunkDelimiter;
|
||||
goto case State.ReadChunkDelimiter; // fall-through
|
||||
}
|
||||
case State.ReadChunkDelimiter:
|
||||
{
|
||||
int wIdx = buffer.WriterIndex;
|
||||
int rIdx = buffer.ReaderIndex;
|
||||
while (wIdx > rIdx)
|
||||
{
|
||||
byte next = buffer.GetByte(rIdx++);
|
||||
if (next == HttpConstants.LineFeed)
|
||||
{
|
||||
this.currentState = State.ReadChunkSize;
|
||||
break;
|
||||
}
|
||||
}
|
||||
buffer.SetReaderIndex(rIdx);
|
||||
return;
|
||||
}
|
||||
case State.ReadChunkFooter:
|
||||
{
|
||||
try
|
||||
{
|
||||
ILastHttpContent lastTrialer = this.ReadTrailingHeaders(buffer);
|
||||
if (lastTrialer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
output.Add(lastTrialer);
|
||||
this.ResetNow();
|
||||
return;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
output.Add(this.InvalidChunk(buffer, exception));
|
||||
return;
|
||||
}
|
||||
}
|
||||
case State.BadMessage:
|
||||
{
|
||||
// Keep discarding until disconnection.
|
||||
buffer.SkipBytes(buffer.ReadableBytes);
|
||||
break;
|
||||
}
|
||||
case State.Upgraded:
|
||||
{
|
||||
int readableBytes = buffer.ReadableBytes;
|
||||
if (readableBytes > 0)
|
||||
{
|
||||
// Keep on consuming as otherwise we may trigger an DecoderException,
|
||||
// other handler will replace this codec with the upgraded protocol codec to
|
||||
// take the traffic over at some point then.
|
||||
// See https://github.com/netty/netty/issues/2173
|
||||
output.Add(buffer.ReadBytes(readableBytes));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DecodeLast(IChannelHandlerContext context, IByteBuffer input, List<object> output)
|
||||
{
|
||||
base.DecodeLast(context, input, output);
|
||||
|
||||
if (this.resetRequested)
|
||||
{
|
||||
// If a reset was requested by decodeLast() we need to do it now otherwise we may produce a
|
||||
// LastHttpContent while there was already one.
|
||||
this.ResetNow();
|
||||
}
|
||||
|
||||
// Handle the last unfinished message.
|
||||
if (this.message != null)
|
||||
{
|
||||
bool chunked = HttpUtil.IsTransferEncodingChunked(this.message);
|
||||
if (this.currentState == State.ReadVariableLengthContent
|
||||
&& !input.IsReadable() && !chunked)
|
||||
{
|
||||
// End of connection.
|
||||
output.Add(EmptyLastHttpContent.Default);
|
||||
this.ResetNow();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentState == State.ReadHeader)
|
||||
{
|
||||
// If we are still in the state of reading headers we need to create a new invalid message that
|
||||
// signals that the connection was closed before we received the headers.
|
||||
output.Add(this.InvalidMessage(Unpooled.Empty,
|
||||
new PrematureChannelClosureException("Connection closed before received headers")));
|
||||
this.ResetNow();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the closure of the connection signifies the end of the content.
|
||||
bool prematureClosure;
|
||||
if (this.IsDecodingRequest() || chunked)
|
||||
{
|
||||
// The last request did not wait for a response.
|
||||
prematureClosure = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Compare the length of the received content and the 'Content-Length' header.
|
||||
// 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.
|
||||
prematureClosure = this.ContentLength() > 0;
|
||||
}
|
||||
|
||||
if (!prematureClosure)
|
||||
{
|
||||
output.Add(EmptyLastHttpContent.Default);
|
||||
}
|
||||
this.ResetNow();
|
||||
}
|
||||
}
|
||||
|
||||
public override void UserEventTriggered(IChannelHandlerContext context, object evt)
|
||||
{
|
||||
if (evt is HttpExpectationFailedEvent)
|
||||
{
|
||||
switch (this.currentState)
|
||||
{
|
||||
case State.ReadFixedLengthContent:
|
||||
case State.ReadVariableLengthContent:
|
||||
case State.ReadChunkSize:
|
||||
this.Reset();
|
||||
break;
|
||||
}
|
||||
}
|
||||
base.UserEventTriggered(context, evt);
|
||||
}
|
||||
|
||||
protected virtual bool IsContentAlwaysEmpty(IHttpMessage msg)
|
||||
{
|
||||
if (msg is IHttpResponse res)
|
||||
{
|
||||
int code = res.Status.Code;
|
||||
|
||||
// Correctly handle return codes of 1xx.
|
||||
//
|
||||
// See:
|
||||
// - http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html Section 4.4
|
||||
// - https://github.com/netty/netty/issues/222
|
||||
if (code >= 100 && code < 200)
|
||||
{
|
||||
// One exception: Hixie 76 websocket handshake response
|
||||
return !(code == 101 && !res.Headers.Contains(HttpHeaderNames.SecWebsocketAccept)
|
||||
&& res.Headers.Contains(HttpHeaderNames.Upgrade, HttpHeaderValues.Websocket, true));
|
||||
}
|
||||
switch (code)
|
||||
{
|
||||
case 204:
|
||||
case 304:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected bool IsSwitchingToNonHttp1Protocol(IHttpResponse msg)
|
||||
{
|
||||
if (msg.Status.Code != HttpResponseStatus.SwitchingProtocols.Code)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !msg.Headers.TryGet(HttpHeaderNames.Upgrade, out ICharSequence newProtocol)
|
||||
|| !AsciiString.Contains(newProtocol, HttpVersion.Http10String)
|
||||
&& !AsciiString.Contains(newProtocol, HttpVersion.Http11String);
|
||||
}
|
||||
|
||||
// Resets the state of the decoder so that it is ready to decode a new message.
|
||||
// This method is useful for handling a rejected request with {@code Expect: 100-continue} header.
|
||||
public void Reset() => this.resetRequested = true;
|
||||
|
||||
void ResetNow()
|
||||
{
|
||||
IHttpMessage msg = this.message;
|
||||
this.message = null;
|
||||
this.name = null;
|
||||
this.value = null;
|
||||
this.contentLength = long.MinValue;
|
||||
this.lineParser.Reset();
|
||||
this.headerParser.Reset();
|
||||
this.trailer = null;
|
||||
if (!this.IsDecodingRequest())
|
||||
{
|
||||
if (msg is IHttpResponse res && this.IsSwitchingToNonHttp1Protocol(res))
|
||||
{
|
||||
this.currentState = State.Upgraded;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.resetRequested = false;
|
||||
this.currentState = State.SkipControlChars;
|
||||
}
|
||||
|
||||
IHttpMessage InvalidMessage(IByteBuffer buf, Exception cause)
|
||||
{
|
||||
this.currentState = State.BadMessage;
|
||||
|
||||
// Advance the readerIndex so that ByteToMessageDecoder does not complain
|
||||
// when we produced an invalid message without consuming anything.
|
||||
buf.SkipBytes(buf.ReadableBytes);
|
||||
|
||||
if (this.message != null)
|
||||
{
|
||||
this.message.Result = DecoderResult.Failure(cause);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.message = this.CreateInvalidMessage();
|
||||
this.message.Result = DecoderResult.Failure(cause);
|
||||
}
|
||||
|
||||
IHttpMessage ret = this.message;
|
||||
this.message = null;
|
||||
return ret;
|
||||
}
|
||||
|
||||
IHttpContent InvalidChunk(IByteBuffer buf, Exception cause)
|
||||
{
|
||||
this.currentState = State.BadMessage;
|
||||
|
||||
// Advance the readerIndex so that ByteToMessageDecoder does not complain
|
||||
// when we produced an invalid message without consuming anything.
|
||||
buf.SkipBytes(buf.ReadableBytes);
|
||||
|
||||
IHttpContent chunk = new DefaultLastHttpContent(Unpooled.Empty);
|
||||
chunk.Result = DecoderResult.Failure(cause);
|
||||
this.message = null;
|
||||
this.trailer = null;
|
||||
return chunk;
|
||||
}
|
||||
|
||||
static bool SkipControlCharacters(IByteBuffer buffer)
|
||||
{
|
||||
bool skiped = false;
|
||||
int wIdx = buffer.WriterIndex;
|
||||
int rIdx = buffer.ReaderIndex;
|
||||
while (wIdx > rIdx)
|
||||
{
|
||||
byte c = buffer.GetByte(rIdx++);
|
||||
if (!CharUtil.IsISOControl(c) && !IsWhiteSpace(c))
|
||||
{
|
||||
rIdx--;
|
||||
skiped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
buffer.SetReaderIndex(rIdx);
|
||||
return skiped;
|
||||
}
|
||||
|
||||
State? ReadHeaders(IByteBuffer buffer)
|
||||
{
|
||||
IHttpMessage httpMessage = this.message;
|
||||
HttpHeaders headers = httpMessage.Headers;
|
||||
|
||||
AppendableCharSequence line = this.headerParser.Parse(buffer);
|
||||
if (line == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// ReSharper disable once ConvertIfDoToWhile
|
||||
if (line.Count > 0)
|
||||
{
|
||||
do
|
||||
{
|
||||
byte firstChar = line.Bytes[0];
|
||||
if (this.name != null && (firstChar == ' ' || firstChar == '\t'))
|
||||
{
|
||||
ICharSequence trimmedLine = CharUtil.Trim(line);
|
||||
this.value = new AsciiString($"{this.value} {trimmedLine}");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this.name != null)
|
||||
{
|
||||
headers.Add(this.name, this.value);
|
||||
}
|
||||
this.SplitHeader(line);
|
||||
}
|
||||
|
||||
line = this.headerParser.Parse(buffer);
|
||||
if (line == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
} while (line.Count > 0);
|
||||
}
|
||||
|
||||
// Add the last header.
|
||||
if (this.name != null)
|
||||
{
|
||||
headers.Add(this.name, this.value);
|
||||
}
|
||||
// reset name and value fields
|
||||
this.name = null;
|
||||
this.value = null;
|
||||
|
||||
State nextState;
|
||||
|
||||
if (this.IsContentAlwaysEmpty(httpMessage))
|
||||
{
|
||||
HttpUtil.SetTransferEncodingChunked(httpMessage, false);
|
||||
nextState = State.SkipControlChars;
|
||||
}
|
||||
else if (HttpUtil.IsTransferEncodingChunked(httpMessage))
|
||||
{
|
||||
nextState = State.ReadChunkSize;
|
||||
}
|
||||
else if (this.ContentLength() >= 0)
|
||||
{
|
||||
nextState = State.ReadFixedLengthContent;
|
||||
}
|
||||
else
|
||||
{
|
||||
nextState = State.ReadVariableLengthContent;
|
||||
}
|
||||
return nextState;
|
||||
}
|
||||
|
||||
long ContentLength()
|
||||
{
|
||||
if (this.contentLength == long.MinValue)
|
||||
{
|
||||
this.contentLength = HttpUtil.GetContentLength(this.message, -1L);
|
||||
}
|
||||
return this.contentLength;
|
||||
}
|
||||
|
||||
ILastHttpContent ReadTrailingHeaders(IByteBuffer buffer)
|
||||
{
|
||||
AppendableCharSequence line = this.headerParser.Parse(buffer);
|
||||
if (line == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
AsciiString lastHeader = null;
|
||||
if (line.Count > 0)
|
||||
{
|
||||
ILastHttpContent trailingHeaders = this.trailer;
|
||||
if (trailingHeaders == null)
|
||||
{
|
||||
trailingHeaders = new DefaultLastHttpContent(Unpooled.Empty, this.ValidateHeaders);
|
||||
this.trailer = trailingHeaders;
|
||||
}
|
||||
do
|
||||
{
|
||||
byte firstChar = line.Bytes[0];
|
||||
if (lastHeader != null && (firstChar == ' ' || firstChar == '\t'))
|
||||
{
|
||||
IList<ICharSequence> current = trailingHeaders.TrailingHeaders.GetAll(lastHeader);
|
||||
if (current.Count > 0)
|
||||
{
|
||||
int lastPos = current.Count - 1;
|
||||
ICharSequence lineTrimmed = CharUtil.Trim(line);
|
||||
current[lastPos] = new AsciiString($"{current[lastPos]} {lineTrimmed}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.SplitHeader(line);
|
||||
AsciiString headerName = this.name;
|
||||
if (!HttpHeaderNames.ContentLength.ContentEqualsIgnoreCase(headerName)
|
||||
&& !HttpHeaderNames.TransferEncoding.ContentEqualsIgnoreCase(headerName)
|
||||
&& !HttpHeaderNames.Trailer.ContentEqualsIgnoreCase(headerName))
|
||||
{
|
||||
trailingHeaders.TrailingHeaders.Add(headerName, this.value);
|
||||
}
|
||||
lastHeader = this.name;
|
||||
// reset name and value fields
|
||||
this.name = null;
|
||||
this.value = null;
|
||||
}
|
||||
|
||||
line = this.headerParser.Parse(buffer);
|
||||
if (line == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
} while (line.Count > 0);
|
||||
|
||||
this.trailer = null;
|
||||
return trailingHeaders;
|
||||
}
|
||||
|
||||
return EmptyLastHttpContent.Default;
|
||||
}
|
||||
|
||||
protected abstract bool IsDecodingRequest();
|
||||
|
||||
protected abstract IHttpMessage CreateMessage(AsciiString[] initialLine);
|
||||
|
||||
protected abstract IHttpMessage CreateInvalidMessage();
|
||||
|
||||
static int GetChunkSize(AsciiString hex)
|
||||
{
|
||||
hex = hex.Trim();
|
||||
for (int i = hex.Offset; i < hex.Count; i++)
|
||||
{
|
||||
byte c = hex.Array[i];
|
||||
if (c == ';' || IsWhiteSpace(c) || CharUtil.IsISOControl(c))
|
||||
{
|
||||
hex = (AsciiString)hex.SubSequence(0, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return hex.ParseInt(16);
|
||||
}
|
||||
|
||||
static AsciiString[] SplitInitialLine(AppendableCharSequence sb)
|
||||
{
|
||||
byte[] chars = sb.Bytes;
|
||||
int length = sb.Count;
|
||||
|
||||
int aStart = FindNonWhitespace(chars, 0, length);
|
||||
int aEnd = FindWhitespace(chars, aStart, length);
|
||||
|
||||
int bStart = FindNonWhitespace(chars, aEnd, length);
|
||||
int bEnd = FindWhitespace(chars, bStart, length);
|
||||
|
||||
int cStart = FindNonWhitespace(chars, bEnd, length);
|
||||
int cEnd = FindEndOfString(chars, length);
|
||||
|
||||
return new[]
|
||||
{
|
||||
sb.SubStringUnsafe(aStart, aEnd),
|
||||
sb.SubStringUnsafe(bStart, bEnd),
|
||||
cStart < cEnd ? sb.SubStringUnsafe(cStart, cEnd) : AsciiString.Empty
|
||||
};
|
||||
}
|
||||
|
||||
void SplitHeader(AppendableCharSequence sb)
|
||||
{
|
||||
byte[] chars = sb.Bytes;
|
||||
int length = sb.Count;
|
||||
int nameEnd;
|
||||
int colonEnd;
|
||||
|
||||
int nameStart = FindNonWhitespace(chars, 0, length);
|
||||
for (nameEnd = nameStart; nameEnd < length; nameEnd++)
|
||||
{
|
||||
byte ch = chars[nameEnd];
|
||||
if (ch == ':' || IsWhiteSpace(ch))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (colonEnd = nameEnd; colonEnd < length; colonEnd++)
|
||||
{
|
||||
if (chars[colonEnd] == ':')
|
||||
{
|
||||
colonEnd++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.name = sb.SubStringUnsafe(nameStart, nameEnd);
|
||||
int valueStart = FindNonWhitespace(chars, colonEnd, length);
|
||||
if (valueStart == length)
|
||||
{
|
||||
this.value = AsciiString.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
int valueEnd = FindEndOfString(chars, length);
|
||||
this.value = sb.SubStringUnsafe(valueStart, valueEnd);
|
||||
}
|
||||
}
|
||||
|
||||
static int FindNonWhitespace(byte[] sb, int offset, int length)
|
||||
{
|
||||
for (int result = offset; result < length; ++result)
|
||||
{
|
||||
if (!IsWhiteSpace(sb[result]))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
static int FindWhitespace(byte[] sb, int offset, int length)
|
||||
{
|
||||
for (int result = offset; result < length; ++result)
|
||||
{
|
||||
if (IsWhiteSpace(sb[result]))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
static int FindEndOfString(byte[] sb, int length)
|
||||
{
|
||||
for (int result = length - 1; result > 0; --result)
|
||||
{
|
||||
if (!IsWhiteSpace(sb[result]))
|
||||
{
|
||||
return result + 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
class HeaderParser : IByteProcessor
|
||||
{
|
||||
readonly AppendableCharSequence seq;
|
||||
readonly int maxLength;
|
||||
int size;
|
||||
|
||||
internal HeaderParser(AppendableCharSequence seq, int maxLength)
|
||||
{
|
||||
this.seq = seq;
|
||||
this.maxLength = maxLength;
|
||||
}
|
||||
|
||||
public virtual AppendableCharSequence Parse(IByteBuffer buffer)
|
||||
{
|
||||
int oldSize = this.size;
|
||||
this.seq.Reset();
|
||||
int i = buffer.ForEachByte(this);
|
||||
if (i == -1)
|
||||
{
|
||||
this.size = oldSize;
|
||||
return null;
|
||||
}
|
||||
buffer.SetReaderIndex(i + 1);
|
||||
return this.seq;
|
||||
}
|
||||
|
||||
public void Reset() => this.size = 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool Process(byte value)
|
||||
{
|
||||
if (value == HttpConstants.CarriageReturn)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (value == HttpConstants.LineFeed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (++this.size > this.maxLength)
|
||||
{
|
||||
// TODO: Respond with Bad Request and discard the traffic
|
||||
// or close the connection.
|
||||
// No need to notify the upstream handlers - just log.
|
||||
// If decoding a response, just throw an exception.
|
||||
ThrowTooLongFrameException(this, this.maxLength);
|
||||
}
|
||||
|
||||
this.seq.Append(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ThrowTooLongFrameException(HeaderParser parser, int length)
|
||||
{
|
||||
throw GetTooLongFrameException();
|
||||
|
||||
TooLongFrameException GetTooLongFrameException()
|
||||
{
|
||||
return new TooLongFrameException(parser.NewExceptionMessage(length));
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual string NewExceptionMessage(int length) => $"HTTP header is larger than {length} bytes.";
|
||||
}
|
||||
|
||||
sealed class LineParser : HeaderParser
|
||||
{
|
||||
internal LineParser(AppendableCharSequence seq, int maxLength)
|
||||
: base(seq, maxLength)
|
||||
{
|
||||
}
|
||||
|
||||
public override AppendableCharSequence Parse(IByteBuffer buffer)
|
||||
{
|
||||
this.Reset();
|
||||
return base.Parse(buffer);
|
||||
}
|
||||
|
||||
protected override string NewExceptionMessage(int maxLength) => $"An HTTP line is larger than {maxLength} bytes.";
|
||||
}
|
||||
|
||||
// Similar to char.IsWhiteSpace for ascii
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static bool IsWhiteSpace(byte c)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case HttpConstants.HorizontalSpace:
|
||||
case HttpConstants.HorizontalTab:
|
||||
case HttpConstants.CarriageReturn:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,252 @@
|
|||
// 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
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public abstract class HttpObjectEncoder<T> : MessageToMessageEncoder<object> where T : IHttpMessage
|
||||
{
|
||||
const float HeadersWeightNew = 1 / 5f;
|
||||
const float HeadersWeightHistorical = 1 - HeadersWeightNew;
|
||||
const float TrailersWeightNew = HeadersWeightNew;
|
||||
const float TrailersWeightHistorical = HeadersWeightHistorical;
|
||||
|
||||
const int StInit = 0;
|
||||
const int StContentNonChunk = 1;
|
||||
const int StContentChunk = 2;
|
||||
const int StContentAlwaysEmpty = 3;
|
||||
|
||||
int state = StInit;
|
||||
|
||||
// Used to calculate an exponential moving average of the encoded size of the initial line and the headers for
|
||||
// a guess for future buffer allocations.
|
||||
float headersEncodedSizeAccumulator = 256;
|
||||
|
||||
// Used to calculate an exponential moving average of the encoded size of the trailers for
|
||||
// a guess for future buffer allocations.
|
||||
float trailersEncodedSizeAccumulator = 256;
|
||||
|
||||
protected override void Encode(IChannelHandlerContext context, object message, List<object> output)
|
||||
{
|
||||
IByteBuffer buf = null;
|
||||
if (message is IHttpMessage)
|
||||
{
|
||||
if (this.state != StInit)
|
||||
{
|
||||
throw new InvalidOperationException($"unexpected message type: {StringUtil.SimpleClassName(message)}");
|
||||
}
|
||||
|
||||
var m = (T)message;
|
||||
|
||||
buf = context.Allocator.Buffer((int)this.headersEncodedSizeAccumulator);
|
||||
// Encode the message.
|
||||
this.EncodeInitialLine(buf, m);
|
||||
this.state = this.IsContentAlwaysEmpty(m) ? StContentAlwaysEmpty
|
||||
: HttpUtil.IsTransferEncodingChunked(m) ? StContentChunk : StContentNonChunk;
|
||||
|
||||
this.SanitizeHeadersBeforeEncode(m, this.state == StContentAlwaysEmpty);
|
||||
|
||||
this.EncodeHeaders(m.Headers, buf);
|
||||
buf.WriteShort(HttpConstants.CrlfShort);
|
||||
|
||||
this.headersEncodedSizeAccumulator = HeadersWeightNew * PadSizeForAccumulation(buf.ReadableBytes)
|
||||
+ HeadersWeightHistorical * this.headersEncodedSizeAccumulator;
|
||||
}
|
||||
|
||||
// Bypass the encoder in case of an empty buffer, so that the following idiom works:
|
||||
//
|
||||
// ch.write(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
|
||||
//
|
||||
// See https://github.com/netty/netty/issues/2983 for more information.
|
||||
if (message is IByteBuffer potentialEmptyBuf)
|
||||
{
|
||||
if (!potentialEmptyBuf.IsReadable())
|
||||
{
|
||||
output.Add(potentialEmptyBuf.Retain());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (message is IHttpContent || message is IByteBuffer || message is IFileRegion)
|
||||
{
|
||||
switch (this.state)
|
||||
{
|
||||
case StInit:
|
||||
throw new InvalidOperationException($"unexpected message type: {StringUtil.SimpleClassName(message)}");
|
||||
case StContentNonChunk:
|
||||
long contentLength = ContentLength(message);
|
||||
if (contentLength > 0)
|
||||
{
|
||||
if (buf != null && buf.WritableBytes >= contentLength && message is IHttpContent)
|
||||
{
|
||||
// merge into other buffer for performance reasons
|
||||
buf.WriteBytes(((IHttpContent)message).Content);
|
||||
output.Add(buf);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (buf != null)
|
||||
{
|
||||
output.Add(buf);
|
||||
}
|
||||
output.Add(EncodeAndRetain(message));
|
||||
}
|
||||
|
||||
if (message is ILastHttpContent)
|
||||
{
|
||||
this.state = StInit;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
goto case StContentAlwaysEmpty; // fall-through!
|
||||
case StContentAlwaysEmpty:
|
||||
// ReSharper disable once ConvertIfStatementToNullCoalescingExpression
|
||||
if (buf != null)
|
||||
{
|
||||
// We allocated a buffer so add it now.
|
||||
output.Add(buf);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Need to produce some output otherwise an
|
||||
// IllegalStateException will be thrown as we did not write anything
|
||||
// Its ok to just write an EMPTY_BUFFER as if there are reference count issues these will be
|
||||
// propagated as the caller of the encode(...) method will release the original
|
||||
// buffer.
|
||||
// Writing an empty buffer will not actually write anything on the wire, so if there is a user
|
||||
// error with msg it will not be visible externally
|
||||
output.Add(Unpooled.Empty);
|
||||
}
|
||||
|
||||
break;
|
||||
case StContentChunk:
|
||||
if (buf != null)
|
||||
{
|
||||
// We allocated a buffer so add it now.
|
||||
output.Add(buf);
|
||||
}
|
||||
this.EncodeChunkedContent(context, message, ContentLength(message), output);
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new EncoderException($"unexpected state {this.state}: {StringUtil.SimpleClassName(message)}");
|
||||
}
|
||||
|
||||
if (message is ILastHttpContent)
|
||||
{
|
||||
this.state = StInit;
|
||||
}
|
||||
}
|
||||
else if (buf != null)
|
||||
{
|
||||
output.Add(buf);
|
||||
}
|
||||
}
|
||||
|
||||
protected void EncodeHeaders(HttpHeaders headers, IByteBuffer buf)
|
||||
{
|
||||
foreach (HeaderEntry<AsciiString, ICharSequence> header in headers)
|
||||
{
|
||||
HttpHeadersEncoder.EncoderHeader(header.Key, header.Value, buf);
|
||||
}
|
||||
}
|
||||
|
||||
void EncodeChunkedContent(IChannelHandlerContext context, object message, long contentLength, ICollection<object> output)
|
||||
{
|
||||
if (contentLength > 0)
|
||||
{
|
||||
var lengthHex = new AsciiString(Convert.ToString(contentLength, 16), Encoding.ASCII);
|
||||
IByteBuffer buf = context.Allocator.Buffer(lengthHex.Count + 2);
|
||||
buf.WriteCharSequence(lengthHex, Encoding.ASCII);
|
||||
buf.WriteShort(HttpConstants.CrlfShort);
|
||||
output.Add(buf);
|
||||
output.Add(EncodeAndRetain(message));
|
||||
output.Add(HttpConstants.CrlfBuf.Duplicate());
|
||||
}
|
||||
|
||||
if (message is ILastHttpContent content)
|
||||
{
|
||||
HttpHeaders headers = content.TrailingHeaders;
|
||||
if (headers.IsEmpty)
|
||||
{
|
||||
output.Add(HttpConstants.ZeroCrlfCrlfBuf.Duplicate());
|
||||
}
|
||||
else
|
||||
{
|
||||
IByteBuffer buf = context.Allocator.Buffer((int)this.trailersEncodedSizeAccumulator);
|
||||
buf.WriteMedium(HttpConstants.ZeroCrlfMedium);
|
||||
this.EncodeHeaders(headers, buf);
|
||||
buf.WriteShort(HttpConstants.CrlfShort);
|
||||
this.trailersEncodedSizeAccumulator = TrailersWeightNew * PadSizeForAccumulation(buf.ReadableBytes)
|
||||
+ TrailersWeightHistorical * this.trailersEncodedSizeAccumulator;
|
||||
output.Add(buf);
|
||||
}
|
||||
}
|
||||
else if (contentLength == 0)
|
||||
{
|
||||
// Need to produce some output otherwise an
|
||||
// IllegalstateException will be thrown
|
||||
output.Add(ReferenceCountUtil.Retain(message));
|
||||
}
|
||||
}
|
||||
|
||||
// Allows to sanitize headers of the message before encoding these.
|
||||
protected virtual void SanitizeHeadersBeforeEncode(T msg, bool isAlwaysEmpty)
|
||||
{
|
||||
// noop
|
||||
}
|
||||
|
||||
protected virtual bool IsContentAlwaysEmpty(T msg) => false;
|
||||
|
||||
public override bool AcceptOutboundMessage(object msg) => msg is IHttpObject || msg is IByteBuffer || msg is IFileRegion;
|
||||
|
||||
static object EncodeAndRetain(object message)
|
||||
{
|
||||
if (message is IByteBuffer buffer)
|
||||
{
|
||||
return buffer.Retain();
|
||||
}
|
||||
if (message is IHttpContent content)
|
||||
{
|
||||
return content.Content.Retain();
|
||||
}
|
||||
if (message is IFileRegion region)
|
||||
{
|
||||
return region.Retain();
|
||||
}
|
||||
throw new InvalidOperationException($"unexpected message type: {StringUtil.SimpleClassName(message)}");
|
||||
}
|
||||
|
||||
static long ContentLength(object message)
|
||||
{
|
||||
if (message is IHttpContent content)
|
||||
{
|
||||
return content.Content.ReadableBytes;
|
||||
}
|
||||
if (message is IByteBuffer buffer)
|
||||
{
|
||||
return buffer.ReadableBytes;
|
||||
}
|
||||
if (message is IFileRegion region)
|
||||
{
|
||||
return region.Count;
|
||||
}
|
||||
throw new InvalidOperationException($"unexpected message type: {StringUtil.SimpleClassName(message)}");
|
||||
}
|
||||
|
||||
// Add some additional overhead to the buffer. The rational is that it is better to slightly over allocate and waste
|
||||
// some memory, rather than under allocate and require a resize/copy.
|
||||
// @param readableBytes The readable bytes in the buffer.
|
||||
// @return The {@code readableBytes} with some additional padding.
|
||||
static int PadSizeForAccumulation(int readableBytes) => (readableBytes << 2) / 3;
|
||||
|
||||
protected internal abstract void EncodeInitialLine(IByteBuffer buf, T message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
// 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
|
||||
{
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public class HttpRequestDecoder : HttpObjectDecoder
|
||||
{
|
||||
public HttpRequestDecoder()
|
||||
{
|
||||
}
|
||||
|
||||
public HttpRequestDecoder(
|
||||
int maxInitialLineLength, int maxHeaderSize, int maxChunkSize)
|
||||
: base(maxInitialLineLength, maxHeaderSize, maxChunkSize, true)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpRequestDecoder(
|
||||
int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, bool validateHeaders)
|
||||
: base(maxInitialLineLength, maxHeaderSize, maxChunkSize, true, validateHeaders)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpRequestDecoder(
|
||||
int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, bool validateHeaders,
|
||||
int initialBufferSize)
|
||||
: base(maxInitialLineLength, maxHeaderSize, maxChunkSize, true, validateHeaders, initialBufferSize)
|
||||
{
|
||||
}
|
||||
|
||||
protected sealed override IHttpMessage CreateMessage(AsciiString[] initialLine) =>
|
||||
new DefaultHttpRequest(
|
||||
HttpVersion.ValueOf(initialLine[2]),
|
||||
HttpMethod.ValueOf(initialLine[0]), initialLine[1].ToString(), this.ValidateHeaders);
|
||||
|
||||
protected override IHttpMessage CreateInvalidMessage() => new DefaultFullHttpRequest(HttpVersion.Http10, HttpMethod.Get, "/bad-request", this.ValidateHeaders);
|
||||
|
||||
protected override bool IsDecodingRequest() => true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
// 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
|
||||
{
|
||||
using System;
|
||||
using System.Text;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
using static HttpConstants;
|
||||
|
||||
public class HttpRequestEncoder : HttpObjectEncoder<IHttpRequest>
|
||||
{
|
||||
const char Slash = '/';
|
||||
const char QuestionMark = '?';
|
||||
const int SlashAndSpaceShort = (Slash << 8) | HorizontalSpace;
|
||||
const int SpaceSlashAndSpaceMedium = (HorizontalSpace << 16) | SlashAndSpaceShort;
|
||||
|
||||
public override bool AcceptOutboundMessage(object msg) => base.AcceptOutboundMessage(msg) && !(msg is IHttpResponse);
|
||||
|
||||
protected internal override void EncodeInitialLine(IByteBuffer buf, IHttpRequest request)
|
||||
{
|
||||
ByteBufferUtil.Copy(request.Method.AsciiName, buf);
|
||||
|
||||
string uri = request.Uri;
|
||||
|
||||
if (string.IsNullOrEmpty(uri))
|
||||
{
|
||||
// Add / as absolute path if no is present.
|
||||
// See http://tools.ietf.org/html/rfc2616#section-5.1.2
|
||||
buf.WriteMedium(SpaceSlashAndSpaceMedium);
|
||||
}
|
||||
else
|
||||
{
|
||||
var uriCharSequence = new StringBuilderCharSequence();
|
||||
uriCharSequence.Append(uri);
|
||||
|
||||
bool needSlash = false;
|
||||
int start = uri.IndexOf("://", StringComparison.Ordinal);
|
||||
if (start != -1 && uri[0] != Slash)
|
||||
{
|
||||
start += 3;
|
||||
// Correctly handle query params.
|
||||
// See https://github.com/netty/netty/issues/2732
|
||||
int index = uri.IndexOf(QuestionMark, start);
|
||||
if (index == -1)
|
||||
{
|
||||
if (uri.LastIndexOf(Slash) < start)
|
||||
{
|
||||
needSlash = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (uri.LastIndexOf(Slash, index) < start)
|
||||
{
|
||||
uriCharSequence.Insert(index, Slash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteByte(HorizontalSpace).WriteCharSequence(uriCharSequence, Encoding.UTF8);
|
||||
if (needSlash)
|
||||
{
|
||||
// write "/ " after uri
|
||||
buf.WriteShort(SlashAndSpaceShort);
|
||||
}
|
||||
else
|
||||
{
|
||||
buf.WriteByte(HorizontalSpace);
|
||||
}
|
||||
}
|
||||
|
||||
request.ProtocolVersion.Encode(buf);
|
||||
buf.WriteShort(CrlfShort);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
// 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
|
||||
{
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public class HttpResponseDecoder : HttpObjectDecoder
|
||||
{
|
||||
static readonly HttpResponseStatus UnknownStatus = new HttpResponseStatus(999, new AsciiString("Unknown"));
|
||||
|
||||
public HttpResponseDecoder()
|
||||
{
|
||||
}
|
||||
|
||||
public HttpResponseDecoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize)
|
||||
: base(maxInitialLineLength, maxHeaderSize, maxChunkSize, true)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpResponseDecoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, bool validateHeaders)
|
||||
: base(maxInitialLineLength, maxHeaderSize, maxChunkSize, true, validateHeaders)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpResponseDecoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, bool validateHeaders, int initialBufferSize)
|
||||
: base(maxInitialLineLength, maxHeaderSize, maxChunkSize, true, validateHeaders, initialBufferSize)
|
||||
{
|
||||
}
|
||||
|
||||
protected sealed override IHttpMessage CreateMessage(AsciiString[] initialLine) =>
|
||||
new DefaultHttpResponse(
|
||||
HttpVersion.ValueOf(initialLine[0]),
|
||||
HttpResponseStatus.ValueOf(initialLine[1].ParseInt() , initialLine[2]), this.ValidateHeaders);
|
||||
|
||||
protected override IHttpMessage CreateInvalidMessage() => new DefaultFullHttpResponse(HttpVersion.Http10, UnknownStatus, this.ValidateHeaders);
|
||||
|
||||
protected override bool IsDecodingRequest() => false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
// 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
|
||||
{
|
||||
using DotNetty.Buffers;
|
||||
|
||||
public class HttpResponseEncoder : HttpObjectEncoder<IHttpResponse>
|
||||
{
|
||||
public override bool AcceptOutboundMessage(object msg) => base.AcceptOutboundMessage(msg) && !(msg is IHttpRequest);
|
||||
|
||||
protected internal override void EncodeInitialLine(IByteBuffer buf, IHttpResponse response)
|
||||
{
|
||||
response.ProtocolVersion.Encode(buf);
|
||||
buf.WriteByte(HttpConstants.HorizontalSpace);
|
||||
response.Status.Encode(buf);
|
||||
buf.WriteShort(HttpConstants.CrlfShort);
|
||||
}
|
||||
|
||||
protected override void SanitizeHeadersBeforeEncode(IHttpResponse msg, bool isAlwaysEmpty)
|
||||
{
|
||||
if (isAlwaysEmpty)
|
||||
{
|
||||
HttpResponseStatus status = msg.Status;
|
||||
if (status.CodeClass == HttpStatusClass.Informational
|
||||
|| status.Code == HttpResponseStatus.NoContent.Code)
|
||||
{
|
||||
|
||||
// Stripping Content-Length:
|
||||
// See https://tools.ietf.org/html/rfc7230#section-3.3.2
|
||||
msg.Headers.Remove(HttpHeaderNames.ContentLength);
|
||||
|
||||
// Stripping Transfer-Encoding:
|
||||
// See https://tools.ietf.org/html/rfc7230#section-3.3.1
|
||||
msg.Headers.Remove(HttpHeaderNames.TransferEncoding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool IsContentAlwaysEmpty(IHttpResponse msg)
|
||||
{
|
||||
// Correctly handle special cases as stated in:
|
||||
// https://tools.ietf.org/html/rfc7230#section-3.3.3
|
||||
HttpResponseStatus status = msg.Status;
|
||||
|
||||
if (status.CodeClass == HttpStatusClass.Informational)
|
||||
{
|
||||
if (status.Code == HttpResponseStatus.SwitchingProtocols.Code)
|
||||
{
|
||||
// We need special handling for WebSockets version 00 as it will include an body.
|
||||
// Fortunally this version should not really be used in the wild very often.
|
||||
// See https://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00#section-1.2
|
||||
return msg.Headers.Contains(HttpHeaderNames.SecWebsocketVersion);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return status.Code == HttpResponseStatus.NoContent.Code
|
||||
|| status.Code == HttpResponseStatus.NotModified.Code;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,560 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ConvertToAutoPropertyWhenPossible
|
||||
// ReSharper disable ConvertToAutoProperty
|
||||
namespace DotNetty.Codecs.Http
|
||||
{
|
||||
using System;
|
||||
using System.Text;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
/**
|
||||
* The response code and its description of HTTP or its derived protocols, such as
|
||||
* <a href="http://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol">RTSP</a> and
|
||||
* <a href="http://en.wikipedia.org/wiki/Internet_Content_Adaptation_Protocol">ICAP</a>.
|
||||
*/
|
||||
public class HttpResponseStatus : IComparable<HttpResponseStatus>
|
||||
{
|
||||
/**
|
||||
* 100 Continue
|
||||
*/
|
||||
public static readonly HttpResponseStatus Continue = NewStatus(100, "Continue");
|
||||
|
||||
/**
|
||||
* 101 Switching Protocols
|
||||
*/
|
||||
public static readonly HttpResponseStatus SwitchingProtocols = NewStatus(101, "Switching Protocols");
|
||||
|
||||
/**
|
||||
* 102 Processing (WebDAV, RFC2518)
|
||||
*/
|
||||
public static readonly HttpResponseStatus Processing = NewStatus(102, "Processing");
|
||||
|
||||
/**
|
||||
* 200 OK
|
||||
*/
|
||||
public static readonly HttpResponseStatus OK = NewStatus(200, "OK");
|
||||
|
||||
/**
|
||||
* 201 Created
|
||||
*/
|
||||
public static readonly HttpResponseStatus Created = NewStatus(201, "Created");
|
||||
|
||||
/**
|
||||
* 202 Accepted
|
||||
*/
|
||||
public static readonly HttpResponseStatus Accepted = NewStatus(202, "Accepted");
|
||||
|
||||
/**
|
||||
* 203 Non-Authoritative Information (since HTTP/1.1)
|
||||
*/
|
||||
public static readonly HttpResponseStatus NonAuthoritativeInformation = NewStatus(203, "Non-Authoritative Information");
|
||||
|
||||
/**
|
||||
* 204 No Content
|
||||
*/
|
||||
public static readonly HttpResponseStatus NoContent = NewStatus(204, "No Content");
|
||||
|
||||
/**
|
||||
* 205 Reset Content
|
||||
*/
|
||||
public static readonly HttpResponseStatus ResetContent = NewStatus(205, "Reset Content");
|
||||
|
||||
/**
|
||||
* 206 Partial Content
|
||||
*/
|
||||
public static readonly HttpResponseStatus PartialContent = NewStatus(206, "Partial Content");
|
||||
|
||||
/**
|
||||
* 207 Multi-Status (WebDAV, RFC2518)
|
||||
*/
|
||||
public static readonly HttpResponseStatus MultiStatus = NewStatus(207, "Multi-Status");
|
||||
|
||||
/**
|
||||
* 300 Multiple Choices
|
||||
*/
|
||||
public static readonly HttpResponseStatus MultipleChoices = NewStatus(300, "Multiple Choices");
|
||||
|
||||
/**
|
||||
* 301 Moved Permanently
|
||||
*/
|
||||
public static readonly HttpResponseStatus MovedPermanently = NewStatus(301, "Moved Permanently");
|
||||
|
||||
/**
|
||||
* 302 Found
|
||||
*/
|
||||
public static readonly HttpResponseStatus Found = NewStatus(302, "Found");
|
||||
|
||||
/**
|
||||
* 303 See Other (since HTTP/1.1)
|
||||
*/
|
||||
public static readonly HttpResponseStatus SeeOther = NewStatus(303, "See Other");
|
||||
|
||||
/**
|
||||
* 304 Not Modified
|
||||
*/
|
||||
public static readonly HttpResponseStatus NotModified = NewStatus(304, "Not Modified");
|
||||
|
||||
/**
|
||||
* 305 Use Proxy (since HTTP/1.1)
|
||||
*/
|
||||
public static readonly HttpResponseStatus UseProxy = NewStatus(305, "Use Proxy");
|
||||
|
||||
/**
|
||||
* 307 Temporary Redirect (since HTTP/1.1)
|
||||
*/
|
||||
public static readonly HttpResponseStatus TemporaryRedirect = NewStatus(307, "Temporary Redirect");
|
||||
|
||||
/**
|
||||
* 308 Permanent Redirect (RFC7538)
|
||||
*/
|
||||
public static readonly HttpResponseStatus PermanentRedirect = NewStatus(308, "Permanent Redirect");
|
||||
|
||||
/**
|
||||
* 400 Bad Request
|
||||
*/
|
||||
public static readonly HttpResponseStatus BadRequest = NewStatus(400, "Bad Request");
|
||||
|
||||
/**
|
||||
* 401 Unauthorized
|
||||
*/
|
||||
public static readonly HttpResponseStatus Unauthorized = NewStatus(401, "Unauthorized");
|
||||
|
||||
/**
|
||||
* 402 Payment Required
|
||||
*/
|
||||
public static readonly HttpResponseStatus PaymentRequired = NewStatus(402, "Payment Required");
|
||||
|
||||
/**
|
||||
* 403 Forbidden
|
||||
*/
|
||||
public static readonly HttpResponseStatus Forbidden = NewStatus(403, "Forbidden");
|
||||
|
||||
/**
|
||||
* 404 Not Found
|
||||
*/
|
||||
public static readonly HttpResponseStatus NotFound = NewStatus(404, "Not Found");
|
||||
|
||||
/**
|
||||
* 405 Method Not Allowed
|
||||
*/
|
||||
public static readonly HttpResponseStatus MethodNotAllowed = NewStatus(405, "Method Not Allowed");
|
||||
|
||||
/**
|
||||
* 406 Not Acceptable
|
||||
*/
|
||||
public static readonly HttpResponseStatus NotAcceptable = NewStatus(406, "Not Acceptable");
|
||||
|
||||
/**
|
||||
* 407 Proxy Authentication Required
|
||||
*/
|
||||
public static readonly HttpResponseStatus ProxyAuthenticationRequired = NewStatus(407, "Proxy Authentication Required");
|
||||
|
||||
/**
|
||||
* 408 Request Timeout
|
||||
*/
|
||||
public static readonly HttpResponseStatus RequestTimeout = NewStatus(408, "Request Timeout");
|
||||
|
||||
/**
|
||||
* 409 Conflict
|
||||
*/
|
||||
public static readonly HttpResponseStatus Conflict = NewStatus(409, "Conflict");
|
||||
|
||||
/**
|
||||
* 410 Gone
|
||||
*/
|
||||
public static readonly HttpResponseStatus Gone = NewStatus(410, "Gone");
|
||||
|
||||
/**
|
||||
* 411 Length Required
|
||||
*/
|
||||
public static readonly HttpResponseStatus LengthRequired = NewStatus(411, "Length Required");
|
||||
|
||||
/**
|
||||
* 412 Precondition Failed
|
||||
*/
|
||||
public static readonly HttpResponseStatus PreconditionFailed = NewStatus(412, "Precondition Failed");
|
||||
|
||||
/**
|
||||
* 413 Request Entity Too Large
|
||||
*/
|
||||
public static readonly HttpResponseStatus RequestEntityTooLarge = NewStatus(413, "Request Entity Too Large");
|
||||
|
||||
/**
|
||||
* 414 Request-URI Too Long
|
||||
*/
|
||||
public static readonly HttpResponseStatus RequestUriTooLong = NewStatus(414, "Request-URI Too Long");
|
||||
|
||||
/**
|
||||
* 415 Unsupported Media Type
|
||||
*/
|
||||
public static readonly HttpResponseStatus UnsupportedMediaType = NewStatus(415, "Unsupported Media Type");
|
||||
|
||||
/**
|
||||
* 416 Requested Range Not Satisfiable
|
||||
*/
|
||||
public static readonly HttpResponseStatus RequestedRangeNotSatisfiable = NewStatus(416, "Requested Range Not Satisfiable");
|
||||
|
||||
/**
|
||||
* 417 Expectation Failed
|
||||
*/
|
||||
public static readonly HttpResponseStatus ExpectationFailed = NewStatus(417, "Expectation Failed");
|
||||
|
||||
/**
|
||||
* 421 Misdirected Request
|
||||
*
|
||||
* <a href="https://tools.ietf.org/html/draft-ietf-httpbis-http2-15#section-9.1.2">421 Status Code</a>
|
||||
*/
|
||||
public static readonly HttpResponseStatus MisdirectedRequest = NewStatus(421, "Misdirected Request");
|
||||
|
||||
/**
|
||||
* 422 Unprocessable Entity (WebDAV, RFC4918)
|
||||
*/
|
||||
public static readonly HttpResponseStatus UnprocessableEntity = NewStatus(422, "Unprocessable Entity");
|
||||
|
||||
/**
|
||||
* 423 Locked (WebDAV, RFC4918)
|
||||
*/
|
||||
public static readonly HttpResponseStatus Locked = NewStatus(423, "Locked");
|
||||
|
||||
/**
|
||||
* 424 Failed Dependency (WebDAV, RFC4918)
|
||||
*/
|
||||
public static readonly HttpResponseStatus FailedDependency = NewStatus(424, "Failed Dependency");
|
||||
|
||||
/**
|
||||
* 425 Unordered Collection (WebDAV, RFC3648)
|
||||
*/
|
||||
public static readonly HttpResponseStatus UnorderedCollection = NewStatus(425, "Unordered Collection");
|
||||
|
||||
/**
|
||||
* 426 Upgrade Required (RFC2817)
|
||||
*/
|
||||
public static readonly HttpResponseStatus UpgradeRequired = NewStatus(426, "Upgrade Required");
|
||||
|
||||
/**
|
||||
* 428 Precondition Required (RFC6585)
|
||||
*/
|
||||
public static readonly HttpResponseStatus PreconditionRequired = NewStatus(428, "Precondition Required");
|
||||
|
||||
/**
|
||||
* 429 Too Many Requests (RFC6585)
|
||||
*/
|
||||
public static readonly HttpResponseStatus TooManyRequests = NewStatus(429, "Too Many Requests");
|
||||
|
||||
/**
|
||||
* 431 Request Header Fields Too Large (RFC6585)
|
||||
*/
|
||||
public static readonly HttpResponseStatus RequestHeaderFieldsTooLarge = NewStatus(431, "Request Header Fields Too Large");
|
||||
|
||||
/**
|
||||
* 500 Internal Server Error
|
||||
*/
|
||||
public static readonly HttpResponseStatus InternalServerError = NewStatus(500, "Internal Server Error");
|
||||
|
||||
/**
|
||||
* 501 Not Implemented
|
||||
*/
|
||||
public static readonly HttpResponseStatus NotImplemented = NewStatus(501, "Not Implemented");
|
||||
|
||||
/**
|
||||
* 502 Bad Gateway
|
||||
*/
|
||||
public static readonly HttpResponseStatus BadGateway = NewStatus(502, "Bad Gateway");
|
||||
|
||||
/**
|
||||
* 503 Service Unavailable
|
||||
*/
|
||||
public static readonly HttpResponseStatus ServiceUnavailable = NewStatus(503, "Service Unavailable");
|
||||
|
||||
/**
|
||||
* 504 Gateway Timeout
|
||||
*/
|
||||
public static readonly HttpResponseStatus GatewayTimeout = NewStatus(504, "Gateway Timeout");
|
||||
|
||||
/**
|
||||
* 505 HTTP Version Not Supported
|
||||
*/
|
||||
public static readonly HttpResponseStatus HttpVersionNotSupported = NewStatus(505, "HTTP Version Not Supported");
|
||||
|
||||
/**
|
||||
* 506 Variant Also Negotiates (RFC2295)
|
||||
*/
|
||||
public static readonly HttpResponseStatus VariantAlsoNegotiates = NewStatus(506, "Variant Also Negotiates");
|
||||
|
||||
/**
|
||||
* 507 Insufficient Storage (WebDAV, RFC4918)
|
||||
*/
|
||||
public static readonly HttpResponseStatus InsufficientStorage = NewStatus(507, "Insufficient Storage");
|
||||
|
||||
/**
|
||||
* 510 Not Extended (RFC2774)
|
||||
*/
|
||||
public static readonly HttpResponseStatus NotExtended = NewStatus(510, "Not Extended");
|
||||
|
||||
/**
|
||||
* 511 Network Authentication Required (RFC6585)
|
||||
*/
|
||||
public static readonly HttpResponseStatus NetworkAuthenticationRequired = NewStatus(511, "Network Authentication Required");
|
||||
|
||||
static HttpResponseStatus NewStatus(int statusCode, string reasonPhrase) => new HttpResponseStatus(statusCode, new AsciiString(reasonPhrase), true);
|
||||
|
||||
// Returns the {@link HttpResponseStatus} represented by the specified code.
|
||||
// If the specified code is a standard HTTP getStatus code, a cached instance
|
||||
// will be returned. Otherwise, a new instance will be returned.
|
||||
public static HttpResponseStatus ValueOf(int code) => ValueOf0(code) ?? new HttpResponseStatus(code);
|
||||
|
||||
static HttpResponseStatus ValueOf0(int code)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case 100:
|
||||
return Continue;
|
||||
case 101:
|
||||
return SwitchingProtocols;
|
||||
case 102:
|
||||
return Processing;
|
||||
case 200:
|
||||
return OK;
|
||||
case 201:
|
||||
return Created;
|
||||
case 202:
|
||||
return Accepted;
|
||||
case 203:
|
||||
return NonAuthoritativeInformation;
|
||||
case 204:
|
||||
return NoContent;
|
||||
case 205:
|
||||
return ResetContent;
|
||||
case 206:
|
||||
return PartialContent;
|
||||
case 207:
|
||||
return MultiStatus;
|
||||
case 300:
|
||||
return MultipleChoices;
|
||||
case 301:
|
||||
return MovedPermanently;
|
||||
case 302:
|
||||
return Found;
|
||||
case 303:
|
||||
return SeeOther;
|
||||
case 304:
|
||||
return NotModified;
|
||||
case 305:
|
||||
return UseProxy;
|
||||
case 307:
|
||||
return TemporaryRedirect;
|
||||
case 308:
|
||||
return PermanentRedirect;
|
||||
case 400:
|
||||
return BadRequest;
|
||||
case 401:
|
||||
return Unauthorized;
|
||||
case 402:
|
||||
return PaymentRequired;
|
||||
case 403:
|
||||
return Forbidden;
|
||||
case 404:
|
||||
return NotFound;
|
||||
case 405:
|
||||
return MethodNotAllowed;
|
||||
case 406:
|
||||
return NotAcceptable;
|
||||
case 407:
|
||||
return ProxyAuthenticationRequired;
|
||||
case 408:
|
||||
return RequestTimeout;
|
||||
case 409:
|
||||
return Conflict;
|
||||
case 410:
|
||||
return Gone;
|
||||
case 411:
|
||||
return LengthRequired;
|
||||
case 412:
|
||||
return PreconditionFailed;
|
||||
case 413:
|
||||
return RequestEntityTooLarge;
|
||||
case 414:
|
||||
return RequestUriTooLong;
|
||||
case 415:
|
||||
return UnsupportedMediaType;
|
||||
case 416:
|
||||
return RequestedRangeNotSatisfiable;
|
||||
case 417:
|
||||
return ExpectationFailed;
|
||||
case 421:
|
||||
return MisdirectedRequest;
|
||||
case 422:
|
||||
return UnprocessableEntity;
|
||||
case 423:
|
||||
return Locked;
|
||||
case 424:
|
||||
return FailedDependency;
|
||||
case 425:
|
||||
return UnorderedCollection;
|
||||
case 426:
|
||||
return UpgradeRequired;
|
||||
case 428:
|
||||
return PreconditionRequired;
|
||||
case 429:
|
||||
return TooManyRequests;
|
||||
case 431:
|
||||
return RequestHeaderFieldsTooLarge;
|
||||
case 500:
|
||||
return InternalServerError;
|
||||
case 501:
|
||||
return NotImplemented;
|
||||
case 502:
|
||||
return BadGateway;
|
||||
case 503:
|
||||
return ServiceUnavailable;
|
||||
case 504:
|
||||
return GatewayTimeout;
|
||||
case 505:
|
||||
return HttpVersionNotSupported;
|
||||
case 506:
|
||||
return VariantAlsoNegotiates;
|
||||
case 507:
|
||||
return InsufficientStorage;
|
||||
case 510:
|
||||
return NotExtended;
|
||||
case 511:
|
||||
return NetworkAuthenticationRequired;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static HttpResponseStatus ValueOf(int code, AsciiString reasonPhrase)
|
||||
{
|
||||
HttpResponseStatus responseStatus = ValueOf0(code);
|
||||
return responseStatus != null && responseStatus.ReasonPhrase.ContentEquals(reasonPhrase)
|
||||
? responseStatus
|
||||
: new HttpResponseStatus(code, reasonPhrase);
|
||||
}
|
||||
|
||||
public static HttpResponseStatus ParseLine(ICharSequence line) => line is AsciiString asciiString ? ParseLine(asciiString) : ParseLine(line.ToString());
|
||||
|
||||
public static HttpResponseStatus ParseLine(string line)
|
||||
{
|
||||
try
|
||||
{
|
||||
int space = line.IndexOf(' ');
|
||||
return space == -1
|
||||
? ValueOf(int.Parse(line))
|
||||
: ValueOf(int.Parse(line.Substring(0, space)), new AsciiString(line.Substring(space + 1)));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ArgumentException($"malformed status line: {line}", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static HttpResponseStatus ParseLine(AsciiString line)
|
||||
{
|
||||
try
|
||||
{
|
||||
int space = line.ForEachByte(ByteProcessor.FindAsciiSpace);
|
||||
return space == -1
|
||||
? ValueOf(line.ParseInt())
|
||||
: ValueOf(line.ParseInt(0, space), (AsciiString)line.SubSequence(space + 1));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ArgumentException($"malformed status line: {line}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
readonly int code;
|
||||
readonly AsciiString codeAsText;
|
||||
readonly HttpStatusClass codeClass;
|
||||
|
||||
readonly AsciiString reasonPhrase;
|
||||
readonly byte[] bytes;
|
||||
|
||||
HttpResponseStatus(int code)
|
||||
: this(code, new AsciiString($"{HttpStatusClass.ValueOf(code).DefaultReasonPhrase} ({code})"), false)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpResponseStatus(int code, AsciiString reasonPhrase)
|
||||
: this(code, reasonPhrase, false)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpResponseStatus(int code, AsciiString reasonPhrase, bool bytes)
|
||||
{
|
||||
if (code < 0)
|
||||
{
|
||||
throw new ArgumentException($"code: {code} (expected: 0+)");
|
||||
}
|
||||
if (reasonPhrase == null)
|
||||
{
|
||||
throw new ArgumentException(nameof(reasonPhrase));
|
||||
}
|
||||
|
||||
// ReSharper disable once ForCanBeConvertedToForeach
|
||||
for (int i = 0; i < reasonPhrase.Count; i++)
|
||||
{
|
||||
char c = reasonPhrase[i];
|
||||
// Check prohibited characters.
|
||||
switch (c)
|
||||
{
|
||||
case '\n':
|
||||
case '\r':
|
||||
throw new ArgumentException($"reasonPhrase contains one of the following prohibited characters: \\r\\n: {reasonPhrase}");
|
||||
}
|
||||
}
|
||||
|
||||
this.code = code;
|
||||
this.codeAsText = new AsciiString(Convert.ToString(code));
|
||||
this.reasonPhrase = reasonPhrase;
|
||||
this.bytes = bytes ? Encoding.ASCII.GetBytes($"{code} {reasonPhrase}") : null;
|
||||
this.codeClass = HttpStatusClass.ValueOf(code);
|
||||
}
|
||||
|
||||
public int Code => this.code;
|
||||
|
||||
public AsciiString CodeAsText => this.codeAsText;
|
||||
|
||||
public AsciiString ReasonPhrase => this.reasonPhrase;
|
||||
|
||||
public HttpStatusClass CodeClass => this.codeClass;
|
||||
|
||||
public override int GetHashCode() => this.code;
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (!(obj is HttpResponseStatus other))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return this.code == other.code;
|
||||
}
|
||||
|
||||
public int CompareTo(HttpResponseStatus other) => this.code - other.code;
|
||||
|
||||
public override string ToString() =>
|
||||
new StringBuilder(this.ReasonPhrase.Count + 4)
|
||||
.Append(this.Code)
|
||||
.Append(' ')
|
||||
.Append(this.ReasonPhrase)
|
||||
.ToString();
|
||||
|
||||
internal void Encode(IByteBuffer buf)
|
||||
{
|
||||
if (this.bytes == null)
|
||||
{
|
||||
ByteBufferUtil.Copy(this.codeAsText, buf);
|
||||
buf.WriteByte(HttpConstants.HorizontalSpace);
|
||||
buf.WriteCharSequence(this.reasonPhrase, Encoding.ASCII);
|
||||
}
|
||||
else
|
||||
{
|
||||
buf.WriteBytes(this.bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ConvertToAutoPropertyWhenPossible
|
||||
// ReSharper disable ConvertToAutoProperty
|
||||
namespace DotNetty.Codecs.Http
|
||||
{
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public sealed class HttpScheme
|
||||
{
|
||||
readonly int port;
|
||||
readonly AsciiString name;
|
||||
|
||||
HttpScheme(int port, string name)
|
||||
{
|
||||
this.port = port;
|
||||
this.name = AsciiString.Cached(name);
|
||||
}
|
||||
|
||||
public AsciiString Name => this.name;
|
||||
|
||||
public int Port => this.port;
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (!(obj is HttpScheme other))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return other.port == this.port && other.name.Equals(this.name);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => this.port * 31 + this.name.GetHashCode();
|
||||
|
||||
public override string ToString() => this.name.ToString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
// 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
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public class HttpServerCodec : CombinedChannelDuplexHandler<HttpRequestDecoder, HttpResponseEncoder>,
|
||||
HttpServerUpgradeHandler.ISourceCodec
|
||||
{
|
||||
/** A queue that is used for correlating a request and a response. */
|
||||
readonly Queue<HttpMethod> queue = new Queue<HttpMethod>();
|
||||
|
||||
public HttpServerCodec() : this(4096, 8192, 8192)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpServerCodec(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize)
|
||||
{
|
||||
this.Init(new HttpServerRequestDecoder(this, maxInitialLineLength, maxHeaderSize, maxChunkSize),
|
||||
new HttpServerResponseEncoder(this));
|
||||
}
|
||||
|
||||
public HttpServerCodec(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, bool validateHeaders)
|
||||
{
|
||||
this.Init(new HttpServerRequestDecoder(this, maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders),
|
||||
new HttpServerResponseEncoder(this));
|
||||
}
|
||||
|
||||
public HttpServerCodec(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, bool validateHeaders, int initialBufferSize)
|
||||
{
|
||||
this.Init(new HttpServerRequestDecoder(this, maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders, initialBufferSize),
|
||||
new HttpServerResponseEncoder(this));
|
||||
}
|
||||
|
||||
public void UpgradeFrom(IChannelHandlerContext ctx) => ctx.Channel.Pipeline.Remove(this);
|
||||
|
||||
sealed class HttpServerRequestDecoder : HttpRequestDecoder
|
||||
{
|
||||
readonly HttpServerCodec serverCodec;
|
||||
|
||||
public HttpServerRequestDecoder(HttpServerCodec serverCodec, int maxInitialLineLength, int maxHeaderSize, int maxChunkSize)
|
||||
: base(maxInitialLineLength, maxHeaderSize, maxChunkSize)
|
||||
{
|
||||
this.serverCodec = serverCodec;
|
||||
}
|
||||
|
||||
public HttpServerRequestDecoder(HttpServerCodec serverCodec, int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, bool validateHeaders)
|
||||
:base(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders)
|
||||
{
|
||||
this.serverCodec = serverCodec;
|
||||
}
|
||||
|
||||
public HttpServerRequestDecoder(HttpServerCodec serverCodec,
|
||||
int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, bool validateHeaders, int initialBufferSize)
|
||||
: base(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders, initialBufferSize)
|
||||
{
|
||||
this.serverCodec = serverCodec;
|
||||
}
|
||||
|
||||
protected override void Decode(IChannelHandlerContext context, IByteBuffer buffer, List<object> output)
|
||||
{
|
||||
int oldSize = output.Count;
|
||||
base.Decode(context, buffer, output);
|
||||
int size = output.Count;
|
||||
for (int i = oldSize; i < size; i++)
|
||||
{
|
||||
if (output[i] is IHttpRequest request)
|
||||
{
|
||||
this.serverCodec.queue.Enqueue(request.Method);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class HttpServerResponseEncoder : HttpResponseEncoder
|
||||
{
|
||||
readonly HttpServerCodec serverCodec;
|
||||
HttpMethod method;
|
||||
|
||||
public HttpServerResponseEncoder(HttpServerCodec serverCodec)
|
||||
{
|
||||
this.serverCodec = serverCodec;
|
||||
}
|
||||
|
||||
protected override void SanitizeHeadersBeforeEncode(IHttpResponse msg, bool isAlwaysEmpty)
|
||||
{
|
||||
if (!isAlwaysEmpty && ReferenceEquals(this.method, HttpMethod.Connect) && msg.Status.CodeClass == HttpStatusClass.Success)
|
||||
{
|
||||
// Stripping Transfer-Encoding:
|
||||
// See https://tools.ietf.org/html/rfc7230#section-3.3.1
|
||||
msg.Headers.Remove(HttpHeaderNames.TransferEncoding);
|
||||
return;
|
||||
}
|
||||
|
||||
base.SanitizeHeadersBeforeEncode(msg, isAlwaysEmpty);
|
||||
}
|
||||
|
||||
|
||||
protected override bool IsContentAlwaysEmpty(IHttpResponse msg)
|
||||
{
|
||||
this.method = this.serverCodec.queue.Count > 0 ? this.serverCodec.queue.Dequeue() : null;
|
||||
return HttpMethod.Head.Equals(this.method) || base.IsContentAlwaysEmpty(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
// 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
|
||||
{
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public class HttpServerExpectContinueHandler : ChannelHandlerAdapter
|
||||
{
|
||||
static readonly IFullHttpResponse ExpectationFailed = new DefaultFullHttpResponse(
|
||||
HttpVersion.Http11, HttpResponseStatus.ExpectationFailed, Unpooled.Empty);
|
||||
|
||||
static readonly IFullHttpResponse Accept = new DefaultFullHttpResponse(
|
||||
HttpVersion.Http11, HttpResponseStatus.Continue, Unpooled.Empty);
|
||||
|
||||
static HttpServerExpectContinueHandler()
|
||||
{
|
||||
ExpectationFailed.Headers.Set(HttpHeaderNames.ContentLength, HttpHeaderValues.Zero);
|
||||
Accept.Headers.Set(HttpHeaderNames.ContentLength, HttpHeaderValues.Zero);
|
||||
}
|
||||
|
||||
protected virtual IHttpResponse AcceptMessage(IHttpRequest request) => (IHttpResponse)Accept.RetainedDuplicate();
|
||||
|
||||
protected virtual IHttpResponse RejectResponse(IHttpRequest request) => (IHttpResponse)ExpectationFailed.RetainedDuplicate();
|
||||
|
||||
public override void ChannelRead(IChannelHandlerContext context, object message)
|
||||
{
|
||||
if (message is IHttpRequest req)
|
||||
{
|
||||
if (HttpUtil.Is100ContinueExpected(req))
|
||||
{
|
||||
IHttpResponse accept = this.AcceptMessage(req);
|
||||
|
||||
if (accept == null)
|
||||
{
|
||||
// the expectation failed so we refuse the request.
|
||||
IHttpResponse rejection = this.RejectResponse(req);
|
||||
ReferenceCountUtil.Release(message);
|
||||
context.WriteAndFlushAsync(rejection)
|
||||
.ContinueWith(CloseOnFailure, context, TaskContinuationOptions.ExecuteSynchronously);
|
||||
return;
|
||||
}
|
||||
|
||||
context.WriteAndFlushAsync(accept)
|
||||
.ContinueWith(CloseOnFailure, context, TaskContinuationOptions.ExecuteSynchronously);
|
||||
req.Headers.Remove(HttpHeaderNames.Expect);
|
||||
}
|
||||
base.ChannelRead(context, message);
|
||||
}
|
||||
}
|
||||
|
||||
static Task CloseOnFailure(Task task, object state)
|
||||
{
|
||||
if (task.IsFaulted)
|
||||
{
|
||||
var context = (IChannelHandlerContext)state;
|
||||
return context.CloseAsync();
|
||||
}
|
||||
return TaskEx.Completed;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
// 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
|
||||
{
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
using static HttpUtil;
|
||||
|
||||
public class HttpServerKeepAliveHandler : ChannelDuplexHandler
|
||||
{
|
||||
static readonly AsciiString MultipartPrefix = new AsciiString("multipart");
|
||||
|
||||
bool persistentConnection = true;
|
||||
// Track pending responses to support client pipelining: https://tools.ietf.org/html/rfc7230#section-6.3.2
|
||||
int pendingResponses;
|
||||
|
||||
public override void ChannelRead(IChannelHandlerContext context, object message)
|
||||
{
|
||||
// read message and track if it was keepAlive
|
||||
if (message is IHttpRequest request)
|
||||
{
|
||||
if (this.persistentConnection)
|
||||
{
|
||||
this.pendingResponses += 1;
|
||||
this.persistentConnection = IsKeepAlive(request);
|
||||
}
|
||||
}
|
||||
base.ChannelRead(context, message);
|
||||
}
|
||||
|
||||
public override Task WriteAsync(IChannelHandlerContext context, object message)
|
||||
{
|
||||
// modify message on way out to add headers if needed
|
||||
if (message is IHttpResponse response)
|
||||
{
|
||||
this.TrackResponse(response);
|
||||
// Assume the response writer knows if they can persist or not and sets isKeepAlive on the response
|
||||
if (!IsKeepAlive(response) || !IsSelfDefinedMessageLength(response))
|
||||
{
|
||||
// No longer keep alive as the client can't tell when the message is done unless we close connection
|
||||
this.pendingResponses = 0;
|
||||
this.persistentConnection = false;
|
||||
}
|
||||
// Server might think it can keep connection alive, but we should fix response header if we know better
|
||||
if (!this.ShouldKeepAlive())
|
||||
{
|
||||
SetKeepAlive(response, false);
|
||||
}
|
||||
}
|
||||
if (message is ILastHttpContent && !this.ShouldKeepAlive())
|
||||
{
|
||||
return base.WriteAsync(context, message)
|
||||
.ContinueWith(CloseOnComplete, context, TaskContinuationOptions.ExecuteSynchronously);
|
||||
}
|
||||
return base.WriteAsync(context, message);
|
||||
}
|
||||
|
||||
static Task CloseOnComplete(Task task, object state)
|
||||
{
|
||||
var context = (IChannelHandlerContext)state;
|
||||
return context.CloseAsync();
|
||||
}
|
||||
|
||||
void TrackResponse(IHttpResponse response)
|
||||
{
|
||||
if (!IsInformational(response))
|
||||
{
|
||||
this.pendingResponses -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
bool ShouldKeepAlive() => this.pendingResponses != 0 || this.persistentConnection;
|
||||
|
||||
/// <summary>
|
||||
/// Keep-alive only works if the client can detect when the message has ended without relying on the connection being
|
||||
/// closed.
|
||||
/// https://tools.ietf.org/html/rfc7230#section-6.3
|
||||
/// https://tools.ietf.org/html/rfc7230#section-3.3.2
|
||||
/// https://tools.ietf.org/html/rfc7230#section-3.3.3
|
||||
/// </summary>
|
||||
/// <param name="response">The HttpResponse to check</param>
|
||||
/// <returns>true if the response has a self defined message length.</returns>
|
||||
static bool IsSelfDefinedMessageLength(IHttpResponse response) =>
|
||||
IsContentLengthSet(response) || IsTransferEncodingChunked(response) || IsMultipart(response)
|
||||
|| IsInformational(response) || response.Status.Code == HttpResponseStatus.NoContent.Code;
|
||||
|
||||
static bool IsInformational(IHttpResponse response) => response.Status.CodeClass == HttpStatusClass.Informational;
|
||||
|
||||
static bool IsMultipart(IHttpResponse response)
|
||||
{
|
||||
return response.Headers.TryGet(HttpHeaderNames.ContentType, out ICharSequence contentType)
|
||||
&& contentType.RegionMatchesIgnoreCase(0, MultipartPrefix, 0, MultipartPrefix.Count);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,333 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ConvertToAutoPropertyWhenPossible
|
||||
// ReSharper disable ConvertToAutoProperty
|
||||
namespace DotNetty.Codecs.Http
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public class HttpServerUpgradeHandler : HttpObjectAggregator
|
||||
{
|
||||
/// <summary>
|
||||
/// The source codec that is used in the pipeline initially.
|
||||
/// </summary>
|
||||
public interface ISourceCodec
|
||||
{
|
||||
/// <summary>
|
||||
/// Removes this codec (i.e. all associated handlers) from the pipeline.
|
||||
/// </summary>
|
||||
void UpgradeFrom(IChannelHandlerContext ctx);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A codec that the source can be upgraded to.
|
||||
/// </summary>
|
||||
public interface IUpgradeCodec
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all protocol-specific headers required by this protocol for a successful upgrade.
|
||||
/// Any supplied header will be required to appear in the {@link HttpHeaderNames#CONNECTION} header as well.
|
||||
/// </summary>
|
||||
ICollection<AsciiString> RequiredUpgradeHeaders { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Prepares the {@code upgradeHeaders} for a protocol update based upon the contents of {@code upgradeRequest}.
|
||||
/// This method returns a boolean value to proceed or abort the upgrade in progress. If {@code false} is
|
||||
/// returned, the upgrade is aborted and the {@code upgradeRequest} will be passed through the inbound pipeline
|
||||
/// as if no upgrade was performed. If {@code true} is returned, the upgrade will proceed to the next
|
||||
/// step which invokes {@link #upgradeTo}. When returning {@code true}, you can add headers to
|
||||
/// the {@code upgradeHeaders} so that they are added to the 101 Switching protocols response.
|
||||
/// </summary>
|
||||
bool PrepareUpgradeResponse(IChannelHandlerContext ctx, IFullHttpRequest upgradeRequest, HttpHeaders upgradeHeaders);
|
||||
|
||||
/// <summary>
|
||||
/// Performs an HTTP protocol upgrade from the source codec. This method is responsible for
|
||||
/// adding all handlers required for the new protocol.
|
||||
///
|
||||
/// ctx the context for the current handler.
|
||||
/// upgradeRequest the request that triggered the upgrade to this protocol.
|
||||
/// </summary>
|
||||
void UpgradeTo(IChannelHandlerContext ctx, IFullHttpRequest upgradeRequest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new UpgradeCodec for the requested protocol name.
|
||||
/// </summary>
|
||||
public interface IUpgradeCodecFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked by {@link HttpServerUpgradeHandler} for all the requested protocol names in the order of
|
||||
/// the client preference.The first non-{@code null} {@link UpgradeCodec} returned by this method
|
||||
/// will be selected.
|
||||
/// </summary>
|
||||
IUpgradeCodec NewUpgradeCodec(ICharSequence protocol);
|
||||
}
|
||||
|
||||
public sealed class UpgradeEvent : IReferenceCounted
|
||||
{
|
||||
readonly ICharSequence protocol;
|
||||
readonly IFullHttpRequest upgradeRequest;
|
||||
|
||||
internal UpgradeEvent(ICharSequence protocol, IFullHttpRequest upgradeRequest)
|
||||
{
|
||||
this.protocol = protocol;
|
||||
this.upgradeRequest = upgradeRequest;
|
||||
}
|
||||
|
||||
public ICharSequence Protocol => this.protocol;
|
||||
|
||||
public IFullHttpRequest UpgradeRequest => this.upgradeRequest;
|
||||
|
||||
public int ReferenceCount => this.upgradeRequest.ReferenceCount;
|
||||
|
||||
public IReferenceCounted Retain()
|
||||
{
|
||||
this.upgradeRequest.Retain();
|
||||
return this;
|
||||
}
|
||||
|
||||
public IReferenceCounted Retain(int increment)
|
||||
{
|
||||
this.upgradeRequest.Retain(increment);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IReferenceCounted Touch()
|
||||
{
|
||||
this.upgradeRequest.Touch();
|
||||
return this;
|
||||
}
|
||||
|
||||
public IReferenceCounted Touch(object hint)
|
||||
{
|
||||
this.upgradeRequest.Touch(hint);
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Release() => this.upgradeRequest.Release();
|
||||
|
||||
public bool Release(int decrement) => this.upgradeRequest.Release(decrement);
|
||||
|
||||
public override string ToString() => $"UpgradeEvent [protocol={this.protocol}, upgradeRequest={this.upgradeRequest}]";
|
||||
}
|
||||
|
||||
readonly ISourceCodec sourceCodec;
|
||||
readonly IUpgradeCodecFactory upgradeCodecFactory;
|
||||
bool handlingUpgrade;
|
||||
|
||||
public HttpServerUpgradeHandler(ISourceCodec sourceCodec, IUpgradeCodecFactory upgradeCodecFactory)
|
||||
: this(sourceCodec, upgradeCodecFactory, 0)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpServerUpgradeHandler(ISourceCodec sourceCodec, IUpgradeCodecFactory upgradeCodecFactory, int maxContentLength)
|
||||
: base(maxContentLength)
|
||||
{
|
||||
Contract.Requires(sourceCodec != null);
|
||||
Contract.Requires(upgradeCodecFactory != null);
|
||||
|
||||
this.sourceCodec = sourceCodec;
|
||||
this.upgradeCodecFactory = upgradeCodecFactory;
|
||||
}
|
||||
|
||||
protected override void Decode(IChannelHandlerContext context, IHttpObject message, List<object> output)
|
||||
{
|
||||
// Determine if we're already handling an upgrade request or just starting a new one.
|
||||
this.handlingUpgrade |= IsUpgradeRequest(message);
|
||||
if (!this.handlingUpgrade)
|
||||
{
|
||||
// Not handling an upgrade request, just pass it to the next handler.
|
||||
ReferenceCountUtil.Retain(message);
|
||||
output.Add(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message is IFullHttpRequest fullRequest)
|
||||
{
|
||||
ReferenceCountUtil.Retain(fullRequest);
|
||||
output.Add(fullRequest);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Call the base class to handle the aggregation of the full request.
|
||||
base.Decode(context, message, output);
|
||||
if (output.Count == 0)
|
||||
{
|
||||
// The full request hasn't been created yet, still awaiting more data.
|
||||
return;
|
||||
}
|
||||
|
||||
// Finished aggregating the full request, get it from the output list.
|
||||
Debug.Assert(output.Count == 1);
|
||||
this.handlingUpgrade = false;
|
||||
fullRequest = (IFullHttpRequest)output[0];
|
||||
}
|
||||
|
||||
if (this.Upgrade(context, fullRequest))
|
||||
{
|
||||
// The upgrade was successful, remove the message from the output list
|
||||
// so that it's not propagated to the next handler. This request will
|
||||
// be propagated as a user event instead.
|
||||
output.Clear();
|
||||
}
|
||||
|
||||
// The upgrade did not succeed, just allow the full request to propagate to the
|
||||
// next handler.
|
||||
}
|
||||
|
||||
static bool IsUpgradeRequest(IHttpObject msg)
|
||||
{
|
||||
if (!(msg is IHttpRequest request))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return request.Headers.Contains(HttpHeaderNames.Upgrade);
|
||||
}
|
||||
|
||||
bool Upgrade(IChannelHandlerContext ctx, IFullHttpRequest request)
|
||||
{
|
||||
// Select the best protocol based on those requested in the UPGRADE header.
|
||||
IList<ICharSequence> requestedProtocols = SplitHeader(request.Headers.Get(HttpHeaderNames.Upgrade, null));
|
||||
int numRequestedProtocols = requestedProtocols.Count;
|
||||
IUpgradeCodec upgradeCodec = null;
|
||||
ICharSequence upgradeProtocol = null;
|
||||
for (int i = 0; i < numRequestedProtocols; i++)
|
||||
{
|
||||
ICharSequence p = requestedProtocols[i];
|
||||
IUpgradeCodec c = this.upgradeCodecFactory.NewUpgradeCodec(p);
|
||||
if (c != null)
|
||||
{
|
||||
upgradeProtocol = p;
|
||||
upgradeCodec = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (upgradeCodec == null)
|
||||
{
|
||||
// None of the requested protocols are supported, don't upgrade.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure the CONNECTION header is present.
|
||||
;
|
||||
if (!request.Headers.TryGet(HttpHeaderNames.Connection, out ICharSequence connectionHeader))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure the CONNECTION header contains UPGRADE as well as all protocol-specific headers.
|
||||
ICollection<AsciiString> requiredHeaders = upgradeCodec.RequiredUpgradeHeaders;
|
||||
IList<ICharSequence> values = SplitHeader(connectionHeader);
|
||||
if (!AsciiString.ContainsContentEqualsIgnoreCase(values, HttpHeaderNames.Upgrade)
|
||||
|| !AsciiString.ContainsAllContentEqualsIgnoreCase(values, requiredHeaders))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure that all required protocol-specific headers are found in the request.
|
||||
foreach (AsciiString requiredHeader in requiredHeaders)
|
||||
{
|
||||
if (!request.Headers.Contains(requiredHeader))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare and send the upgrade response. Wait for this write to complete before upgrading,
|
||||
// since we need the old codec in-place to properly encode the response.
|
||||
IFullHttpResponse upgradeResponse = CreateUpgradeResponse(upgradeProtocol);
|
||||
if (!upgradeCodec.PrepareUpgradeResponse(ctx, request, upgradeResponse.Headers))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create the user event to be fired once the upgrade completes.
|
||||
var upgradeEvent = new UpgradeEvent(upgradeProtocol, request);
|
||||
|
||||
IUpgradeCodec finalUpgradeCodec = upgradeCodec;
|
||||
ctx.WriteAndFlushAsync(upgradeResponse).ContinueWith(t =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (t.Status == TaskStatus.RanToCompletion)
|
||||
{
|
||||
// Perform the upgrade to the new protocol.
|
||||
this.sourceCodec.UpgradeFrom(ctx);
|
||||
finalUpgradeCodec.UpgradeTo(ctx, request);
|
||||
|
||||
// Notify that the upgrade has occurred. Retain the event to offset
|
||||
// the release() in the finally block.
|
||||
ctx.FireUserEventTriggered(upgradeEvent.Retain());
|
||||
|
||||
// Remove this handler from the pipeline.
|
||||
ctx.Channel.Pipeline.Remove(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx.Channel.CloseAsync();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Release the event if the upgrade event wasn't fired.
|
||||
upgradeEvent.Release();
|
||||
}
|
||||
}, TaskContinuationOptions.ExecuteSynchronously);
|
||||
return true;
|
||||
}
|
||||
|
||||
static IFullHttpResponse CreateUpgradeResponse(ICharSequence upgradeProtocol)
|
||||
{
|
||||
var res = new DefaultFullHttpResponse(HttpVersion.Http11, HttpResponseStatus.SwitchingProtocols,
|
||||
Unpooled.Empty, false);
|
||||
res.Headers.Add(HttpHeaderNames.Connection, HttpHeaderValues.Upgrade);
|
||||
res.Headers.Add(HttpHeaderNames.Upgrade, upgradeProtocol);
|
||||
return res;
|
||||
}
|
||||
|
||||
static IList<ICharSequence> SplitHeader(ICharSequence header)
|
||||
{
|
||||
var builder = new StringBuilder(header.Count);
|
||||
var protocols = new List<ICharSequence>(4);
|
||||
// ReSharper disable once ForCanBeConvertedToForeach
|
||||
for (int i = 0; i < header.Count; ++i)
|
||||
{
|
||||
char c = header[i];
|
||||
if (char.IsWhiteSpace(c))
|
||||
{
|
||||
// Don't include any whitespace.
|
||||
continue;
|
||||
}
|
||||
if (c == ',')
|
||||
{
|
||||
// Add the string and reset the builder for the next protocol.
|
||||
// Add the string and reset the builder for the next protocol.
|
||||
protocols.Add(new AsciiString(builder.ToString()));
|
||||
builder.Length = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(c);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last protocol
|
||||
if (builder.Length > 0)
|
||||
{
|
||||
protocols.Add(new AsciiString(builder.ToString()));
|
||||
}
|
||||
|
||||
return protocols;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ConvertToAutoProperty
|
||||
namespace DotNetty.Codecs.Http
|
||||
{
|
||||
using System;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public struct HttpStatusClass : IEquatable<HttpStatusClass>
|
||||
{
|
||||
public static readonly HttpStatusClass Informational = new HttpStatusClass(100, 200, "Informational");
|
||||
|
||||
public static readonly HttpStatusClass Success = new HttpStatusClass(200, 300, "Success");
|
||||
|
||||
public static readonly HttpStatusClass Redirection = new HttpStatusClass(300, 400, "Redirection");
|
||||
|
||||
public static readonly HttpStatusClass ClientError = new HttpStatusClass(400, 500, "Client Error");
|
||||
|
||||
public static readonly HttpStatusClass ServerError = new HttpStatusClass(500, 600, "Server Error");
|
||||
|
||||
public static readonly HttpStatusClass Unknown = new HttpStatusClass(0, 0, "Unknown Status");
|
||||
|
||||
public static HttpStatusClass ValueOf(int code)
|
||||
{
|
||||
if (Contains(Informational, code))
|
||||
{
|
||||
return Informational;
|
||||
}
|
||||
if (Contains(Success, code))
|
||||
{
|
||||
return Success;
|
||||
}
|
||||
if (Contains(Redirection, code))
|
||||
{
|
||||
return Redirection;
|
||||
}
|
||||
if (Contains(ClientError, code))
|
||||
{
|
||||
return ClientError;
|
||||
}
|
||||
if (Contains(ServerError, code))
|
||||
{
|
||||
return ServerError;
|
||||
}
|
||||
return Unknown;
|
||||
}
|
||||
|
||||
public static HttpStatusClass ValueOf(ICharSequence code)
|
||||
{
|
||||
if (code != null && code.Count == 3)
|
||||
{
|
||||
char c0 = code[0];
|
||||
return IsDigit(c0) && IsDigit(code[1]) && IsDigit(code[2])
|
||||
? ValueOf(Digit(c0) * 100)
|
||||
: Unknown;
|
||||
}
|
||||
|
||||
return Unknown;
|
||||
}
|
||||
|
||||
static int Digit(char c) => c - '0';
|
||||
|
||||
static bool IsDigit(char c) => c >= '0' && c <= '9';
|
||||
|
||||
readonly int min;
|
||||
readonly int max;
|
||||
readonly AsciiString defaultReasonPhrase;
|
||||
|
||||
HttpStatusClass(int min, int max, string defaultReasonPhrase)
|
||||
{
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.defaultReasonPhrase = AsciiString.Cached(defaultReasonPhrase);
|
||||
}
|
||||
|
||||
public bool Contains(int code) => Contains(this, code);
|
||||
|
||||
public static bool Contains(HttpStatusClass httpStatusClass, int code)
|
||||
{
|
||||
if ((httpStatusClass.min & httpStatusClass.max) == 0)
|
||||
{
|
||||
return code < 100 || code >= 600;
|
||||
}
|
||||
|
||||
return code >= httpStatusClass.min && code < httpStatusClass.max;
|
||||
}
|
||||
|
||||
public AsciiString DefaultReasonPhrase => this.defaultReasonPhrase;
|
||||
|
||||
public bool Equals(HttpStatusClass other) => this.min == other.min && this.max == other.max;
|
||||
|
||||
public override bool Equals(object obj) => obj is HttpStatusClass && this.Equals((HttpStatusClass)obj);
|
||||
|
||||
public override int GetHashCode() => this.min.GetHashCode() ^ this.max.GetHashCode();
|
||||
|
||||
public static bool operator !=(HttpStatusClass left, HttpStatusClass right) => !(left == right);
|
||||
|
||||
public static bool operator ==(HttpStatusClass left, HttpStatusClass right) => left.Equals(right);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,287 @@
|
|||
// 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
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public static class HttpUtil
|
||||
{
|
||||
static readonly AsciiString CharsetEquals = new AsciiString(HttpHeaderValues.Charset + "=");
|
||||
static readonly AsciiString Semicolon = AsciiString.Cached(";");
|
||||
|
||||
public static bool IsKeepAlive(IHttpMessage message)
|
||||
{
|
||||
if (message.Headers.TryGet(HttpHeaderNames.Connection, out ICharSequence connection)
|
||||
&& HttpHeaderValues.Close.ContentEqualsIgnoreCase(connection))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (message.ProtocolVersion.IsKeepAliveDefault)
|
||||
{
|
||||
return !HttpHeaderValues.Close.ContentEqualsIgnoreCase(connection);
|
||||
}
|
||||
else
|
||||
{
|
||||
return HttpHeaderValues.KeepAlive.ContentEqualsIgnoreCase(connection);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetKeepAlive(IHttpMessage message, bool keepAlive) => SetKeepAlive(message.Headers, message.ProtocolVersion, keepAlive);
|
||||
|
||||
public static void SetKeepAlive(HttpHeaders headers, HttpVersion httpVersion, bool keepAlive)
|
||||
{
|
||||
if (httpVersion.IsKeepAliveDefault)
|
||||
{
|
||||
if (keepAlive)
|
||||
{
|
||||
headers.Remove(HttpHeaderNames.Connection);
|
||||
}
|
||||
else
|
||||
{
|
||||
headers.Set(HttpHeaderNames.Connection, HttpHeaderValues.Close);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (keepAlive)
|
||||
{
|
||||
headers.Set(HttpHeaderNames.Connection, HttpHeaderValues.KeepAlive);
|
||||
}
|
||||
else
|
||||
{
|
||||
headers.Remove(HttpHeaderNames.Connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static long GetContentLength(IHttpMessage message)
|
||||
{
|
||||
if (message.Headers.TryGet(HttpHeaderNames.ContentLength, out ICharSequence value))
|
||||
{
|
||||
return CharUtil.ParseLong(value);
|
||||
}
|
||||
|
||||
// We know the content length if it's a Web Socket message even if
|
||||
// Content-Length header is missing.
|
||||
long webSocketContentLength = GetWebSocketContentLength(message);
|
||||
if (webSocketContentLength >= 0)
|
||||
{
|
||||
return webSocketContentLength;
|
||||
}
|
||||
|
||||
// Otherwise we don't.
|
||||
throw new FormatException($"header not found: {HttpHeaderNames.ContentLength}");
|
||||
}
|
||||
|
||||
public static long GetContentLength(IHttpMessage message, long defaultValue)
|
||||
{
|
||||
if (message.Headers.TryGet(HttpHeaderNames.ContentLength, out ICharSequence value))
|
||||
{
|
||||
return CharUtil.ParseLong(value);
|
||||
}
|
||||
|
||||
// We know the content length if it's a Web Socket message even if
|
||||
// Content-Length header is missing.
|
||||
long webSocketContentLength = GetWebSocketContentLength(message);
|
||||
if (webSocketContentLength >= 0)
|
||||
{
|
||||
return webSocketContentLength;
|
||||
}
|
||||
|
||||
// Otherwise we don't.
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public static int GetContentLength(IHttpMessage message, int defaultValue) =>
|
||||
(int)Math.Min(int.MaxValue, GetContentLength(message, (long)defaultValue));
|
||||
|
||||
static int GetWebSocketContentLength(IHttpMessage message)
|
||||
{
|
||||
// WebSocket messages have constant content-lengths.
|
||||
HttpHeaders h = message.Headers;
|
||||
if (message is IHttpRequest req)
|
||||
{
|
||||
if (HttpMethod.Get.Equals(req.Method)
|
||||
&& h.Contains(HttpHeaderNames.SecWebsocketKey1)
|
||||
&& h.Contains(HttpHeaderNames.SecWebsocketKey2))
|
||||
{
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
else if (message is IHttpResponse res)
|
||||
{
|
||||
if (res.Status.Code == 101
|
||||
&& h.Contains(HttpHeaderNames.SecWebsocketOrigin)
|
||||
&& h.Contains(HttpHeaderNames.SecWebsocketLocation))
|
||||
{
|
||||
return 16;
|
||||
}
|
||||
}
|
||||
|
||||
// Not a web socket message
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static void SetContentLength(IHttpMessage message, long length) => message.Headers.Set(HttpHeaderNames.ContentLength, length);
|
||||
|
||||
public static bool IsContentLengthSet(IHttpMessage message) => message.Headers.Contains(HttpHeaderNames.ContentLength);
|
||||
|
||||
public static bool Is100ContinueExpected(IHttpMessage message)
|
||||
{
|
||||
if (!IsExpectHeaderValid(message))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ICharSequence expectValue = message.Headers.Get(HttpHeaderNames.Expect, null);
|
||||
// unquoted tokens in the expect header are case-insensitive, thus 100-continue is case insensitive
|
||||
return HttpHeaderValues.Continue.ContentEqualsIgnoreCase(expectValue);
|
||||
}
|
||||
|
||||
internal static bool IsUnsupportedExpectation(IHttpMessage message)
|
||||
{
|
||||
if (!IsExpectHeaderValid(message))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return message.Headers.TryGet(HttpHeaderNames.Expect, out ICharSequence expectValue)
|
||||
&& !HttpHeaderValues.Continue.ContentEqualsIgnoreCase(expectValue);
|
||||
}
|
||||
|
||||
// Expect: 100-continue is for requests only and it works only on HTTP/1.1 or later. Note further that RFC 7231
|
||||
// section 5.1.1 says "A server that receives a 100-continue expectation in an HTTP/1.0 request MUST ignore
|
||||
// that expectation."
|
||||
static bool IsExpectHeaderValid(IHttpMessage message) => message is IHttpRequest
|
||||
&& message.ProtocolVersion.CompareTo(HttpVersion.Http11) >= 0;
|
||||
|
||||
public static void Set100ContinueExpected(IHttpMessage message, bool expected)
|
||||
{
|
||||
if (expected)
|
||||
{
|
||||
message.Headers.Set(HttpHeaderNames.Expect, HttpHeaderValues.Continue);
|
||||
}
|
||||
else
|
||||
{
|
||||
message.Headers.Remove(HttpHeaderNames.Expect);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsTransferEncodingChunked(IHttpMessage message) => message.Headers.Contains(HttpHeaderNames.TransferEncoding, HttpHeaderValues.Chunked, true);
|
||||
|
||||
public static void SetTransferEncodingChunked(IHttpMessage m, bool chunked)
|
||||
{
|
||||
if (chunked)
|
||||
{
|
||||
m.Headers.Set(HttpHeaderNames.TransferEncoding, HttpHeaderValues.Chunked);
|
||||
m.Headers.Remove(HttpHeaderNames.ContentLength);
|
||||
}
|
||||
else
|
||||
{
|
||||
IList<ICharSequence> encodings = m.Headers.GetAll(HttpHeaderNames.TransferEncoding);
|
||||
if (encodings.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var values = new List<ICharSequence>(encodings);
|
||||
foreach (ICharSequence value in encodings)
|
||||
{
|
||||
if (HttpHeaderValues.Chunked.ContentEqualsIgnoreCase(value))
|
||||
{
|
||||
values.Remove(value);
|
||||
}
|
||||
}
|
||||
if (values.Count == 0)
|
||||
{
|
||||
m.Headers.Remove(HttpHeaderNames.TransferEncoding);
|
||||
}
|
||||
else
|
||||
{
|
||||
m.Headers.Set(HttpHeaderNames.TransferEncoding, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Encoding GetCharset(IHttpMessage message) => GetCharset(message, Encoding.UTF8);
|
||||
|
||||
public static Encoding GetCharset(ICharSequence contentTypeValue) => contentTypeValue != null ? GetCharset(contentTypeValue, Encoding.UTF8) : Encoding.UTF8;
|
||||
|
||||
public static Encoding GetCharset(IHttpMessage message, Encoding defaultCharset)
|
||||
{
|
||||
return message.Headers.TryGet(HttpHeaderNames.ContentType, out ICharSequence contentTypeValue)
|
||||
? GetCharset(contentTypeValue, defaultCharset)
|
||||
: defaultCharset;
|
||||
}
|
||||
|
||||
public static Encoding GetCharset(ICharSequence contentTypeValue, Encoding defaultCharset)
|
||||
{
|
||||
if (contentTypeValue != null)
|
||||
{
|
||||
ICharSequence charsetCharSequence = GetCharsetAsSequence(contentTypeValue);
|
||||
if (charsetCharSequence != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Encoding.GetEncoding(charsetCharSequence.ToString());
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return defaultCharset;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return defaultCharset;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return defaultCharset;
|
||||
}
|
||||
}
|
||||
|
||||
public static ICharSequence GetCharsetAsSequence(IHttpMessage message)
|
||||
=> message.Headers.TryGet(HttpHeaderNames.ContentType, out ICharSequence contentTypeValue) ? GetCharsetAsSequence(contentTypeValue) : null;
|
||||
|
||||
public static ICharSequence GetCharsetAsSequence(ICharSequence contentTypeValue)
|
||||
{
|
||||
if (contentTypeValue == null)
|
||||
{
|
||||
throw new ArgumentException(nameof(contentTypeValue));
|
||||
}
|
||||
int indexOfCharset = AsciiString.IndexOfIgnoreCaseAscii(contentTypeValue, CharsetEquals, 0);
|
||||
if (indexOfCharset != AsciiString.IndexNotFound)
|
||||
{
|
||||
int indexOfEncoding = indexOfCharset + CharsetEquals.Count;
|
||||
if (indexOfEncoding < contentTypeValue.Count)
|
||||
{
|
||||
return contentTypeValue.SubSequence(indexOfEncoding, contentTypeValue.Count);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static ICharSequence GetMimeType(IHttpMessage message) =>
|
||||
message.Headers.TryGet(HttpHeaderNames.ContentType, out ICharSequence contentTypeValue) ? GetMimeType(contentTypeValue) : null;
|
||||
|
||||
public static ICharSequence GetMimeType(ICharSequence contentTypeValue)
|
||||
{
|
||||
if (contentTypeValue == null)
|
||||
{
|
||||
throw new ArgumentException(nameof(contentTypeValue));
|
||||
}
|
||||
int indexOfSemicolon = AsciiString.IndexOfIgnoreCaseAscii(contentTypeValue, Semicolon, 0);
|
||||
if (indexOfSemicolon != AsciiString.IndexNotFound)
|
||||
{
|
||||
return contentTypeValue.SubSequence(0, indexOfSemicolon);
|
||||
}
|
||||
|
||||
return contentTypeValue.Count > 0 ? contentTypeValue : null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ConvertToAutoPropertyWhenPossible
|
||||
// ReSharper disable ConvertToAutoProperty
|
||||
namespace DotNetty.Codecs.Http
|
||||
{
|
||||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public class HttpVersion : IComparable<HttpVersion>, IComparable
|
||||
{
|
||||
static readonly Regex VersionPattern = new Regex("^(\\S+)/(\\d+)\\.(\\d+)$", RegexOptions.Compiled);
|
||||
|
||||
internal static readonly AsciiString Http10String = new AsciiString("HTTP/1.0");
|
||||
internal static readonly AsciiString Http11String = new AsciiString("HTTP/1.1");
|
||||
|
||||
public static readonly HttpVersion Http10 = new HttpVersion("HTTP", 1, 0, false, true);
|
||||
public static readonly HttpVersion Http11 = new HttpVersion("HTTP", 1, 1, true, true);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static HttpVersion ValueOf(AsciiString text)
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentException_NullText();
|
||||
}
|
||||
|
||||
// ReSharper disable once PossibleNullReferenceException
|
||||
HttpVersion version = ValueOfInline(text.Array);
|
||||
if (version != null)
|
||||
{
|
||||
return version;
|
||||
}
|
||||
|
||||
// Fall back to slow path
|
||||
text = text.Trim();
|
||||
|
||||
if (text.Count == 0)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentException_EmptyText();
|
||||
}
|
||||
|
||||
// Try to match without convert to uppercase first as this is what 99% of all clients
|
||||
// will send anyway. Also there is a change to the RFC to make it clear that it is
|
||||
// expected to be case-sensitive
|
||||
//
|
||||
// See:
|
||||
// * http://trac.tools.ietf.org/wg/httpbis/trac/ticket/1
|
||||
// * http://trac.tools.ietf.org/wg/httpbis/trac/wiki
|
||||
//
|
||||
return Version0(text) ?? new HttpVersion(text.ToString(), true);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static HttpVersion ValueOfInline(byte[] bytes)
|
||||
{
|
||||
if (bytes.Length != 8) return null;
|
||||
|
||||
if (bytes[0] != (byte)'H') return null;
|
||||
if (bytes[1] != (byte)'T') return null;
|
||||
if (bytes[2] != (byte)'T') return null;
|
||||
if (bytes[3] != (byte)'P') return null;
|
||||
if (bytes[4] != (byte)'/') return null;
|
||||
if (bytes[5] != (byte)'1') return null;
|
||||
if (bytes[6] != (byte)'.') return null;
|
||||
switch (bytes[7])
|
||||
{
|
||||
case (byte)'1':
|
||||
return Http11;
|
||||
case (byte)'0':
|
||||
return Http10;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static HttpVersion Version0(AsciiString text)
|
||||
{
|
||||
if (Http11String.Equals(text))
|
||||
{
|
||||
return Http11;
|
||||
}
|
||||
if (Http10String.Equals(text))
|
||||
{
|
||||
return Http10;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
readonly string protocolName;
|
||||
readonly int majorVersion;
|
||||
readonly int minorVersion;
|
||||
readonly AsciiString text;
|
||||
readonly bool keepAliveDefault;
|
||||
readonly byte[] bytes;
|
||||
|
||||
public HttpVersion(string text, bool keepAliveDefault)
|
||||
{
|
||||
Contract.Requires(text != null);
|
||||
|
||||
text = text.Trim().ToUpper();
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
throw new ArgumentException("empty text");
|
||||
}
|
||||
|
||||
Match match = VersionPattern.Match(text);
|
||||
if (!match.Success)
|
||||
{
|
||||
throw new ArgumentException($"invalid version format: {text}");
|
||||
}
|
||||
|
||||
this.protocolName = match.Groups[1].Value;
|
||||
this.majorVersion = int.Parse(match.Groups[2].Value);
|
||||
this.minorVersion = int.Parse(match.Groups[3].Value);
|
||||
this.text = new AsciiString($"{this.ProtocolName}/{this.MajorVersion}.{this.MinorVersion}");
|
||||
this.keepAliveDefault = keepAliveDefault;
|
||||
this.bytes = null;
|
||||
}
|
||||
|
||||
HttpVersion(string protocolName, int majorVersion, int minorVersion, bool keepAliveDefault, bool bytes)
|
||||
{
|
||||
if (protocolName == null)
|
||||
{
|
||||
throw new ArgumentException(nameof(protocolName));
|
||||
}
|
||||
|
||||
protocolName = protocolName.Trim().ToUpper();
|
||||
if (string.IsNullOrEmpty(protocolName))
|
||||
{
|
||||
throw new ArgumentException("empty protocolName");
|
||||
}
|
||||
|
||||
// ReSharper disable once ForCanBeConvertedToForeach
|
||||
for (int i = 0; i < protocolName.Length; i++)
|
||||
{
|
||||
char c = protocolName[i];
|
||||
if (CharUtil.IsISOControl(c) || char.IsWhiteSpace(c))
|
||||
{
|
||||
throw new ArgumentException($"invalid character {c} in protocolName");
|
||||
}
|
||||
}
|
||||
|
||||
if (majorVersion < 0)
|
||||
{
|
||||
throw new ArgumentException("negative majorVersion");
|
||||
}
|
||||
if (minorVersion < 0)
|
||||
{
|
||||
throw new ArgumentException("negative minorVersion");
|
||||
}
|
||||
|
||||
this.protocolName = protocolName;
|
||||
this.majorVersion = majorVersion;
|
||||
this.minorVersion = minorVersion;
|
||||
this.text = new AsciiString(protocolName + '/' + majorVersion + '.' + minorVersion);
|
||||
this.keepAliveDefault = keepAliveDefault;
|
||||
|
||||
this.bytes = bytes ? this.text.Array : null;
|
||||
}
|
||||
|
||||
public string ProtocolName => this.protocolName;
|
||||
|
||||
public int MajorVersion => this.majorVersion;
|
||||
|
||||
public int MinorVersion => this.minorVersion;
|
||||
|
||||
public AsciiString Text => this.text;
|
||||
|
||||
public bool IsKeepAliveDefault => this.keepAliveDefault;
|
||||
|
||||
public override string ToString() => this.text.ToString();
|
||||
|
||||
public override int GetHashCode() => (this.protocolName.GetHashCode() * 31 + this.majorVersion) * 31 + this.minorVersion;
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (!(obj is HttpVersion that))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.minorVersion == that.minorVersion
|
||||
&& this.majorVersion == that.majorVersion
|
||||
&& this.protocolName.Equals(that.protocolName);
|
||||
}
|
||||
|
||||
public int CompareTo(HttpVersion other)
|
||||
{
|
||||
int v = string.CompareOrdinal(this.protocolName, other.protocolName);
|
||||
if (v != 0)
|
||||
{
|
||||
return v;
|
||||
}
|
||||
|
||||
v = this.majorVersion - other.majorVersion;
|
||||
if (v != 0)
|
||||
{
|
||||
return v;
|
||||
}
|
||||
|
||||
return this.minorVersion - other.minorVersion;
|
||||
}
|
||||
|
||||
public int CompareTo(object obj)
|
||||
{
|
||||
if (ReferenceEquals(this, obj))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!(obj is HttpVersion))
|
||||
{
|
||||
throw new ArgumentException($"{nameof(obj)} must be of {nameof(HttpVersion)} type");
|
||||
}
|
||||
|
||||
return this.CompareTo((HttpVersion)obj);
|
||||
}
|
||||
|
||||
internal void Encode(IByteBuffer buf)
|
||||
{
|
||||
if (this.bytes == null)
|
||||
{
|
||||
buf.WriteCharSequence(this.text, Encoding.ASCII);
|
||||
}
|
||||
else
|
||||
{
|
||||
buf.WriteBytes(this.bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
// 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
|
||||
{
|
||||
public interface IFullHttpMessage : IHttpMessage, ILastHttpContent
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
// 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
|
||||
{
|
||||
public interface IFullHttpRequest : IHttpRequest, IFullHttpMessage
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
// 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
|
||||
{
|
||||
public interface IFullHttpResponse : IHttpResponse, IFullHttpMessage
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// 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
|
||||
{
|
||||
using DotNetty.Buffers;
|
||||
|
||||
public interface IHttpContent : IHttpObject, IByteBufferHolder
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// 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
|
||||
{
|
||||
public interface IHttpMessage : IHttpObject
|
||||
{
|
||||
HttpVersion ProtocolVersion { get; }
|
||||
|
||||
IHttpMessage SetProtocolVersion(HttpVersion version);
|
||||
|
||||
HttpHeaders Headers { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
// 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
|
||||
{
|
||||
public interface IHttpObject : IDecoderResultProvider
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// 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
|
||||
{
|
||||
public interface IHttpRequest : IHttpMessage
|
||||
{
|
||||
HttpMethod Method { get; }
|
||||
|
||||
IHttpRequest SetMethod(HttpMethod method);
|
||||
|
||||
string Uri { get; }
|
||||
|
||||
IHttpRequest SetUri(string uri);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
// 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
|
||||
{
|
||||
public interface IHttpResponse : IHttpMessage
|
||||
{
|
||||
HttpResponseStatus Status { get; }
|
||||
|
||||
IHttpResponse SetStatus(HttpResponseStatus status);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
// 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
|
||||
{
|
||||
public interface ILastHttpContent : IHttpContent
|
||||
{
|
||||
HttpHeaders TrailingHeaders { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,341 @@
|
|||
// 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.Multipart
|
||||
{
|
||||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common;
|
||||
using DotNetty.Common.Internal.Logging;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public abstract class AbstractDiskHttpData : AbstractHttpData
|
||||
{
|
||||
static readonly IInternalLogger Logger = InternalLoggerFactory.GetInstance<AbstractDiskHttpData>();
|
||||
|
||||
FileStream fileStream;
|
||||
|
||||
protected AbstractDiskHttpData(string name, Encoding charset, long size) : base(name, charset, size)
|
||||
{
|
||||
}
|
||||
|
||||
protected abstract string DiskFilename { get; }
|
||||
|
||||
protected abstract string Prefix { get; }
|
||||
|
||||
protected abstract string BaseDirectory { get; }
|
||||
|
||||
protected abstract string Postfix { get; }
|
||||
|
||||
protected abstract bool DeleteOnExit { get; }
|
||||
|
||||
FileStream TempFile()
|
||||
{
|
||||
string newpostfix;
|
||||
string diskFilename = this.DiskFilename;
|
||||
if (diskFilename != null)
|
||||
{
|
||||
newpostfix = '_' + diskFilename;
|
||||
}
|
||||
else
|
||||
{
|
||||
newpostfix = this.Postfix;
|
||||
}
|
||||
string directory = this.BaseDirectory == null
|
||||
? Path.GetTempPath()
|
||||
: Path.Combine(Path.GetTempPath(), this.BaseDirectory);
|
||||
// File.createTempFile
|
||||
string fileName = Path.Combine(directory, $"{this.Prefix}{Path.GetRandomFileName()}{newpostfix}");
|
||||
FileStream tmpFile = File.Create(fileName, 4096, // DefaultBufferSize
|
||||
this.DeleteOnExit ? FileOptions.DeleteOnClose : FileOptions.None);
|
||||
return tmpFile;
|
||||
}
|
||||
|
||||
public override void SetContent(IByteBuffer buffer)
|
||||
{
|
||||
Contract.Requires(buffer != null);
|
||||
try
|
||||
{
|
||||
this.Size = buffer.ReadableBytes;
|
||||
this.CheckSize(this.Size);
|
||||
if (this.DefinedSize > 0 && this.DefinedSize < this.Size)
|
||||
{
|
||||
throw new IOException($"Out of size: {this.Size} > {this.DefinedSize}");
|
||||
}
|
||||
if (this.fileStream == null)
|
||||
{
|
||||
this.fileStream = this.TempFile();
|
||||
}
|
||||
if (buffer.ReadableBytes == 0)
|
||||
{
|
||||
// empty file
|
||||
return;
|
||||
}
|
||||
|
||||
buffer.GetBytes(buffer.ReaderIndex, this.fileStream, buffer.ReadableBytes);
|
||||
buffer.SetReaderIndex(buffer.ReaderIndex + buffer.ReadableBytes);
|
||||
this.fileStream.Flush();
|
||||
this.SetCompleted();
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Release the buffer as it was retained before and we not need a reference to it at all
|
||||
// See https://github.com/netty/netty/issues/1516
|
||||
buffer.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public override void AddContent(IByteBuffer buffer, bool last)
|
||||
{
|
||||
if (buffer != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
int localsize = buffer.ReadableBytes;
|
||||
this.CheckSize(this.Size + localsize);
|
||||
if (this.DefinedSize > 0 && this.DefinedSize < this.Size + localsize)
|
||||
{
|
||||
throw new IOException($"Out of size: {this.Size} > {this.DefinedSize}");
|
||||
}
|
||||
if (this.fileStream == null)
|
||||
{
|
||||
this.fileStream = this.TempFile();
|
||||
}
|
||||
buffer.GetBytes(buffer.ReaderIndex, this.fileStream, buffer.ReadableBytes);
|
||||
buffer.SetReaderIndex(buffer.ReaderIndex + localsize);
|
||||
this.fileStream.Flush();
|
||||
|
||||
this.Size += buffer.ReadableBytes;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Release the buffer as it was retained before and we not need a reference to it at all
|
||||
// See https://github.com/netty/netty/issues/1516
|
||||
buffer.Release();
|
||||
}
|
||||
}
|
||||
if (last)
|
||||
{
|
||||
if (this.fileStream == null)
|
||||
{
|
||||
this.fileStream = this.TempFile();
|
||||
}
|
||||
this.SetCompleted();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (buffer == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(buffer));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetContent(Stream source)
|
||||
{
|
||||
Contract.Requires(source != null);
|
||||
|
||||
if (this.fileStream != null)
|
||||
{
|
||||
this.Delete();
|
||||
}
|
||||
|
||||
this.fileStream = this.TempFile();
|
||||
int written = 0;
|
||||
var bytes = new byte[4096 * 4];
|
||||
while (true)
|
||||
{
|
||||
int read = source.Read(bytes, 0, bytes.Length);
|
||||
if (read <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
written += read;
|
||||
this.CheckSize(written);
|
||||
this.fileStream.Write(bytes, 0, read);
|
||||
}
|
||||
this.fileStream.Flush();
|
||||
// Reset the position to start for reads
|
||||
this.fileStream.Position -= written;
|
||||
|
||||
this.Size = written;
|
||||
if (this.DefinedSize > 0 && this.DefinedSize < this.Size)
|
||||
{
|
||||
try
|
||||
{
|
||||
Delete(this.fileStream);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
Logger.Warn("Failed to delete: {} {}", this.fileStream, error);
|
||||
}
|
||||
this.fileStream = null;
|
||||
throw new IOException($"Out of size: {this.Size} > {this.DefinedSize}");
|
||||
}
|
||||
//isRenamed = true;
|
||||
this.SetCompleted();
|
||||
}
|
||||
|
||||
public override void Delete()
|
||||
{
|
||||
if (this.fileStream != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Delete(this.fileStream);
|
||||
}
|
||||
catch (IOException error)
|
||||
{
|
||||
Logger.Warn("Failed to delete file.", error);
|
||||
}
|
||||
|
||||
this.fileStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override byte[] GetBytes() => this.fileStream == null
|
||||
? ArrayExtensions.ZeroBytes : ReadFrom(this.fileStream);
|
||||
|
||||
public override IByteBuffer GetByteBuffer()
|
||||
{
|
||||
if (this.fileStream == null)
|
||||
{
|
||||
return Unpooled.Empty;
|
||||
}
|
||||
|
||||
byte[] array = ReadFrom(this.fileStream);
|
||||
return Unpooled.WrappedBuffer(array);
|
||||
}
|
||||
|
||||
public override IByteBuffer GetChunk(int length)
|
||||
{
|
||||
if (this.fileStream == null || length == 0)
|
||||
{
|
||||
return Unpooled.Empty;
|
||||
}
|
||||
int read = 0;
|
||||
var bytes = new byte[length];
|
||||
while (read < length)
|
||||
{
|
||||
int readnow = this.fileStream.Read(bytes, read, length - read);
|
||||
if (readnow <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
read += readnow;
|
||||
}
|
||||
if (read == 0)
|
||||
{
|
||||
return Unpooled.Empty;
|
||||
}
|
||||
IByteBuffer buffer = Unpooled.WrappedBuffer(bytes);
|
||||
buffer.SetReaderIndex(0);
|
||||
buffer.SetWriterIndex(read);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public override string GetString() => this.GetString(HttpConstants.DefaultEncoding);
|
||||
|
||||
public override string GetString(Encoding encoding)
|
||||
{
|
||||
if (this.fileStream == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
byte[] array = ReadFrom(this.fileStream);
|
||||
if (encoding == null)
|
||||
{
|
||||
encoding = HttpConstants.DefaultEncoding;
|
||||
}
|
||||
|
||||
return encoding.GetString(array);
|
||||
}
|
||||
|
||||
public override bool IsInMemory => false;
|
||||
|
||||
public override bool RenameTo(FileStream destination)
|
||||
{
|
||||
Contract.Requires(destination != null);
|
||||
if (this.fileStream == null)
|
||||
{
|
||||
throw new InvalidOperationException("No file defined so cannot be renamed");
|
||||
}
|
||||
|
||||
// must copy
|
||||
long chunkSize = 8196;
|
||||
int position = 0;
|
||||
while (position < this.Size)
|
||||
{
|
||||
if (chunkSize < this.Size - position)
|
||||
{
|
||||
chunkSize = this.Size - position;
|
||||
}
|
||||
|
||||
var buffer = new byte[chunkSize];
|
||||
int read = this.fileStream.Read(buffer, 0, (int)chunkSize);
|
||||
if (read <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
destination.Write(buffer, 0, read);
|
||||
position += read;
|
||||
}
|
||||
|
||||
if (position == this.Size)
|
||||
{
|
||||
try
|
||||
{
|
||||
Delete(this.fileStream);
|
||||
}
|
||||
catch (IOException exception)
|
||||
{
|
||||
Logger.Warn("Failed to delete file.", exception);
|
||||
}
|
||||
this.fileStream = destination;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
Delete(destination);
|
||||
}
|
||||
catch (IOException exception)
|
||||
{
|
||||
Logger.Warn("Failed to delete file.", exception);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void Delete(FileStream fileStream)
|
||||
{
|
||||
string fileName = fileStream.Name;
|
||||
fileStream.Dispose();
|
||||
File.Delete(fileName);
|
||||
}
|
||||
|
||||
static byte[] ReadFrom(Stream fileStream)
|
||||
{
|
||||
long srcsize = fileStream.Length;
|
||||
if (srcsize > int.MaxValue)
|
||||
{
|
||||
throw new ArgumentException("File too big to be loaded in memory");
|
||||
}
|
||||
|
||||
var array = new byte[(int)srcsize];
|
||||
fileStream.Read(array, 0, array.Length);
|
||||
return array;
|
||||
}
|
||||
|
||||
public override FileStream GetFile() => this.fileStream;
|
||||
|
||||
public override IReferenceCounted Touch(object hint) => this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ConvertToAutoPropertyWhenPossible
|
||||
// ReSharper disable ConvertToAutoProperty
|
||||
// ReSharper disable ConvertToAutoPropertyWithPrivateSetter
|
||||
namespace DotNetty.Codecs.Http.Multipart
|
||||
{
|
||||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public abstract class AbstractHttpData : AbstractReferenceCounted, IHttpData
|
||||
{
|
||||
static readonly Regex StripPattern = new Regex("(?:^\\s+|\\s+$|\\n)", RegexOptions.Compiled);
|
||||
static readonly Regex ReplacePattern = new Regex("[\\r\\t]", RegexOptions.Compiled);
|
||||
|
||||
readonly string name;
|
||||
protected long DefinedSize;
|
||||
protected long Size;
|
||||
Encoding charset = HttpConstants.DefaultEncoding;
|
||||
bool completed;
|
||||
long maxSize = DefaultHttpDataFactory.MaxSize;
|
||||
|
||||
protected AbstractHttpData(string name, Encoding charset, long size)
|
||||
{
|
||||
Contract.Requires(name != null);
|
||||
|
||||
name = StripPattern.Replace(name, " ");
|
||||
name = ReplacePattern.Replace(name, "");
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
throw new ArgumentException("empty name");
|
||||
}
|
||||
|
||||
this.name = name;
|
||||
if (charset != null)
|
||||
{
|
||||
this.charset = charset;
|
||||
}
|
||||
|
||||
this.DefinedSize = size;
|
||||
}
|
||||
|
||||
public long MaxSize
|
||||
{
|
||||
get => this.maxSize;
|
||||
set => this.maxSize = value;
|
||||
}
|
||||
|
||||
public void CheckSize(long newSize)
|
||||
{
|
||||
if (this.MaxSize >= 0 && newSize > this.MaxSize)
|
||||
{
|
||||
throw new IOException("Size exceed allowed maximum capacity");
|
||||
}
|
||||
}
|
||||
|
||||
public string Name => this.name;
|
||||
|
||||
public bool IsCompleted => this.completed;
|
||||
|
||||
protected void SetCompleted() => this.completed = true;
|
||||
|
||||
public Encoding Charset
|
||||
{
|
||||
get => this.charset;
|
||||
set
|
||||
{
|
||||
Contract.Requires(value != null);
|
||||
this.charset = value;
|
||||
}
|
||||
}
|
||||
|
||||
public long Length => this.Size;
|
||||
|
||||
public long DefinedLength => this.DefinedSize;
|
||||
|
||||
public IByteBuffer Content
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
return this.GetByteBuffer();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new ChannelException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Deallocate() => this.Delete();
|
||||
|
||||
public abstract int CompareTo(IInterfaceHttpData other);
|
||||
|
||||
public abstract HttpDataType DataType { get; }
|
||||
|
||||
public abstract IByteBufferHolder Copy();
|
||||
|
||||
public abstract IByteBufferHolder Duplicate();
|
||||
|
||||
public abstract IByteBufferHolder RetainedDuplicate();
|
||||
|
||||
public abstract void SetContent(IByteBuffer buffer);
|
||||
|
||||
public abstract void SetContent(Stream source);
|
||||
|
||||
public abstract void AddContent(IByteBuffer buffer, bool last);
|
||||
|
||||
public abstract void Delete();
|
||||
|
||||
public abstract byte[] GetBytes();
|
||||
|
||||
public abstract IByteBuffer GetByteBuffer();
|
||||
|
||||
public abstract IByteBuffer GetChunk(int length);
|
||||
|
||||
public virtual string GetString() => this.GetString(this.charset);
|
||||
|
||||
public abstract string GetString(Encoding encoding);
|
||||
|
||||
public abstract bool RenameTo(FileStream destination);
|
||||
|
||||
public abstract bool IsInMemory { get; }
|
||||
|
||||
public abstract FileStream GetFile();
|
||||
|
||||
public abstract IByteBufferHolder Replace(IByteBuffer content);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
// 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.Multipart
|
||||
{
|
||||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common;
|
||||
|
||||
public abstract class AbstractMemoryHttpData : AbstractHttpData
|
||||
{
|
||||
IByteBuffer byteBuf;
|
||||
int chunkPosition;
|
||||
|
||||
protected AbstractMemoryHttpData(string name, Encoding charset, long size)
|
||||
: base(name, charset, size)
|
||||
{
|
||||
}
|
||||
|
||||
public override void SetContent(IByteBuffer buffer)
|
||||
{
|
||||
Contract.Requires(buffer != null);
|
||||
|
||||
long localsize = buffer.ReadableBytes;
|
||||
this.CheckSize(localsize);
|
||||
if (this.DefinedSize > 0 && this.DefinedSize < localsize)
|
||||
{
|
||||
throw new IOException($"Out of size: {localsize} > {this.DefinedSize}");
|
||||
}
|
||||
this.byteBuf?.Release();
|
||||
|
||||
this.byteBuf = buffer;
|
||||
this.Size = localsize;
|
||||
this.SetCompleted();
|
||||
}
|
||||
|
||||
public override void SetContent(Stream inputStream)
|
||||
{
|
||||
Contract.Requires(inputStream != null);
|
||||
|
||||
if (!inputStream.CanRead)
|
||||
{
|
||||
throw new ArgumentException($"{nameof(inputStream)} is not readable");
|
||||
}
|
||||
|
||||
IByteBuffer buffer = Unpooled.Buffer();
|
||||
var bytes = new byte[4096 * 4];
|
||||
int written = 0;
|
||||
while (true)
|
||||
{
|
||||
int read = inputStream.Read(bytes, 0, bytes.Length);
|
||||
if (read <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
buffer.WriteBytes(bytes, 0, read);
|
||||
written += read;
|
||||
this.CheckSize(written);
|
||||
}
|
||||
this.Size = written;
|
||||
if (this.DefinedSize > 0 && this.DefinedSize < this.Size)
|
||||
{
|
||||
throw new IOException($"Out of size: {this.Size} > {this.DefinedSize}");
|
||||
}
|
||||
|
||||
this.byteBuf?.Release();
|
||||
this.byteBuf = buffer;
|
||||
this.SetCompleted();
|
||||
}
|
||||
|
||||
public override void AddContent(IByteBuffer buffer, bool last)
|
||||
{
|
||||
if (buffer != null)
|
||||
{
|
||||
long localsize = buffer.ReadableBytes;
|
||||
this.CheckSize(this.Size + localsize);
|
||||
if (this.DefinedSize > 0 && this.DefinedSize < this.Size + localsize)
|
||||
{
|
||||
throw new IOException($"Out of size: {(this.Size + localsize)} > {this.DefinedSize}");
|
||||
}
|
||||
|
||||
this.Size += localsize;
|
||||
if (this.byteBuf == null)
|
||||
{
|
||||
this.byteBuf = buffer;
|
||||
}
|
||||
else if (this.byteBuf is CompositeByteBuffer buf)
|
||||
{
|
||||
buf.AddComponent(true, buffer);
|
||||
buf.SetWriterIndex((int)this.Size);
|
||||
}
|
||||
else
|
||||
{
|
||||
CompositeByteBuffer compositeBuffer = Unpooled.CompositeBuffer(int.MaxValue);
|
||||
compositeBuffer.AddComponents(true, this.byteBuf, buffer);
|
||||
compositeBuffer.SetWriterIndex((int)this.Size);
|
||||
this.byteBuf = compositeBuffer;
|
||||
}
|
||||
}
|
||||
if (last)
|
||||
{
|
||||
this.SetCompleted();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (buffer == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(buffer));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Delete()
|
||||
{
|
||||
if (this.byteBuf != null)
|
||||
{
|
||||
this.byteBuf.Release();
|
||||
this.byteBuf = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override byte[] GetBytes()
|
||||
{
|
||||
if (this.byteBuf == null)
|
||||
{
|
||||
return Unpooled.Empty.Array;
|
||||
}
|
||||
|
||||
var array = new byte[this.byteBuf.ReadableBytes];
|
||||
this.byteBuf.GetBytes(this.byteBuf.ReaderIndex, array);
|
||||
return array;
|
||||
}
|
||||
|
||||
public override string GetString() => this.GetString(HttpConstants.DefaultEncoding);
|
||||
|
||||
public override string GetString(Encoding encoding)
|
||||
{
|
||||
if (this.byteBuf == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
if (encoding == null)
|
||||
{
|
||||
encoding = HttpConstants.DefaultEncoding;
|
||||
}
|
||||
return this.byteBuf.ToString(encoding);
|
||||
}
|
||||
|
||||
public override IByteBuffer GetByteBuffer() => this.byteBuf;
|
||||
|
||||
public override IByteBuffer GetChunk(int length)
|
||||
{
|
||||
if (this.byteBuf == null || length == 0 || this.byteBuf.ReadableBytes == 0)
|
||||
{
|
||||
this.chunkPosition = 0;
|
||||
return Unpooled.Empty;
|
||||
}
|
||||
int sizeLeft = this.byteBuf.ReadableBytes - this.chunkPosition;
|
||||
if (sizeLeft == 0)
|
||||
{
|
||||
this.chunkPosition = 0;
|
||||
return Unpooled.Empty;
|
||||
}
|
||||
int sliceLength = length;
|
||||
if (sizeLeft < length)
|
||||
{
|
||||
sliceLength = sizeLeft;
|
||||
}
|
||||
|
||||
IByteBuffer chunk = this.byteBuf.RetainedSlice(this.chunkPosition, sliceLength);
|
||||
this.chunkPosition += sliceLength;
|
||||
return chunk;
|
||||
}
|
||||
|
||||
public override bool IsInMemory => true;
|
||||
|
||||
public override bool RenameTo(FileStream destination)
|
||||
{
|
||||
Contract.Requires(destination != null);
|
||||
|
||||
if (!destination.CanWrite)
|
||||
{
|
||||
throw new ArgumentException($"{nameof(destination)} is not writable");
|
||||
}
|
||||
if (this.byteBuf == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
this.byteBuf.GetBytes(this.byteBuf.ReaderIndex, destination, this.byteBuf.ReadableBytes);
|
||||
destination.Flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
public override FileStream GetFile() => throw new IOException("Not represented by a stream");
|
||||
|
||||
public override IReferenceCounted Touch(object hint)
|
||||
{
|
||||
this.byteBuf?.Touch(hint);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
// 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.Multipart
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
sealed class CaseIgnoringComparator : IEqualityComparer<ICharSequence>, IComparer<ICharSequence>
|
||||
{
|
||||
public static readonly IEqualityComparer<ICharSequence> Default = new CaseIgnoringComparator();
|
||||
|
||||
CaseIgnoringComparator()
|
||||
{
|
||||
}
|
||||
|
||||
public int Compare(ICharSequence x, ICharSequence y)
|
||||
{
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (x == null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (y == null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
int o1Length = x.Count;
|
||||
int o2Length = y.Count;
|
||||
int min = Math.Min(o1Length, o2Length);
|
||||
for (int i = 0; i < min; i++)
|
||||
{
|
||||
char c1 = x[i];
|
||||
char c2 = y[i];
|
||||
if (c1 != c2)
|
||||
{
|
||||
c1 = char.ToUpper(c1);
|
||||
c2 = char.ToUpper(c2);
|
||||
if (c1 != c2)
|
||||
{
|
||||
c1 = char.ToLower(c1);
|
||||
c2 = char.ToLower(c2);
|
||||
if (c1 != c2)
|
||||
{
|
||||
return c1 - c2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return o1Length - o2Length;
|
||||
}
|
||||
|
||||
public bool Equals(ICharSequence x, ICharSequence y)
|
||||
{
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (x == null || y == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int o1Length = x.Count;
|
||||
int o2Length = y.Count;
|
||||
|
||||
if (o1Length != o2Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < o1Length; i++)
|
||||
{
|
||||
char c1 = x[i];
|
||||
char c2 = y[i];
|
||||
if (c1 != c2)
|
||||
{
|
||||
c1 = char.ToUpper(c1);
|
||||
c2 = char.ToUpper(c2);
|
||||
if (c1 != c2)
|
||||
{
|
||||
c1 = char.ToLower(c1);
|
||||
c2 = char.ToLower(c2);
|
||||
if (c1 != c2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public int GetHashCode(ICharSequence obj) => obj.HashCode(true);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,283 @@
|
|||
// 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.Multipart
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
public class DefaultHttpDataFactory : IHttpDataFactory
|
||||
{
|
||||
// Proposed default MINSIZE as 16 KB.
|
||||
public static readonly long MinSize = 0x4000;
|
||||
|
||||
// Proposed default MAXSIZE = -1 as UNLIMITED
|
||||
public static readonly long MaxSize = -1;
|
||||
|
||||
readonly bool useDisk;
|
||||
readonly bool checkSize;
|
||||
readonly long minSize;
|
||||
long maxSize = MaxSize;
|
||||
readonly Encoding charset = HttpConstants.DefaultEncoding;
|
||||
|
||||
// Keep all HttpDatas until cleanAllHttpData() is called.
|
||||
readonly ConcurrentDictionary<IHttpRequest, List<IHttpData>> requestFileDeleteMap =
|
||||
new ConcurrentDictionary<IHttpRequest, List<IHttpData>>(IdentityComparer.Default);
|
||||
|
||||
// HttpData will be in memory if less than default size (16KB).
|
||||
// The type will be Mixed.
|
||||
public DefaultHttpDataFactory()
|
||||
{
|
||||
this.useDisk = false;
|
||||
this.checkSize = true;
|
||||
this.minSize = MinSize;
|
||||
}
|
||||
|
||||
public DefaultHttpDataFactory(Encoding charset) : this()
|
||||
{
|
||||
this.charset = charset;
|
||||
}
|
||||
|
||||
// HttpData will be always on Disk if useDisk is True, else always in Memory if False
|
||||
public DefaultHttpDataFactory(bool useDisk)
|
||||
{
|
||||
this.useDisk = useDisk;
|
||||
this.checkSize = false;
|
||||
}
|
||||
|
||||
public DefaultHttpDataFactory(bool useDisk, Encoding charset) : this(useDisk)
|
||||
{
|
||||
this.charset = charset;
|
||||
}
|
||||
|
||||
public DefaultHttpDataFactory(long minSize)
|
||||
{
|
||||
this.useDisk = false;
|
||||
this.checkSize = true;
|
||||
this.minSize = minSize;
|
||||
}
|
||||
|
||||
public DefaultHttpDataFactory(long minSize, Encoding charset) : this(minSize)
|
||||
{
|
||||
this.charset = charset;
|
||||
}
|
||||
|
||||
public void SetMaxLimit(long max) => this.maxSize = max;
|
||||
|
||||
List<IHttpData> GetList(IHttpRequest request)
|
||||
{
|
||||
List<IHttpData> list = this.requestFileDeleteMap.GetOrAdd(request, _ => new List<IHttpData>());
|
||||
return list;
|
||||
}
|
||||
|
||||
public IAttribute CreateAttribute(IHttpRequest request, string name)
|
||||
{
|
||||
if (this.useDisk)
|
||||
{
|
||||
var diskAttribute = new DiskAttribute(name, this.charset);
|
||||
diskAttribute.MaxSize = this.maxSize;
|
||||
List<IHttpData> list = this.GetList(request);
|
||||
list.Add(diskAttribute);
|
||||
return diskAttribute;
|
||||
}
|
||||
if (this.checkSize)
|
||||
{
|
||||
var mixedAttribute = new MixedAttribute(name, this.minSize, this.charset);
|
||||
mixedAttribute.MaxSize = this.maxSize;
|
||||
List<IHttpData> list = this.GetList(request);
|
||||
list.Add(mixedAttribute);
|
||||
return mixedAttribute;
|
||||
}
|
||||
var attribute = new MemoryAttribute(name);
|
||||
attribute.MaxSize = this.maxSize;
|
||||
return attribute;
|
||||
}
|
||||
|
||||
public IAttribute CreateAttribute(IHttpRequest request, string name, long definedSize)
|
||||
{
|
||||
if (this.useDisk)
|
||||
{
|
||||
var diskAttribute = new DiskAttribute(name, definedSize, this.charset);
|
||||
diskAttribute.MaxSize = this.maxSize;
|
||||
List<IHttpData> list = this.GetList(request);
|
||||
list.Add(diskAttribute);
|
||||
return diskAttribute;
|
||||
}
|
||||
if (this.checkSize)
|
||||
{
|
||||
var mixedAttribute = new MixedAttribute(name, definedSize, this.minSize, this.charset);
|
||||
mixedAttribute.MaxSize = this.maxSize;
|
||||
List<IHttpData> list = this.GetList(request);
|
||||
list.Add(mixedAttribute);
|
||||
return mixedAttribute;
|
||||
}
|
||||
var attribute = new MemoryAttribute(name, definedSize);
|
||||
attribute.MaxSize = this.maxSize;
|
||||
return attribute;
|
||||
}
|
||||
|
||||
static void CheckHttpDataSize(IHttpData data)
|
||||
{
|
||||
try
|
||||
{
|
||||
data.CheckSize(data.Length);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
throw new ArgumentException("Attribute bigger than maxSize allowed");
|
||||
}
|
||||
}
|
||||
|
||||
public IAttribute CreateAttribute(IHttpRequest request, string name, string value)
|
||||
{
|
||||
if (this.useDisk)
|
||||
{
|
||||
IAttribute attribute;
|
||||
try
|
||||
{
|
||||
attribute = new DiskAttribute(name, value, this.charset);
|
||||
attribute.MaxSize = this.maxSize;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// revert to Mixed mode
|
||||
attribute = new MixedAttribute(name, value, this.minSize, this.charset);
|
||||
attribute.MaxSize = this.maxSize;
|
||||
}
|
||||
CheckHttpDataSize(attribute);
|
||||
List<IHttpData> list = this.GetList(request);
|
||||
list.Add(attribute);
|
||||
return attribute;
|
||||
}
|
||||
if (this.checkSize)
|
||||
{
|
||||
var mixedAttribute = new MixedAttribute(name, value, this.minSize, this.charset);
|
||||
mixedAttribute.MaxSize = this.maxSize;
|
||||
CheckHttpDataSize(mixedAttribute);
|
||||
List<IHttpData> list = this.GetList(request);
|
||||
list.Add(mixedAttribute);
|
||||
return mixedAttribute;
|
||||
}
|
||||
try
|
||||
{
|
||||
var attribute = new MemoryAttribute(name, value, this.charset);
|
||||
attribute.MaxSize = this.maxSize;
|
||||
CheckHttpDataSize(attribute);
|
||||
return attribute;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new ArgumentException($"({request}, {name}, {value})", e);
|
||||
}
|
||||
}
|
||||
|
||||
public IFileUpload CreateFileUpload(IHttpRequest request, string name, string fileName,
|
||||
string contentType, string contentTransferEncoding, Encoding encoding,
|
||||
long size)
|
||||
{
|
||||
if (this.useDisk)
|
||||
{
|
||||
var fileUpload = new DiskFileUpload(name, fileName, contentType,
|
||||
contentTransferEncoding, encoding, size);
|
||||
fileUpload.MaxSize = this.maxSize;
|
||||
CheckHttpDataSize(fileUpload);
|
||||
List<IHttpData> list = this.GetList(request);
|
||||
list.Add(fileUpload);
|
||||
return fileUpload;
|
||||
}
|
||||
if (this.checkSize)
|
||||
{
|
||||
var fileUpload = new MixedFileUpload(name, fileName, contentType,
|
||||
contentTransferEncoding, encoding, size, this.minSize);
|
||||
fileUpload.MaxSize = this.maxSize;
|
||||
CheckHttpDataSize(fileUpload);
|
||||
List<IHttpData> list = this.GetList(request);
|
||||
list.Add(fileUpload);
|
||||
return fileUpload;
|
||||
}
|
||||
var memoryFileUpload = new MemoryFileUpload(name, fileName, contentType,
|
||||
contentTransferEncoding, encoding, size);
|
||||
memoryFileUpload.MaxSize = this.maxSize;
|
||||
CheckHttpDataSize(memoryFileUpload);
|
||||
return memoryFileUpload;
|
||||
}
|
||||
|
||||
public void RemoveHttpDataFromClean(IHttpRequest request, IInterfaceHttpData data)
|
||||
{
|
||||
if (!(data is IHttpData httpData))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not use getList because it adds empty list to requestFileDeleteMap
|
||||
// if request is not found
|
||||
if (!this.requestFileDeleteMap.TryGetValue(request, out List<IHttpData> list))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Can't simply call list.remove(data), because different data items may be equal.
|
||||
// Need to check identity.
|
||||
int index = -1;
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
if (ReferenceEquals(list[i], httpData))
|
||||
{
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index != -1)
|
||||
{
|
||||
list.RemoveAt(index);
|
||||
}
|
||||
if (list.Count == 0)
|
||||
{
|
||||
this.requestFileDeleteMap.TryRemove(request, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public void CleanRequestHttpData(IHttpRequest request)
|
||||
{
|
||||
if (this.requestFileDeleteMap.TryRemove(request, out List<IHttpData> list))
|
||||
{
|
||||
foreach (IHttpData data in list)
|
||||
{
|
||||
data.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CleanAllHttpData()
|
||||
{
|
||||
while (!this.requestFileDeleteMap.IsEmpty)
|
||||
{
|
||||
IHttpRequest[] keys = this.requestFileDeleteMap.Keys.ToArray();
|
||||
foreach (IHttpRequest key in keys)
|
||||
{
|
||||
if (this.requestFileDeleteMap.TryRemove(key, out List<IHttpData> list))
|
||||
{
|
||||
foreach (IHttpData data in list)
|
||||
{
|
||||
data.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Similar to IdentityHashMap in Java
|
||||
sealed class IdentityComparer : IEqualityComparer<IHttpRequest>
|
||||
{
|
||||
internal static readonly IdentityComparer Default = new IdentityComparer();
|
||||
|
||||
public bool Equals(IHttpRequest x, IHttpRequest y) => ReferenceEquals(x, y);
|
||||
|
||||
public int GetHashCode(IHttpRequest obj) => obj.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
// 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.Multipart
|
||||
{
|
||||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public class DiskAttribute : AbstractDiskHttpData, IAttribute
|
||||
{
|
||||
public static string DiskBaseDirectory;
|
||||
public static bool DeleteOnExitTemporaryFile = true;
|
||||
public static readonly string FilePrefix = "Attr_";
|
||||
public static readonly string FilePostfix = ".att";
|
||||
|
||||
public DiskAttribute(string name)
|
||||
: this(name, HttpConstants.DefaultEncoding)
|
||||
{
|
||||
}
|
||||
|
||||
public DiskAttribute(string name, long definedSize)
|
||||
: this(name, definedSize, HttpConstants.DefaultEncoding)
|
||||
{
|
||||
}
|
||||
|
||||
public DiskAttribute(string name, Encoding charset)
|
||||
: base(name, charset, 0)
|
||||
{
|
||||
}
|
||||
|
||||
public DiskAttribute(string name, long definedSize, Encoding charset)
|
||||
: base(name, charset, definedSize)
|
||||
{
|
||||
}
|
||||
|
||||
public DiskAttribute(string name, string value)
|
||||
: this(name, value, HttpConstants.DefaultEncoding)
|
||||
{
|
||||
}
|
||||
|
||||
public DiskAttribute(string name, string value, Encoding charset)
|
||||
: base(name, charset, 0) // Attribute have no default size
|
||||
{
|
||||
this.Value = value;
|
||||
}
|
||||
|
||||
public override HttpDataType DataType => HttpDataType.Attribute;
|
||||
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
byte[] bytes = this.GetBytes();
|
||||
return this.Charset.GetString(bytes);
|
||||
}
|
||||
set
|
||||
{
|
||||
Contract.Requires(value != null);
|
||||
|
||||
byte[] bytes = this.Charset.GetBytes(value);
|
||||
this.CheckSize(bytes.Length);
|
||||
IByteBuffer buffer = Unpooled.WrappedBuffer(bytes);
|
||||
if (this.DefinedSize > 0)
|
||||
{
|
||||
this.DefinedSize = buffer.ReadableBytes;
|
||||
}
|
||||
this.SetContent(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
public override void AddContent(IByteBuffer buffer, bool last)
|
||||
{
|
||||
long newDefinedSize = this.Size + buffer.ReadableBytes;
|
||||
this.CheckSize(newDefinedSize);
|
||||
if (this.DefinedSize > 0 && this.DefinedSize < newDefinedSize)
|
||||
{
|
||||
this.DefinedSize = newDefinedSize;
|
||||
}
|
||||
base.AddContent(buffer, last);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => this.Name.GetHashCode();
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is IAttribute attribute)
|
||||
{
|
||||
return this.Name.Equals(attribute.Name, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int CompareTo(IInterfaceHttpData other)
|
||||
{
|
||||
if (!(other is IAttribute))
|
||||
{
|
||||
throw new ArgumentException($"Cannot compare {this.DataType} with {other.DataType}");
|
||||
}
|
||||
|
||||
return this.CompareTo((IAttribute)other);
|
||||
}
|
||||
|
||||
public int CompareTo(IAttribute attribute) => string.Compare(this.Name, attribute.Name, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
try
|
||||
{
|
||||
return $"{this.Name}={this.Value}";
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
return $"{this.Name}={e}";
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool DeleteOnExit => DeleteOnExitTemporaryFile;
|
||||
|
||||
protected override string BaseDirectory => DiskBaseDirectory;
|
||||
|
||||
protected override string DiskFilename => $"{this.Name}{this.Postfix}";
|
||||
|
||||
protected override string Postfix => FilePostfix;
|
||||
|
||||
protected override string Prefix => FilePrefix;
|
||||
|
||||
public override IByteBufferHolder Copy() => this.Replace(this.Content?.Copy());
|
||||
|
||||
public override IByteBufferHolder Duplicate() => this.Replace(this.Content?.Duplicate());
|
||||
|
||||
public override IByteBufferHolder RetainedDuplicate()
|
||||
{
|
||||
IByteBuffer content = this.Content;
|
||||
if (content != null)
|
||||
{
|
||||
content = content.RetainedDuplicate();
|
||||
bool success = false;
|
||||
try
|
||||
{
|
||||
var duplicate = (IAttribute)this.Replace(content);
|
||||
success = true;
|
||||
return duplicate;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!success)
|
||||
{
|
||||
content.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return this.Replace(null);
|
||||
}
|
||||
}
|
||||
|
||||
public override IByteBufferHolder Replace(IByteBuffer content)
|
||||
{
|
||||
var attr = new DiskAttribute(this.Name);
|
||||
attr.Charset = this.Charset;
|
||||
if (content != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
attr.SetContent(content);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new ChannelException(e);
|
||||
}
|
||||
}
|
||||
return attr;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ConvertToAutoProperty
|
||||
namespace DotNetty.Codecs.Http.Multipart
|
||||
{
|
||||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public class DiskFileUpload : AbstractDiskHttpData, IFileUpload
|
||||
{
|
||||
public static string FileBaseDirectory;
|
||||
public static bool DeleteOnExitTemporaryFile = true;
|
||||
public static string FilePrefix = "FUp_";
|
||||
public static readonly string FilePostfix = ".tmp";
|
||||
|
||||
string filename;
|
||||
string contentType;
|
||||
string contentTransferEncoding;
|
||||
|
||||
public DiskFileUpload(string name, string filename, string contentType,
|
||||
string contentTransferEncoding, Encoding charset, long size)
|
||||
: base(name, charset, size)
|
||||
{
|
||||
Contract.Requires(filename != null);
|
||||
Contract.Requires(contentType != null);
|
||||
|
||||
this.filename = filename;
|
||||
this.contentType = contentType;
|
||||
this.contentTransferEncoding = contentTransferEncoding;
|
||||
}
|
||||
|
||||
public override HttpDataType DataType => HttpDataType.FileUpload;
|
||||
|
||||
public string FileName
|
||||
{
|
||||
get => this.filename;
|
||||
set
|
||||
{
|
||||
Contract.Requires(value != null);
|
||||
this.filename = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override int GetHashCode() => FileUploadUtil.HashCode(this);
|
||||
|
||||
public override bool Equals(object obj) => obj is IFileUpload fileUpload && FileUploadUtil.Equals(this, fileUpload);
|
||||
|
||||
public override int CompareTo(IInterfaceHttpData other)
|
||||
{
|
||||
if (!(other is IFileUpload))
|
||||
{
|
||||
throw new ArgumentException($"Cannot compare {this.DataType} with {other.DataType}");
|
||||
}
|
||||
|
||||
return this.CompareTo((IFileUpload)other);
|
||||
}
|
||||
|
||||
public int CompareTo(IFileUpload other) => FileUploadUtil.CompareTo(this, other);
|
||||
|
||||
public string ContentType
|
||||
{
|
||||
get => this.contentType;
|
||||
set
|
||||
{
|
||||
Contract.Requires(value != null);
|
||||
this.contentType = value;
|
||||
}
|
||||
}
|
||||
|
||||
public string ContentTransferEncoding
|
||||
{
|
||||
get => this.contentTransferEncoding;
|
||||
set => this.contentTransferEncoding = value;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
FileStream fileStream = null;
|
||||
try
|
||||
{
|
||||
fileStream = this.GetFile();
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Should not occur.
|
||||
}
|
||||
|
||||
return HttpHeaderNames.ContentDisposition + ": " +
|
||||
HttpHeaderValues.FormData + "; " + HttpHeaderValues.Name + "=\"" + this.Name +
|
||||
"\"; " + HttpHeaderValues.FileName + "=\"" + this.filename + "\"\r\n" +
|
||||
HttpHeaderNames.ContentType + ": " + this.contentType +
|
||||
(this.Charset != null ? "; " + HttpHeaderValues.Charset + '=' + this.Charset.WebName + "\r\n" : "\r\n") +
|
||||
HttpHeaderNames.ContentLength + ": " + this.Length + "\r\n" +
|
||||
"Completed: " + this.IsCompleted +
|
||||
"\r\nIsInMemory: " + this.IsInMemory + "\r\nRealFile: " +
|
||||
(fileStream != null ? fileStream.Name : "null") + " DefaultDeleteAfter: " +
|
||||
DeleteOnExitTemporaryFile;
|
||||
}
|
||||
|
||||
protected override bool DeleteOnExit => DeleteOnExitTemporaryFile;
|
||||
|
||||
protected override string BaseDirectory => FileBaseDirectory;
|
||||
|
||||
protected override string DiskFilename => "upload";
|
||||
|
||||
protected override string Postfix => FilePostfix;
|
||||
|
||||
protected override string Prefix => FilePrefix;
|
||||
|
||||
public override IByteBufferHolder Copy() => this.Replace(this.Content?.Copy());
|
||||
|
||||
public override IByteBufferHolder Duplicate() => this.Replace(this.Content?.Duplicate());
|
||||
|
||||
public override IByteBufferHolder RetainedDuplicate()
|
||||
{
|
||||
IByteBuffer content = this.Content;
|
||||
if (content != null)
|
||||
{
|
||||
content = content.RetainedDuplicate();
|
||||
bool success = false;
|
||||
try
|
||||
{
|
||||
var duplicate = (IFileUpload)this.Replace(content);
|
||||
success = true;
|
||||
return duplicate;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!success)
|
||||
{
|
||||
content.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return this.Replace(null);
|
||||
}
|
||||
}
|
||||
|
||||
public override IByteBufferHolder Replace(IByteBuffer content)
|
||||
{
|
||||
var upload = new DiskFileUpload(
|
||||
this.Name, this.FileName, this.ContentType, this.ContentTransferEncoding, this.Charset, this.Size);
|
||||
if (content != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
upload.SetContent(content);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new ChannelException(e);
|
||||
}
|
||||
}
|
||||
|
||||
return upload;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// 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.Multipart
|
||||
{
|
||||
using System;
|
||||
|
||||
public class EndOfDataDecoderException : DecoderException
|
||||
{
|
||||
public EndOfDataDecoderException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public EndOfDataDecoderException(Exception innerException)
|
||||
: base(innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// 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.Multipart
|
||||
{
|
||||
using System;
|
||||
|
||||
public class ErrorDataDecoderException : DecoderException
|
||||
{
|
||||
public ErrorDataDecoderException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public ErrorDataDecoderException(Exception innerException)
|
||||
: base(innerException)
|
||||
{
|
||||
}
|
||||
|
||||
public ErrorDataDecoderException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// 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.Multipart
|
||||
{
|
||||
using System;
|
||||
|
||||
public class ErrorDataEncoderException : Exception
|
||||
{
|
||||
public ErrorDataEncoderException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
public ErrorDataEncoderException(Exception innerException)
|
||||
: base(null, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
public ErrorDataEncoderException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче