зеркало из https://github.com/Azure/DotNetty.git
WebSocket support. (#400)
This commit is contained in:
Родитель
9e3a84189f
Коммит
2c0c10939e
14
DotNetty.sln
14
DotNetty.sln
|
@ -103,6 +103,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetty.Codecs.Http", "src
|
|||
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
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebSockets.Server", "examples\WebSockets.Server\WebSockets.Server.csproj", "{EA387B4B-DAD0-4E34-B8A3-79EA4616726A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebSockets.Client", "examples\WebSockets.Client\WebSockets.Client.csproj", "{3326DB6E-023E-483F-9A1C-5905D3091B57}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -273,6 +277,14 @@ Global
|
|||
{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
|
||||
{EA387B4B-DAD0-4E34-B8A3-79EA4616726A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EA387B4B-DAD0-4E34-B8A3-79EA4616726A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EA387B4B-DAD0-4E34-B8A3-79EA4616726A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EA387B4B-DAD0-4E34-B8A3-79EA4616726A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3326DB6E-023E-483F-9A1C-5905D3091B57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3326DB6E-023E-483F-9A1C-5905D3091B57}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3326DB6E-023E-483F-9A1C-5905D3091B57}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3326DB6E-023E-483F-9A1C-5905D3091B57}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -319,6 +331,8 @@ Global
|
|||
{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}
|
||||
{EA387B4B-DAD0-4E34-B8A3-79EA4616726A} = {F716F1EF-81EF-4020-914A-5422A13A9E13}
|
||||
{3326DB6E-023E-483F-9A1C-5905D3091B57} = {F716F1EF-81EF-4020-914A-5422A13A9E13}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
{9FE6A783-C20D-4097-9988-4178E2C4CE75} = {126EA539-4B28-4B07-8B5D-D1D7F794D189}
|
||||
|
|
|
@ -21,5 +21,14 @@ namespace Examples.Common
|
|||
public static int Port => int.Parse(ExampleHelper.Configuration["port"]);
|
||||
|
||||
public static int Size => int.Parse(ExampleHelper.Configuration["size"]);
|
||||
|
||||
public static bool UseLibuv
|
||||
{
|
||||
get
|
||||
{
|
||||
string libuv = ExampleHelper.Configuration["libuv"];
|
||||
return !string.IsNullOrEmpty(libuv) && bool.Parse(libuv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace WebSockets.Client
|
||||
{
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Codecs.Http;
|
||||
using DotNetty.Codecs.Http.WebSockets;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions.Compression;
|
||||
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 async Task RunClientAsync()
|
||||
{
|
||||
var builder = new UriBuilder
|
||||
{
|
||||
Scheme = ClientSettings.IsSsl ? "wss" : "ws",
|
||||
Host = ClientSettings.Host.ToString(),
|
||||
Port = ClientSettings.Port
|
||||
};
|
||||
|
||||
string path = ExampleHelper.Configuration["path"];
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
builder.Path = path;
|
||||
}
|
||||
|
||||
Uri uri = builder.Uri;
|
||||
ExampleHelper.SetConsoleLogger();
|
||||
|
||||
bool useLibuv = ClientSettings.UseLibuv;
|
||||
Console.WriteLine("Transport type : " + (useLibuv ? "Libuv" : "Socket"));
|
||||
|
||||
IEventLoopGroup group;
|
||||
if (useLibuv)
|
||||
{
|
||||
group = new EventLoopGroup();
|
||||
}
|
||||
else
|
||||
{
|
||||
group = new MultithreadEventLoopGroup();
|
||||
}
|
||||
|
||||
X509Certificate2 cert = null;
|
||||
string targetHost = null;
|
||||
if (ClientSettings.IsSsl)
|
||||
{
|
||||
cert = new X509Certificate2(Path.Combine(ExampleHelper.ProcessDirectory, "dotnetty.com.pfx"), "password");
|
||||
targetHost = cert.GetNameInfo(X509NameType.DnsName, false);
|
||||
}
|
||||
try
|
||||
{
|
||||
var bootstrap = new Bootstrap();
|
||||
bootstrap
|
||||
.Group(group)
|
||||
.Option(ChannelOption.TcpNodelay, true);
|
||||
if (useLibuv)
|
||||
{
|
||||
bootstrap.Channel<TcpChannel>();
|
||||
}
|
||||
else
|
||||
{
|
||||
bootstrap.Channel<TcpSocketChannel>();
|
||||
}
|
||||
|
||||
// Connect with V13 (RFC 6455 aka HyBi-17). You can change it to V08 or V00.
|
||||
// If you change it to V00, ping is not supported and remember to change
|
||||
// HttpResponseDecoder to WebSocketHttpResponseDecoder in the pipeline.
|
||||
var handler =new WebSocketClientHandler(
|
||||
WebSocketClientHandshakerFactory.NewHandshaker(
|
||||
uri, WebSocketVersion.V13, null, true, new DefaultHttpHeaders()));
|
||||
|
||||
bootstrap.Handler(new ActionChannelInitializer<IChannel>(channel =>
|
||||
{
|
||||
IChannelPipeline pipeline = channel.Pipeline;
|
||||
if (cert != null)
|
||||
{
|
||||
pipeline.AddLast("tls", new TlsHandler(stream => new SslStream(stream, true, (sender, certificate, chain, errors) => true), new ClientTlsSettings(targetHost)));
|
||||
}
|
||||
|
||||
pipeline.AddLast(
|
||||
new HttpClientCodec(),
|
||||
new HttpObjectAggregator(8192),
|
||||
WebSocketClientCompressionHandler.Instance,
|
||||
handler);
|
||||
}));
|
||||
|
||||
IChannel ch = await bootstrap.ConnectAsync(new IPEndPoint(ClientSettings.Host, ClientSettings.Port));
|
||||
await handler.HandshakeCompletion;
|
||||
|
||||
Console.WriteLine("WebSocket handshake completed.\n");
|
||||
Console.WriteLine("\t[bye]:Quit \n\t [ping]:Send ping frame\n\t Enter any text and Enter: Send text frame");
|
||||
while (true)
|
||||
{
|
||||
string msg = Console.ReadLine();
|
||||
if (msg == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else if ("bye".Equals(msg.ToLower()))
|
||||
{
|
||||
await ch.WriteAndFlushAsync(new CloseWebSocketFrame());
|
||||
break;
|
||||
}
|
||||
else if ("ping".Equals(msg.ToLower()))
|
||||
{
|
||||
var frame = new PingWebSocketFrame(Unpooled.WrappedBuffer(new byte[] { 8, 1, 8, 1 }));
|
||||
await ch.WriteAndFlushAsync(frame);
|
||||
}
|
||||
else
|
||||
{
|
||||
WebSocketFrame frame = new TextWebSocketFrame(msg);
|
||||
await ch.WriteAndFlushAsync(frame);
|
||||
}
|
||||
}
|
||||
|
||||
await ch.CloseAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await group.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1));
|
||||
}
|
||||
}
|
||||
|
||||
static void Main() => RunClientAsync().Wait();
|
||||
}
|
||||
}
|
|
@ -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 WebSockets.Client
|
||||
{
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Codecs.Http;
|
||||
using DotNetty.Codecs.Http.WebSockets;
|
||||
using DotNetty.Common.Concurrency;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public class WebSocketClientHandler : SimpleChannelInboundHandler<object>
|
||||
{
|
||||
readonly WebSocketClientHandshaker handshaker;
|
||||
readonly TaskCompletionSource completionSource;
|
||||
|
||||
public WebSocketClientHandler(WebSocketClientHandshaker handshaker)
|
||||
{
|
||||
this.handshaker = handshaker;
|
||||
this.completionSource = new TaskCompletionSource();
|
||||
}
|
||||
|
||||
public Task HandshakeCompletion => this.completionSource.Task;
|
||||
|
||||
public override void ChannelActive(IChannelHandlerContext ctx) =>
|
||||
this.handshaker.HandshakeAsync(ctx.Channel).LinkOutcome(this.completionSource);
|
||||
|
||||
public override void ChannelInactive(IChannelHandlerContext context)
|
||||
{
|
||||
Console.WriteLine("WebSocket Client disconnected!");
|
||||
}
|
||||
|
||||
protected override void ChannelRead0(IChannelHandlerContext ctx, object msg)
|
||||
{
|
||||
IChannel ch = ctx.Channel;
|
||||
if (!this.handshaker.IsHandshakeComplete)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.handshaker.FinishHandshake(ch, (IFullHttpResponse)msg);
|
||||
Console.WriteLine("WebSocket Client connected!");
|
||||
this.completionSource.TryComplete();
|
||||
}
|
||||
catch (WebSocketHandshakeException e)
|
||||
{
|
||||
Console.WriteLine("WebSocket Client failed to connect");
|
||||
this.completionSource.TrySetException(e);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (msg is IFullHttpResponse response)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Unexpected FullHttpResponse (getStatus={response.Status}, content={response.Content.ToString(Encoding.UTF8)})");
|
||||
}
|
||||
|
||||
if (msg is TextWebSocketFrame textFrame)
|
||||
{
|
||||
Console.WriteLine($"WebSocket Client received message: {textFrame.Text()}");
|
||||
}
|
||||
else if (msg is PongWebSocketFrame)
|
||||
{
|
||||
Console.WriteLine("WebSocket Client received pong");
|
||||
}
|
||||
else if (msg is CloseWebSocketFrame)
|
||||
{
|
||||
Console.WriteLine("WebSocket Client received closing");
|
||||
ch.CloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public override void ExceptionCaught(IChannelHandlerContext ctx, Exception exception)
|
||||
{
|
||||
Console.WriteLine("Exception: " + exception);
|
||||
this.completionSource.TrySetException(exception);
|
||||
ctx.CloseAsync();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<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>
|
||||
</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,7 @@
|
|||
{
|
||||
"ssl": "false",
|
||||
"host": "127.0.0.1",
|
||||
"port": "8080",
|
||||
"path": "/websocket",
|
||||
"libuv": "true"
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace WebSockets.Server
|
||||
{
|
||||
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 ? "Enabled" : "Disabled")}");
|
||||
Console.WriteLine($"Current latency mode for garbage collection: {GCSettings.LatencyMode}");
|
||||
Console.WriteLine("\n");
|
||||
|
||||
IEventLoopGroup bossGroup;
|
||||
IEventLoopGroup workGroup;
|
||||
if (useLibuv)
|
||||
{
|
||||
var dispatcher = new DispatcherEventLoopGroup();
|
||||
bossGroup = dispatcher;
|
||||
workGroup = new WorkerEventLoopGroup(dispatcher);
|
||||
}
|
||||
else
|
||||
{
|
||||
bossGroup = 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(bossGroup, 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(new HttpServerCodec());
|
||||
pipeline.AddLast(new HttpObjectAggregator(65536));
|
||||
pipeline.AddLast(new WebSocketServerHandler());
|
||||
}));
|
||||
|
||||
int port = ServerSettings.Port;
|
||||
IChannel bootstrapChannel = await bootstrap.BindAsync(IPAddress.Loopback, port);
|
||||
|
||||
Console.WriteLine("Open your web browser and navigate to "
|
||||
+ $"{(ServerSettings.IsSsl ? "https" : "http")}"
|
||||
+ $"://127.0.0.1:{port}/");
|
||||
Console.WriteLine("Listening on "
|
||||
+ $"{(ServerSettings.IsSsl ? "wss" : "ws")}"
|
||||
+ $"://127.0.0.1:{port}/websocket");
|
||||
Console.ReadLine();
|
||||
|
||||
await bootstrapChannel.CloseAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
workGroup.ShutdownGracefullyAsync().Wait();
|
||||
bossGroup.ShutdownGracefullyAsync().Wait();
|
||||
}
|
||||
}
|
||||
|
||||
static void Main() => RunServerAsync().Wait();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace WebSockets.Server
|
||||
{
|
||||
using System.Text;
|
||||
using DotNetty.Buffers;
|
||||
|
||||
static class WebSocketServerBenchmarkPage
|
||||
{
|
||||
const string Newline = "\r\n";
|
||||
|
||||
public static IByteBuffer GetContent(string webSocketLocation) =>
|
||||
Unpooled.WrappedBuffer(
|
||||
Encoding.ASCII.GetBytes(
|
||||
"<html><head><title>Web Socket Performance Test</title></head>" + Newline +
|
||||
"<body>" + Newline +
|
||||
"<h2>WebSocket Performance Test</h2>" + Newline +
|
||||
"<label>Connection Status:</label>" + Newline +
|
||||
"<label id=\"connectionLabel\"></label><br />" + Newline +
|
||||
|
||||
"<form onsubmit=\"return false;\">" + Newline +
|
||||
"Message size:" +
|
||||
"<input type=\"text\" id=\"messageSize\" value=\"1024\"/><br>" + Newline +
|
||||
"Number of messages:" +
|
||||
"<input type=\"text\" id=\"nrMessages\" value=\"100000\"/><br>" + Newline +
|
||||
"Data Type:" +
|
||||
"<input type=\"radio\" name=\"type\" id=\"typeText\" value=\"text\" checked>text" +
|
||||
"<input type=\"radio\" name=\"type\" id=\"typeBinary\" value=\"binary\">binary<br>" + Newline +
|
||||
"Mode:<br>" + Newline +
|
||||
"<input type=\"radio\" name=\"mode\" id=\"modeSingle\" value=\"single\" checked>" +
|
||||
"Wait for response after each messages<br>" + Newline +
|
||||
"<input type=\"radio\" name=\"mode\" id=\"modeAll\" value=\"all\">" +
|
||||
"Send all messages and then wait for all responses<br>" + Newline +
|
||||
"<input type=\"checkbox\" id=\"verifiyResponses\">Verify responded messages<br>" + Newline +
|
||||
"<input type=\"button\" value=\"Start Benchmark\"" + Newline +
|
||||
" onclick=\"startBenchmark()\" />" + Newline +
|
||||
"<h3>Output</h3>" + Newline +
|
||||
"<textarea id=\"output\" style=\"width:500px;height:300px;\"></textarea>" + Newline +
|
||||
"<br>" + Newline +
|
||||
"<input type=\"button\" value=\"Clear\" onclick=\"clearText()\">" + Newline +
|
||||
"</form>" + Newline +
|
||||
|
||||
"<script type=\"text/javascript\">" + Newline +
|
||||
"var benchRunning = false;" + Newline +
|
||||
"var messageSize = 0;" + Newline +
|
||||
"var totalMessages = 0;" + Newline +
|
||||
"var rcvdMessages = 0;" + Newline +
|
||||
"var isBinary = true;" + Newline +
|
||||
"var isSingle = true;" + Newline +
|
||||
"var verifiyResponses = false;" + Newline +
|
||||
"var benchData = null;" + Newline +
|
||||
"var startTime;" + Newline +
|
||||
"var endTime;" + Newline +
|
||||
"var socket;" + Newline +
|
||||
"var output = document.getElementById('output');" + Newline +
|
||||
"var connectionLabel = document.getElementById('connectionLabel');" + Newline +
|
||||
"if (!window.WebSocket) {" + Newline +
|
||||
" window.WebSocket = window.MozWebSocket;" + Newline +
|
||||
'}' + Newline +
|
||||
"if (window.WebSocket) {" + Newline +
|
||||
" socket = new WebSocket(\"" + webSocketLocation + "\");" + Newline +
|
||||
" socket.binaryType = 'arraybuffer';" + Newline +
|
||||
" socket.onmessage = function(event) {" + Newline +
|
||||
" if (verifiyResponses) {" + Newline +
|
||||
" if (isBinary) {" + Newline +
|
||||
" if (!(event.data instanceof ArrayBuffer) || " + Newline +
|
||||
" event.data.byteLength != benchData.byteLength) {" + Newline +
|
||||
" onInvalidResponse(benchData, event.data);" + Newline +
|
||||
" return;" + Newline +
|
||||
" } else {" + Newline +
|
||||
" var v = new Uint8Array(event.data);" + Newline +
|
||||
" for (var j = 0; j < benchData.byteLength; j++) {" + Newline +
|
||||
" if (v[j] != benchData[j]) {" + Newline +
|
||||
" onInvalidResponse(benchData, event.data);" + Newline +
|
||||
" return;" + Newline +
|
||||
" }" + Newline +
|
||||
" }" + Newline +
|
||||
" }" + Newline +
|
||||
" } else {" + Newline +
|
||||
" if (event.data != benchData) {" + Newline +
|
||||
" onInvalidResponse(benchData, event.data);" + Newline +
|
||||
" return;" + Newline +
|
||||
" }" + Newline +
|
||||
" }" + Newline +
|
||||
" }" + Newline +
|
||||
" rcvdMessages++;" + Newline +
|
||||
" if (rcvdMessages == totalMessages) {" + Newline +
|
||||
" onFinished();" + Newline +
|
||||
" } else if (isSingle) {" + Newline +
|
||||
" socket.send(benchData);" + Newline +
|
||||
" }" + Newline +
|
||||
" };" + Newline +
|
||||
" socket.onopen = function(event) {" + Newline +
|
||||
" connectionLabel.innerHTML = \"Connected\";" + Newline +
|
||||
" };" + Newline +
|
||||
" socket.onclose = function(event) {" + Newline +
|
||||
" benchRunning = false;" + Newline +
|
||||
" connectionLabel.innerHTML = \"Disconnected\";" + Newline +
|
||||
" };" + Newline +
|
||||
"} else {" + Newline +
|
||||
" alert(\"Your browser does not support Web Socket.\");" + Newline +
|
||||
'}' + Newline +
|
||||
Newline +
|
||||
"function onInvalidResponse(sent,recvd) {" + Newline +
|
||||
" socket.close();" + Newline +
|
||||
" alert(\"Error: Sent data did not match the received data!\");" + Newline +
|
||||
"}" + Newline +
|
||||
Newline +
|
||||
"function clearText() {" + Newline +
|
||||
" output.value=\"\";" + Newline +
|
||||
"}" + Newline +
|
||||
Newline +
|
||||
"function createBenchData() {" + Newline +
|
||||
" if (isBinary) {" + Newline +
|
||||
" benchData = new Uint8Array(messageSize);" + Newline +
|
||||
" for (var i=0; i < messageSize; i++) {" + Newline +
|
||||
" benchData[i] += Math.floor(Math.random() * 255);" + Newline +
|
||||
" }" + Newline +
|
||||
" } else { " + Newline +
|
||||
" benchData = \"\";" + Newline +
|
||||
" for (var i=0; i < messageSize; i++) {" + Newline +
|
||||
" benchData += String.fromCharCode(Math.floor(Math.random() * (123 - 65) + 65));" + Newline +
|
||||
" }" + Newline +
|
||||
" }" + Newline +
|
||||
"}" + Newline +
|
||||
Newline +
|
||||
"function startBenchmark(message) {" + Newline +
|
||||
" if (!window.WebSocket || benchRunning) { return; }" + Newline +
|
||||
" if (socket.readyState == WebSocket.OPEN) {" + Newline +
|
||||
" isBinary = document.getElementById('typeBinary').checked;" + Newline +
|
||||
" isSingle = document.getElementById('modeSingle').checked;" + Newline +
|
||||
" verifiyResponses = document.getElementById('verifiyResponses').checked;" + Newline +
|
||||
" messageSize = parseInt(document.getElementById('messageSize').value);" + Newline +
|
||||
" totalMessages = parseInt(document.getElementById('nrMessages').value);" + Newline +
|
||||
" if (isNaN(messageSize) || isNaN(totalMessages)) return;" + Newline +
|
||||
" createBenchData();" + Newline +
|
||||
" output.value = output.value + '\\nStarting Benchmark';" + Newline +
|
||||
" rcvdMessages = 0;" + Newline +
|
||||
" benchRunning = true;" + Newline +
|
||||
" startTime = new Date();" + Newline +
|
||||
" if (isSingle) {" + Newline +
|
||||
" socket.send(benchData);" + Newline +
|
||||
" } else {" + Newline +
|
||||
" for (var i = 0; i < totalMessages; i++) socket.send(benchData);" + Newline +
|
||||
" }" + Newline +
|
||||
" } else {" + Newline +
|
||||
" alert(\"The socket is not open.\");" + Newline +
|
||||
" }" + Newline +
|
||||
'}' + Newline +
|
||||
Newline +
|
||||
"function onFinished() {" + Newline +
|
||||
" endTime = new Date();" + Newline +
|
||||
" var duration = (endTime - startTime) / 1000.0;" + Newline +
|
||||
" output.value = output.value + '\\nTest took: ' + duration + 's';" + Newline +
|
||||
" var messagesPerS = totalMessages / duration;" + Newline +
|
||||
" output.value = output.value + '\\nPerformance: ' + messagesPerS + ' Messages/s';" + Newline +
|
||||
" output.value = output.value + ' in each direction';" + Newline +
|
||||
" output.value = output.value + '\\nRound trip: ' + 1000.0/messagesPerS + 'ms';" + Newline +
|
||||
" var throughput = messageSize * totalMessages / duration;" + Newline +
|
||||
" var throughputText;" + Newline +
|
||||
" if (isBinary) throughputText = throughput / (1024*1024) + ' MB/s';" + Newline +
|
||||
" else throughputText = throughput / (1000*1000) + ' MChars/s';" + Newline +
|
||||
" output.value = output.value + '\\nThroughput: ' + throughputText;" + Newline +
|
||||
" output.value = output.value + ' in each direction';" + Newline +
|
||||
" benchRunning = false;" + Newline +
|
||||
"}" + Newline +
|
||||
"</script>" + Newline +
|
||||
"</body>" + Newline +
|
||||
"</html>" + Newline));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace WebSockets.Server
|
||||
{
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Codecs.Http;
|
||||
using DotNetty.Codecs.Http.WebSockets;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
using Examples.Common;
|
||||
|
||||
using static DotNetty.Codecs.Http.HttpVersion;
|
||||
using static DotNetty.Codecs.Http.HttpResponseStatus;
|
||||
|
||||
public sealed class WebSocketServerHandler : SimpleChannelInboundHandler<object>
|
||||
{
|
||||
const string WebsocketPath = "/websocket";
|
||||
|
||||
WebSocketServerHandshaker handshaker;
|
||||
|
||||
protected override void ChannelRead0(IChannelHandlerContext ctx, object msg)
|
||||
{
|
||||
if (msg is IFullHttpRequest request)
|
||||
{
|
||||
this.HandleHttpRequest(ctx, request);
|
||||
}
|
||||
else if (msg is WebSocketFrame frame)
|
||||
{
|
||||
this.HandleWebSocketFrame(ctx, frame);
|
||||
}
|
||||
}
|
||||
|
||||
public override void ChannelReadComplete(IChannelHandlerContext context) => context.Flush();
|
||||
|
||||
void HandleHttpRequest(IChannelHandlerContext ctx, IFullHttpRequest req)
|
||||
{
|
||||
// Handle a bad request.
|
||||
if (!req.Result.IsSuccess)
|
||||
{
|
||||
SendHttpResponse(ctx, req, new DefaultFullHttpResponse(Http11, BadRequest));
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow only GET methods.
|
||||
if (!Equals(req.Method, HttpMethod.Get))
|
||||
{
|
||||
SendHttpResponse(ctx, req, new DefaultFullHttpResponse(Http11, Forbidden));
|
||||
return;
|
||||
}
|
||||
|
||||
// Send the demo page and favicon.ico
|
||||
if ("/".Equals(req.Uri))
|
||||
{
|
||||
IByteBuffer content = WebSocketServerBenchmarkPage.GetContent(GetWebSocketLocation(req));
|
||||
var res = new DefaultFullHttpResponse(Http11, OK, content);
|
||||
|
||||
res.Headers.Set(HttpHeaderNames.ContentType, "text/html; charset=UTF-8");
|
||||
HttpUtil.SetContentLength(res, content.ReadableBytes);
|
||||
|
||||
SendHttpResponse(ctx, req, res);
|
||||
return;
|
||||
}
|
||||
if ("/favicon.ico".Equals(req.Uri))
|
||||
{
|
||||
var res = new DefaultFullHttpResponse(Http11, NotFound);
|
||||
SendHttpResponse(ctx, req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handshake
|
||||
var wsFactory = new WebSocketServerHandshakerFactory(
|
||||
GetWebSocketLocation(req), null, true, 5 * 1024 * 1024);
|
||||
this.handshaker = wsFactory.NewHandshaker(req);
|
||||
if (this.handshaker == null)
|
||||
{
|
||||
WebSocketServerHandshakerFactory.SendUnsupportedVersionResponse(ctx.Channel);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.handshaker.HandshakeAsync(ctx.Channel, req);
|
||||
}
|
||||
}
|
||||
|
||||
void HandleWebSocketFrame(IChannelHandlerContext ctx, WebSocketFrame frame)
|
||||
{
|
||||
// Check for closing frame
|
||||
if (frame is CloseWebSocketFrame)
|
||||
{
|
||||
this.handshaker.CloseAsync(ctx.Channel, (CloseWebSocketFrame)frame.Retain());
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame is PingWebSocketFrame)
|
||||
{
|
||||
ctx.WriteAsync(new PongWebSocketFrame((IByteBuffer)frame.Content.Retain()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame is TextWebSocketFrame)
|
||||
{
|
||||
// Echo the frame
|
||||
ctx.WriteAsync(frame.Retain());
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame is BinaryWebSocketFrame)
|
||||
{
|
||||
// Echo the frame
|
||||
ctx.WriteAsync(frame.Retain());
|
||||
}
|
||||
}
|
||||
|
||||
static void SendHttpResponse(IChannelHandlerContext ctx, IFullHttpRequest req, IFullHttpResponse res)
|
||||
{
|
||||
// Generate an error page if response getStatus code is not OK (200).
|
||||
if (res.Status.Code != 200)
|
||||
{
|
||||
IByteBuffer buf = Unpooled.CopiedBuffer(Encoding.UTF8.GetBytes(res.Status.ToString()));
|
||||
res.Content.WriteBytes(buf);
|
||||
buf.Release();
|
||||
HttpUtil.SetContentLength(res, res.Content.ReadableBytes);
|
||||
}
|
||||
|
||||
// Send the response and close the connection if necessary.
|
||||
Task task = ctx.Channel.WriteAndFlushAsync(res);
|
||||
if (!HttpUtil.IsKeepAlive(req) || res.Status.Code != 200)
|
||||
{
|
||||
task.ContinueWith((t, c) => ((IChannelHandlerContext)c).CloseAsync(),
|
||||
ctx, TaskContinuationOptions.ExecuteSynchronously);
|
||||
}
|
||||
}
|
||||
|
||||
public override void ExceptionCaught(IChannelHandlerContext ctx, Exception e)
|
||||
{
|
||||
Console.WriteLine($"{nameof(WebSocketServerHandler)} {0}", e);
|
||||
ctx.CloseAsync();
|
||||
}
|
||||
|
||||
static string GetWebSocketLocation(IFullHttpRequest req)
|
||||
{
|
||||
bool result = req.Headers.TryGet(HttpHeaderNames.Host, out ICharSequence value);
|
||||
Debug.Assert(result, "Host header does not exist.");
|
||||
string location= value.ToString() + WebsocketPath;
|
||||
|
||||
if (ServerSettings.IsSsl)
|
||||
{
|
||||
return "wss://" + location;
|
||||
}
|
||||
else
|
||||
{
|
||||
return "ws://" + location;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<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,4 @@
|
|||
{
|
||||
"port": "8080",
|
||||
"libuv": "true"
|
||||
}
|
|
@ -338,7 +338,7 @@ namespace DotNetty.Buffers
|
|||
return length == 0 ? Empty : CopiedBuffer(new string(array, offset, length), encoding);
|
||||
}
|
||||
|
||||
static IByteBuffer CopiedBuffer(string value, Encoding encoding) => ByteBufferUtil.EncodeString0(Allocator, true, value, encoding, 0);
|
||||
public static IByteBuffer CopiedBuffer(string value, Encoding encoding) => ByteBufferUtil.EncodeString0(Allocator, true, value, encoding, 0);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new 4-byte big-endian buffer that holds the specified 32-bit integer.
|
||||
|
|
|
@ -9,6 +9,12 @@ namespace DotNetty.Codecs.Http
|
|||
|
||||
public sealed class HttpScheme
|
||||
{
|
||||
// Scheme for non-secure HTTP connection.
|
||||
public static readonly HttpScheme Http = new HttpScheme(80, "http");
|
||||
|
||||
// Scheme for secure HTTP connection.
|
||||
public static readonly HttpScheme Https = new HttpScheme(443, "https");
|
||||
|
||||
readonly int port;
|
||||
readonly AsciiString name;
|
||||
|
||||
|
|
|
@ -98,7 +98,6 @@ namespace DotNetty.Codecs.Http
|
|||
base.SanitizeHeadersBeforeEncode(msg, isAlwaysEmpty);
|
||||
}
|
||||
|
||||
|
||||
protected override bool IsContentAlwaysEmpty(IHttpResponse msg)
|
||||
{
|
||||
this.method = this.serverCodec.queue.Count > 0 ? this.serverCodec.queue.Dequeue() : null;
|
||||
|
|
|
@ -219,7 +219,6 @@ namespace DotNetty.Codecs.Http
|
|||
}
|
||||
|
||||
// Make sure the CONNECTION header is present.
|
||||
;
|
||||
if (!request.Headers.TryGet(HttpHeaderNames.Connection, out ICharSequence connectionHeader))
|
||||
{
|
||||
return false;
|
||||
|
@ -310,7 +309,6 @@ namespace DotNetty.Codecs.Http
|
|||
}
|
||||
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;
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using DotNetty.Buffers;
|
||||
|
||||
public class BinaryWebSocketFrame : WebSocketFrame
|
||||
{
|
||||
public BinaryWebSocketFrame()
|
||||
: base(Unpooled.Buffer(0))
|
||||
{
|
||||
}
|
||||
|
||||
public BinaryWebSocketFrame(IByteBuffer binaryData)
|
||||
: base(binaryData)
|
||||
{
|
||||
}
|
||||
|
||||
public BinaryWebSocketFrame(bool finalFragment, int rsv, IByteBuffer binaryData)
|
||||
: base(finalFragment, rsv, binaryData)
|
||||
{
|
||||
}
|
||||
|
||||
public override IByteBufferHolder Replace(IByteBuffer content) => new BinaryWebSocketFrame(this.IsFinalFragment, this.Rsv, content);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using System.Text;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public class CloseWebSocketFrame : WebSocketFrame
|
||||
{
|
||||
public CloseWebSocketFrame()
|
||||
: base(Unpooled.Buffer(0))
|
||||
{
|
||||
}
|
||||
|
||||
public CloseWebSocketFrame(int statusCode, ICharSequence reasonText)
|
||||
: this(true, 0, statusCode, reasonText)
|
||||
{
|
||||
}
|
||||
|
||||
public CloseWebSocketFrame(bool finalFragment, int rsv)
|
||||
: this(finalFragment, rsv, Unpooled.Buffer(0))
|
||||
{
|
||||
}
|
||||
|
||||
public CloseWebSocketFrame(bool finalFragment, int rsv, int statusCode, ICharSequence reasonText)
|
||||
: base(finalFragment, rsv, NewBinaryData(statusCode, reasonText))
|
||||
{
|
||||
}
|
||||
|
||||
static IByteBuffer NewBinaryData(int statusCode, ICharSequence reasonText)
|
||||
{
|
||||
if (reasonText == null)
|
||||
{
|
||||
reasonText = StringCharSequence.Empty;
|
||||
}
|
||||
|
||||
IByteBuffer binaryData = Unpooled.Buffer(2 + reasonText.Count);
|
||||
binaryData.WriteShort(statusCode);
|
||||
if (reasonText.Count > 0)
|
||||
{
|
||||
binaryData.WriteCharSequence(reasonText, Encoding.UTF8);
|
||||
}
|
||||
|
||||
binaryData.SetReaderIndex(0);
|
||||
return binaryData;
|
||||
}
|
||||
|
||||
public CloseWebSocketFrame(bool finalFragment, int rsv, IByteBuffer binaryData)
|
||||
: base(finalFragment, rsv, binaryData)
|
||||
{
|
||||
}
|
||||
|
||||
///<summary>
|
||||
/// Returns the closing status code as per http://tools.ietf.org/html/rfc6455#section-7.4 RFC 6455.
|
||||
/// If a getStatus code is set, -1 is returned.
|
||||
/// </summary>
|
||||
public int StatusCode()
|
||||
{
|
||||
IByteBuffer binaryData = this.Content;
|
||||
if (binaryData == null || binaryData.Capacity == 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
binaryData.SetReaderIndex(0);
|
||||
int statusCode = binaryData.ReadShort();
|
||||
binaryData.SetReaderIndex(0);
|
||||
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
///<summary>
|
||||
/// Returns the reason text as per http://tools.ietf.org/html/rfc6455#section-7.4 RFC 6455
|
||||
/// If a reason text is not supplied, an empty string is returned.
|
||||
/// </summary>
|
||||
public ICharSequence ReasonText()
|
||||
{
|
||||
IByteBuffer binaryData = this.Content;
|
||||
if (binaryData == null || binaryData.Capacity <= 2)
|
||||
{
|
||||
return StringCharSequence.Empty;
|
||||
}
|
||||
|
||||
binaryData.SetReaderIndex(2);
|
||||
string reasonText = binaryData.ToString(Encoding.UTF8);
|
||||
binaryData.SetReaderIndex(0);
|
||||
|
||||
return new StringCharSequence(reasonText);
|
||||
}
|
||||
|
||||
public override IByteBufferHolder Replace(IByteBuffer content) => new CloseWebSocketFrame(this.IsFinalFragment, this.Rsv, content);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using System.Text;
|
||||
using DotNetty.Buffers;
|
||||
|
||||
public class ContinuationWebSocketFrame : WebSocketFrame
|
||||
{
|
||||
public ContinuationWebSocketFrame()
|
||||
: this(Unpooled.Buffer(0))
|
||||
{
|
||||
}
|
||||
|
||||
public ContinuationWebSocketFrame(IByteBuffer binaryData)
|
||||
: base(binaryData)
|
||||
{
|
||||
}
|
||||
|
||||
public ContinuationWebSocketFrame(bool finalFragment, int rsv, IByteBuffer binaryData)
|
||||
: base(finalFragment, rsv, binaryData)
|
||||
{
|
||||
}
|
||||
|
||||
public ContinuationWebSocketFrame(bool finalFragment, int rsv, string text)
|
||||
: this(finalFragment, rsv, FromText(text))
|
||||
{
|
||||
}
|
||||
|
||||
public string Text() => this.Content.ToString(Encoding.UTF8);
|
||||
|
||||
static IByteBuffer FromText(string text) => string.IsNullOrEmpty(text)
|
||||
? Unpooled.Empty : Unpooled.CopiedBuffer(text, Encoding.UTF8);
|
||||
|
||||
public override IByteBufferHolder Replace(IByteBuffer content) => new ContinuationWebSocketFrame(this.IsFinalFragment, this.Rsv, content);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
// 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.WebSockets.Extensions.Compression
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Codecs.Compression;
|
||||
using DotNetty.Transport.Channels;
|
||||
using DotNetty.Transport.Channels.Embedded;
|
||||
|
||||
abstract class DeflateDecoder : WebSocketExtensionDecoder
|
||||
{
|
||||
internal static readonly byte[] FrameTail = { 0x00, 0x00, 0xff, 0xff };
|
||||
|
||||
readonly bool noContext;
|
||||
|
||||
EmbeddedChannel decoder;
|
||||
|
||||
protected DeflateDecoder(bool noContext)
|
||||
{
|
||||
this.noContext = noContext;
|
||||
}
|
||||
|
||||
protected abstract bool AppendFrameTail(WebSocketFrame msg);
|
||||
|
||||
protected abstract int NewRsv(WebSocketFrame msg);
|
||||
|
||||
protected override void Decode(IChannelHandlerContext ctx, WebSocketFrame msg, List<object> output)
|
||||
{
|
||||
if (this.decoder == null)
|
||||
{
|
||||
if (!(msg is TextWebSocketFrame) && !(msg is BinaryWebSocketFrame))
|
||||
{
|
||||
throw new CodecException($"unexpected initial frame type: {msg.GetType().Name}");
|
||||
}
|
||||
|
||||
this.decoder = new EmbeddedChannel(ZlibCodecFactory.NewZlibDecoder(ZlibWrapper.None));
|
||||
}
|
||||
|
||||
bool readable = msg.Content.IsReadable();
|
||||
this.decoder.WriteInbound(msg.Content.Retain());
|
||||
if (this.AppendFrameTail(msg))
|
||||
{
|
||||
this.decoder.WriteInbound(Unpooled.WrappedBuffer(FrameTail));
|
||||
}
|
||||
|
||||
CompositeByteBuffer compositeUncompressedContent = ctx.Allocator.CompositeDirectBuffer();
|
||||
for (;;)
|
||||
{
|
||||
var partUncompressedContent = this.decoder.ReadInbound<IByteBuffer>();
|
||||
if (partUncompressedContent == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!partUncompressedContent.IsReadable())
|
||||
{
|
||||
partUncompressedContent.Release();
|
||||
continue;
|
||||
}
|
||||
|
||||
compositeUncompressedContent.AddComponent(true, partUncompressedContent);
|
||||
}
|
||||
|
||||
// Correctly handle empty frames
|
||||
// See https://github.com/netty/netty/issues/4348
|
||||
if (readable && compositeUncompressedContent.NumComponents <= 0)
|
||||
{
|
||||
compositeUncompressedContent.Release();
|
||||
throw new CodecException("cannot read uncompressed buffer");
|
||||
}
|
||||
|
||||
if (msg.IsFinalFragment && this.noContext)
|
||||
{
|
||||
this.Cleanup();
|
||||
}
|
||||
|
||||
WebSocketFrame outMsg;
|
||||
if (msg is TextWebSocketFrame)
|
||||
{
|
||||
outMsg = new TextWebSocketFrame(msg.IsFinalFragment, this.NewRsv(msg), compositeUncompressedContent);
|
||||
}
|
||||
else if (msg is BinaryWebSocketFrame)
|
||||
{
|
||||
outMsg = new BinaryWebSocketFrame(msg.IsFinalFragment, this.NewRsv(msg), compositeUncompressedContent);
|
||||
}
|
||||
else if (msg is ContinuationWebSocketFrame)
|
||||
{
|
||||
outMsg = new ContinuationWebSocketFrame(msg.IsFinalFragment, this.NewRsv(msg), compositeUncompressedContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new CodecException($"unexpected frame type: {msg.GetType().Name}");
|
||||
}
|
||||
|
||||
output.Add(outMsg);
|
||||
}
|
||||
|
||||
public override void HandlerRemoved(IChannelHandlerContext ctx)
|
||||
{
|
||||
this.Cleanup();
|
||||
base.HandlerRemoved(ctx);
|
||||
}
|
||||
|
||||
public override void ChannelInactive(IChannelHandlerContext ctx)
|
||||
{
|
||||
this.Cleanup();
|
||||
base.ChannelInactive(ctx);
|
||||
}
|
||||
|
||||
void Cleanup()
|
||||
{
|
||||
if (this.decoder != null)
|
||||
{
|
||||
// Clean-up the previous encoder if not cleaned up correctly.
|
||||
if (this.decoder.Finish())
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
var buf = this.decoder.ReadOutbound<IByteBuffer>();
|
||||
if (buf == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
// Release the buffer
|
||||
buf.Release();
|
||||
}
|
||||
}
|
||||
this.decoder = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
// 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.WebSockets.Extensions.Compression
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Codecs.Compression;
|
||||
using DotNetty.Transport.Channels;
|
||||
using DotNetty.Transport.Channels.Embedded;
|
||||
|
||||
using static DeflateDecoder;
|
||||
|
||||
abstract class DeflateEncoder : WebSocketExtensionEncoder
|
||||
{
|
||||
readonly int compressionLevel;
|
||||
readonly int windowSize;
|
||||
readonly bool noContext;
|
||||
|
||||
EmbeddedChannel encoder;
|
||||
|
||||
protected DeflateEncoder(int compressionLevel, int windowSize, bool noContext)
|
||||
{
|
||||
this.compressionLevel = compressionLevel;
|
||||
this.windowSize = windowSize;
|
||||
this.noContext = noContext;
|
||||
}
|
||||
|
||||
protected abstract int Rsv(WebSocketFrame msg);
|
||||
|
||||
protected abstract bool RemoveFrameTail(WebSocketFrame msg);
|
||||
|
||||
protected override void Encode(IChannelHandlerContext ctx, WebSocketFrame msg, List<object> output)
|
||||
{
|
||||
if (this.encoder == null)
|
||||
{
|
||||
this.encoder = new EmbeddedChannel(
|
||||
ZlibCodecFactory.NewZlibEncoder(
|
||||
ZlibWrapper.None,
|
||||
this.compressionLevel,
|
||||
this.windowSize,
|
||||
8));
|
||||
}
|
||||
|
||||
this.encoder.WriteOutbound(msg.Content.Retain());
|
||||
|
||||
CompositeByteBuffer fullCompressedContent = ctx.Allocator.CompositeBuffer();
|
||||
for (;;)
|
||||
{
|
||||
var partCompressedContent = this.encoder.ReadOutbound<IByteBuffer>();
|
||||
if (partCompressedContent == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!partCompressedContent.IsReadable())
|
||||
{
|
||||
partCompressedContent.Release();
|
||||
continue;
|
||||
}
|
||||
|
||||
fullCompressedContent.AddComponent(true, partCompressedContent);
|
||||
}
|
||||
|
||||
if (fullCompressedContent.NumComponents <= 0)
|
||||
{
|
||||
fullCompressedContent.Release();
|
||||
throw new CodecException("cannot read compressed buffer");
|
||||
}
|
||||
|
||||
if (msg.IsFinalFragment && this.noContext)
|
||||
{
|
||||
this.Cleanup();
|
||||
}
|
||||
|
||||
IByteBuffer compressedContent;
|
||||
if (this.RemoveFrameTail(msg))
|
||||
{
|
||||
int realLength = fullCompressedContent.ReadableBytes - FrameTail.Length;
|
||||
compressedContent = fullCompressedContent.Slice(0, realLength);
|
||||
}
|
||||
else
|
||||
{
|
||||
compressedContent = fullCompressedContent;
|
||||
}
|
||||
|
||||
WebSocketFrame outMsg;
|
||||
if (msg is TextWebSocketFrame)
|
||||
{
|
||||
outMsg = new TextWebSocketFrame(msg.IsFinalFragment, this.Rsv(msg), compressedContent);
|
||||
}
|
||||
else if (msg is BinaryWebSocketFrame)
|
||||
{
|
||||
outMsg = new BinaryWebSocketFrame(msg.IsFinalFragment, this.Rsv(msg), compressedContent);
|
||||
}
|
||||
else if (msg is ContinuationWebSocketFrame)
|
||||
{
|
||||
outMsg = new ContinuationWebSocketFrame(msg.IsFinalFragment, this.Rsv(msg), compressedContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new CodecException($"unexpected frame type: {msg.GetType().Name}");
|
||||
}
|
||||
|
||||
output.Add(outMsg);
|
||||
}
|
||||
|
||||
public override void HandlerRemoved(IChannelHandlerContext ctx)
|
||||
{
|
||||
this.Cleanup();
|
||||
base.HandlerRemoved(ctx);
|
||||
}
|
||||
|
||||
void Cleanup()
|
||||
{
|
||||
if (this.encoder != null)
|
||||
{
|
||||
// Clean-up the previous encoder if not cleaned up correctly.
|
||||
if (this.encoder.Finish())
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
var buf = this.encoder.ReadOutbound<IByteBuffer>();
|
||||
if (buf == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
// Release the buffer
|
||||
buf.Release();
|
||||
}
|
||||
}
|
||||
this.encoder = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
// 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.WebSockets.Extensions.Compression
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using static DeflateFrameServerExtensionHandshaker;
|
||||
|
||||
public sealed class DeflateFrameClientExtensionHandshaker : IWebSocketClientExtensionHandshaker
|
||||
{
|
||||
readonly int compressionLevel;
|
||||
readonly bool useWebkitExtensionName;
|
||||
|
||||
public DeflateFrameClientExtensionHandshaker(bool useWebkitExtensionName)
|
||||
: this(6, useWebkitExtensionName)
|
||||
{
|
||||
}
|
||||
|
||||
public DeflateFrameClientExtensionHandshaker(int compressionLevel, bool useWebkitExtensionName)
|
||||
{
|
||||
if (compressionLevel < 0 || compressionLevel > 9)
|
||||
{
|
||||
throw new ArgumentException($"compressionLevel: {compressionLevel} (expected: 0-9)");
|
||||
}
|
||||
this.compressionLevel = compressionLevel;
|
||||
this.useWebkitExtensionName = useWebkitExtensionName;
|
||||
}
|
||||
|
||||
public WebSocketExtensionData NewRequestData() => new WebSocketExtensionData(
|
||||
this.useWebkitExtensionName ? XWebkitDeflateFrameExtension : DeflateFrameExtension,
|
||||
new Dictionary<string, string>());
|
||||
|
||||
public IWebSocketClientExtension HandshakeExtension(WebSocketExtensionData extensionData)
|
||||
{
|
||||
if (!XWebkitDeflateFrameExtension.Equals(extensionData.Name) &&
|
||||
!DeflateFrameExtension.Equals(extensionData.Name))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (extensionData.Parameters.Count == 0)
|
||||
{
|
||||
return new DeflateFrameClientExtension(this.compressionLevel);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
sealed class DeflateFrameClientExtension : IWebSocketClientExtension
|
||||
{
|
||||
readonly int compressionLevel;
|
||||
|
||||
public DeflateFrameClientExtension(int compressionLevel)
|
||||
{
|
||||
this.compressionLevel = compressionLevel;
|
||||
}
|
||||
|
||||
public int Rsv => WebSocketRsv.Rsv1;
|
||||
|
||||
public WebSocketExtensionEncoder NewExtensionEncoder() => new PerFrameDeflateEncoder(this.compressionLevel, 15, false);
|
||||
|
||||
public WebSocketExtensionDecoder NewExtensionDecoder() => new PerFrameDeflateDecoder(false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
// 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.WebSockets.Extensions.Compression
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public sealed class DeflateFrameServerExtensionHandshaker : IWebSocketServerExtensionHandshaker
|
||||
{
|
||||
internal static readonly string XWebkitDeflateFrameExtension = "x-webkit-deflate-frame";
|
||||
internal static readonly string DeflateFrameExtension = "deflate-frame";
|
||||
|
||||
readonly int compressionLevel;
|
||||
|
||||
public DeflateFrameServerExtensionHandshaker()
|
||||
: this(6)
|
||||
{
|
||||
}
|
||||
|
||||
public DeflateFrameServerExtensionHandshaker(int compressionLevel)
|
||||
{
|
||||
if (compressionLevel < 0 || compressionLevel > 9)
|
||||
{
|
||||
throw new ArgumentException($"compressionLevel: {compressionLevel} (expected: 0-9)");
|
||||
}
|
||||
this.compressionLevel = compressionLevel;
|
||||
}
|
||||
|
||||
public IWebSocketServerExtension HandshakeExtension(WebSocketExtensionData extensionData)
|
||||
{
|
||||
if (!XWebkitDeflateFrameExtension.Equals(extensionData.Name)
|
||||
&& !DeflateFrameExtension.Equals(extensionData.Name))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (extensionData.Parameters.Count == 0)
|
||||
{
|
||||
return new DeflateFrameServerExtension(this.compressionLevel, extensionData.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
sealed class DeflateFrameServerExtension : IWebSocketServerExtension
|
||||
{
|
||||
readonly string extensionName;
|
||||
readonly int compressionLevel;
|
||||
|
||||
public DeflateFrameServerExtension(int compressionLevel, string extensionName)
|
||||
{
|
||||
this.extensionName = extensionName;
|
||||
this.compressionLevel = compressionLevel;
|
||||
}
|
||||
|
||||
public int Rsv => WebSocketRsv.Rsv1;
|
||||
|
||||
public WebSocketExtensionEncoder NewExtensionEncoder() => new PerFrameDeflateEncoder(this.compressionLevel, 15, false);
|
||||
|
||||
public WebSocketExtensionDecoder NewExtensionDecoder() => new PerFrameDeflateDecoder(false);
|
||||
|
||||
public WebSocketExtensionData NewReponseData() => new WebSocketExtensionData(this.extensionName, new Dictionary<string, string>());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
||||
namespace DotNetty.Codecs.Http.WebSockets.Extensions.Compression
|
||||
{
|
||||
class PerFrameDeflateDecoder : DeflateDecoder
|
||||
{
|
||||
public PerFrameDeflateDecoder(bool noContext)
|
||||
: base(noContext)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool AcceptInboundMessage(object msg) =>
|
||||
(msg is TextWebSocketFrame || msg is BinaryWebSocketFrame || msg is ContinuationWebSocketFrame)
|
||||
&& (((WebSocketFrame) msg).Rsv & WebSocketRsv.Rsv1) > 0;
|
||||
|
||||
protected override int NewRsv(WebSocketFrame msg) => msg.Rsv ^ WebSocketRsv.Rsv1;
|
||||
|
||||
protected override bool AppendFrameTail(WebSocketFrame msg) => true;
|
||||
}
|
||||
}
|
|
@ -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.WebSockets.Extensions.Compression
|
||||
{
|
||||
class PerFrameDeflateEncoder : DeflateEncoder
|
||||
{
|
||||
public PerFrameDeflateEncoder(int compressionLevel, int windowSize, bool noContext)
|
||||
: base(compressionLevel, windowSize, noContext)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool AcceptOutboundMessage(object msg) =>
|
||||
(msg is TextWebSocketFrame || msg is BinaryWebSocketFrame || msg is ContinuationWebSocketFrame)
|
||||
&& ((WebSocketFrame)msg).Content.ReadableBytes > 0
|
||||
&& (((WebSocketFrame)msg).Rsv & WebSocketRsv.Rsv1) == 0;
|
||||
|
||||
protected override int Rsv(WebSocketFrame msg) => msg.Rsv | WebSocketRsv.Rsv1;
|
||||
|
||||
protected override bool RemoveFrameTail(WebSocketFrame msg) => true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
// 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.WebSockets.Extensions.Compression
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DotNetty.Codecs.Compression;
|
||||
|
||||
using static PerMessageDeflateServerExtensionHandshaker;
|
||||
|
||||
public sealed class PerMessageDeflateClientExtensionHandshaker : IWebSocketClientExtensionHandshaker
|
||||
{
|
||||
readonly int compressionLevel;
|
||||
readonly bool allowClientWindowSize;
|
||||
readonly int requestedServerWindowSize;
|
||||
readonly bool allowClientNoContext;
|
||||
readonly bool requestedServerNoContext;
|
||||
|
||||
public PerMessageDeflateClientExtensionHandshaker()
|
||||
: this(6, ZlibCodecFactory.IsSupportingWindowSizeAndMemLevel, MaxWindowSize, false, false)
|
||||
{
|
||||
}
|
||||
|
||||
public PerMessageDeflateClientExtensionHandshaker(int compressionLevel,
|
||||
bool allowClientWindowSize, int requestedServerWindowSize,
|
||||
bool allowClientNoContext, bool requestedServerNoContext)
|
||||
{
|
||||
if (requestedServerWindowSize > MaxWindowSize || requestedServerWindowSize < MinWindowSize)
|
||||
{
|
||||
throw new ArgumentException($"requestedServerWindowSize: {requestedServerWindowSize} (expected: 8-15)");
|
||||
}
|
||||
if (compressionLevel < 0 || compressionLevel > 9)
|
||||
{
|
||||
throw new ArgumentException($"compressionLevel: {compressionLevel} (expected: 0-9)");
|
||||
}
|
||||
this.compressionLevel = compressionLevel;
|
||||
this.allowClientWindowSize = allowClientWindowSize;
|
||||
this.requestedServerWindowSize = requestedServerWindowSize;
|
||||
this.allowClientNoContext = allowClientNoContext;
|
||||
this.requestedServerNoContext = requestedServerNoContext;
|
||||
}
|
||||
|
||||
public WebSocketExtensionData NewRequestData()
|
||||
{
|
||||
var parameters = new Dictionary<string, string>(4);
|
||||
if (this.requestedServerWindowSize != MaxWindowSize)
|
||||
{
|
||||
parameters.Add(ServerNoContext, null);
|
||||
}
|
||||
if (this.allowClientNoContext)
|
||||
{
|
||||
parameters.Add(ClientNoContext, null);
|
||||
}
|
||||
if (this.requestedServerWindowSize != MaxWindowSize)
|
||||
{
|
||||
parameters.Add(ServerMaxWindow, Convert.ToString(this.requestedServerWindowSize));
|
||||
}
|
||||
if (this.allowClientWindowSize)
|
||||
{
|
||||
parameters.Add(ClientMaxWindow, null);
|
||||
}
|
||||
return new WebSocketExtensionData(PerMessageDeflateExtension, parameters);
|
||||
}
|
||||
|
||||
public IWebSocketClientExtension HandshakeExtension(WebSocketExtensionData extensionData)
|
||||
{
|
||||
if (!PerMessageDeflateExtension.Equals(extensionData.Name))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
bool succeed = true;
|
||||
int clientWindowSize = MaxWindowSize;
|
||||
int serverWindowSize = MaxWindowSize;
|
||||
bool serverNoContext = false;
|
||||
bool clientNoContext = false;
|
||||
|
||||
foreach (KeyValuePair<string, string> parameter in extensionData.Parameters)
|
||||
{
|
||||
if (ClientMaxWindow.Equals(parameter.Key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// allowed client_window_size_bits
|
||||
if (this.allowClientWindowSize)
|
||||
{
|
||||
clientWindowSize = int.Parse(parameter.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
succeed = false;
|
||||
}
|
||||
}
|
||||
else if (ServerMaxWindow.Equals(parameter.Key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// acknowledged server_window_size_bits
|
||||
serverWindowSize = int.Parse(parameter.Value);
|
||||
if (clientWindowSize > MaxWindowSize || clientWindowSize < MinWindowSize)
|
||||
{
|
||||
succeed = false;
|
||||
}
|
||||
}
|
||||
else if (ClientNoContext.Equals(parameter.Key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// allowed client_no_context_takeover
|
||||
if (this.allowClientNoContext)
|
||||
{
|
||||
clientNoContext = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
succeed = false;
|
||||
}
|
||||
}
|
||||
else if (ServerNoContext.Equals(parameter.Key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// acknowledged server_no_context_takeover
|
||||
if (this.requestedServerNoContext)
|
||||
{
|
||||
serverNoContext = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
succeed = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// unknown parameter
|
||||
succeed = false;
|
||||
}
|
||||
|
||||
if (!succeed)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ((this.requestedServerNoContext && !serverNoContext)
|
||||
|| this.requestedServerWindowSize != serverWindowSize)
|
||||
{
|
||||
succeed = false;
|
||||
}
|
||||
|
||||
if (succeed)
|
||||
{
|
||||
return new WebSocketPermessageDeflateExtension(serverNoContext, serverWindowSize,
|
||||
clientNoContext, this.compressionLevel);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
sealed class WebSocketPermessageDeflateExtension : IWebSocketClientExtension
|
||||
{
|
||||
readonly bool serverNoContext;
|
||||
readonly int serverWindowSize;
|
||||
readonly bool clientNoContext;
|
||||
readonly int compressionLevel;
|
||||
|
||||
public int Rsv => WebSocketRsv.Rsv1;
|
||||
|
||||
public WebSocketPermessageDeflateExtension(bool serverNoContext, int serverWindowSize,
|
||||
bool clientNoContext, int compressionLevel)
|
||||
{
|
||||
this.serverNoContext = serverNoContext;
|
||||
this.serverWindowSize = serverWindowSize;
|
||||
this.clientNoContext = clientNoContext;
|
||||
this.compressionLevel = compressionLevel;
|
||||
}
|
||||
|
||||
public WebSocketExtensionEncoder NewExtensionEncoder() =>
|
||||
new PerMessageDeflateEncoder(this.compressionLevel, this.serverWindowSize, this.serverNoContext);
|
||||
|
||||
public WebSocketExtensionDecoder NewExtensionDecoder() => new PerMessageDeflateDecoder(this.clientNoContext);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.WebSockets.Extensions.Compression
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
class PerMessageDeflateDecoder : DeflateDecoder
|
||||
{
|
||||
bool compressing;
|
||||
|
||||
public PerMessageDeflateDecoder(bool noContext)
|
||||
: base(noContext)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool AcceptInboundMessage(object msg) =>
|
||||
((msg is TextWebSocketFrame || msg is BinaryWebSocketFrame)
|
||||
&& (((WebSocketFrame)msg).Rsv & WebSocketRsv.Rsv1) > 0)
|
||||
|| (msg is ContinuationWebSocketFrame && this.compressing);
|
||||
|
||||
protected override int NewRsv(WebSocketFrame msg) =>
|
||||
(msg.Rsv & WebSocketRsv.Rsv1) > 0 ? msg.Rsv ^ WebSocketRsv.Rsv1 : msg.Rsv;
|
||||
|
||||
protected override bool AppendFrameTail(WebSocketFrame msg) => msg.IsFinalFragment;
|
||||
|
||||
protected override void Decode(IChannelHandlerContext ctx, WebSocketFrame msg, List<object> output)
|
||||
{
|
||||
base.Decode(ctx, msg, output);
|
||||
|
||||
if (msg.IsFinalFragment)
|
||||
{
|
||||
this.compressing = false;
|
||||
}
|
||||
else if (msg is TextWebSocketFrame || msg is BinaryWebSocketFrame)
|
||||
{
|
||||
this.compressing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.WebSockets.Extensions.Compression
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
class PerMessageDeflateEncoder : DeflateEncoder
|
||||
{
|
||||
bool compressing;
|
||||
|
||||
public PerMessageDeflateEncoder(int compressionLevel, int windowSize, bool noContext)
|
||||
: base(compressionLevel, windowSize, noContext)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool AcceptOutboundMessage(object msg) =>
|
||||
((msg is TextWebSocketFrame || msg is BinaryWebSocketFrame)
|
||||
&& (((WebSocketFrame) msg).Rsv & WebSocketRsv.Rsv1) == 0)
|
||||
|| (msg is ContinuationWebSocketFrame && this.compressing);
|
||||
|
||||
protected override int Rsv(WebSocketFrame msg) =>
|
||||
msg is TextWebSocketFrame || msg is BinaryWebSocketFrame
|
||||
? msg.Rsv | WebSocketRsv.Rsv1
|
||||
: msg.Rsv;
|
||||
|
||||
protected override bool RemoveFrameTail(WebSocketFrame msg) => msg.IsFinalFragment;
|
||||
|
||||
protected override void Encode(IChannelHandlerContext ctx, WebSocketFrame msg, List<object> output)
|
||||
{
|
||||
base.Encode(ctx, msg, output);
|
||||
|
||||
if (msg.IsFinalFragment)
|
||||
{
|
||||
this.compressing = false;
|
||||
}
|
||||
else if (msg is TextWebSocketFrame || msg is BinaryWebSocketFrame)
|
||||
{
|
||||
this.compressing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
// 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.WebSockets.Extensions.Compression
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DotNetty.Codecs.Compression;
|
||||
|
||||
public sealed class PerMessageDeflateServerExtensionHandshaker : IWebSocketServerExtensionHandshaker
|
||||
{
|
||||
public static readonly int MinWindowSize = 8;
|
||||
public static readonly int MaxWindowSize = 15;
|
||||
|
||||
internal static readonly string PerMessageDeflateExtension = "permessage-deflate";
|
||||
internal static readonly string ClientMaxWindow = "client_max_window_bits";
|
||||
internal static readonly string ServerMaxWindow = "server_max_window_bits";
|
||||
internal static readonly string ClientNoContext = "client_no_context_takeover";
|
||||
internal static readonly string ServerNoContext = "server_no_context_takeover";
|
||||
|
||||
readonly int compressionLevel;
|
||||
readonly bool allowServerWindowSize;
|
||||
readonly int preferredClientWindowSize;
|
||||
readonly bool allowServerNoContext;
|
||||
readonly bool preferredClientNoContext;
|
||||
|
||||
public PerMessageDeflateServerExtensionHandshaker()
|
||||
: this(6, ZlibCodecFactory.IsSupportingWindowSizeAndMemLevel, MaxWindowSize, false, false)
|
||||
{
|
||||
}
|
||||
|
||||
public PerMessageDeflateServerExtensionHandshaker(int compressionLevel,
|
||||
bool allowServerWindowSize, int preferredClientWindowSize,
|
||||
bool allowServerNoContext, bool preferredClientNoContext)
|
||||
{
|
||||
if (preferredClientWindowSize > MaxWindowSize || preferredClientWindowSize < MinWindowSize)
|
||||
{
|
||||
throw new ArgumentException($"preferredServerWindowSize: {preferredClientWindowSize} (expected: 8-15)");
|
||||
}
|
||||
if (compressionLevel < 0 || compressionLevel > 9)
|
||||
{
|
||||
throw new ArgumentException($"compressionLevel: {compressionLevel} (expected: 0-9)");
|
||||
}
|
||||
this.compressionLevel = compressionLevel;
|
||||
this.allowServerWindowSize = allowServerWindowSize;
|
||||
this.preferredClientWindowSize = preferredClientWindowSize;
|
||||
this.allowServerNoContext = allowServerNoContext;
|
||||
this.preferredClientNoContext = preferredClientNoContext;
|
||||
}
|
||||
|
||||
public IWebSocketServerExtension HandshakeExtension(WebSocketExtensionData extensionData)
|
||||
{
|
||||
if (!PerMessageDeflateExtension.Equals(extensionData.Name))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
bool deflateEnabled = true;
|
||||
int clientWindowSize = MaxWindowSize;
|
||||
int serverWindowSize = MaxWindowSize;
|
||||
bool serverNoContext = false;
|
||||
bool clientNoContext = false;
|
||||
|
||||
foreach (KeyValuePair<string, string> parameter in extensionData.Parameters)
|
||||
{
|
||||
if (ClientMaxWindow.Equals(parameter.Key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// use preferred clientWindowSize because client is compatible with customization
|
||||
clientWindowSize = this.preferredClientWindowSize;
|
||||
}
|
||||
else if (ServerMaxWindow.Equals(parameter.Key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// use provided windowSize if it is allowed
|
||||
if (this.allowServerWindowSize)
|
||||
{
|
||||
serverWindowSize = int.Parse(parameter.Value);
|
||||
if (serverWindowSize > MaxWindowSize || serverWindowSize < MinWindowSize)
|
||||
{
|
||||
deflateEnabled = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
deflateEnabled = false;
|
||||
}
|
||||
}
|
||||
else if (ClientNoContext.Equals(parameter.Key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// use preferred clientNoContext because client is compatible with customization
|
||||
clientNoContext = this.preferredClientNoContext;
|
||||
}
|
||||
else if (ServerNoContext.Equals(parameter.Key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// use server no context if allowed
|
||||
if (this.allowServerNoContext)
|
||||
{
|
||||
serverNoContext = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
deflateEnabled = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// unknown parameter
|
||||
deflateEnabled = false;
|
||||
}
|
||||
if (!deflateEnabled)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (deflateEnabled)
|
||||
{
|
||||
return new WebSocketPermessageDeflateExtension(this.compressionLevel, serverNoContext,
|
||||
serverWindowSize, clientNoContext, clientWindowSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
sealed class WebSocketPermessageDeflateExtension : IWebSocketServerExtension
|
||||
{
|
||||
readonly int compressionLevel;
|
||||
readonly bool serverNoContext;
|
||||
readonly int serverWindowSize;
|
||||
readonly bool clientNoContext;
|
||||
readonly int clientWindowSize;
|
||||
|
||||
public WebSocketPermessageDeflateExtension(int compressionLevel, bool serverNoContext,
|
||||
int serverWindowSize, bool clientNoContext, int clientWindowSize)
|
||||
{
|
||||
this.compressionLevel = compressionLevel;
|
||||
this.serverNoContext = serverNoContext;
|
||||
this.serverWindowSize = serverWindowSize;
|
||||
this.clientNoContext = clientNoContext;
|
||||
this.clientWindowSize = clientWindowSize;
|
||||
}
|
||||
|
||||
public int Rsv => WebSocketRsv.Rsv1;
|
||||
|
||||
public WebSocketExtensionEncoder NewExtensionEncoder() =>
|
||||
new PerMessageDeflateEncoder(this.compressionLevel, this.clientWindowSize, this.clientNoContext);
|
||||
|
||||
public WebSocketExtensionDecoder NewExtensionDecoder() => new PerMessageDeflateDecoder(this.serverNoContext);
|
||||
|
||||
public WebSocketExtensionData NewReponseData()
|
||||
{
|
||||
var parameters = new Dictionary<string, string>(4);
|
||||
if (this.serverNoContext)
|
||||
{
|
||||
parameters.Add(ServerNoContext, null);
|
||||
}
|
||||
if (this.clientNoContext)
|
||||
{
|
||||
parameters.Add(ClientNoContext, null);
|
||||
}
|
||||
if (this.serverWindowSize != MaxWindowSize)
|
||||
{
|
||||
parameters.Add(ServerMaxWindow, Convert.ToString(this.serverWindowSize));
|
||||
}
|
||||
if (this.clientWindowSize != MaxWindowSize)
|
||||
{
|
||||
parameters.Add(ClientMaxWindow, Convert.ToString(this.clientWindowSize));
|
||||
}
|
||||
return new WebSocketExtensionData(PerMessageDeflateExtension, parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// 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.WebSockets.Extensions.Compression
|
||||
{
|
||||
public sealed class WebSocketClientCompressionHandler : WebSocketClientExtensionHandler
|
||||
{
|
||||
public static readonly WebSocketClientCompressionHandler Instance = new WebSocketClientCompressionHandler();
|
||||
|
||||
public override bool IsSharable => true;
|
||||
|
||||
WebSocketClientCompressionHandler()
|
||||
: base(new PerMessageDeflateClientExtensionHandshaker(),
|
||||
new DeflateFrameClientExtensionHandshaker(false),
|
||||
new DeflateFrameClientExtensionHandshaker(true))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// 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.WebSockets.Extensions.Compression
|
||||
{
|
||||
public class WebSocketServerCompressionHandler : WebSocketServerExtensionHandler
|
||||
{
|
||||
public WebSocketServerCompressionHandler()
|
||||
: base(new PerMessageDeflateServerExtensionHandshaker(), new DeflateFrameServerExtensionHandshaker())
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.WebSockets.Extensions
|
||||
{
|
||||
public interface IWebSocketClientExtension : IWebSocketExtension
|
||||
{
|
||||
}
|
||||
}
|
|
@ -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.WebSockets.Extensions
|
||||
{
|
||||
public interface IWebSocketClientExtensionHandshaker
|
||||
{
|
||||
WebSocketExtensionData NewRequestData();
|
||||
|
||||
IWebSocketClientExtension HandshakeExtension(WebSocketExtensionData extensionData);
|
||||
}
|
||||
}
|
|
@ -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.WebSockets.Extensions
|
||||
{
|
||||
public interface IWebSocketExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// The reserved bit value to ensure that no other extension should interfere.
|
||||
/// </summary>
|
||||
int Rsv { get; }
|
||||
|
||||
WebSocketExtensionEncoder NewExtensionEncoder();
|
||||
|
||||
WebSocketExtensionDecoder NewExtensionDecoder();
|
||||
}
|
||||
|
||||
public static class WebSocketRsv
|
||||
{
|
||||
public static readonly int Rsv1 = 0x04;
|
||||
public static readonly int Rsv2 = 0x02;
|
||||
public static readonly int Rsv3 = 0x01;
|
||||
}
|
||||
}
|
|
@ -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.WebSockets.Extensions
|
||||
{
|
||||
public interface IWebSocketServerExtension : IWebSocketExtension
|
||||
{
|
||||
WebSocketExtensionData NewReponseData();
|
||||
}
|
||||
}
|
|
@ -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.WebSockets.Extensions
|
||||
{
|
||||
public interface IWebSocketServerExtensionHandshaker
|
||||
{
|
||||
IWebSocketServerExtension HandshakeExtension(WebSocketExtensionData extensionData);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
||||
namespace DotNetty.Codecs.Http.WebSockets.Extensions
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public class WebSocketClientExtensionHandler : ChannelHandlerAdapter
|
||||
{
|
||||
readonly List<IWebSocketClientExtensionHandshaker> extensionHandshakers;
|
||||
|
||||
public WebSocketClientExtensionHandler(params IWebSocketClientExtensionHandshaker[] extensionHandshakers)
|
||||
{
|
||||
Contract.Requires(extensionHandshakers != null && extensionHandshakers.Length > 0);
|
||||
this.extensionHandshakers = new List<IWebSocketClientExtensionHandshaker>(extensionHandshakers);
|
||||
}
|
||||
|
||||
public override Task WriteAsync(IChannelHandlerContext ctx, object msg)
|
||||
{
|
||||
if (msg is IHttpRequest request && WebSocketExtensionUtil.IsWebsocketUpgrade(request.Headers))
|
||||
{
|
||||
string headerValue = null;
|
||||
if (request.Headers.TryGet(HttpHeaderNames.SecWebsocketExtensions, out ICharSequence value))
|
||||
{
|
||||
headerValue = value.ToString();
|
||||
}
|
||||
|
||||
foreach (IWebSocketClientExtensionHandshaker extensionHandshaker in this.extensionHandshakers)
|
||||
{
|
||||
WebSocketExtensionData extensionData = extensionHandshaker.NewRequestData();
|
||||
headerValue = WebSocketExtensionUtil.AppendExtension(headerValue,
|
||||
extensionData.Name, extensionData.Parameters);
|
||||
}
|
||||
|
||||
request.Headers.Set(HttpHeaderNames.SecWebsocketExtensions, headerValue);
|
||||
}
|
||||
|
||||
return base.WriteAsync(ctx, msg);
|
||||
}
|
||||
|
||||
public override void ChannelRead(IChannelHandlerContext ctx, object msg)
|
||||
{
|
||||
if (msg is IHttpResponse response
|
||||
&& WebSocketExtensionUtil.IsWebsocketUpgrade(response.Headers))
|
||||
{
|
||||
string extensionsHeader = null;
|
||||
if (response.Headers.TryGet(HttpHeaderNames.SecWebsocketExtensions, out ICharSequence value))
|
||||
{
|
||||
extensionsHeader = value.ToString();
|
||||
}
|
||||
|
||||
if (extensionsHeader != null)
|
||||
{
|
||||
List<WebSocketExtensionData> extensions =
|
||||
WebSocketExtensionUtil.ExtractExtensions(extensionsHeader);
|
||||
var validExtensions = new List<IWebSocketClientExtension>(extensions.Count);
|
||||
int rsv = 0;
|
||||
|
||||
foreach (WebSocketExtensionData extensionData in extensions)
|
||||
{
|
||||
IWebSocketClientExtension validExtension = null;
|
||||
foreach (IWebSocketClientExtensionHandshaker extensionHandshaker in this.extensionHandshakers)
|
||||
{
|
||||
validExtension = extensionHandshaker.HandshakeExtension(extensionData);
|
||||
if (validExtension != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (validExtension != null && (validExtension.Rsv & rsv) == 0)
|
||||
{
|
||||
rsv = rsv | validExtension.Rsv;
|
||||
validExtensions.Add(validExtension);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new CodecException($"invalid WebSocket Extension handshake for \"{extensionsHeader}\"");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (IWebSocketClientExtension validExtension in validExtensions)
|
||||
{
|
||||
WebSocketExtensionDecoder decoder = validExtension.NewExtensionDecoder();
|
||||
WebSocketExtensionEncoder encoder = validExtension.NewExtensionEncoder();
|
||||
ctx.Channel.Pipeline.AddAfter(ctx.Name, decoder.GetType().Name, decoder);
|
||||
ctx.Channel.Pipeline.AddAfter(ctx.Name, encoder.GetType().Name, encoder);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Channel.Pipeline.Remove(ctx.Name);
|
||||
}
|
||||
|
||||
base.ChannelRead(ctx, msg);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
// 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.WebSockets.Extensions
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
|
||||
public sealed class WebSocketExtensionData
|
||||
{
|
||||
readonly string name;
|
||||
readonly Dictionary<string, string> parameters;
|
||||
|
||||
public WebSocketExtensionData(string name, IDictionary<string, string> parameters)
|
||||
{
|
||||
Contract.Requires(name != null);
|
||||
Contract.Requires(parameters != null);
|
||||
|
||||
this.name = name;
|
||||
this.parameters = new Dictionary<string, string>(parameters);
|
||||
}
|
||||
|
||||
public string Name => this.name;
|
||||
|
||||
public Dictionary<string, string> Parameters => this.parameters;
|
||||
}
|
||||
}
|
|
@ -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.WebSockets.Extensions
|
||||
{
|
||||
public abstract class WebSocketExtensionDecoder : MessageToMessageDecoder<WebSocketFrame>
|
||||
{
|
||||
}
|
||||
}
|
|
@ -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.WebSockets.Extensions
|
||||
{
|
||||
public abstract class WebSocketExtensionEncoder : MessageToMessageEncoder<WebSocketFrame>
|
||||
{
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
||||
namespace DotNetty.Codecs.Http.WebSockets.Extensions
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
public static class WebSocketExtensionUtil
|
||||
{
|
||||
const char ExtensionSeparator = ',';
|
||||
const char ParameterSeparator = ';';
|
||||
const char ParameterEqual = '=';
|
||||
|
||||
static readonly Regex Parameter = new Regex("^([^=]+)(=[\\\"]?([^\\\"]+)[\\\"]?)?$", RegexOptions.Compiled);
|
||||
|
||||
internal static bool IsWebsocketUpgrade(HttpHeaders headers) =>
|
||||
headers.ContainsValue(HttpHeaderNames.Connection, HttpHeaderValues.Upgrade, true)
|
||||
&& headers.Contains(HttpHeaderNames.Upgrade, HttpHeaderValues.Websocket, true);
|
||||
|
||||
public static List<WebSocketExtensionData> ExtractExtensions(string extensionHeader)
|
||||
{
|
||||
string[] rawExtensions = extensionHeader.Split(ExtensionSeparator);
|
||||
if (rawExtensions.Length > 0)
|
||||
{
|
||||
var extensions = new List<WebSocketExtensionData>(rawExtensions.Length);
|
||||
foreach (string rawExtension in rawExtensions)
|
||||
{
|
||||
string[] extensionParameters = rawExtension.Split(ParameterSeparator);
|
||||
string name = extensionParameters[0].Trim();
|
||||
Dictionary<string, string> parameters;
|
||||
if (extensionParameters.Length > 1)
|
||||
{
|
||||
parameters = new Dictionary<string, string>(extensionParameters.Length - 1);
|
||||
for (int i = 1; i < extensionParameters.Length; i++)
|
||||
{
|
||||
string parameter = extensionParameters[i].Trim();
|
||||
|
||||
Match match = Parameter.Match(parameter);
|
||||
if (match.Success)
|
||||
{
|
||||
parameters.Add(match.Groups[1].Value, match.Groups[3].Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
parameters = new Dictionary<string, string>();
|
||||
}
|
||||
extensions.Add(new WebSocketExtensionData(name, parameters));
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new List<WebSocketExtensionData>();
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AppendExtension(string currentHeaderValue, string extensionName,
|
||||
Dictionary<string, string> extensionParameters)
|
||||
{
|
||||
var newHeaderValue = new StringBuilder(currentHeaderValue?.Length ?? extensionName.Length + 1);
|
||||
if (currentHeaderValue != null && currentHeaderValue.Trim() != string.Empty)
|
||||
{
|
||||
newHeaderValue.Append(currentHeaderValue);
|
||||
newHeaderValue.Append(ExtensionSeparator);
|
||||
}
|
||||
newHeaderValue.Append(extensionName);
|
||||
foreach (KeyValuePair<string, string> extensionParameter in extensionParameters)
|
||||
{
|
||||
newHeaderValue.Append(ParameterSeparator);
|
||||
newHeaderValue.Append(extensionParameter.Key);
|
||||
if (extensionParameter.Value != null)
|
||||
{
|
||||
newHeaderValue.Append(ParameterEqual);
|
||||
newHeaderValue.Append(extensionParameter.Value);
|
||||
}
|
||||
}
|
||||
return newHeaderValue.ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
// 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.WebSockets.Extensions
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public class WebSocketServerExtensionHandler : ChannelHandlerAdapter
|
||||
{
|
||||
readonly List<IWebSocketServerExtensionHandshaker> extensionHandshakers;
|
||||
|
||||
List<IWebSocketServerExtension> validExtensions;
|
||||
|
||||
public WebSocketServerExtensionHandler(params IWebSocketServerExtensionHandshaker[] extensionHandshakers)
|
||||
{
|
||||
Contract.Requires(extensionHandshakers != null && extensionHandshakers.Length > 0);
|
||||
|
||||
this.extensionHandshakers = new List<IWebSocketServerExtensionHandshaker>(extensionHandshakers);
|
||||
}
|
||||
|
||||
public override void ChannelRead(IChannelHandlerContext ctx, object msg)
|
||||
{
|
||||
if (msg is IHttpRequest request)
|
||||
{
|
||||
if (WebSocketExtensionUtil.IsWebsocketUpgrade(request.Headers))
|
||||
{
|
||||
if (request.Headers.TryGet(HttpHeaderNames.SecWebsocketExtensions, out ICharSequence value)
|
||||
&& value != null)
|
||||
{
|
||||
string extensionsHeader = value.ToString();
|
||||
List<WebSocketExtensionData> extensions =
|
||||
WebSocketExtensionUtil.ExtractExtensions(extensionsHeader);
|
||||
int rsv = 0;
|
||||
|
||||
foreach (WebSocketExtensionData extensionData in extensions)
|
||||
{
|
||||
IWebSocketServerExtension validExtension = null;
|
||||
foreach (IWebSocketServerExtensionHandshaker extensionHandshaker in this.extensionHandshakers)
|
||||
{
|
||||
validExtension = extensionHandshaker.HandshakeExtension(extensionData);
|
||||
if (validExtension != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (validExtension != null && (validExtension.Rsv & rsv) == 0)
|
||||
{
|
||||
if (this.validExtensions == null)
|
||||
{
|
||||
this.validExtensions = new List<IWebSocketServerExtension>(1);
|
||||
}
|
||||
|
||||
rsv = rsv | validExtension.Rsv;
|
||||
this.validExtensions.Add(validExtension);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base.ChannelRead(ctx, msg);
|
||||
}
|
||||
|
||||
public override Task WriteAsync(IChannelHandlerContext ctx, object msg)
|
||||
{
|
||||
Action<Task> continuationAction = null;
|
||||
|
||||
if (msg is IHttpResponse response
|
||||
&& WebSocketExtensionUtil.IsWebsocketUpgrade(response.Headers)
|
||||
&& this.validExtensions != null)
|
||||
{
|
||||
string headerValue = null;
|
||||
if (response.Headers.TryGet(HttpHeaderNames.SecWebsocketExtensions, out ICharSequence value))
|
||||
{
|
||||
headerValue = value?.ToString();
|
||||
}
|
||||
|
||||
foreach (IWebSocketServerExtension extension in this.validExtensions)
|
||||
{
|
||||
WebSocketExtensionData extensionData = extension.NewReponseData();
|
||||
headerValue = WebSocketExtensionUtil.AppendExtension(headerValue,
|
||||
extensionData.Name, extensionData.Parameters);
|
||||
}
|
||||
|
||||
continuationAction = promise =>
|
||||
{
|
||||
if (promise.Status == TaskStatus.RanToCompletion)
|
||||
{
|
||||
foreach (IWebSocketServerExtension extension in this.validExtensions)
|
||||
{
|
||||
WebSocketExtensionDecoder decoder = extension.NewExtensionDecoder();
|
||||
WebSocketExtensionEncoder encoder = extension.NewExtensionEncoder();
|
||||
ctx.Channel.Pipeline.AddAfter(ctx.Name, decoder.GetType().Name, decoder);
|
||||
ctx.Channel.Pipeline.AddAfter(ctx.Name, encoder.GetType().Name, encoder);
|
||||
}
|
||||
}
|
||||
ctx.Channel.Pipeline.Remove(ctx.Name);
|
||||
};
|
||||
|
||||
if (headerValue != null)
|
||||
{
|
||||
response.Headers.Set(HttpHeaderNames.SecWebsocketExtensions, headerValue);
|
||||
}
|
||||
}
|
||||
|
||||
return continuationAction == null
|
||||
? base.WriteAsync(ctx, msg)
|
||||
: base.WriteAsync(ctx, msg)
|
||||
.ContinueWith(continuationAction, TaskContinuationOptions.ExecuteSynchronously);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
/// <summary>
|
||||
/// Marker interface which all WebSocketFrame decoders need to implement. This makes it
|
||||
/// easier to access the added encoder later in the <see cref="IChannelPipeline"/>
|
||||
/// </summary>
|
||||
public interface IWebSocketFrameDecoder : IChannelHandler
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
/// <summary>
|
||||
/// Marker interface which all WebSocketFrame encoders need to implement. This makes it
|
||||
/// easier to access the added encoder later in the <see cref="IChannelPipeline"/>.
|
||||
/// </summary>
|
||||
public interface IWebSocketFrameEncoder : IChannelHandler
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using DotNetty.Buffers;
|
||||
|
||||
public class PingWebSocketFrame : WebSocketFrame
|
||||
{
|
||||
public PingWebSocketFrame()
|
||||
: base(true, 0, Unpooled.Buffer(0))
|
||||
{
|
||||
}
|
||||
|
||||
public PingWebSocketFrame(IByteBuffer binaryData)
|
||||
: base(binaryData)
|
||||
{
|
||||
}
|
||||
|
||||
public PingWebSocketFrame(bool finalFragment, int rsv, IByteBuffer binaryData)
|
||||
: base(finalFragment, rsv, binaryData)
|
||||
{
|
||||
}
|
||||
|
||||
public override IByteBufferHolder Replace(IByteBuffer content) => new PingWebSocketFrame(this.IsFinalFragment, this.Rsv, content);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using DotNetty.Buffers;
|
||||
|
||||
public class PongWebSocketFrame : WebSocketFrame
|
||||
{
|
||||
public PongWebSocketFrame()
|
||||
: base(Unpooled.Buffer(0))
|
||||
{
|
||||
}
|
||||
|
||||
public PongWebSocketFrame(IByteBuffer binaryData)
|
||||
: base(binaryData)
|
||||
{
|
||||
}
|
||||
|
||||
public PongWebSocketFrame(bool finalFragment, int rsv, IByteBuffer binaryData)
|
||||
: base(finalFragment, rsv, binaryData)
|
||||
{
|
||||
}
|
||||
|
||||
public override IByteBufferHolder Replace(IByteBuffer content) => new PongWebSocketFrame(this.IsFinalFragment, this.Rsv, content);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using System.Text;
|
||||
using DotNetty.Buffers;
|
||||
|
||||
public class TextWebSocketFrame : WebSocketFrame
|
||||
{
|
||||
public TextWebSocketFrame()
|
||||
: base(Unpooled.Buffer(0))
|
||||
{
|
||||
}
|
||||
|
||||
public TextWebSocketFrame(string text)
|
||||
: base(FromText(text))
|
||||
{
|
||||
}
|
||||
|
||||
public TextWebSocketFrame(IByteBuffer binaryData)
|
||||
: base(binaryData)
|
||||
{
|
||||
}
|
||||
|
||||
public TextWebSocketFrame(bool finalFragment, int rsv, string text)
|
||||
: base(finalFragment, rsv, FromText(text))
|
||||
{
|
||||
}
|
||||
|
||||
static IByteBuffer FromText(string text) => string.IsNullOrEmpty(text)
|
||||
? Unpooled.Empty : Unpooled.CopiedBuffer(text, Encoding.UTF8);
|
||||
|
||||
public TextWebSocketFrame(bool finalFragment, int rsv, IByteBuffer binaryData)
|
||||
: base(finalFragment, rsv, binaryData)
|
||||
{
|
||||
}
|
||||
|
||||
public string Text() => this.Content.ToString(Encoding.UTF8);
|
||||
|
||||
public override IByteBufferHolder Replace(IByteBuffer content) => new TextWebSocketFrame(this.IsFinalFragment, this.Rsv, content);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public class Utf8FrameValidator : ChannelHandlerAdapter
|
||||
{
|
||||
int fragmentedFramesCount;
|
||||
Utf8Validator utf8Validator;
|
||||
|
||||
public override void ChannelRead(IChannelHandlerContext ctx, object message)
|
||||
{
|
||||
if (message is WebSocketFrame frame)
|
||||
{
|
||||
// Processing for possible fragmented messages for text and binary
|
||||
// frames
|
||||
if (frame.IsFinalFragment)
|
||||
{
|
||||
// Final frame of the sequence. Apparently ping frames are
|
||||
// allowed in the middle of a fragmented message
|
||||
if (!(frame is PingWebSocketFrame))
|
||||
{
|
||||
this.fragmentedFramesCount = 0;
|
||||
|
||||
// Check text for UTF8 correctness
|
||||
if (frame is TextWebSocketFrame
|
||||
|| (this.utf8Validator != null && this.utf8Validator.IsChecking))
|
||||
{
|
||||
// Check UTF-8 correctness for this payload
|
||||
this.CheckUtf8String(ctx, frame.Content);
|
||||
|
||||
// This does a second check to make sure UTF-8
|
||||
// correctness for entire text message
|
||||
this.utf8Validator.Finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not final frame so we can expect more frames in the
|
||||
// fragmented sequence
|
||||
if (this.fragmentedFramesCount == 0)
|
||||
{
|
||||
// First text or binary frame for a fragmented set
|
||||
if (frame is TextWebSocketFrame)
|
||||
{
|
||||
this.CheckUtf8String(ctx, frame.Content);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Subsequent frames - only check if init frame is text
|
||||
if (this.utf8Validator != null && this.utf8Validator.IsChecking)
|
||||
{
|
||||
this.CheckUtf8String(ctx, frame.Content);
|
||||
}
|
||||
}
|
||||
|
||||
// Increment counter
|
||||
this.fragmentedFramesCount++;
|
||||
}
|
||||
}
|
||||
|
||||
base.ChannelRead(ctx, message);
|
||||
}
|
||||
|
||||
void CheckUtf8String(IChannelHandlerContext ctx, IByteBuffer buffer)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (this.utf8Validator == null)
|
||||
{
|
||||
this.utf8Validator = new Utf8Validator();
|
||||
}
|
||||
this.utf8Validator.Check(buffer);
|
||||
}
|
||||
catch (CorruptedFrameException)
|
||||
{
|
||||
if (ctx.Channel.Active)
|
||||
{
|
||||
ctx.WriteAndFlushAsync(Unpooled.Empty)
|
||||
.ContinueWith(t => ctx.Channel.CloseAsync());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using System.Runtime.CompilerServices;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
sealed class Utf8Validator : IByteProcessor
|
||||
{
|
||||
const int Utf8Accept = 0;
|
||||
const int Utf8Reject = 12;
|
||||
|
||||
static readonly byte[] Types = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 7, 7, 7, 7,
|
||||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8,
|
||||
8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
2, 2, 10, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 3, 3, 11, 6, 6, 6, 5, 8, 8, 8, 8, 8,
|
||||
8, 8, 8, 8, 8, 8 };
|
||||
|
||||
static readonly byte[] States = { 0, 12, 24, 36, 60, 96, 84, 12, 12, 12, 48, 72, 12, 12,
|
||||
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 0, 12, 12, 12, 12, 12, 0, 12, 0, 12, 12,
|
||||
12, 24, 12, 12, 12, 12, 12, 24, 12, 24, 12, 12, 12, 12, 12, 12, 12, 12, 12, 24, 12, 12,
|
||||
12, 12, 12, 24, 12, 12, 12, 12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 12, 12, 12, 12, 36,
|
||||
12, 36, 12, 12, 12, 36, 12, 12, 12, 12, 12, 36, 12, 36, 12, 12, 12, 36, 12, 12, 12, 12,
|
||||
12, 12, 12, 12, 12, 12 };
|
||||
|
||||
int state = Utf8Accept;
|
||||
int codep;
|
||||
bool checking;
|
||||
|
||||
public void Check(IByteBuffer buffer)
|
||||
{
|
||||
this.checking = true;
|
||||
buffer.ForEachByte(this);
|
||||
}
|
||||
|
||||
public void Finish()
|
||||
{
|
||||
this.checking = false;
|
||||
this.codep = 0;
|
||||
if (this.state != Utf8Accept)
|
||||
{
|
||||
this.state = Utf8Accept;
|
||||
ThrowCorruptedFrameException();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Process(byte b)
|
||||
{
|
||||
byte type = Types[b & 0xFF];
|
||||
|
||||
this.codep = this.state != Utf8Accept ? b & 0x3f | this.codep << 6 : 0xff >> type & b;
|
||||
|
||||
this.state = States[this.state + type];
|
||||
|
||||
if (this.state == Utf8Reject)
|
||||
{
|
||||
this.checking = false;
|
||||
ThrowCorruptedFrameException();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
static void ThrowCorruptedFrameException() => throw new CorruptedFrameException("bytes are not UTF-8");
|
||||
|
||||
public bool IsChecking => this.checking;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
using static Buffers.ByteBufferUtil;
|
||||
|
||||
public class WebSocket00FrameDecoder : ReplayingDecoder<WebSocket00FrameDecoder.Void>, IWebSocketFrameDecoder
|
||||
{
|
||||
public enum Void
|
||||
{
|
||||
// Empty state
|
||||
}
|
||||
|
||||
const int DefaultMaxFrameSize = 16384;
|
||||
|
||||
readonly long maxFrameSize;
|
||||
bool receivedClosingHandshake;
|
||||
|
||||
public WebSocket00FrameDecoder() : this(DefaultMaxFrameSize)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocket00FrameDecoder(int maxFrameSize) : base(default(Void))
|
||||
{
|
||||
this.maxFrameSize = maxFrameSize;
|
||||
}
|
||||
|
||||
protected override void Decode(IChannelHandlerContext context, IByteBuffer input, List<object> output)
|
||||
{
|
||||
// Discard all data received if closing handshake was received before.
|
||||
if (this.receivedClosingHandshake)
|
||||
{
|
||||
input.SkipBytes(this.ActualReadableBytes);
|
||||
return;
|
||||
}
|
||||
|
||||
// Decode a frame otherwise.
|
||||
byte type = input.ReadByte();
|
||||
WebSocketFrame frame;
|
||||
if ((type & 0x80) == 0x80)
|
||||
{
|
||||
// If the MSB on type is set, decode the frame length
|
||||
frame = this.DecodeBinaryFrame(context, type, input);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Decode a 0xff terminated UTF-8 string
|
||||
frame = this.DecodeTextFrame(context, input);
|
||||
}
|
||||
|
||||
if (frame != null)
|
||||
{
|
||||
output.Add(frame);
|
||||
}
|
||||
}
|
||||
|
||||
WebSocketFrame DecodeBinaryFrame(IChannelHandlerContext ctx, byte type, IByteBuffer buffer)
|
||||
{
|
||||
long frameSize = 0;
|
||||
int lengthFieldSize = 0;
|
||||
byte b;
|
||||
do
|
||||
{
|
||||
b = buffer.ReadByte();
|
||||
frameSize <<= 7;
|
||||
frameSize |= (uint)(b & 0x7f);
|
||||
if (frameSize > this.maxFrameSize)
|
||||
{
|
||||
throw new TooLongFrameException(nameof(WebSocket00FrameDecoder));
|
||||
}
|
||||
lengthFieldSize++;
|
||||
if (lengthFieldSize > 8)
|
||||
{
|
||||
// Perhaps a malicious peer?
|
||||
throw new TooLongFrameException(nameof(WebSocket00FrameDecoder));
|
||||
}
|
||||
} while ((b & 0x80) == 0x80);
|
||||
|
||||
if (type == 0xFF && frameSize == 0)
|
||||
{
|
||||
this.receivedClosingHandshake = true;
|
||||
return new CloseWebSocketFrame();
|
||||
}
|
||||
IByteBuffer payload = ReadBytes(ctx.Allocator, buffer, (int)frameSize);
|
||||
return new BinaryWebSocketFrame(payload);
|
||||
}
|
||||
|
||||
WebSocketFrame DecodeTextFrame(IChannelHandlerContext ctx, IByteBuffer buffer)
|
||||
{
|
||||
int ridx = buffer.ReaderIndex;
|
||||
int rbytes = this.ActualReadableBytes;
|
||||
int delimPos = buffer.IndexOf(ridx, ridx + rbytes, 0xFF);
|
||||
if (delimPos == -1)
|
||||
{
|
||||
// Frame delimiter (0xFF) not found
|
||||
if (rbytes > this.maxFrameSize)
|
||||
{
|
||||
// Frame length exceeded the maximum
|
||||
throw new TooLongFrameException(nameof(WebSocket00FrameDecoder));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Wait until more data is received
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
int frameSize = delimPos - ridx;
|
||||
if (frameSize > this.maxFrameSize)
|
||||
{
|
||||
throw new TooLongFrameException(nameof(WebSocket00FrameDecoder));
|
||||
}
|
||||
|
||||
IByteBuffer binaryData = ReadBytes(ctx.Allocator, buffer, frameSize);
|
||||
buffer.SkipBytes(1);
|
||||
|
||||
int ffDelimPos = binaryData.IndexOf(binaryData.ReaderIndex, binaryData.WriterIndex, 0xFF);
|
||||
if (ffDelimPos >= 0)
|
||||
{
|
||||
binaryData.Release();
|
||||
throw new ArgumentException("a text frame should not contain 0xFF.");
|
||||
}
|
||||
|
||||
return new TextWebSocketFrame(binaryData);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace DotNetty.Codecs.Http.WebSockets
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public class WebSocket00FrameEncoder : MessageToMessageEncoder<WebSocketFrame>, IWebSocketFrameEncoder
|
||||
{
|
||||
static readonly IByteBuffer _0X00 = Unpooled.UnreleasableBuffer(
|
||||
Unpooled.DirectBuffer(1, 1).WriteByte(0x00));
|
||||
|
||||
static readonly IByteBuffer _0XFF = Unpooled.UnreleasableBuffer(
|
||||
Unpooled.DirectBuffer(1, 1).WriteByte(0xFF));
|
||||
|
||||
static readonly IByteBuffer _0XFF_0X00 = Unpooled.UnreleasableBuffer(
|
||||
Unpooled.DirectBuffer(2, 2).WriteByte(0xFF).WriteByte(0x00));
|
||||
|
||||
public override bool IsSharable => true;
|
||||
|
||||
protected override void Encode(IChannelHandlerContext context, WebSocketFrame message, List<object> output)
|
||||
{
|
||||
if (message is TextWebSocketFrame)
|
||||
{
|
||||
// Text frame
|
||||
IByteBuffer data = message.Content;
|
||||
|
||||
output.Add(_0X00.Duplicate());
|
||||
output.Add(data.Retain());
|
||||
output.Add(_0XFF.Duplicate());
|
||||
}
|
||||
else if (message is CloseWebSocketFrame)
|
||||
{
|
||||
// Close frame, needs to call duplicate to allow multiple writes.
|
||||
// See https://github.com/netty/netty/issues/2768
|
||||
output.Add(_0XFF_0X00.Duplicate());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Binary frame
|
||||
IByteBuffer data = message.Content;
|
||||
int dataLen = data.ReadableBytes;
|
||||
|
||||
IByteBuffer buf = context.Allocator.Buffer(5);
|
||||
bool release = true;
|
||||
try
|
||||
{
|
||||
// Encode type.
|
||||
buf.WriteByte(0x80);
|
||||
|
||||
// Encode length.
|
||||
int b1 = dataLen.RightUShift(28) & 0x7F;
|
||||
int b2 = dataLen.RightUShift(14) & 0x7F;
|
||||
int b3 = dataLen.RightUShift(7) & 0x7F;
|
||||
int b4 = dataLen & 0x7F;
|
||||
if (b1 == 0)
|
||||
{
|
||||
if (b2 == 0)
|
||||
{
|
||||
if (b3 == 0)
|
||||
{
|
||||
buf.WriteByte(b4);
|
||||
}
|
||||
else
|
||||
{
|
||||
buf.WriteByte(b3 | 0x80);
|
||||
buf.WriteByte(b4);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
buf.WriteByte(b2 | 0x80);
|
||||
buf.WriteByte(b3 | 0x80);
|
||||
buf.WriteByte(b4);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
buf.WriteByte(b1 | 0x80);
|
||||
buf.WriteByte(b2 | 0x80);
|
||||
buf.WriteByte(b3 | 0x80);
|
||||
buf.WriteByte(b4);
|
||||
}
|
||||
|
||||
// Encode binary data.
|
||||
output.Add(buf);
|
||||
output.Add(data.Retain());
|
||||
release = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (release)
|
||||
{
|
||||
buf.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
public class WebSocket07FrameDecoder : WebSocket08FrameDecoder
|
||||
{
|
||||
public WebSocket07FrameDecoder(bool expectMaskedFrames, bool allowExtensions, int maxFramePayloadLength)
|
||||
: this(expectMaskedFrames, allowExtensions, maxFramePayloadLength, false)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocket07FrameDecoder(bool expectMaskedFrames, bool allowExtensions, int maxFramePayloadLength, bool allowMaskMismatch)
|
||||
: base(expectMaskedFrames, allowExtensions, maxFramePayloadLength, allowMaskMismatch)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
public class WebSocket07FrameEncoder : WebSocket08FrameEncoder
|
||||
{
|
||||
public WebSocket07FrameEncoder(bool maskPayload)
|
||||
: base(maskPayload)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,467 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable UseStringInterpolation
|
||||
namespace DotNetty.Codecs.Http.WebSockets
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Internal.Logging;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
using static Buffers.ByteBufferUtil;
|
||||
|
||||
public class WebSocket08FrameDecoder : ByteToMessageDecoder, IWebSocketFrameDecoder
|
||||
{
|
||||
enum State
|
||||
{
|
||||
ReadingFirst,
|
||||
ReadingSecond,
|
||||
ReadingSize,
|
||||
MaskingKey,
|
||||
Payload,
|
||||
Corrupt
|
||||
}
|
||||
|
||||
static readonly IInternalLogger Logger = InternalLoggerFactory.GetInstance<WebSocket08FrameDecoder>();
|
||||
|
||||
const byte OpcodeCont = 0x0;
|
||||
const byte OpcodeText = 0x1;
|
||||
const byte OpcodeBinary = 0x2;
|
||||
const byte OpcodeClose = 0x8;
|
||||
const byte OpcodePing = 0x9;
|
||||
const byte OpcodePong = 0xA;
|
||||
|
||||
readonly long maxFramePayloadLength;
|
||||
readonly bool allowExtensions;
|
||||
readonly bool expectMaskedFrames;
|
||||
readonly bool allowMaskMismatch;
|
||||
|
||||
int fragmentedFramesCount;
|
||||
bool frameFinalFlag;
|
||||
bool frameMasked;
|
||||
int frameRsv;
|
||||
int frameOpcode;
|
||||
long framePayloadLength;
|
||||
byte[] maskingKey;
|
||||
int framePayloadLen1;
|
||||
bool receivedClosingHandshake;
|
||||
State state = State.ReadingFirst;
|
||||
|
||||
public WebSocket08FrameDecoder(bool expectMaskedFrames, bool allowExtensions, int maxFramePayloadLength)
|
||||
: this(expectMaskedFrames, allowExtensions, maxFramePayloadLength, false)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocket08FrameDecoder(bool expectMaskedFrames, bool allowExtensions, int maxFramePayloadLength, bool allowMaskMismatch)
|
||||
{
|
||||
this.expectMaskedFrames = expectMaskedFrames;
|
||||
this.allowMaskMismatch = allowMaskMismatch;
|
||||
this.allowExtensions = allowExtensions;
|
||||
this.maxFramePayloadLength = maxFramePayloadLength;
|
||||
}
|
||||
|
||||
protected override void Decode(IChannelHandlerContext context, IByteBuffer input, List<object> output)
|
||||
{
|
||||
// Discard all data received if closing handshake was received before.
|
||||
if (this.receivedClosingHandshake)
|
||||
{
|
||||
input.SkipBytes(this.ActualReadableBytes);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this.state)
|
||||
{
|
||||
case State.ReadingFirst:
|
||||
if (!input.IsReadable())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.framePayloadLength = 0;
|
||||
|
||||
// FIN, RSV, OPCODE
|
||||
byte b = input.ReadByte();
|
||||
this.frameFinalFlag = (b & 0x80) != 0;
|
||||
this.frameRsv = (b & 0x70) >> 4;
|
||||
this.frameOpcode = b & 0x0F;
|
||||
|
||||
if (Logger.DebugEnabled)
|
||||
{
|
||||
Logger.Debug("Decoding WebSocket Frame opCode={}", this.frameOpcode);
|
||||
}
|
||||
|
||||
this.state = State.ReadingSecond;
|
||||
goto case State.ReadingSecond;
|
||||
case State.ReadingSecond:
|
||||
if (!input.IsReadable())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// MASK, PAYLOAD LEN 1
|
||||
b = input.ReadByte();
|
||||
this.frameMasked = (b & 0x80) != 0;
|
||||
this.framePayloadLen1 = b & 0x7F;
|
||||
|
||||
if (this.frameRsv != 0 && !this.allowExtensions)
|
||||
{
|
||||
this.ProtocolViolation(context, $"RSV != 0 and no extension negotiated, RSV:{this.frameRsv}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.allowMaskMismatch && this.expectMaskedFrames != this.frameMasked)
|
||||
{
|
||||
this.ProtocolViolation(context, "received a frame that is not masked as expected");
|
||||
return;
|
||||
}
|
||||
|
||||
// control frame (have MSB in opcode set)
|
||||
if (this.frameOpcode > 7)
|
||||
{
|
||||
// control frames MUST NOT be fragmented
|
||||
if (!this.frameFinalFlag)
|
||||
{
|
||||
this.ProtocolViolation(context, "fragmented control frame");
|
||||
return;
|
||||
}
|
||||
|
||||
// control frames MUST have payload 125 octets or less
|
||||
if (this.framePayloadLen1 > 125)
|
||||
{
|
||||
this.ProtocolViolation(context, "control frame with payload length > 125 octets");
|
||||
return;
|
||||
}
|
||||
|
||||
// check for reserved control frame opcodes
|
||||
if (!(this.frameOpcode == OpcodeClose
|
||||
|| this.frameOpcode == OpcodePing
|
||||
|| this.frameOpcode == OpcodePong))
|
||||
{
|
||||
this.ProtocolViolation(context, $"control frame using reserved opcode {this.frameOpcode}");
|
||||
return;
|
||||
}
|
||||
|
||||
// close frame : if there is a body, the first two bytes of the
|
||||
// body MUST be a 2-byte unsigned integer (in network byte
|
||||
// order) representing a getStatus code
|
||||
if (this.frameOpcode == 8 && this.framePayloadLen1 == 1)
|
||||
{
|
||||
this.ProtocolViolation(context, "received close control frame with payload len 1");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else // data frame
|
||||
{
|
||||
// check for reserved data frame opcodes
|
||||
if (!(this.frameOpcode == OpcodeCont || this.frameOpcode == OpcodeText
|
||||
|| this.frameOpcode == OpcodeBinary))
|
||||
{
|
||||
this.ProtocolViolation(context, $"data frame using reserved opcode {this.frameOpcode}");
|
||||
return;
|
||||
}
|
||||
|
||||
// check opcode vs message fragmentation state 1/2
|
||||
if (this.fragmentedFramesCount == 0 && this.frameOpcode == OpcodeCont)
|
||||
{
|
||||
this.ProtocolViolation(context, "received continuation data frame outside fragmented message");
|
||||
return;
|
||||
}
|
||||
|
||||
// check opcode vs message fragmentation state 2/2
|
||||
if (this.fragmentedFramesCount != 0 && this.frameOpcode != OpcodeCont && this.frameOpcode != OpcodePing)
|
||||
{
|
||||
this.ProtocolViolation(context, "received non-continuation data frame while inside fragmented message");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.state = State.ReadingSize;
|
||||
goto case State.ReadingSize;
|
||||
case State.ReadingSize:
|
||||
// Read frame payload length
|
||||
if (this.framePayloadLen1 == 126)
|
||||
{
|
||||
if (input.ReadableBytes < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this.framePayloadLength = input.ReadUnsignedShort();
|
||||
if (this.framePayloadLength < 126)
|
||||
{
|
||||
this.ProtocolViolation(context, "invalid data frame length (not using minimal length encoding)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (this.framePayloadLen1 == 127)
|
||||
{
|
||||
if (input.ReadableBytes < 8)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this.framePayloadLength = input.ReadLong();
|
||||
// TODO: check if it's bigger than 0x7FFFFFFFFFFFFFFF, Maybe
|
||||
// just check if it's negative?
|
||||
|
||||
if (this.framePayloadLength < 65536)
|
||||
{
|
||||
this.ProtocolViolation(context, "invalid data frame length (not using minimal length encoding)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.framePayloadLength = this.framePayloadLen1;
|
||||
}
|
||||
|
||||
if (this.framePayloadLength > this.maxFramePayloadLength)
|
||||
{
|
||||
this.ProtocolViolation(context, $"Max frame length of {this.maxFramePayloadLength} has been exceeded.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Logger.DebugEnabled)
|
||||
{
|
||||
Logger.Debug("Decoding WebSocket Frame length={}", this.framePayloadLength);
|
||||
}
|
||||
|
||||
this.state = State.MaskingKey;
|
||||
goto case State.MaskingKey;
|
||||
case State.MaskingKey:
|
||||
if (this.frameMasked)
|
||||
{
|
||||
if (input.ReadableBytes < 4)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (this.maskingKey == null)
|
||||
{
|
||||
this.maskingKey = new byte[4];
|
||||
}
|
||||
input.ReadBytes(this.maskingKey);
|
||||
}
|
||||
this.state = State.Payload;
|
||||
goto case State.Payload;
|
||||
case State.Payload:
|
||||
if (input.ReadableBytes < this.framePayloadLength)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IByteBuffer payloadBuffer = null;
|
||||
try
|
||||
{
|
||||
payloadBuffer = ReadBytes(context.Allocator, input, ToFrameLength(this.framePayloadLength));
|
||||
|
||||
// Now we have all the data, the next checkpoint must be the next
|
||||
// frame
|
||||
this.state = State.ReadingFirst;
|
||||
|
||||
// Unmask data if needed
|
||||
if (this.frameMasked)
|
||||
{
|
||||
this.Unmask(payloadBuffer);
|
||||
}
|
||||
|
||||
// Processing ping/pong/close frames because they cannot be
|
||||
// fragmented
|
||||
if (this.frameOpcode == OpcodePing)
|
||||
{
|
||||
output.Add(new PingWebSocketFrame(this.frameFinalFlag, this.frameRsv, payloadBuffer));
|
||||
payloadBuffer = null;
|
||||
return;
|
||||
}
|
||||
if (this.frameOpcode == OpcodePong)
|
||||
{
|
||||
output.Add(new PongWebSocketFrame(this.frameFinalFlag, this.frameRsv, payloadBuffer));
|
||||
payloadBuffer = null;
|
||||
return;
|
||||
}
|
||||
if (this.frameOpcode == OpcodeClose)
|
||||
{
|
||||
this.receivedClosingHandshake = true;
|
||||
this.CheckCloseFrameBody(context, payloadBuffer);
|
||||
output.Add(new CloseWebSocketFrame(this.frameFinalFlag, this.frameRsv, payloadBuffer));
|
||||
payloadBuffer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Processing for possible fragmented messages for text and binary
|
||||
// frames
|
||||
if (this.frameFinalFlag)
|
||||
{
|
||||
// Final frame of the sequence. Apparently ping frames are
|
||||
// allowed in the middle of a fragmented message
|
||||
if (this.frameOpcode != OpcodePing)
|
||||
{
|
||||
this.fragmentedFramesCount = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Increment counter
|
||||
this.fragmentedFramesCount++;
|
||||
}
|
||||
|
||||
// Return the frame
|
||||
if (this.frameOpcode == OpcodeText)
|
||||
{
|
||||
output.Add(new TextWebSocketFrame(this.frameFinalFlag, this.frameRsv, payloadBuffer));
|
||||
payloadBuffer = null;
|
||||
return;
|
||||
}
|
||||
else if (this.frameOpcode == OpcodeBinary)
|
||||
{
|
||||
output.Add(new BinaryWebSocketFrame(this.frameFinalFlag, this.frameRsv, payloadBuffer));
|
||||
payloadBuffer = null;
|
||||
return;
|
||||
}
|
||||
else if (this.frameOpcode == OpcodeCont)
|
||||
{
|
||||
output.Add(new ContinuationWebSocketFrame(this.frameFinalFlag, this.frameRsv, payloadBuffer));
|
||||
payloadBuffer = null;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
ThrowNotSupportedException(this.frameOpcode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
payloadBuffer?.Release();
|
||||
}
|
||||
case State.Corrupt:
|
||||
if (input.IsReadable())
|
||||
{
|
||||
// If we don't keep reading Netty will throw an exception saying
|
||||
// we can't return null if no bytes read and state not changed.
|
||||
input.ReadByte();
|
||||
}
|
||||
return;
|
||||
default:
|
||||
throw new Exception("Shouldn't reach here.");
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
static void ThrowNotSupportedException(int frameOpcode)
|
||||
{
|
||||
throw GetNotSupportedException();
|
||||
|
||||
NotSupportedException GetNotSupportedException()
|
||||
{
|
||||
return new NotSupportedException($"Cannot decode web socket frame with opcode: {frameOpcode}");
|
||||
}
|
||||
}
|
||||
|
||||
void Unmask(IByteBuffer frame)
|
||||
{
|
||||
int i = frame.ReaderIndex;
|
||||
int end = frame.WriterIndex;
|
||||
|
||||
int intMask = (this.maskingKey[0] << 24)
|
||||
| (this.maskingKey[1] << 16)
|
||||
| (this.maskingKey[2] << 8)
|
||||
| this.maskingKey[3];
|
||||
|
||||
for (; i + 3 < end; i += 4)
|
||||
{
|
||||
int unmasked = frame.GetInt(i) ^ intMask;
|
||||
frame.SetInt(i, unmasked);
|
||||
}
|
||||
for (; i < end; i++)
|
||||
{
|
||||
frame.SetByte(i, frame.GetByte(i) ^ this.maskingKey[i % 4]);
|
||||
}
|
||||
}
|
||||
|
||||
void ProtocolViolation(IChannelHandlerContext ctx, string reason) => this.ProtocolViolation(ctx, new CorruptedFrameException(reason));
|
||||
|
||||
void ProtocolViolation(IChannelHandlerContext ctx, CorruptedFrameException ex)
|
||||
{
|
||||
this.state = State.Corrupt;
|
||||
if (ctx.Channel.Active)
|
||||
{
|
||||
object closeMessage;
|
||||
if (this.receivedClosingHandshake)
|
||||
{
|
||||
closeMessage = Unpooled.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
closeMessage = new CloseWebSocketFrame(1002, null);
|
||||
}
|
||||
ctx.WriteAndFlushAsync(closeMessage)
|
||||
.ContinueWith((t, c) => ((IChannel)c).CloseAsync(),
|
||||
ctx.Channel, TaskContinuationOptions.ExecuteSynchronously);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static int ToFrameLength(long l)
|
||||
{
|
||||
if (l > int.MaxValue)
|
||||
{
|
||||
ThrowTooLongFrameException(l);
|
||||
}
|
||||
return (int)l;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
static void ThrowTooLongFrameException(long l)
|
||||
{
|
||||
throw GetTooLongFrameException();
|
||||
|
||||
TooLongFrameException GetTooLongFrameException()
|
||||
{
|
||||
return new TooLongFrameException(string.Format("Length: {0}", l));
|
||||
}
|
||||
}
|
||||
|
||||
protected void CheckCloseFrameBody(IChannelHandlerContext ctx, IByteBuffer buffer)
|
||||
{
|
||||
if (buffer == null || !buffer.IsReadable())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (buffer.ReadableBytes == 1)
|
||||
{
|
||||
this.ProtocolViolation(ctx, "Invalid close frame body");
|
||||
}
|
||||
|
||||
// Save reader index
|
||||
int idx = buffer.ReaderIndex;
|
||||
buffer.SetReaderIndex(0);
|
||||
|
||||
// Must have 2 byte integer within the valid range
|
||||
int statusCode = buffer.ReadShort();
|
||||
if (statusCode >= 0 && statusCode <= 999 || statusCode >= 1004 && statusCode <= 1006
|
||||
|| statusCode >= 1012 && statusCode <= 2999)
|
||||
{
|
||||
this.ProtocolViolation(ctx, $"Invalid close frame getStatus code: {statusCode}");
|
||||
}
|
||||
|
||||
// May have UTF-8 message
|
||||
if (buffer.IsReadable())
|
||||
{
|
||||
try
|
||||
{
|
||||
new Utf8Validator().Check(buffer);
|
||||
}
|
||||
catch (CorruptedFrameException ex)
|
||||
{
|
||||
this.ProtocolViolation(ctx, ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore reader index
|
||||
buffer.SetReaderIndex(idx);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,219 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Internal.Logging;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public class WebSocket08FrameEncoder : MessageToMessageEncoder<WebSocketFrame>, IWebSocketFrameEncoder
|
||||
{
|
||||
static readonly IInternalLogger Logger = InternalLoggerFactory.GetInstance<WebSocket08FrameEncoder>();
|
||||
|
||||
const byte OpcodeCont = 0x0;
|
||||
const byte OpcodeText = 0x1;
|
||||
const byte OpcodeBinary = 0x2;
|
||||
const byte OpcodeClose = 0x8;
|
||||
const byte OpcodePing = 0x9;
|
||||
const byte OpcodePong = 0xA;
|
||||
|
||||
///
|
||||
// The size threshold for gathering writes. Non-Masked messages bigger than this size will be be sent fragmented as
|
||||
// a header and a content ByteBuf whereas messages smaller than the size will be merged into a single buffer and
|
||||
// sent at once.
|
||||
// Masked messages will always be sent at once.
|
||||
//
|
||||
const int GatheringWriteThreshold = 1024;
|
||||
|
||||
readonly bool maskPayload;
|
||||
readonly Random random;
|
||||
|
||||
public WebSocket08FrameEncoder(bool maskPayload)
|
||||
{
|
||||
this.maskPayload = maskPayload;
|
||||
this.random = new Random();
|
||||
}
|
||||
|
||||
protected override unsafe void Encode(IChannelHandlerContext ctx, WebSocketFrame msg, List<object> output)
|
||||
{
|
||||
IByteBuffer data = msg.Content;
|
||||
var mask = stackalloc byte[4];
|
||||
|
||||
byte opcode = 0;
|
||||
if (msg is TextWebSocketFrame)
|
||||
{
|
||||
opcode = OpcodeText;
|
||||
}
|
||||
else if (msg is PingWebSocketFrame)
|
||||
{
|
||||
opcode = OpcodePing;
|
||||
}
|
||||
else if (msg is PongWebSocketFrame)
|
||||
{
|
||||
opcode = OpcodePong;
|
||||
}
|
||||
else if (msg is CloseWebSocketFrame)
|
||||
{
|
||||
opcode = OpcodeClose;
|
||||
}
|
||||
else if (msg is BinaryWebSocketFrame)
|
||||
{
|
||||
opcode = OpcodeBinary;
|
||||
}
|
||||
else if (msg is ContinuationWebSocketFrame)
|
||||
{
|
||||
opcode = OpcodeCont;
|
||||
}
|
||||
else
|
||||
{
|
||||
ThrowNotSupportedException(msg);
|
||||
}
|
||||
|
||||
int length = data.ReadableBytes;
|
||||
|
||||
if (Logger.DebugEnabled)
|
||||
{
|
||||
Logger.Debug($"Encoding WebSocket Frame opCode={opcode} length={length}");
|
||||
}
|
||||
|
||||
int b0 = 0;
|
||||
if (msg.IsFinalFragment)
|
||||
{
|
||||
b0 |= 1 << 7;
|
||||
}
|
||||
b0 |= msg.Rsv % 8 << 4;
|
||||
b0 |= opcode % 128;
|
||||
|
||||
if (opcode == OpcodePing && length > 125)
|
||||
{
|
||||
ThrowTooLongFrameException(length);
|
||||
}
|
||||
|
||||
bool release = true;
|
||||
IByteBuffer buf = null;
|
||||
|
||||
try
|
||||
{
|
||||
int maskLength = this.maskPayload ? 4 : 0;
|
||||
if (length <= 125)
|
||||
{
|
||||
int size = 2 + maskLength;
|
||||
if (this.maskPayload || length <= GatheringWriteThreshold)
|
||||
{
|
||||
size += length;
|
||||
}
|
||||
buf = ctx.Allocator.Buffer(size);
|
||||
buf.WriteByte(b0);
|
||||
byte b = (byte)(this.maskPayload ? 0x80 | (byte)length : (byte)length);
|
||||
buf.WriteByte(b);
|
||||
}
|
||||
else if (length <= 0xFFFF)
|
||||
{
|
||||
int size = 4 + maskLength;
|
||||
if (this.maskPayload || length <= GatheringWriteThreshold)
|
||||
{
|
||||
size += length;
|
||||
}
|
||||
buf = ctx.Allocator.Buffer(size);
|
||||
buf.WriteByte(b0);
|
||||
buf.WriteByte(this.maskPayload ? 0xFE : 126);
|
||||
buf.WriteByte(length.RightUShift(8) & 0xFF);
|
||||
buf.WriteByte(length & 0xFF);
|
||||
}
|
||||
else
|
||||
{
|
||||
int size = 10 + maskLength;
|
||||
if (this.maskPayload || length <= GatheringWriteThreshold)
|
||||
{
|
||||
size += length;
|
||||
}
|
||||
buf = ctx.Allocator.Buffer(size);
|
||||
buf.WriteByte(b0);
|
||||
buf.WriteByte(this.maskPayload ? 0xFF : 127);
|
||||
buf.WriteLong(length);
|
||||
}
|
||||
|
||||
// Write payload
|
||||
if (this.maskPayload)
|
||||
{
|
||||
int intMask = (this.random.Next() * int.MaxValue);
|
||||
|
||||
// Mask bytes in BE
|
||||
uint unsignedValue = (uint)intMask;
|
||||
*mask = (byte)(unsignedValue >> 24);
|
||||
*(mask + 1) = (byte)(unsignedValue >> 16);
|
||||
*(mask + 2) = (byte)(unsignedValue >> 8);
|
||||
*(mask + 3) = (byte)unsignedValue;
|
||||
|
||||
// Mask in BE
|
||||
buf.WriteInt(intMask);
|
||||
|
||||
int counter = 0;
|
||||
int i = data.ReaderIndex;
|
||||
int end = data.WriterIndex;
|
||||
|
||||
for (; i + 3 < end; i += 4)
|
||||
{
|
||||
int intData = data.GetInt(i);
|
||||
buf.WriteInt(intData ^ intMask);
|
||||
}
|
||||
for (; i < end; i++)
|
||||
{
|
||||
byte byteData = data.GetByte(i);
|
||||
buf.WriteByte(byteData ^ mask[counter++ % 4]);
|
||||
}
|
||||
output.Add(buf);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (buf.WritableBytes >= data.ReadableBytes)
|
||||
{
|
||||
// merge buffers as this is cheaper then a gathering write if the payload is small enough
|
||||
buf.WriteBytes(data);
|
||||
output.Add(buf);
|
||||
}
|
||||
else
|
||||
{
|
||||
output.Add(buf);
|
||||
output.Add(data.Retain());
|
||||
}
|
||||
}
|
||||
release = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (release)
|
||||
{
|
||||
buf?.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
static void ThrowTooLongFrameException(int length)
|
||||
{
|
||||
throw GetTooLongFrameException();
|
||||
|
||||
TooLongFrameException GetTooLongFrameException()
|
||||
{
|
||||
return new TooLongFrameException(string.Format("invalid payload for PING (payload length must be <= 125, was {0}", length));
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
static void ThrowNotSupportedException(WebSocketFrame msg)
|
||||
{
|
||||
throw GetNotSupportedException();
|
||||
|
||||
NotSupportedException GetNotSupportedException()
|
||||
{
|
||||
return new NotSupportedException(string.Format("Cannot encode frame of type: {0}", msg.GetType().Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
public class WebSocket13FrameDecoder : WebSocket08FrameDecoder
|
||||
{
|
||||
public WebSocket13FrameDecoder(bool expectMaskedFrames, bool allowExtensions, int maxFramePayloadLength)
|
||||
: this(expectMaskedFrames, allowExtensions, maxFramePayloadLength, false)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocket13FrameDecoder(bool expectMaskedFrames, bool allowExtensions,int maxFramePayloadLength,
|
||||
bool allowMaskMismatch)
|
||||
: base(expectMaskedFrames, allowExtensions, maxFramePayloadLength, allowMaskMismatch)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
public class WebSocket13FrameEncoder : WebSocket08FrameEncoder
|
||||
{
|
||||
public WebSocket13FrameEncoder(bool maskPayload)
|
||||
: base(maskPayload)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.WebSockets
|
||||
{
|
||||
using System.Diagnostics.Contracts;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Handlers.Streams;
|
||||
|
||||
public sealed class WebSocketChunkedInput : IChunkedInput<WebSocketFrame>
|
||||
{
|
||||
readonly IChunkedInput<IByteBuffer> input;
|
||||
readonly int rsv;
|
||||
|
||||
public WebSocketChunkedInput(IChunkedInput<IByteBuffer> input)
|
||||
: this(input, 0)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketChunkedInput(IChunkedInput<IByteBuffer> input, int rsv)
|
||||
{
|
||||
Contract.Requires(input != null);
|
||||
|
||||
this.input = input;
|
||||
this.rsv = rsv;
|
||||
}
|
||||
|
||||
public bool IsEndOfInput => this.input.IsEndOfInput;
|
||||
|
||||
public void Close() => this.input.Close();
|
||||
|
||||
public WebSocketFrame ReadChunk(IByteBufferAllocator allocator)
|
||||
{
|
||||
IByteBuffer buf = this.input.ReadChunk(allocator);
|
||||
return buf != null ? new ContinuationWebSocketFrame(this.input.IsEndOfInput, this.rsv, buf) : null;
|
||||
}
|
||||
|
||||
public long Length => this.input.Length;
|
||||
|
||||
public long Progress => this.input.Progress;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,425 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Common.Concurrency;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public abstract class WebSocketClientHandshaker
|
||||
{
|
||||
static readonly ClosedChannelException DefaultClosedChannelException = new ClosedChannelException();
|
||||
|
||||
static readonly string HttpSchemePrefix = HttpScheme.Http + "://";
|
||||
static readonly string HttpsSchemePrefix = HttpScheme.Https + "://";
|
||||
|
||||
readonly Uri uri;
|
||||
|
||||
readonly WebSocketVersion version;
|
||||
|
||||
volatile bool handshakeComplete;
|
||||
|
||||
readonly string expectedSubprotocol;
|
||||
|
||||
volatile string actualSubprotocol;
|
||||
|
||||
protected readonly HttpHeaders CustomHeaders;
|
||||
|
||||
readonly int maxFramePayloadLength;
|
||||
|
||||
protected WebSocketClientHandshaker(Uri uri, WebSocketVersion version, string subprotocol,
|
||||
HttpHeaders customHeaders, int maxFramePayloadLength)
|
||||
{
|
||||
this.uri = uri;
|
||||
this.version = version;
|
||||
this.expectedSubprotocol = subprotocol;
|
||||
this.CustomHeaders = customHeaders;
|
||||
this.maxFramePayloadLength = maxFramePayloadLength;
|
||||
}
|
||||
|
||||
public Uri Uri => this.uri;
|
||||
|
||||
public WebSocketVersion Version => this.version;
|
||||
|
||||
public int MaxFramePayloadLength => this.maxFramePayloadLength;
|
||||
|
||||
public bool IsHandshakeComplete => this.handshakeComplete;
|
||||
|
||||
void SetHandshakeComplete() => this.handshakeComplete = true;
|
||||
|
||||
public string ExpectedSubprotocol => this.expectedSubprotocol;
|
||||
|
||||
public string ActualSubprotocol
|
||||
{
|
||||
get => this.actualSubprotocol;
|
||||
private set => this.actualSubprotocol = value;
|
||||
}
|
||||
|
||||
public Task HandshakeAsync(IChannel channel)
|
||||
{
|
||||
IFullHttpRequest request = this.NewHandshakeRequest();
|
||||
|
||||
var decoder = channel.Pipeline.Get<HttpResponseDecoder>();
|
||||
if (decoder == null)
|
||||
{
|
||||
var codec = channel.Pipeline.Get<HttpClientCodec>();
|
||||
if (codec == null)
|
||||
{
|
||||
return TaskEx.FromException(new InvalidOperationException("ChannelPipeline does not contain a HttpResponseDecoder or HttpClientCodec"));
|
||||
}
|
||||
}
|
||||
|
||||
var completion = new TaskCompletionSource();
|
||||
channel.WriteAndFlushAsync(request).ContinueWith((t, state) =>
|
||||
{
|
||||
var tcs = (TaskCompletionSource)state;
|
||||
switch (t.Status)
|
||||
{
|
||||
case TaskStatus.RanToCompletion:
|
||||
IChannelPipeline p = channel.Pipeline;
|
||||
IChannelHandlerContext ctx = p.Context<HttpRequestEncoder>() ?? p.Context<HttpClientCodec>();
|
||||
if (ctx == null)
|
||||
{
|
||||
tcs.TrySetException(new InvalidOperationException("ChannelPipeline does not contain a HttpRequestEncoder or HttpClientCodec"));
|
||||
return;
|
||||
}
|
||||
|
||||
p.AddAfter(ctx.Name, "ws-encoder", this.NewWebSocketEncoder());
|
||||
tcs.TryComplete();
|
||||
break;
|
||||
case TaskStatus.Canceled:
|
||||
tcs.TrySetCanceled();
|
||||
break;
|
||||
case TaskStatus.Faulted:
|
||||
tcs.TryUnwrap(t.Exception);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
},
|
||||
completion,
|
||||
TaskContinuationOptions.ExecuteSynchronously);
|
||||
|
||||
return completion.Task;
|
||||
}
|
||||
|
||||
protected internal abstract IFullHttpRequest NewHandshakeRequest();
|
||||
|
||||
public void FinishHandshake(IChannel channel, IFullHttpResponse response)
|
||||
{
|
||||
this.Verify(response);
|
||||
|
||||
// Verify the subprotocol that we received from the server.
|
||||
// This must be one of our expected subprotocols - or null/empty if we didn't want to speak a subprotocol
|
||||
string receivedProtocol = null;
|
||||
if (response.Headers.TryGet(HttpHeaderNames.SecWebsocketProtocol, out ICharSequence headerValue))
|
||||
{
|
||||
receivedProtocol = headerValue.ToString().Trim();
|
||||
}
|
||||
|
||||
string expectedProtocol = this.expectedSubprotocol ?? "";
|
||||
bool protocolValid = false;
|
||||
|
||||
if (expectedProtocol.Length == 0 && receivedProtocol == null)
|
||||
{
|
||||
// No subprotocol required and none received
|
||||
protocolValid = true;
|
||||
this.ActualSubprotocol = this.expectedSubprotocol; // null or "" - we echo what the user requested
|
||||
}
|
||||
else if (expectedProtocol.Length > 0 && !string.IsNullOrEmpty(receivedProtocol))
|
||||
{
|
||||
// We require a subprotocol and received one -> verify it
|
||||
foreach (string protocol in expectedProtocol.Split(','))
|
||||
{
|
||||
if (protocol.Trim().Equals(receivedProtocol))
|
||||
{
|
||||
protocolValid = true;
|
||||
this.ActualSubprotocol = receivedProtocol;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} // else mixed cases - which are all errors
|
||||
|
||||
if (!protocolValid)
|
||||
{
|
||||
throw new WebSocketHandshakeException($"Invalid subprotocol. Actual: {receivedProtocol}. Expected one of: {this.expectedSubprotocol}");
|
||||
}
|
||||
|
||||
this.SetHandshakeComplete();
|
||||
|
||||
IChannelPipeline p = channel.Pipeline;
|
||||
// Remove decompressor from pipeline if its in use
|
||||
var decompressor = p.Get<HttpContentDecompressor>();
|
||||
if (decompressor != null)
|
||||
{
|
||||
p.Remove(decompressor);
|
||||
}
|
||||
|
||||
// Remove aggregator if present before
|
||||
var aggregator = p.Get<HttpObjectAggregator>();
|
||||
if (aggregator != null)
|
||||
{
|
||||
p.Remove(aggregator);
|
||||
}
|
||||
|
||||
IChannelHandlerContext ctx = p.Context<HttpResponseDecoder>();
|
||||
if (ctx == null)
|
||||
{
|
||||
ctx = p.Context<HttpClientCodec>();
|
||||
if (ctx == null)
|
||||
{
|
||||
throw new InvalidOperationException("ChannelPipeline does not contain a HttpRequestEncoder or HttpClientCodec");
|
||||
}
|
||||
|
||||
var codec = (HttpClientCodec)ctx.Handler;
|
||||
// Remove the encoder part of the codec as the user may start writing frames after this method returns.
|
||||
codec.RemoveOutboundHandler();
|
||||
|
||||
p.AddAfter(ctx.Name, "ws-decoder", this.NewWebSocketDecoder());
|
||||
|
||||
// Delay the removal of the decoder so the user can setup the pipeline if needed to handle
|
||||
// WebSocketFrame messages.
|
||||
// See https://github.com/netty/netty/issues/4533
|
||||
channel.EventLoop.Execute(() => p.Remove(codec));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (p.Get<HttpRequestEncoder>() != null)
|
||||
{
|
||||
// Remove the encoder part of the codec as the user may start writing frames after this method returns.
|
||||
p.Remove<HttpRequestEncoder>();
|
||||
}
|
||||
|
||||
IChannelHandlerContext context = ctx;
|
||||
p.AddAfter(context.Name, "ws-decoder", this.NewWebSocketDecoder());
|
||||
|
||||
// Delay the removal of the decoder so the user can setup the pipeline if needed to handle
|
||||
// WebSocketFrame messages.
|
||||
// See https://github.com/netty/netty/issues/4533
|
||||
channel.EventLoop.Execute(() => p.Remove(context.Handler));
|
||||
}
|
||||
}
|
||||
|
||||
public Task ProcessHandshakeAsync(IChannel channel, IHttpResponse response)
|
||||
{
|
||||
var completionSource = new TaskCompletionSource();
|
||||
if (response is IFullHttpResponse res)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.FinishHandshake(channel, res);
|
||||
completionSource.TryComplete();
|
||||
}
|
||||
catch (Exception cause)
|
||||
{
|
||||
completionSource.TrySetException(cause);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
IChannelPipeline p = channel.Pipeline;
|
||||
IChannelHandlerContext ctx = p.Context<HttpResponseDecoder>();
|
||||
if (ctx == null)
|
||||
{
|
||||
ctx = p.Context<HttpClientCodec>();
|
||||
if (ctx == null)
|
||||
{
|
||||
completionSource.TrySetException(new InvalidOperationException("ChannelPipeline does not contain a HttpResponseDecoder or HttpClientCodec"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add aggregator and ensure we feed the HttpResponse so it is aggregated. A limit of 8192 should be more
|
||||
// then enough for the websockets handshake payload.
|
||||
//
|
||||
// TODO: Make handshake work without HttpObjectAggregator at all.
|
||||
const string AggregatorName = "httpAggregator";
|
||||
p.AddAfter(ctx.Name, AggregatorName, new HttpObjectAggregator(8192));
|
||||
p.AddAfter(AggregatorName, "handshaker", new Handshaker(this, channel, completionSource));
|
||||
try
|
||||
{
|
||||
ctx.FireChannelRead(ReferenceCountUtil.Retain(response));
|
||||
}
|
||||
catch (Exception cause)
|
||||
{
|
||||
completionSource.TrySetException(cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return completionSource.Task;
|
||||
}
|
||||
|
||||
sealed class Handshaker : SimpleChannelInboundHandler<IFullHttpResponse>
|
||||
{
|
||||
readonly WebSocketClientHandshaker clientHandshaker;
|
||||
readonly IChannel channel;
|
||||
readonly TaskCompletionSource completion;
|
||||
|
||||
public Handshaker(WebSocketClientHandshaker clientHandshaker, IChannel channel, TaskCompletionSource completion)
|
||||
{
|
||||
this.clientHandshaker = clientHandshaker;
|
||||
this.channel = channel;
|
||||
this.completion = completion;
|
||||
}
|
||||
|
||||
protected override void ChannelRead0(IChannelHandlerContext ctx, IFullHttpResponse msg)
|
||||
{
|
||||
// Remove and do the actual handshake
|
||||
ctx.Channel.Pipeline.Remove(this);
|
||||
try
|
||||
{
|
||||
this.clientHandshaker.FinishHandshake(this.channel, msg);
|
||||
this.completion.TryComplete();
|
||||
}
|
||||
catch (Exception cause)
|
||||
{
|
||||
this.completion.TrySetException(cause);
|
||||
}
|
||||
}
|
||||
|
||||
public override void ExceptionCaught(IChannelHandlerContext ctx, Exception cause)
|
||||
{
|
||||
// Remove ourself and fail the handshake promise.
|
||||
ctx.Channel.Pipeline.Remove(this);
|
||||
this.completion.TrySetException(cause);
|
||||
}
|
||||
|
||||
public override void ChannelInactive(IChannelHandlerContext ctx)
|
||||
{
|
||||
// Fail promise if Channel was closed
|
||||
this.completion.TrySetException(DefaultClosedChannelException);
|
||||
ctx.FireChannelInactive();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void Verify(IFullHttpResponse response);
|
||||
|
||||
protected internal abstract IWebSocketFrameDecoder NewWebSocketDecoder();
|
||||
|
||||
protected internal abstract IWebSocketFrameEncoder NewWebSocketEncoder();
|
||||
|
||||
public Task CloseAsync(IChannel channel, CloseWebSocketFrame frame)
|
||||
{
|
||||
Contract.Requires(channel != null);
|
||||
return channel.WriteAndFlushAsync(frame);
|
||||
}
|
||||
|
||||
internal static string RawPath(Uri wsUrl) => wsUrl.IsAbsoluteUri ? wsUrl.PathAndQuery : "/";
|
||||
|
||||
internal static string WebsocketHostValue(Uri wsUrl)
|
||||
{
|
||||
string scheme;
|
||||
Uri uri;
|
||||
if (wsUrl.IsAbsoluteUri)
|
||||
{
|
||||
scheme = wsUrl.Scheme;
|
||||
uri = wsUrl;
|
||||
}
|
||||
else
|
||||
{
|
||||
scheme = null;
|
||||
uri = AbsoluteUri(wsUrl);
|
||||
}
|
||||
|
||||
int port = OriginalPort(uri);
|
||||
if (port == -1)
|
||||
{
|
||||
return uri.Host;
|
||||
}
|
||||
string host = uri.Host;
|
||||
if (port == HttpScheme.Http.Port)
|
||||
{
|
||||
return HttpScheme.Http.Name.ContentEquals(scheme)
|
||||
|| WebSocketScheme.WS.Name.ContentEquals(scheme)
|
||||
? host : NetUtil.ToSocketAddressString(host, port);
|
||||
}
|
||||
if (port == HttpScheme.Https.Port)
|
||||
{
|
||||
return HttpScheme.Https.Name.ToString().Equals(scheme)
|
||||
|| WebSocketScheme.WSS.Name.ToString().Equals(scheme)
|
||||
? host : NetUtil.ToSocketAddressString(host, port);
|
||||
}
|
||||
|
||||
// if the port is not standard (80/443) its needed to add the port to the header.
|
||||
// See http://tools.ietf.org/html/rfc6454#section-6.2
|
||||
return NetUtil.ToSocketAddressString(host, port);
|
||||
}
|
||||
|
||||
internal static string WebsocketOriginValue(Uri wsUrl)
|
||||
{
|
||||
string scheme;
|
||||
Uri uri;
|
||||
if (wsUrl.IsAbsoluteUri)
|
||||
{
|
||||
scheme = wsUrl.Scheme;
|
||||
uri = wsUrl;
|
||||
}
|
||||
else
|
||||
{
|
||||
scheme = null;
|
||||
uri = AbsoluteUri(wsUrl);
|
||||
}
|
||||
|
||||
string schemePrefix;
|
||||
int port = uri.Port;
|
||||
int defaultPort;
|
||||
|
||||
if (WebSocketScheme.WSS.Name.ContentEquals(scheme)
|
||||
|| HttpScheme.Https.Name.ContentEquals(scheme)
|
||||
|| (scheme == null && port == WebSocketScheme.WSS.Port))
|
||||
{
|
||||
|
||||
schemePrefix = HttpsSchemePrefix;
|
||||
defaultPort = WebSocketScheme.WSS.Port;
|
||||
}
|
||||
else
|
||||
{
|
||||
schemePrefix = HttpSchemePrefix;
|
||||
defaultPort = WebSocketScheme.WS.Port;
|
||||
}
|
||||
|
||||
// Convert uri-host to lower case (by RFC 6454, chapter 4 "Origin of a URI")
|
||||
string host = uri.Host.ToLower();
|
||||
|
||||
if (port != defaultPort && port != -1)
|
||||
{
|
||||
// if the port is not standard (80/443) its needed to add the port to the header.
|
||||
// See http://tools.ietf.org/html/rfc6454#section-6.2
|
||||
return schemePrefix + NetUtil.ToSocketAddressString(host, port);
|
||||
}
|
||||
return schemePrefix + host;
|
||||
}
|
||||
|
||||
static Uri AbsoluteUri(Uri uri)
|
||||
{
|
||||
if (uri.IsAbsoluteUri)
|
||||
{
|
||||
return uri;
|
||||
}
|
||||
|
||||
string relativeUri = uri.OriginalString;
|
||||
return new Uri(relativeUri.StartsWith("//")
|
||||
? HttpScheme.Http + ":" + relativeUri
|
||||
: HttpSchemePrefix + relativeUri);
|
||||
}
|
||||
|
||||
static int OriginalPort(Uri uri)
|
||||
{
|
||||
int index = uri.Scheme.Length + 3 + uri.Host.Length;
|
||||
|
||||
if (index < uri.OriginalString.Length
|
||||
&& uri.OriginalString[index] == ':')
|
||||
{
|
||||
return uri.Port;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Internal;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public class WebSocketClientHandshaker00 : WebSocketClientHandshaker
|
||||
{
|
||||
static readonly AsciiString Websocket = AsciiString.Cached("WebSocket");
|
||||
|
||||
IByteBuffer expectedChallengeResponseBytes;
|
||||
|
||||
public WebSocketClientHandshaker00(Uri webSocketUrl, WebSocketVersion version, string subprotocol,
|
||||
HttpHeaders customHeaders, int maxFramePayloadLength)
|
||||
: base(webSocketUrl, version, subprotocol, customHeaders, maxFramePayloadLength)
|
||||
{
|
||||
}
|
||||
|
||||
protected internal override unsafe IFullHttpRequest NewHandshakeRequest()
|
||||
{
|
||||
// Make keys
|
||||
int spaces1 = WebSocketUtil.RandomNumber(1, 12);
|
||||
int spaces2 = WebSocketUtil.RandomNumber(1, 12);
|
||||
|
||||
int max1 = int.MaxValue / spaces1;
|
||||
int max2 = int.MaxValue / spaces2;
|
||||
|
||||
int number1 = WebSocketUtil.RandomNumber(0, max1);
|
||||
int number2 = WebSocketUtil.RandomNumber(0, max2);
|
||||
|
||||
int product1 = number1 * spaces1;
|
||||
int product2 = number2 * spaces2;
|
||||
|
||||
string key1 = Convert.ToString(product1);
|
||||
string key2 = Convert.ToString(product2);
|
||||
|
||||
key1 = InsertRandomCharacters(key1);
|
||||
key2 = InsertRandomCharacters(key2);
|
||||
|
||||
key1 = InsertSpaces(key1, spaces1);
|
||||
key2 = InsertSpaces(key2, spaces2);
|
||||
|
||||
byte[] key3 = WebSocketUtil.RandomBytes(8);
|
||||
var challenge = new byte[16];
|
||||
fixed (byte* bytes = challenge)
|
||||
{
|
||||
Unsafe.WriteUnaligned(bytes, number1);
|
||||
Unsafe.WriteUnaligned(bytes + 4, number2);
|
||||
PlatformDependent.CopyMemory(key3, 0, bytes + 8, 8);
|
||||
}
|
||||
|
||||
this.expectedChallengeResponseBytes = Unpooled.WrappedBuffer(WebSocketUtil.Md5(challenge));
|
||||
|
||||
// Get path
|
||||
Uri wsUrl = this.Uri;
|
||||
string path = RawPath(wsUrl);
|
||||
|
||||
// Format request
|
||||
var request = new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Get, path);
|
||||
HttpHeaders headers = request.Headers;
|
||||
headers.Add(HttpHeaderNames.Upgrade, Websocket)
|
||||
.Add(HttpHeaderNames.Connection, HttpHeaderValues.Upgrade)
|
||||
.Add(HttpHeaderNames.Host, WebsocketHostValue(wsUrl))
|
||||
.Add(HttpHeaderNames.Origin, WebsocketOriginValue(wsUrl))
|
||||
.Add(HttpHeaderNames.SecWebsocketKey1, key1)
|
||||
.Add(HttpHeaderNames.SecWebsocketKey2, key2);
|
||||
|
||||
string expectedSubprotocol = this.ExpectedSubprotocol;
|
||||
if (!string.IsNullOrEmpty(expectedSubprotocol))
|
||||
{
|
||||
headers.Add(HttpHeaderNames.SecWebsocketProtocol, expectedSubprotocol);
|
||||
}
|
||||
|
||||
if (this.CustomHeaders != null)
|
||||
{
|
||||
headers.Add(this.CustomHeaders);
|
||||
}
|
||||
|
||||
// Set Content-Length to workaround some known defect.
|
||||
// See also: http://www.ietf.org/mail-archive/web/hybi/current/msg02149.html
|
||||
headers.Set(HttpHeaderNames.ContentLength, key3.Length);
|
||||
request.Content.WriteBytes(key3);
|
||||
return request;
|
||||
}
|
||||
|
||||
protected override void Verify(IFullHttpResponse response)
|
||||
{
|
||||
if (!response.Status.Equals(HttpResponseStatus.SwitchingProtocols))
|
||||
{
|
||||
throw new WebSocketHandshakeException($"Invalid handshake response getStatus: {response.Status}");
|
||||
}
|
||||
|
||||
HttpHeaders headers = response.Headers;
|
||||
|
||||
if (!headers.TryGet(HttpHeaderNames.Upgrade, out ICharSequence upgrade)
|
||||
||!Websocket.ContentEqualsIgnoreCase(upgrade))
|
||||
{
|
||||
throw new WebSocketHandshakeException($"Invalid handshake response upgrade: {upgrade}");
|
||||
}
|
||||
|
||||
if (!headers.ContainsValue(HttpHeaderNames.Connection, HttpHeaderValues.Upgrade, true))
|
||||
{
|
||||
headers.TryGet(HttpHeaderNames.Connection, out upgrade);
|
||||
throw new WebSocketHandshakeException($"Invalid handshake response connection: {upgrade}");
|
||||
}
|
||||
|
||||
IByteBuffer challenge = response.Content;
|
||||
if (!challenge.Equals(this.expectedChallengeResponseBytes))
|
||||
{
|
||||
throw new WebSocketHandshakeException("Invalid challenge");
|
||||
}
|
||||
}
|
||||
|
||||
static string InsertRandomCharacters(string key)
|
||||
{
|
||||
int count = WebSocketUtil.RandomNumber(1, 12);
|
||||
|
||||
var randomChars = new char[count];
|
||||
int randCount = 0;
|
||||
while (randCount < count)
|
||||
{
|
||||
int rand = unchecked((int)(WebSocketUtil.RandomNext() * 0x7e + 0x21));
|
||||
if (0x21 < rand && rand < 0x2f || 0x3a < rand && rand < 0x7e)
|
||||
{
|
||||
randomChars[randCount] = (char)rand;
|
||||
randCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int split = WebSocketUtil.RandomNumber(0, key.Length);
|
||||
string part1 = key.Substring(0, split);
|
||||
string part2 = key.Substring(split);
|
||||
key = part1 + randomChars[i] + part2;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
static string InsertSpaces(string key, int spaces)
|
||||
{
|
||||
for (int i = 0; i < spaces; i++)
|
||||
{
|
||||
int split = WebSocketUtil.RandomNumber(1, key.Length - 1);
|
||||
string part1 = key.Substring(0, split);
|
||||
string part2 = key.Substring(split);
|
||||
key = part1 + ' ' + part2;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
protected internal override IWebSocketFrameDecoder NewWebSocketDecoder() => new WebSocket00FrameDecoder(this.MaxFramePayloadLength);
|
||||
|
||||
protected internal override IWebSocketFrameEncoder NewWebSocketEncoder() => new WebSocket00FrameEncoder();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using System;
|
||||
using System.Text;
|
||||
using DotNetty.Common.Internal.Logging;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public class WebSocketClientHandshaker07 : WebSocketClientHandshaker
|
||||
{
|
||||
static readonly IInternalLogger Logger = InternalLoggerFactory.GetInstance<WebSocketClientHandshaker07>();
|
||||
public static readonly string MagicGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
|
||||
AsciiString expectedChallengeResponseString;
|
||||
|
||||
readonly bool allowExtensions;
|
||||
readonly bool performMasking;
|
||||
readonly bool allowMaskMismatch;
|
||||
|
||||
public WebSocketClientHandshaker07(Uri webSocketUrl, WebSocketVersion version, string subprotocol,
|
||||
bool allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength)
|
||||
: this(webSocketUrl, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, true, false)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketClientHandshaker07(Uri webSocketUrl, WebSocketVersion version, string subprotocol,
|
||||
bool allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
|
||||
bool performMasking, bool allowMaskMismatch)
|
||||
: base(webSocketUrl, version, subprotocol, customHeaders, maxFramePayloadLength)
|
||||
{
|
||||
this.allowExtensions = allowExtensions;
|
||||
this.performMasking = performMasking;
|
||||
this.allowMaskMismatch = allowMaskMismatch;
|
||||
}
|
||||
|
||||
protected internal override IFullHttpRequest NewHandshakeRequest()
|
||||
{
|
||||
// Get path
|
||||
Uri wsUrl = this.Uri;
|
||||
string path = RawPath(wsUrl);
|
||||
|
||||
// Get 16 bit nonce and base 64 encode it
|
||||
byte[] nonce = WebSocketUtil.RandomBytes(16);
|
||||
string key = WebSocketUtil.Base64String(nonce);
|
||||
|
||||
string acceptSeed = key + MagicGuid;
|
||||
byte[] sha1 = WebSocketUtil.Sha1(Encoding.ASCII.GetBytes(acceptSeed));
|
||||
this.expectedChallengeResponseString = new AsciiString(WebSocketUtil.Base64String(sha1));
|
||||
|
||||
if (Logger.DebugEnabled)
|
||||
{
|
||||
Logger.Debug("WebSocket version 07 client handshake key: {}, expected response: {}",
|
||||
key, this.expectedChallengeResponseString);
|
||||
}
|
||||
|
||||
// Format request
|
||||
var request = new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Get, path);
|
||||
HttpHeaders headers = request.Headers;
|
||||
|
||||
headers.Add(HttpHeaderNames.Upgrade, HttpHeaderValues.Websocket)
|
||||
.Add(HttpHeaderNames.Connection, HttpHeaderValues.Upgrade)
|
||||
.Add(HttpHeaderNames.SecWebsocketKey, key)
|
||||
.Add(HttpHeaderNames.Host, WebsocketHostValue(wsUrl))
|
||||
.Add(HttpHeaderNames.SecWebsocketOrigin, WebsocketOriginValue(wsUrl));
|
||||
|
||||
string expectedSubprotocol = this.ExpectedSubprotocol;
|
||||
if (!string.IsNullOrEmpty(expectedSubprotocol))
|
||||
{
|
||||
headers.Add(HttpHeaderNames.SecWebsocketProtocol, expectedSubprotocol);
|
||||
}
|
||||
|
||||
headers.Add(HttpHeaderNames.SecWebsocketVersion, "7");
|
||||
|
||||
if (this.CustomHeaders != null)
|
||||
{
|
||||
headers.Add(this.CustomHeaders);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
protected override void Verify(IFullHttpResponse response)
|
||||
{
|
||||
HttpResponseStatus status = HttpResponseStatus.SwitchingProtocols;
|
||||
HttpHeaders headers = response.Headers;
|
||||
|
||||
if (!response.Status.Equals(status))
|
||||
{
|
||||
throw new WebSocketHandshakeException($"Invalid handshake response getStatus: {response.Status}");
|
||||
}
|
||||
|
||||
if (headers.TryGet(HttpHeaderNames.Upgrade, out ICharSequence upgrade)
|
||||
|| !HttpHeaderValues.Websocket.ContentEqualsIgnoreCase(upgrade))
|
||||
{
|
||||
throw new WebSocketHandshakeException($"Invalid handshake response upgrade: {upgrade}");
|
||||
}
|
||||
|
||||
if (!headers.ContainsValue(HttpHeaderNames.Connection, HttpHeaderValues.Upgrade, true))
|
||||
{
|
||||
headers.TryGet(HttpHeaderNames.Connection, out upgrade);
|
||||
throw new WebSocketHandshakeException($"Invalid handshake response connection: {upgrade}");
|
||||
}
|
||||
|
||||
if (headers.TryGet(HttpHeaderNames.SecWebsocketAccept, out ICharSequence accept)
|
||||
|| !accept.Equals(this.expectedChallengeResponseString))
|
||||
{
|
||||
throw new WebSocketHandshakeException($"Invalid challenge. Actual: {accept}. Expected: {this.expectedChallengeResponseString}");
|
||||
}
|
||||
}
|
||||
|
||||
protected internal override IWebSocketFrameDecoder NewWebSocketDecoder() => new WebSocket07FrameDecoder(
|
||||
false, this.allowExtensions, this.MaxFramePayloadLength, this.allowMaskMismatch);
|
||||
|
||||
protected internal override IWebSocketFrameEncoder NewWebSocketEncoder() => new WebSocket07FrameEncoder(this.performMasking);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using System;
|
||||
using System.Text;
|
||||
using DotNetty.Common.Internal.Logging;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public class WebSocketClientHandshaker08 : WebSocketClientHandshaker
|
||||
{
|
||||
static readonly IInternalLogger Logger = InternalLoggerFactory.GetInstance<WebSocketClientHandshaker08>();
|
||||
|
||||
public static readonly string MagicGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
|
||||
AsciiString expectedChallengeResponseString;
|
||||
|
||||
readonly bool allowExtensions;
|
||||
readonly bool performMasking;
|
||||
readonly bool allowMaskMismatch;
|
||||
|
||||
public WebSocketClientHandshaker08(Uri webSocketUrl, WebSocketVersion version, string subprotocol,
|
||||
bool allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength)
|
||||
: this(webSocketUrl, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, true, false)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketClientHandshaker08(Uri webSocketUrl, WebSocketVersion version, string subprotocol,
|
||||
bool allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
|
||||
bool performMasking, bool allowMaskMismatch)
|
||||
: base(webSocketUrl, version, subprotocol, customHeaders, maxFramePayloadLength)
|
||||
{
|
||||
this.allowExtensions = allowExtensions;
|
||||
this.performMasking = performMasking;
|
||||
this.allowMaskMismatch = allowMaskMismatch;
|
||||
}
|
||||
|
||||
protected internal override IFullHttpRequest NewHandshakeRequest()
|
||||
{
|
||||
// Get path
|
||||
Uri wsUrl = this.Uri;
|
||||
string path = RawPath(wsUrl);
|
||||
|
||||
// Get 16 bit nonce and base 64 encode it
|
||||
byte[] nonce = WebSocketUtil.RandomBytes(16);
|
||||
string key = WebSocketUtil.Base64String(nonce);
|
||||
|
||||
string acceptSeed = key + MagicGuid;
|
||||
byte[] sha1 = WebSocketUtil.Sha1(Encoding.ASCII.GetBytes(acceptSeed));
|
||||
this.expectedChallengeResponseString = new AsciiString(WebSocketUtil.Base64String(sha1));
|
||||
|
||||
if (Logger.DebugEnabled)
|
||||
{
|
||||
Logger.Debug("WebSocket version 08 client handshake key: {}, expected response: {}",
|
||||
key, this.expectedChallengeResponseString);
|
||||
}
|
||||
|
||||
// Format request
|
||||
var request = new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Get, path);
|
||||
HttpHeaders headers = request.Headers;
|
||||
|
||||
headers.Add(HttpHeaderNames.Upgrade, HttpHeaderValues.Websocket)
|
||||
.Add(HttpHeaderNames.Connection, HttpHeaderValues.Upgrade)
|
||||
.Add(HttpHeaderNames.SecWebsocketKey, key)
|
||||
.Add(HttpHeaderNames.Host, WebsocketHostValue(wsUrl))
|
||||
.Add(HttpHeaderNames.SecWebsocketOrigin, WebsocketOriginValue(wsUrl));
|
||||
|
||||
string expectedSubprotocol = this.ExpectedSubprotocol;
|
||||
if (!string.IsNullOrEmpty(expectedSubprotocol))
|
||||
{
|
||||
headers.Add(HttpHeaderNames.SecWebsocketProtocol, expectedSubprotocol);
|
||||
}
|
||||
|
||||
headers.Add(HttpHeaderNames.SecWebsocketVersion, "8");
|
||||
|
||||
if (this.CustomHeaders != null)
|
||||
{
|
||||
headers.Add(this.CustomHeaders);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
protected override void Verify(IFullHttpResponse response)
|
||||
{
|
||||
HttpResponseStatus status = HttpResponseStatus.SwitchingProtocols;
|
||||
HttpHeaders headers = response.Headers;
|
||||
|
||||
if (!response.Status.Equals(status))
|
||||
{
|
||||
throw new WebSocketHandshakeException($"Invalid handshake response getStatus: {response.Status}");
|
||||
}
|
||||
|
||||
if (!headers.TryGet(HttpHeaderNames.Upgrade, out ICharSequence upgrade)
|
||||
|| !HttpHeaderValues.Websocket.ContentEqualsIgnoreCase(upgrade))
|
||||
{
|
||||
throw new WebSocketHandshakeException($"Invalid handshake response upgrade: {upgrade}");
|
||||
}
|
||||
|
||||
if (!headers.ContainsValue(HttpHeaderNames.Connection, HttpHeaderValues.Upgrade, true))
|
||||
{
|
||||
headers.TryGet(HttpHeaderNames.Connection, out upgrade);
|
||||
throw new WebSocketHandshakeException($"Invalid handshake response connection: {upgrade}");
|
||||
}
|
||||
|
||||
if (!headers.TryGet(HttpHeaderNames.SecWebsocketAccept, out ICharSequence accept)
|
||||
|| !accept.Equals(this.expectedChallengeResponseString))
|
||||
{
|
||||
throw new WebSocketHandshakeException($"Invalid challenge. Actual: {accept}. Expected: {this.expectedChallengeResponseString}");
|
||||
}
|
||||
}
|
||||
|
||||
protected internal override IWebSocketFrameDecoder NewWebSocketDecoder() => new WebSocket08FrameDecoder(
|
||||
false, this.allowExtensions, this.MaxFramePayloadLength, this.allowMaskMismatch);
|
||||
|
||||
protected internal override IWebSocketFrameEncoder NewWebSocketEncoder() => new WebSocket08FrameEncoder(this.performMasking);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using System;
|
||||
using System.Text;
|
||||
using DotNetty.Common.Internal.Logging;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public class WebSocketClientHandshaker13 : WebSocketClientHandshaker
|
||||
{
|
||||
static readonly IInternalLogger Logger = InternalLoggerFactory.GetInstance<WebSocketClientHandshaker13>();
|
||||
|
||||
public static readonly string MagicGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
|
||||
AsciiString expectedChallengeResponseString;
|
||||
|
||||
readonly bool allowExtensions;
|
||||
readonly bool performMasking;
|
||||
readonly bool allowMaskMismatch;
|
||||
|
||||
public WebSocketClientHandshaker13(Uri webSocketUrl, WebSocketVersion version, string subprotocol,
|
||||
bool allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength)
|
||||
: this(webSocketUrl, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, true, false)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketClientHandshaker13(Uri webSocketUrl, WebSocketVersion version, string subprotocol,
|
||||
bool allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
|
||||
bool performMasking, bool allowMaskMismatch)
|
||||
: base(webSocketUrl, version, subprotocol, customHeaders, maxFramePayloadLength)
|
||||
{
|
||||
|
||||
this.allowExtensions = allowExtensions;
|
||||
this.performMasking = performMasking;
|
||||
this.allowMaskMismatch = allowMaskMismatch;
|
||||
}
|
||||
|
||||
protected internal override IFullHttpRequest NewHandshakeRequest()
|
||||
{
|
||||
// Get path
|
||||
Uri wsUrl = this.Uri;
|
||||
string path = RawPath(wsUrl);
|
||||
|
||||
// Get 16 bit nonce and base 64 encode it
|
||||
byte[] nonce = WebSocketUtil.RandomBytes(16);
|
||||
string key = WebSocketUtil.Base64String(nonce);
|
||||
|
||||
string acceptSeed = key + MagicGuid;
|
||||
byte[] sha1 = WebSocketUtil.Sha1(Encoding.ASCII.GetBytes(acceptSeed));
|
||||
this.expectedChallengeResponseString = new AsciiString(WebSocketUtil.Base64String(sha1));
|
||||
|
||||
if (Logger.DebugEnabled)
|
||||
{
|
||||
Logger.Debug("WebSocket version 13 client handshake key: {}, expected response: {}",
|
||||
key, this.expectedChallengeResponseString);
|
||||
}
|
||||
|
||||
// Format request
|
||||
var request = new DefaultFullHttpRequest(HttpVersion.Http11, HttpMethod.Get, path);
|
||||
HttpHeaders headers = request.Headers;
|
||||
|
||||
headers.Add(HttpHeaderNames.Upgrade, HttpHeaderValues.Websocket)
|
||||
.Add(HttpHeaderNames.Connection, HttpHeaderValues.Upgrade)
|
||||
.Add(HttpHeaderNames.SecWebsocketKey, key)
|
||||
.Add(HttpHeaderNames.Host, WebsocketHostValue(wsUrl))
|
||||
.Add(HttpHeaderNames.SecWebsocketOrigin, WebsocketOriginValue(wsUrl));
|
||||
|
||||
string expectedSubprotocol = this.ExpectedSubprotocol;
|
||||
if (!string.IsNullOrEmpty(expectedSubprotocol))
|
||||
{
|
||||
headers.Add(HttpHeaderNames.SecWebsocketProtocol, expectedSubprotocol);
|
||||
}
|
||||
|
||||
headers.Add(HttpHeaderNames.SecWebsocketVersion, "13");
|
||||
|
||||
if (this.CustomHeaders != null)
|
||||
{
|
||||
headers.Add(this.CustomHeaders);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
protected override void Verify(IFullHttpResponse response)
|
||||
{
|
||||
HttpResponseStatus status = HttpResponseStatus.SwitchingProtocols;
|
||||
HttpHeaders headers = response.Headers;
|
||||
|
||||
if (!response.Status.Equals(status))
|
||||
{
|
||||
throw new WebSocketHandshakeException($"Invalid handshake response getStatus: {response.Status}");
|
||||
}
|
||||
|
||||
if (!headers.TryGet(HttpHeaderNames.Upgrade, out ICharSequence upgrade)
|
||||
|| !HttpHeaderValues.Websocket.ContentEqualsIgnoreCase(upgrade))
|
||||
{
|
||||
throw new WebSocketHandshakeException($"Invalid handshake response upgrade: {upgrade}");
|
||||
}
|
||||
|
||||
if (!headers.ContainsValue(HttpHeaderNames.Connection, HttpHeaderValues.Upgrade, true))
|
||||
{
|
||||
headers.TryGet(HttpHeaderNames.Connection, out upgrade);
|
||||
throw new WebSocketHandshakeException($"Invalid handshake response connection: {upgrade}");
|
||||
}
|
||||
|
||||
if (!headers.TryGet(HttpHeaderNames.SecWebsocketAccept, out ICharSequence accept)
|
||||
|| !accept.Equals(this.expectedChallengeResponseString))
|
||||
{
|
||||
throw new WebSocketHandshakeException($"Invalid challenge. Actual: {accept}. Expected: {this.expectedChallengeResponseString}");
|
||||
}
|
||||
}
|
||||
|
||||
protected internal override IWebSocketFrameDecoder NewWebSocketDecoder() => new WebSocket13FrameDecoder(
|
||||
false, this.allowExtensions, this.MaxFramePayloadLength, this.allowMaskMismatch);
|
||||
|
||||
protected internal override IWebSocketFrameEncoder NewWebSocketEncoder() => new WebSocket13FrameEncoder(this.performMasking);
|
||||
}
|
||||
}
|
|
@ -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.WebSockets
|
||||
{
|
||||
using System;
|
||||
|
||||
using static WebSocketVersion;
|
||||
|
||||
public static class WebSocketClientHandshakerFactory
|
||||
{
|
||||
public static WebSocketClientHandshaker NewHandshaker(Uri webSocketUrl, WebSocketVersion version, string subprotocol, bool allowExtensions, HttpHeaders customHeaders) =>
|
||||
NewHandshaker(webSocketUrl, version, subprotocol, allowExtensions, customHeaders, 65536);
|
||||
|
||||
public static WebSocketClientHandshaker NewHandshaker(Uri webSocketUrl, WebSocketVersion version, string subprotocol, bool allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) =>
|
||||
NewHandshaker(webSocketUrl, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, true, false);
|
||||
|
||||
public static WebSocketClientHandshaker NewHandshaker(
|
||||
Uri webSocketUrl, WebSocketVersion version, string subprotocol,
|
||||
bool allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
|
||||
bool performMasking, bool allowMaskMismatch)
|
||||
{
|
||||
if (version == V13)
|
||||
{
|
||||
return new WebSocketClientHandshaker13(
|
||||
webSocketUrl, V13, subprotocol, allowExtensions, customHeaders,
|
||||
maxFramePayloadLength, performMasking, allowMaskMismatch);
|
||||
}
|
||||
if (version == V08)
|
||||
{
|
||||
return new WebSocketClientHandshaker08(
|
||||
webSocketUrl, V08, subprotocol, allowExtensions, customHeaders,
|
||||
maxFramePayloadLength, performMasking, allowMaskMismatch);
|
||||
}
|
||||
if (version == V07)
|
||||
{
|
||||
return new WebSocketClientHandshaker07(
|
||||
webSocketUrl, V07, subprotocol, allowExtensions, customHeaders,
|
||||
maxFramePayloadLength, performMasking, allowMaskMismatch);
|
||||
}
|
||||
if (version == V00)
|
||||
{
|
||||
return new WebSocketClientHandshaker00(
|
||||
webSocketUrl, V00, subprotocol, customHeaders, maxFramePayloadLength);
|
||||
}
|
||||
|
||||
throw new WebSocketHandshakeException($"Protocol version {version}not supported.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
||||
// ReSharper disable ConvertToAutoProperty
|
||||
// ReSharper disable once ConvertToAutoPropertyWhenPossible
|
||||
// ReSharper disable ConvertToAutoPropertyWhenPossible
|
||||
namespace DotNetty.Codecs.Http.WebSockets
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public class WebSocketClientProtocolHandler : WebSocketProtocolHandler
|
||||
{
|
||||
readonly WebSocketClientHandshaker handshaker;
|
||||
readonly bool handleCloseFrames;
|
||||
|
||||
public WebSocketClientHandshaker Handshaker => this.handshaker;
|
||||
|
||||
/// <summary>
|
||||
/// Events that are fired to notify about handshake status
|
||||
/// </summary>
|
||||
public enum ClientHandshakeStateEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The Handshake was started but the server did not response yet to the request
|
||||
/// </summary>
|
||||
HandshakeIssued,
|
||||
|
||||
/// <summary>
|
||||
/// The Handshake was complete succesful and so the channel was upgraded to websockets
|
||||
/// </summary>
|
||||
HandshakeComplete
|
||||
}
|
||||
|
||||
public WebSocketClientProtocolHandler(Uri webSocketUrl, WebSocketVersion version, string subprotocol,
|
||||
bool allowExtensions, HttpHeaders customHeaders,
|
||||
int maxFramePayloadLength, bool handleCloseFrames,
|
||||
bool performMasking, bool allowMaskMismatch)
|
||||
: this(WebSocketClientHandshakerFactory.NewHandshaker(webSocketUrl, version, subprotocol,
|
||||
allowExtensions, customHeaders, maxFramePayloadLength,
|
||||
performMasking, allowMaskMismatch), handleCloseFrames)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketClientProtocolHandler(Uri webSocketUrl, WebSocketVersion version, string subprotocol,
|
||||
bool allowExtensions, HttpHeaders customHeaders,
|
||||
int maxFramePayloadLength, bool handleCloseFrames)
|
||||
: this(webSocketUrl, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
|
||||
handleCloseFrames, true, false)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketClientProtocolHandler(Uri webSocketUrl, WebSocketVersion version, string subprotocol,
|
||||
bool allowExtensions, HttpHeaders customHeaders,
|
||||
int maxFramePayloadLength)
|
||||
: this(webSocketUrl, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, true)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, bool handleCloseFrames)
|
||||
{
|
||||
this.handshaker = handshaker;
|
||||
this.handleCloseFrames = handleCloseFrames;
|
||||
}
|
||||
|
||||
public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker)
|
||||
: this(handshaker, true)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Decode(IChannelHandlerContext ctx, WebSocketFrame frame, List<object> output)
|
||||
{
|
||||
if (this.handleCloseFrames && frame is CloseWebSocketFrame)
|
||||
{
|
||||
ctx.CloseAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
base.Decode(ctx, frame, output);
|
||||
}
|
||||
|
||||
public override void HandlerAdded(IChannelHandlerContext ctx)
|
||||
{
|
||||
IChannelPipeline cp = ctx.Channel.Pipeline;
|
||||
if (cp.Get<WebSocketClientProtocolHandshakeHandler>() == null)
|
||||
{
|
||||
// Add the WebSocketClientProtocolHandshakeHandler before this one.
|
||||
ctx.Channel.Pipeline.AddBefore(ctx.Name, nameof(WebSocketClientProtocolHandshakeHandler),
|
||||
new WebSocketClientProtocolHandshakeHandler(this.handshaker));
|
||||
}
|
||||
if (cp.Get<Utf8FrameValidator>() == null)
|
||||
{
|
||||
// Add the UFT8 checking before this one.
|
||||
ctx.Channel.Pipeline.AddBefore(ctx.Name, nameof(Utf8FrameValidator),
|
||||
new Utf8FrameValidator());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
||||
namespace DotNetty.Codecs.Http.WebSockets
|
||||
{
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
class WebSocketClientProtocolHandshakeHandler : ChannelHandlerAdapter
|
||||
{
|
||||
readonly WebSocketClientHandshaker handshaker;
|
||||
|
||||
internal WebSocketClientProtocolHandshakeHandler(WebSocketClientHandshaker handshaker)
|
||||
{
|
||||
this.handshaker = handshaker;
|
||||
}
|
||||
|
||||
public override void ChannelActive(IChannelHandlerContext context)
|
||||
{
|
||||
base.ChannelActive(context);
|
||||
this.handshaker.HandshakeAsync(context.Channel)
|
||||
.ContinueWith((t, state) =>
|
||||
{
|
||||
var ctx = (IChannelHandlerContext)state;
|
||||
if (t.Status == TaskStatus.RanToCompletion)
|
||||
{
|
||||
ctx.FireUserEventTriggered(WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HandshakeIssued);
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx.FireExceptionCaught(t.Exception);
|
||||
}
|
||||
},
|
||||
context,
|
||||
TaskContinuationOptions.ExecuteSynchronously);
|
||||
}
|
||||
|
||||
public override void ChannelRead(IChannelHandlerContext ctx, object msg)
|
||||
{
|
||||
if (!(msg is IFullHttpResponse))
|
||||
{
|
||||
ctx.FireChannelRead(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
var response = (IFullHttpResponse)msg;
|
||||
try
|
||||
{
|
||||
if (!this.handshaker.IsHandshakeComplete)
|
||||
{
|
||||
this.handshaker.FinishHandshake(ctx.Channel, response);
|
||||
ctx.FireUserEventTriggered(WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HandshakeComplete);
|
||||
ctx.Channel.Pipeline.Remove(this);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("WebSocketClientHandshaker should have been finished yet");
|
||||
}
|
||||
finally
|
||||
{
|
||||
response.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
||||
// ReSharper disable ConvertToAutoProperty
|
||||
namespace DotNetty.Codecs.Http.WebSockets
|
||||
{
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public abstract class WebSocketFrame : DefaultByteBufferHolder
|
||||
{
|
||||
// Flag to indicate if this frame is the final fragment in a message. The first fragment (frame) may also be the
|
||||
// final fragment.
|
||||
readonly bool finalFragment;
|
||||
|
||||
// RSV1, RSV2, RSV3 used for extensions
|
||||
readonly int rsv;
|
||||
|
||||
protected WebSocketFrame(IByteBuffer binaryData)
|
||||
: this(true, 0, binaryData)
|
||||
{
|
||||
}
|
||||
|
||||
protected WebSocketFrame(bool finalFragment, int rsv, IByteBuffer binaryData)
|
||||
: base(binaryData)
|
||||
{
|
||||
this.finalFragment = finalFragment;
|
||||
this.rsv = rsv;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flag to indicate if this frame is the final fragment in a message. The first fragment (frame)
|
||||
/// may also be the final fragment.
|
||||
/// </summary>
|
||||
public bool IsFinalFragment => this.finalFragment;
|
||||
|
||||
/// <summary>
|
||||
/// RSV1, RSV2, RSV3 used for extensions
|
||||
/// </summary>
|
||||
public int Rsv => this.rsv;
|
||||
|
||||
public override string ToString() => StringUtil.SimpleClassName(this) + "(data: " + this.ContentToString() + ')';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using System;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Codecs;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public class WebSocketFrameAggregator : MessageAggregator<WebSocketFrame, WebSocketFrame, ContinuationWebSocketFrame, WebSocketFrame>
|
||||
{
|
||||
public WebSocketFrameAggregator(int maxContentLength)
|
||||
: base(maxContentLength)
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool IsStartMessage(WebSocketFrame msg) => msg is TextWebSocketFrame || msg is BinaryWebSocketFrame;
|
||||
|
||||
protected override bool IsContentMessage(WebSocketFrame msg) => msg is ContinuationWebSocketFrame;
|
||||
|
||||
protected override bool IsLastContentMessage(ContinuationWebSocketFrame msg) => this.IsContentMessage(msg) && msg.IsFinalFragment;
|
||||
|
||||
protected override bool IsAggregated(WebSocketFrame msg)
|
||||
{
|
||||
if (msg.IsFinalFragment)
|
||||
{
|
||||
return !this.IsContentMessage(msg);
|
||||
}
|
||||
|
||||
return !this.IsStartMessage(msg) && !this.IsContentMessage(msg);
|
||||
}
|
||||
|
||||
protected override bool IsContentLengthInvalid(WebSocketFrame start, int maxContentLength) => false;
|
||||
|
||||
protected override object NewContinueResponse(WebSocketFrame start, int maxContentLength, IChannelPipeline pipeline) => null;
|
||||
|
||||
protected override bool CloseAfterContinueResponse(object msg) => throw new NotSupportedException();
|
||||
|
||||
protected override bool IgnoreContentAfterContinueResponse(object msg) => throw new NotSupportedException();
|
||||
|
||||
protected override WebSocketFrame BeginAggregation(WebSocketFrame start, IByteBuffer content)
|
||||
{
|
||||
if (start is TextWebSocketFrame)
|
||||
{
|
||||
return new TextWebSocketFrame(true, start.Rsv, content);
|
||||
}
|
||||
|
||||
if (start is BinaryWebSocketFrame)
|
||||
{
|
||||
return new BinaryWebSocketFrame(true, start.Rsv, content);
|
||||
}
|
||||
|
||||
// Should not reach here.
|
||||
throw new Exception("Unkonw WebSocketFrame type, must be either TextWebSocketFrame or BinaryWebSocketFrame");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.WebSockets
|
||||
{
|
||||
using System;
|
||||
|
||||
public class WebSocketHandshakeException : Exception
|
||||
{
|
||||
public WebSocketHandshakeException(string message, Exception innereException)
|
||||
: base(message, innereException)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketHandshakeException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketHandshakeException(Exception innerException)
|
||||
: base(null, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public abstract class WebSocketProtocolHandler : MessageToMessageDecoder<WebSocketFrame>
|
||||
{
|
||||
readonly bool dropPongFrames;
|
||||
|
||||
internal WebSocketProtocolHandler() : this(true)
|
||||
{
|
||||
}
|
||||
|
||||
internal WebSocketProtocolHandler(bool dropPongFrames)
|
||||
{
|
||||
this.dropPongFrames = dropPongFrames;
|
||||
}
|
||||
|
||||
protected override void Decode(IChannelHandlerContext ctx, WebSocketFrame frame, List<object> output)
|
||||
{
|
||||
if (frame is PingWebSocketFrame)
|
||||
{
|
||||
frame.Content.Retain();
|
||||
ctx.Channel.WriteAndFlushAsync(new PongWebSocketFrame(frame.Content));
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame is PongWebSocketFrame && this.dropPongFrames)
|
||||
{
|
||||
// Pong frames need to get ignored
|
||||
return;
|
||||
}
|
||||
|
||||
output.Add(frame.Retain());
|
||||
}
|
||||
|
||||
public override void ExceptionCaught(IChannelHandlerContext ctx, Exception cause)
|
||||
{
|
||||
ctx.FireExceptionCaught(cause);
|
||||
ctx.CloseAsync();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 ConvertToAutoProperty
|
||||
// ReSharper disable ConvertToAutoPropertyWhenPossible
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace DotNetty.Codecs.Http.WebSockets
|
||||
{
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public sealed class WebSocketScheme
|
||||
{
|
||||
// Scheme for non-secure WebSocket connection.
|
||||
public static readonly WebSocketScheme WS = new WebSocketScheme(80, "ws");
|
||||
|
||||
// Scheme for secure WebSocket connection.
|
||||
public static readonly WebSocketScheme WSS = new WebSocketScheme(443, "wss");
|
||||
|
||||
readonly int port;
|
||||
readonly AsciiString name;
|
||||
|
||||
WebSocketScheme(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) => obj is WebSocketScheme other
|
||||
&& 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,257 @@
|
|||
// 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
|
||||
// ReSharper disable ConvertToAutoPropertyWithPrivateSetter
|
||||
namespace DotNetty.Codecs.Http.WebSockets
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Common.Concurrency;
|
||||
using DotNetty.Common.Internal;
|
||||
using DotNetty.Common.Internal.Logging;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public abstract class WebSocketServerHandshaker
|
||||
{
|
||||
protected static readonly IInternalLogger Logger = InternalLoggerFactory.GetInstance<WebSocketServerHandshaker>();
|
||||
static readonly ClosedChannelException ClosedChannelException = new ClosedChannelException();
|
||||
|
||||
readonly string uri;
|
||||
|
||||
readonly string[] subprotocols;
|
||||
|
||||
readonly WebSocketVersion version;
|
||||
|
||||
readonly int maxFramePayloadLength;
|
||||
|
||||
string selectedSubprotocol;
|
||||
|
||||
// Use this as wildcard to support all requested sub-protocols
|
||||
public static readonly string SubProtocolWildcard = "*";
|
||||
|
||||
protected WebSocketServerHandshaker(WebSocketVersion version, string uri, string subprotocols, int maxFramePayloadLength)
|
||||
{
|
||||
this.version = version;
|
||||
this.uri = uri;
|
||||
if (subprotocols != null)
|
||||
{
|
||||
string[] subprotocolArray = subprotocols.Split(',');
|
||||
for (int i = 0; i < subprotocolArray.Length; i++)
|
||||
{
|
||||
subprotocolArray[i] = subprotocolArray[i].Trim();
|
||||
}
|
||||
this.subprotocols = subprotocolArray;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.subprotocols = EmptyArrays.EmptyStrings;
|
||||
}
|
||||
this.maxFramePayloadLength = maxFramePayloadLength;
|
||||
}
|
||||
|
||||
public string Uri => this.uri;
|
||||
|
||||
public ISet<string> Subprotocols()
|
||||
{
|
||||
var ret = new HashSet<string>(this.subprotocols);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public WebSocketVersion Version => this. version;
|
||||
|
||||
public int MaxFramePayloadLength => this.maxFramePayloadLength;
|
||||
|
||||
public Task HandshakeAsync(IChannel channel, IFullHttpRequest req) => this.HandshakeAsync(channel, req, null);
|
||||
|
||||
public Task HandshakeAsync(IChannel channel, IFullHttpRequest req, HttpHeaders responseHeaders)
|
||||
{
|
||||
var completion = new TaskCompletionSource();
|
||||
this.Handshake(channel, req, responseHeaders, completion);
|
||||
return completion.Task;
|
||||
}
|
||||
|
||||
public void Handshake(IChannel channel, IFullHttpRequest req, HttpHeaders responseHeaders, TaskCompletionSource completion)
|
||||
{
|
||||
if (Logger.DebugEnabled)
|
||||
{
|
||||
Logger.Debug("{} WebSocket version {} server handshake", channel, this.version);
|
||||
}
|
||||
|
||||
IFullHttpResponse response = this.NewHandshakeResponse(req, responseHeaders);
|
||||
IChannelPipeline p = channel.Pipeline;
|
||||
if (p.Get<HttpObjectAggregator>() != null)
|
||||
{
|
||||
p.Remove<HttpObjectAggregator>();
|
||||
}
|
||||
|
||||
if (p.Get<HttpContentCompressor>() != null)
|
||||
{
|
||||
p.Remove<HttpContentCompressor>();
|
||||
}
|
||||
|
||||
IChannelHandlerContext ctx = p.Context<HttpRequestDecoder>();
|
||||
string encoderName;
|
||||
if (ctx == null)
|
||||
{
|
||||
// this means the user use a HttpServerCodec
|
||||
ctx = p.Context<HttpServerCodec>();
|
||||
if (ctx == null)
|
||||
{
|
||||
completion.TrySetException(new InvalidOperationException("No HttpDecoder and no HttpServerCodec in the pipeline"));
|
||||
return;
|
||||
}
|
||||
|
||||
p.AddBefore(ctx.Name, "wsdecoder", this.NewWebsocketDecoder());
|
||||
p.AddBefore(ctx.Name, "wsencoder", this.NewWebSocketEncoder());
|
||||
encoderName = ctx.Name;
|
||||
}
|
||||
else
|
||||
{
|
||||
p.Replace(ctx.Name, "wsdecoder", this.NewWebsocketDecoder());
|
||||
|
||||
encoderName = p.Context<HttpResponseEncoder>().Name;
|
||||
p.AddBefore(encoderName, "wsencoder", this.NewWebSocketEncoder());
|
||||
}
|
||||
|
||||
channel.WriteAndFlushAsync(response).ContinueWith(t =>
|
||||
{
|
||||
if (t.Status == TaskStatus.RanToCompletion)
|
||||
{
|
||||
p.Remove(encoderName);
|
||||
completion.TryComplete();
|
||||
}
|
||||
else
|
||||
{
|
||||
completion.TrySetException(t.Exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Task HandshakeAsync(IChannel channel, IHttpRequest req, HttpHeaders responseHeaders)
|
||||
{
|
||||
if (req is IFullHttpRequest request)
|
||||
{
|
||||
return this.HandshakeAsync(channel, request, responseHeaders);
|
||||
}
|
||||
if (Logger.DebugEnabled)
|
||||
{
|
||||
Logger.Debug("{} WebSocket version {} server handshake", channel, this.version);
|
||||
}
|
||||
IChannelPipeline p = channel.Pipeline;
|
||||
IChannelHandlerContext ctx = p.Context<HttpRequestDecoder>();
|
||||
if (ctx == null)
|
||||
{
|
||||
// this means the user use a HttpServerCodec
|
||||
ctx = p.Context<HttpServerCodec>();
|
||||
if (ctx == null)
|
||||
{
|
||||
return TaskEx.FromException(new InvalidOperationException("No HttpDecoder and no HttpServerCodec in the pipeline"));
|
||||
}
|
||||
}
|
||||
|
||||
// Add aggregator and ensure we feed the HttpRequest so it is aggregated. A limit o 8192 should be more then
|
||||
// enough for the websockets handshake payload.
|
||||
//
|
||||
// TODO: Make handshake work without HttpObjectAggregator at all.
|
||||
string aggregatorName = "httpAggregator";
|
||||
p.AddAfter(ctx.Name, aggregatorName, new HttpObjectAggregator(8192));
|
||||
var completion = new TaskCompletionSource();
|
||||
p.AddAfter(aggregatorName, "handshaker", new Handshaker(this, channel, responseHeaders, completion));
|
||||
try
|
||||
{
|
||||
ctx.FireChannelRead(ReferenceCountUtil.Retain(req));
|
||||
}
|
||||
catch (Exception cause)
|
||||
{
|
||||
completion.TrySetException(cause);
|
||||
}
|
||||
return completion.Task;
|
||||
}
|
||||
|
||||
sealed class Handshaker : SimpleChannelInboundHandler<IFullHttpRequest>
|
||||
{
|
||||
readonly WebSocketServerHandshaker serverHandshaker;
|
||||
readonly IChannel channel;
|
||||
readonly HttpHeaders responseHeaders;
|
||||
readonly TaskCompletionSource completion;
|
||||
|
||||
public Handshaker(WebSocketServerHandshaker serverHandshaker, IChannel channel, HttpHeaders responseHeaders, TaskCompletionSource completion)
|
||||
{
|
||||
this.serverHandshaker = serverHandshaker;
|
||||
this.channel = channel;
|
||||
this.responseHeaders = responseHeaders;
|
||||
this.completion = completion;
|
||||
}
|
||||
|
||||
protected override void ChannelRead0(IChannelHandlerContext ctx, IFullHttpRequest msg)
|
||||
{
|
||||
// Remove ourself and do the actual handshake
|
||||
ctx.Channel.Pipeline.Remove(this);
|
||||
this.serverHandshaker.Handshake(this.channel, msg, this.responseHeaders, this.completion);
|
||||
}
|
||||
|
||||
public override void ExceptionCaught(IChannelHandlerContext ctx, Exception cause)
|
||||
{
|
||||
// Remove ourself and fail the handshake promise.
|
||||
ctx.Channel.Pipeline.Remove(this);
|
||||
this.completion.TrySetException(cause);
|
||||
ctx.FireExceptionCaught(cause);
|
||||
}
|
||||
|
||||
public override void ChannelInactive(IChannelHandlerContext ctx)
|
||||
{
|
||||
// Fail promise if Channel was closed
|
||||
this.completion.TrySetException(ClosedChannelException);
|
||||
ctx.FireChannelInactive();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract IFullHttpResponse NewHandshakeResponse(IFullHttpRequest req, HttpHeaders responseHeaders);
|
||||
|
||||
public virtual Task CloseAsync(IChannel channel, CloseWebSocketFrame frame)
|
||||
{
|
||||
Contract.Requires(channel != null);
|
||||
|
||||
return channel.WriteAndFlushAsync(frame).ContinueWith((t, s) => ((IChannel)s).CloseAsync(),
|
||||
channel, TaskContinuationOptions.ExecuteSynchronously);
|
||||
}
|
||||
|
||||
protected string SelectSubprotocol(string requestedSubprotocols)
|
||||
{
|
||||
if (requestedSubprotocols == null || this.subprotocols.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string[] requestedSubprotocolArray = requestedSubprotocols.Split(',');
|
||||
foreach (string p in requestedSubprotocolArray)
|
||||
{
|
||||
string requestedSubprotocol = p.Trim();
|
||||
|
||||
foreach (string supportedSubprotocol in this.subprotocols)
|
||||
{
|
||||
if (SubProtocolWildcard.Equals(supportedSubprotocol)
|
||||
|| requestedSubprotocol.Equals(supportedSubprotocol))
|
||||
{
|
||||
this.selectedSubprotocol = requestedSubprotocol;
|
||||
return requestedSubprotocol;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No match found
|
||||
return null;
|
||||
}
|
||||
|
||||
public string SelectedSubprotocol => this.selectedSubprotocol;
|
||||
|
||||
protected internal abstract IWebSocketFrameDecoder NewWebsocketDecoder();
|
||||
|
||||
protected internal abstract IWebSocketFrameEncoder NewWebSocketEncoder();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using System.Diagnostics;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public class WebSocketServerHandshaker00 : WebSocketServerHandshaker
|
||||
{
|
||||
static readonly Regex BeginningDigit = new Regex("[^0-9]", RegexOptions.Compiled);
|
||||
static readonly Regex BeginningSpace = new Regex("[^ ]", RegexOptions.Compiled);
|
||||
|
||||
public WebSocketServerHandshaker00(string webSocketUrl, string subprotocols, int maxFramePayloadLength)
|
||||
: base(WebSocketVersion.V00, webSocketUrl, subprotocols, maxFramePayloadLength)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IFullHttpResponse NewHandshakeResponse(IFullHttpRequest req, HttpHeaders headers)
|
||||
{
|
||||
// Serve the WebSocket handshake request.
|
||||
if (!req.Headers.ContainsValue(HttpHeaderNames.Connection, HttpHeaderValues.Upgrade, true)
|
||||
|| !req.Headers.TryGet(HttpHeaderNames.Upgrade, out ICharSequence value)
|
||||
|| !HttpHeaderValues.Websocket.ContentEqualsIgnoreCase(value))
|
||||
{
|
||||
throw new WebSocketHandshakeException("not a WebSocket handshake request: missing upgrade");
|
||||
}
|
||||
|
||||
// Hixie 75 does not contain these headers while Hixie 76 does
|
||||
bool isHixie76 = req.Headers.Contains(HttpHeaderNames.SecWebsocketKey1)
|
||||
&& req.Headers.Contains(HttpHeaderNames.SecWebsocketKey2);
|
||||
|
||||
// Create the WebSocket handshake response.
|
||||
var res = new DefaultFullHttpResponse(HttpVersion.Http11,
|
||||
new HttpResponseStatus(101, new AsciiString(isHixie76 ? "WebSocket Protocol Handshake" : "Web Socket Protocol Handshake")));
|
||||
if (headers != null)
|
||||
{
|
||||
res.Headers.Add(headers);
|
||||
}
|
||||
|
||||
res.Headers.Add(HttpHeaderNames.Upgrade, HttpHeaderValues.Websocket);
|
||||
res.Headers.Add(HttpHeaderNames.Connection, HttpHeaderValues.Upgrade);
|
||||
|
||||
// Fill in the headers and contents depending on handshake getMethod.
|
||||
if (isHixie76)
|
||||
{
|
||||
// New handshake getMethod with a challenge:
|
||||
value = req.Headers.Get(HttpHeaderNames.Origin, null);
|
||||
Debug.Assert(value != null);
|
||||
res.Headers.Add(HttpHeaderNames.SecWebsocketOrigin, value);
|
||||
res.Headers.Add(HttpHeaderNames.SecWebsocketLocation, this.Uri);
|
||||
|
||||
if (req.Headers.TryGet(HttpHeaderNames.SecWebsocketProtocol, out ICharSequence subprotocols))
|
||||
{
|
||||
string selectedSubprotocol = this.SelectSubprotocol(subprotocols.ToString());
|
||||
if (selectedSubprotocol == null)
|
||||
{
|
||||
if (Logger.DebugEnabled)
|
||||
{
|
||||
Logger.Debug("Requested subprotocol(s) not supported: {}", subprotocols);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
res.Headers.Add(HttpHeaderNames.SecWebsocketProtocol, selectedSubprotocol);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the answer of the challenge.
|
||||
value = req.Headers.Get(HttpHeaderNames.SecWebsocketKey1, null);
|
||||
Debug.Assert(value != null, $"{HttpHeaderNames.SecWebsocketKey1} must exist");
|
||||
string key1 = value.ToString();
|
||||
value = req.Headers.Get(HttpHeaderNames.SecWebsocketKey2, null);
|
||||
Debug.Assert(value != null, $"{HttpHeaderNames.SecWebsocketKey2} must exist");
|
||||
string key2 = value.ToString();
|
||||
int a = (int)(long.Parse(BeginningDigit.Replace(key1, "")) /
|
||||
BeginningSpace.Replace(key1, "").Length);
|
||||
int b = (int)(long.Parse(BeginningDigit.Replace(key2, "")) /
|
||||
BeginningSpace.Replace(key2, "").Length);
|
||||
long c = req.Content.ReadLong();
|
||||
IByteBuffer input = Unpooled.Buffer(16);
|
||||
input.WriteInt(a);
|
||||
input.WriteInt(b);
|
||||
input.WriteLong(c);
|
||||
res.Content.WriteBytes(WebSocketUtil.Md5(input.Array));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Old Hixie 75 handshake getMethod with no challenge:
|
||||
value = req.Headers.Get(HttpHeaderNames.Origin, null);
|
||||
Debug.Assert(value != null);
|
||||
res.Headers.Add(HttpHeaderNames.WebsocketOrigin, value);
|
||||
res.Headers.Add(HttpHeaderNames.WebsocketLocation, this.Uri);
|
||||
|
||||
if (req.Headers.TryGet(HttpHeaderNames.WebsocketProtocol, out ICharSequence protocol))
|
||||
{
|
||||
res.Headers.Add(HttpHeaderNames.WebsocketProtocol, this.SelectSubprotocol(protocol.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public override Task CloseAsync(IChannel channel, CloseWebSocketFrame frame) => channel.WriteAndFlushAsync(frame);
|
||||
|
||||
protected internal override IWebSocketFrameDecoder NewWebsocketDecoder() => new WebSocket00FrameDecoder(this.MaxFramePayloadLength);
|
||||
|
||||
protected internal override IWebSocketFrameEncoder NewWebSocketEncoder() => new WebSocket00FrameEncoder();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using System.Text;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public class WebSocketServerHandshaker07 : WebSocketServerHandshaker
|
||||
{
|
||||
public static readonly string Websocket07AcceptGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
|
||||
readonly bool allowExtensions;
|
||||
readonly bool allowMaskMismatch;
|
||||
|
||||
public WebSocketServerHandshaker07(string webSocketUrl, string subprotocols, bool allowExtensions, int maxFramePayloadLength)
|
||||
: this(webSocketUrl, subprotocols, allowExtensions, maxFramePayloadLength, false)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketServerHandshaker07(string webSocketUrl, string subprotocols, bool allowExtensions, int maxFramePayloadLength,
|
||||
bool allowMaskMismatch)
|
||||
: base(WebSocketVersion.V07, webSocketUrl, subprotocols, maxFramePayloadLength)
|
||||
{
|
||||
this.allowExtensions = allowExtensions;
|
||||
this.allowMaskMismatch = allowMaskMismatch;
|
||||
}
|
||||
|
||||
protected override IFullHttpResponse NewHandshakeResponse(IFullHttpRequest req, HttpHeaders headers)
|
||||
{
|
||||
var res = new DefaultFullHttpResponse(HttpVersion.Http11, HttpResponseStatus.SwitchingProtocols);
|
||||
|
||||
if (headers != null)
|
||||
{
|
||||
res.Headers.Add(headers);
|
||||
}
|
||||
|
||||
if (!req.Headers.TryGet(HttpHeaderNames.SecWebsocketKey, out ICharSequence key)
|
||||
|| key == null)
|
||||
{
|
||||
throw new WebSocketHandshakeException("not a WebSocket request: missing key");
|
||||
}
|
||||
string acceptSeed = key + Websocket07AcceptGuid;
|
||||
byte[] sha1 = WebSocketUtil.Sha1(Encoding.ASCII.GetBytes(acceptSeed));
|
||||
string accept = WebSocketUtil.Base64String(sha1);
|
||||
|
||||
if (Logger.DebugEnabled)
|
||||
{
|
||||
Logger.Debug("WebSocket version 07 server handshake key: {}, response: {}.", key, accept);
|
||||
}
|
||||
|
||||
res.Headers.Add(HttpHeaderNames.Upgrade, HttpHeaderValues.Websocket);
|
||||
res.Headers.Add(HttpHeaderNames.Connection, HttpHeaderValues.Upgrade);
|
||||
res.Headers.Add(HttpHeaderNames.SecWebsocketAccept, accept);
|
||||
|
||||
|
||||
if (req.Headers.TryGet(HttpHeaderNames.SecWebsocketProtocol, out ICharSequence subprotocols)
|
||||
&& subprotocols != null)
|
||||
{
|
||||
string selectedSubprotocol = this.SelectSubprotocol(subprotocols.ToString());
|
||||
if (selectedSubprotocol == null)
|
||||
{
|
||||
if (Logger.DebugEnabled)
|
||||
{
|
||||
Logger.Debug("Requested subprotocol(s) not supported: {}", subprotocols);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
res.Headers.Add(HttpHeaderNames.SecWebsocketProtocol, selectedSubprotocol);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
protected internal override IWebSocketFrameDecoder NewWebsocketDecoder() => new WebSocket07FrameDecoder(
|
||||
true, this.allowExtensions, this.MaxFramePayloadLength, this.allowMaskMismatch);
|
||||
|
||||
protected internal override IWebSocketFrameEncoder NewWebSocketEncoder() => new WebSocket07FrameEncoder(false);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using System.Text;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public class WebSocketServerHandshaker08 : WebSocketServerHandshaker
|
||||
{
|
||||
public static readonly string Websocket08AcceptGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
|
||||
readonly bool allowExtensions;
|
||||
readonly bool allowMaskMismatch;
|
||||
|
||||
public WebSocketServerHandshaker08(string webSocketUrl, string subprotocols, bool allowExtensions, int maxFramePayloadLength)
|
||||
: this(webSocketUrl, subprotocols, allowExtensions, maxFramePayloadLength, false)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketServerHandshaker08(string webSocketUrl, string subprotocols, bool allowExtensions, int maxFramePayloadLength,
|
||||
bool allowMaskMismatch)
|
||||
: base(WebSocketVersion.V08, webSocketUrl, subprotocols, maxFramePayloadLength)
|
||||
{
|
||||
this.allowExtensions = allowExtensions;
|
||||
this.allowMaskMismatch = allowMaskMismatch;
|
||||
}
|
||||
|
||||
protected override IFullHttpResponse NewHandshakeResponse(IFullHttpRequest req, HttpHeaders headers)
|
||||
{
|
||||
var res = new DefaultFullHttpResponse(HttpVersion.Http11, HttpResponseStatus.SwitchingProtocols);
|
||||
|
||||
if (headers != null)
|
||||
{
|
||||
res.Headers.Add(headers);
|
||||
}
|
||||
|
||||
if (!req.Headers.TryGet(HttpHeaderNames.SecWebsocketKey, out ICharSequence key)
|
||||
|| key == null)
|
||||
{
|
||||
throw new WebSocketHandshakeException("not a WebSocket request: missing key");
|
||||
}
|
||||
string acceptSeed = key + Websocket08AcceptGuid;
|
||||
byte[] sha1 = WebSocketUtil.Sha1(Encoding.ASCII.GetBytes(acceptSeed));
|
||||
string accept = WebSocketUtil.Base64String(sha1);
|
||||
|
||||
if (Logger.DebugEnabled)
|
||||
{
|
||||
Logger.Debug("WebSocket version 08 server handshake key: {}, response: {}", key, accept);
|
||||
}
|
||||
|
||||
res.Headers.Add(HttpHeaderNames.Upgrade, HttpHeaderValues.Websocket);
|
||||
res.Headers.Add(HttpHeaderNames.Connection, HttpHeaderValues.Upgrade);
|
||||
res.Headers.Add(HttpHeaderNames.SecWebsocketAccept, accept);
|
||||
|
||||
if (req.Headers.TryGet(HttpHeaderNames.SecWebsocketProtocol, out ICharSequence subprotocols)
|
||||
&& subprotocols != null)
|
||||
{
|
||||
string selectedSubprotocol = this.SelectSubprotocol(subprotocols.ToString());
|
||||
if (selectedSubprotocol == null)
|
||||
{
|
||||
if (Logger.DebugEnabled)
|
||||
{
|
||||
Logger.Debug("Requested subprotocol(s) not supported: {}", subprotocols);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
res.Headers.Add(HttpHeaderNames.SecWebsocketProtocol, selectedSubprotocol);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
protected internal override IWebSocketFrameDecoder NewWebsocketDecoder() => new WebSocket08FrameDecoder(
|
||||
true, this.allowExtensions, this.MaxFramePayloadLength, this.allowMaskMismatch);
|
||||
|
||||
protected internal override IWebSocketFrameEncoder NewWebSocketEncoder() => new WebSocket08FrameEncoder(false);
|
||||
}
|
||||
}
|
|
@ -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.WebSockets
|
||||
{
|
||||
using System.Text;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public class WebSocketServerHandshaker13 : WebSocketServerHandshaker
|
||||
{
|
||||
public static readonly string Websocket13AcceptGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
|
||||
readonly bool allowExtensions;
|
||||
readonly bool allowMaskMismatch;
|
||||
|
||||
public WebSocketServerHandshaker13(string webSocketUrl, string subprotocols, bool allowExtensions, int maxFramePayloadLength)
|
||||
: this(webSocketUrl, subprotocols, allowExtensions, maxFramePayloadLength, false)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketServerHandshaker13(string webSocketUrl, string subprotocols, bool allowExtensions, int maxFramePayloadLength,
|
||||
bool allowMaskMismatch)
|
||||
: base(WebSocketVersion.V13, webSocketUrl, subprotocols, maxFramePayloadLength)
|
||||
{
|
||||
this.allowExtensions = allowExtensions;
|
||||
this.allowMaskMismatch = allowMaskMismatch;
|
||||
}
|
||||
|
||||
protected override IFullHttpResponse NewHandshakeResponse(IFullHttpRequest req, HttpHeaders headers)
|
||||
{
|
||||
var res = new DefaultFullHttpResponse(HttpVersion.Http11, HttpResponseStatus.SwitchingProtocols);
|
||||
if (headers != null)
|
||||
{
|
||||
res.Headers.Add(headers);
|
||||
}
|
||||
|
||||
if (!req.Headers.TryGet(HttpHeaderNames.SecWebsocketKey, out ICharSequence key)
|
||||
|| key == null)
|
||||
{
|
||||
throw new WebSocketHandshakeException("not a WebSocket request: missing key");
|
||||
}
|
||||
string acceptSeed = key.ToString() + Websocket13AcceptGuid;
|
||||
byte[] sha1 = WebSocketUtil.Sha1(Encoding.ASCII.GetBytes(acceptSeed));
|
||||
string accept = WebSocketUtil.Base64String(sha1);
|
||||
|
||||
if (Logger.DebugEnabled)
|
||||
{
|
||||
Logger.Debug("WebSocket version 13 server handshake key: {}, response: {}", key, accept);
|
||||
}
|
||||
|
||||
res.Headers.Add(HttpHeaderNames.Upgrade, HttpHeaderValues.Websocket);
|
||||
res.Headers.Add(HttpHeaderNames.Connection, HttpHeaderValues.Upgrade);
|
||||
res.Headers.Add(HttpHeaderNames.SecWebsocketAccept, accept);
|
||||
|
||||
if (req.Headers.TryGet(HttpHeaderNames.SecWebsocketProtocol, out ICharSequence subprotocols)
|
||||
&& subprotocols != null)
|
||||
{
|
||||
string selectedSubprotocol = this.SelectSubprotocol(subprotocols.ToString());
|
||||
if (selectedSubprotocol == null)
|
||||
{
|
||||
if (Logger.DebugEnabled)
|
||||
{
|
||||
Logger.Debug("Requested subprotocol(s) not supported: {}", subprotocols);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
res.Headers.Add(HttpHeaderNames.SecWebsocketProtocol, selectedSubprotocol);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
protected internal override IWebSocketFrameDecoder NewWebsocketDecoder() => new WebSocket13FrameDecoder(
|
||||
true, this.allowExtensions, this.MaxFramePayloadLength, this.allowMaskMismatch);
|
||||
|
||||
protected internal override IWebSocketFrameEncoder NewWebSocketEncoder() => new WebSocket13FrameEncoder(false);
|
||||
}
|
||||
}
|
|
@ -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.WebSockets
|
||||
{
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
public class WebSocketServerHandshakerFactory
|
||||
{
|
||||
readonly string webSocketUrl;
|
||||
|
||||
readonly string subprotocols;
|
||||
|
||||
readonly bool allowExtensions;
|
||||
|
||||
readonly int maxFramePayloadLength;
|
||||
|
||||
readonly bool allowMaskMismatch;
|
||||
|
||||
public WebSocketServerHandshakerFactory(string webSocketUrl, string subprotocols, bool allowExtensions)
|
||||
: this(webSocketUrl, subprotocols, allowExtensions, 65536)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketServerHandshakerFactory(string webSocketUrl, string subprotocols, bool allowExtensions,
|
||||
int maxFramePayloadLength)
|
||||
: this(webSocketUrl, subprotocols, allowExtensions, maxFramePayloadLength, false)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketServerHandshakerFactory(string webSocketUrl, string subprotocols, bool allowExtensions,
|
||||
int maxFramePayloadLength, bool allowMaskMismatch)
|
||||
{
|
||||
this.webSocketUrl = webSocketUrl;
|
||||
this.subprotocols = subprotocols;
|
||||
this.allowExtensions = allowExtensions;
|
||||
this.maxFramePayloadLength = maxFramePayloadLength;
|
||||
this.allowMaskMismatch = allowMaskMismatch;
|
||||
}
|
||||
|
||||
public WebSocketServerHandshaker NewHandshaker(IHttpRequest req)
|
||||
{
|
||||
if (req.Headers.TryGet(HttpHeaderNames.SecWebsocketVersion, out ICharSequence version)
|
||||
&& version != null)
|
||||
{
|
||||
if (version.Equals(WebSocketVersion.V13.ToHttpHeaderValue()))
|
||||
{
|
||||
// Version 13 of the wire protocol - RFC 6455 (version 17 of the draft hybi specification).
|
||||
return new WebSocketServerHandshaker13(
|
||||
this.webSocketUrl,
|
||||
this.subprotocols,
|
||||
this.allowExtensions,
|
||||
this.maxFramePayloadLength,
|
||||
this.allowMaskMismatch);
|
||||
}
|
||||
else if (version.Equals(WebSocketVersion.V08.ToHttpHeaderValue()))
|
||||
{
|
||||
// Version 8 of the wire protocol - version 10 of the draft hybi specification.
|
||||
return new WebSocketServerHandshaker08(
|
||||
this.webSocketUrl,
|
||||
this.subprotocols,
|
||||
this.allowExtensions,
|
||||
this.maxFramePayloadLength,
|
||||
this.allowMaskMismatch);
|
||||
}
|
||||
else if (version.Equals(WebSocketVersion.V07.ToHttpHeaderValue()))
|
||||
{
|
||||
// Version 8 of the wire protocol - version 07 of the draft hybi specification.
|
||||
return new WebSocketServerHandshaker07(
|
||||
this.webSocketUrl,
|
||||
this.subprotocols,
|
||||
this.allowExtensions,
|
||||
this.maxFramePayloadLength,
|
||||
this.allowMaskMismatch);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Assume version 00 where version header was not specified
|
||||
return new WebSocketServerHandshaker00(this.webSocketUrl, this.subprotocols, this.maxFramePayloadLength);
|
||||
}
|
||||
}
|
||||
|
||||
public static Task SendUnsupportedVersionResponse(IChannel channel)
|
||||
{
|
||||
var res = new DefaultFullHttpResponse(
|
||||
HttpVersion.Http11,
|
||||
HttpResponseStatus.UpgradeRequired);
|
||||
res.Headers.Set(HttpHeaderNames.SecWebsocketVersion, WebSocketVersion.V13.ToHttpHeaderValue());
|
||||
HttpUtil.SetContentLength(res, 0);
|
||||
return channel.WriteAndFlushAsync(res);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
using static HttpVersion;
|
||||
|
||||
public class WebSocketServerProtocolHandler : WebSocketProtocolHandler
|
||||
{
|
||||
public sealed class HandshakeComplete
|
||||
{
|
||||
readonly string requestUri;
|
||||
readonly HttpHeaders requestHeaders;
|
||||
readonly string selectedSubprotocol;
|
||||
|
||||
internal HandshakeComplete(string requestUri, HttpHeaders requestHeaders, string selectedSubprotocol)
|
||||
{
|
||||
this.requestUri = requestUri;
|
||||
this.requestHeaders = requestHeaders;
|
||||
this.selectedSubprotocol = selectedSubprotocol;
|
||||
}
|
||||
|
||||
public string RequestUri => this.requestUri;
|
||||
|
||||
public HttpHeaders RequestHeaders => this.requestHeaders;
|
||||
|
||||
public string SelectedSubprotocol => this.selectedSubprotocol;
|
||||
}
|
||||
|
||||
static readonly AttributeKey<WebSocketServerHandshaker> HandshakerAttrKey =
|
||||
AttributeKey<WebSocketServerHandshaker>.ValueOf("HANDSHAKER");
|
||||
|
||||
readonly string websocketPath;
|
||||
readonly string subprotocols;
|
||||
readonly bool allowExtensions;
|
||||
readonly int maxFramePayloadLength;
|
||||
readonly bool allowMaskMismatch;
|
||||
readonly bool checkStartsWith;
|
||||
|
||||
public WebSocketServerProtocolHandler(string websocketPath)
|
||||
: this(websocketPath, null, false)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketServerProtocolHandler(string websocketPath, bool checkStartsWith)
|
||||
: this(websocketPath, null, false, 65536, false, checkStartsWith)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketServerProtocolHandler(string websocketPath, string subprotocols)
|
||||
: this(websocketPath, subprotocols, false)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketServerProtocolHandler(string websocketPath, string subprotocols, bool allowExtensions)
|
||||
: this(websocketPath, subprotocols, allowExtensions, 65536)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketServerProtocolHandler(string websocketPath, string subprotocols,
|
||||
bool allowExtensions, int maxFrameSize)
|
||||
: this(websocketPath, subprotocols, allowExtensions, maxFrameSize, false)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketServerProtocolHandler(string websocketPath, string subprotocols,
|
||||
bool allowExtensions, int maxFrameSize, bool allowMaskMismatch)
|
||||
: this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, false)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketServerProtocolHandler(string websocketPath, string subprotocols, bool allowExtensions,
|
||||
int maxFrameSize, bool allowMaskMismatch, bool checkStartsWith)
|
||||
: this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, checkStartsWith, true)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketServerProtocolHandler(string websocketPath, string subprotocols,
|
||||
bool allowExtensions, int maxFrameSize, bool allowMaskMismatch, bool checkStartsWith, bool dropPongFrames)
|
||||
: base(dropPongFrames)
|
||||
{
|
||||
this.websocketPath = websocketPath;
|
||||
this.subprotocols = subprotocols;
|
||||
this.allowExtensions = allowExtensions;
|
||||
this.maxFramePayloadLength = maxFrameSize;
|
||||
this.allowMaskMismatch = allowMaskMismatch;
|
||||
this.checkStartsWith = checkStartsWith;
|
||||
}
|
||||
|
||||
public override void HandlerAdded(IChannelHandlerContext ctx)
|
||||
{
|
||||
IChannelPipeline cp = ctx.Channel.Pipeline;
|
||||
if (cp.Get<WebSocketServerProtocolHandshakeHandler>() == null)
|
||||
{
|
||||
// Add the WebSocketHandshakeHandler before this one.
|
||||
ctx.Channel.Pipeline.AddBefore(ctx.Name, nameof(WebSocketServerProtocolHandshakeHandler),
|
||||
new WebSocketServerProtocolHandshakeHandler(
|
||||
this.websocketPath,
|
||||
this.subprotocols,
|
||||
this.allowExtensions,
|
||||
this.maxFramePayloadLength,
|
||||
this.allowMaskMismatch,
|
||||
this.checkStartsWith));
|
||||
}
|
||||
|
||||
if (cp.Get<Utf8FrameValidator>() == null)
|
||||
{
|
||||
// Add the UFT8 checking before this one.
|
||||
ctx.Channel.Pipeline.AddBefore(ctx.Name, nameof(Utf8FrameValidator), new Utf8FrameValidator());
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Decode(IChannelHandlerContext ctx, WebSocketFrame frame, List<object> output)
|
||||
{
|
||||
if (frame is CloseWebSocketFrame socketFrame)
|
||||
{
|
||||
WebSocketServerHandshaker handshaker = GetHandshaker(ctx.Channel);
|
||||
if (handshaker != null)
|
||||
{
|
||||
frame.Retain();
|
||||
handshaker.CloseAsync(ctx.Channel, socketFrame);
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx.WriteAndFlushAsync(Unpooled.Empty)
|
||||
.ContinueWith((t, c) => ((IChannelHandlerContext)c).CloseAsync(),
|
||||
ctx, TaskContinuationOptions.ExecuteSynchronously);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
base.Decode(ctx, frame, output);
|
||||
}
|
||||
|
||||
public override void ExceptionCaught(IChannelHandlerContext ctx, Exception cause)
|
||||
{
|
||||
if (cause is WebSocketHandshakeException)
|
||||
{
|
||||
var response = new DefaultFullHttpResponse(Http11, HttpResponseStatus.BadRequest,
|
||||
Unpooled.WrappedBuffer(Encoding.ASCII.GetBytes(cause.Message)));
|
||||
ctx.Channel.WriteAndFlushAsync(response)
|
||||
.ContinueWith((t, c) => ((IChannelHandlerContext)c).CloseAsync(),
|
||||
ctx, TaskContinuationOptions.ExecuteSynchronously);
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx.FireExceptionCaught(cause);
|
||||
ctx.CloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
internal static WebSocketServerHandshaker GetHandshaker(IChannel channel) => channel.GetAttribute(HandshakerAttrKey).Get();
|
||||
|
||||
internal static void SetHandshaker(IChannel channel, WebSocketServerHandshaker handshaker) => channel.GetAttribute(HandshakerAttrKey).Set(handshaker);
|
||||
|
||||
internal static IChannelHandler ForbiddenHttpRequestResponder() => new ForbiddenResponseHandler();
|
||||
|
||||
sealed class ForbiddenResponseHandler : ChannelHandlerAdapter
|
||||
{
|
||||
public override void ChannelRead(IChannelHandlerContext ctx, object msg)
|
||||
{
|
||||
if (msg is IFullHttpRequest request)
|
||||
{
|
||||
request.Release();
|
||||
var response = new DefaultFullHttpResponse(Http11, HttpResponseStatus.Forbidden);
|
||||
ctx.Channel.WriteAndFlushAsync(response);
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx.FireChannelRead(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Handlers.Tls;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
using static HttpUtil;
|
||||
using static HttpMethod;
|
||||
using static HttpVersion;
|
||||
using static HttpResponseStatus;
|
||||
|
||||
class WebSocketServerProtocolHandshakeHandler : ChannelHandlerAdapter
|
||||
{
|
||||
readonly string websocketPath;
|
||||
readonly string subprotocols;
|
||||
readonly bool allowExtensions;
|
||||
readonly int maxFramePayloadSize;
|
||||
readonly bool allowMaskMismatch;
|
||||
readonly bool checkStartsWith;
|
||||
|
||||
internal WebSocketServerProtocolHandshakeHandler(string websocketPath, string subprotocols,
|
||||
bool allowExtensions, int maxFrameSize, bool allowMaskMismatch)
|
||||
: this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, false)
|
||||
{
|
||||
}
|
||||
|
||||
internal WebSocketServerProtocolHandshakeHandler(string websocketPath, string subprotocols,
|
||||
bool allowExtensions, int maxFrameSize, bool allowMaskMismatch, bool checkStartsWith)
|
||||
{
|
||||
this.websocketPath = websocketPath;
|
||||
this.subprotocols = subprotocols;
|
||||
this.allowExtensions = allowExtensions;
|
||||
this.maxFramePayloadSize = maxFrameSize;
|
||||
this.allowMaskMismatch = allowMaskMismatch;
|
||||
this.checkStartsWith = checkStartsWith;
|
||||
}
|
||||
|
||||
public override void ChannelRead(IChannelHandlerContext ctx, object msg)
|
||||
{
|
||||
var req = (IFullHttpRequest)msg;
|
||||
if (this.IsNotWebSocketPath(req))
|
||||
{
|
||||
ctx.FireChannelRead(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!Equals(req.Method, Get))
|
||||
{
|
||||
SendHttpResponse(ctx, req, new DefaultFullHttpResponse(Http11, Forbidden));
|
||||
return;
|
||||
}
|
||||
|
||||
var wsFactory = new WebSocketServerHandshakerFactory(
|
||||
GetWebSocketLocation(ctx.Channel.Pipeline, req, this.websocketPath), this.subprotocols,
|
||||
this.allowExtensions, this.maxFramePayloadSize, this.allowMaskMismatch);
|
||||
WebSocketServerHandshaker handshaker = wsFactory.NewHandshaker(req);
|
||||
if (handshaker == null)
|
||||
{
|
||||
WebSocketServerHandshakerFactory.SendUnsupportedVersionResponse(ctx.Channel);
|
||||
}
|
||||
else
|
||||
{
|
||||
Task task = handshaker.HandshakeAsync(ctx.Channel, req);
|
||||
task.ContinueWith(t =>
|
||||
{
|
||||
if (t.Status != TaskStatus.RanToCompletion)
|
||||
{
|
||||
ctx.FireExceptionCaught(t.Exception);
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx.FireUserEventTriggered(new WebSocketServerProtocolHandler.HandshakeComplete(
|
||||
req.Uri, req.Headers, handshaker.SelectedSubprotocol));
|
||||
}
|
||||
},
|
||||
TaskContinuationOptions.ExecuteSynchronously);
|
||||
|
||||
WebSocketServerProtocolHandler.SetHandshaker(ctx.Channel, handshaker);
|
||||
ctx.Channel.Pipeline.Replace(this, "WS403Responder",
|
||||
WebSocketServerProtocolHandler.ForbiddenHttpRequestResponder());
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
req.Release();
|
||||
}
|
||||
}
|
||||
|
||||
bool IsNotWebSocketPath(IFullHttpRequest req) => this.checkStartsWith
|
||||
? !req.Uri.StartsWith(this.websocketPath)
|
||||
: !req.Uri.Equals(this.websocketPath);
|
||||
|
||||
static void SendHttpResponse(IChannelHandlerContext ctx, IHttpRequest req, IHttpResponse res)
|
||||
{
|
||||
Task task = ctx.Channel.WriteAndFlushAsync(res);
|
||||
if (!IsKeepAlive(req) || res.Status.Code != 200)
|
||||
{
|
||||
task.ContinueWith((t, c) => ((IChannel)c).CloseAsync(),
|
||||
ctx.Channel, TaskContinuationOptions.ExecuteSynchronously);
|
||||
}
|
||||
}
|
||||
|
||||
static string GetWebSocketLocation(IChannelPipeline cp, IHttpRequest req, string path)
|
||||
{
|
||||
string protocol = "ws";
|
||||
if (cp.Get<TlsHandler>() != null)
|
||||
{
|
||||
// SSL in use so use Secure WebSockets
|
||||
protocol = "wss";
|
||||
}
|
||||
|
||||
string host = null;
|
||||
if (req.Headers.TryGet(HttpHeaderNames.Host, out ICharSequence value))
|
||||
{
|
||||
host = value.ToString();
|
||||
}
|
||||
return $"{protocol}://{host}{path}";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
// 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.WebSockets
|
||||
{
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using DotNetty.Codecs.Base64;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common;
|
||||
using DotNetty.Common.Internal;
|
||||
|
||||
static class WebSocketUtil
|
||||
{
|
||||
static readonly Random Random = PlatformDependent.GetThreadLocalRandom();
|
||||
|
||||
static readonly ThreadLocalMD5 LocalMd5 = new ThreadLocalMD5();
|
||||
|
||||
sealed class ThreadLocalMD5 : FastThreadLocal<MD5>
|
||||
{
|
||||
protected override MD5 GetInitialValue() => MD5.Create();
|
||||
}
|
||||
|
||||
static readonly ThreadLocalSha1 LocalSha1 = new ThreadLocalSha1();
|
||||
|
||||
sealed class ThreadLocalSha1 : FastThreadLocal<SHA1>
|
||||
{
|
||||
protected override SHA1 GetInitialValue() => SHA1.Create();
|
||||
}
|
||||
|
||||
internal static byte[] Md5(byte[] data)
|
||||
{
|
||||
MD5 md5 = LocalMd5.Value;
|
||||
md5.Initialize();
|
||||
return md5.ComputeHash(data);
|
||||
}
|
||||
|
||||
internal static byte[] Sha1(byte[] data)
|
||||
{
|
||||
SHA1 sha1 = LocalSha1.Value;
|
||||
sha1.Initialize();
|
||||
return sha1.ComputeHash(data);
|
||||
}
|
||||
|
||||
internal static string Base64String(byte[] data)
|
||||
{
|
||||
IByteBuffer encodedData = Unpooled.WrappedBuffer(data);
|
||||
IByteBuffer encoded = Base64.Encode(encodedData);
|
||||
string encodedString = encoded.ToString(Encoding.UTF8);
|
||||
encoded.Release();
|
||||
return encodedString;
|
||||
}
|
||||
|
||||
internal static byte[] RandomBytes(int size)
|
||||
{
|
||||
var bytes = new byte[size];
|
||||
Random.NextBytes(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
internal static int RandomNumber(int minimum, int maximum) => unchecked((int)(Random.NextDouble() * maximum + minimum));
|
||||
|
||||
// Math.Random()
|
||||
internal static double RandomNext() => Random.NextDouble();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
// ReSharper disable ParameterOnlyUsedForPreconditionCheck.Local
|
||||
namespace DotNetty.Codecs.Http.WebSockets
|
||||
{
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using DotNetty.Common.Utilities;
|
||||
|
||||
public sealed class WebSocketVersion
|
||||
{
|
||||
public static readonly WebSocketVersion Unknown = new WebSocketVersion("");
|
||||
|
||||
// http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00
|
||||
// draft-ietf-hybi-thewebsocketprotocol- 00.
|
||||
public static readonly WebSocketVersion V00 = new WebSocketVersion("0");
|
||||
|
||||
// http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07
|
||||
// draft-ietf-hybi-thewebsocketprotocol- 07
|
||||
public static readonly WebSocketVersion V07 = new WebSocketVersion("7");
|
||||
|
||||
// http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
|
||||
// draft-ietf-hybi-thewebsocketprotocol- 10
|
||||
public static readonly WebSocketVersion V08 = new WebSocketVersion("8");
|
||||
|
||||
// http://tools.ietf.org/html/rfc6455 This was originally
|
||||
// http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17
|
||||
//draft-ietf-hybi-thewebsocketprotocol- 17>
|
||||
public static readonly WebSocketVersion V13 = new WebSocketVersion("13");
|
||||
|
||||
readonly AsciiString value;
|
||||
|
||||
WebSocketVersion(string value)
|
||||
{
|
||||
this.value = AsciiString.Cached(value);
|
||||
}
|
||||
|
||||
public override string ToString() => this.value.ToString();
|
||||
|
||||
public AsciiString ToHttpHeaderValue()
|
||||
{
|
||||
ThrowIfUnknown(this);
|
||||
return this.value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
static void ThrowIfUnknown(WebSocketVersion webSocketVersion)
|
||||
{
|
||||
if (webSocketVersion == Unknown)
|
||||
{
|
||||
throw new InvalidOperationException("Unknown web socket version");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -363,6 +363,14 @@ namespace DotNetty.Codecs
|
|||
}
|
||||
}
|
||||
|
||||
protected virtual void DecodeLast(IChannelHandlerContext context, IByteBuffer input, List<object> output) => this.Decode(context, input, output);
|
||||
protected virtual void DecodeLast(IChannelHandlerContext context, IByteBuffer input, List<object> output)
|
||||
{
|
||||
if (input.IsReadable())
|
||||
{
|
||||
// Only call decode() if there is something left in the buffer to decode.
|
||||
// See https://github.com/netty/netty/issues/4386
|
||||
this.Decode(context, input, output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -72,6 +72,7 @@ namespace DotNetty.Codecs.Compression
|
|||
|
||||
this.wrapperOverhead = ZlibUtil.WrapperOverhead(wrapper);
|
||||
}
|
||||
|
||||
public JZlibEncoder(byte[] dictionary) : this(6, dictionary)
|
||||
{
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
// 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.Compression
|
||||
{
|
||||
public static class ZlibCodecFactory
|
||||
{
|
||||
public static bool IsSupportingWindowSizeAndMemLevel => true;
|
||||
|
||||
public static ZlibEncoder NewZlibEncoder(int compressionLevel) => new JZlibEncoder(compressionLevel);
|
||||
|
||||
public static ZlibEncoder NewZlibEncoder(ZlibWrapper wrapper) => new JZlibEncoder(wrapper);
|
||||
|
|
|
@ -46,7 +46,11 @@ namespace DotNetty.Codecs.Compression
|
|||
int overhead;
|
||||
switch (wrapper)
|
||||
{
|
||||
case ZlibWrapper.None:
|
||||
overhead = 0;
|
||||
break;
|
||||
case ZlibWrapper.Zlib:
|
||||
case ZlibWrapper.ZlibOrNone:
|
||||
overhead = 2;
|
||||
break;
|
||||
case ZlibWrapper.Gzip:
|
||||
|
|
|
@ -757,6 +757,37 @@ namespace DotNetty.Common.Utilities
|
|||
return new AsciiString(this.value, start, end - start + 1, false);
|
||||
}
|
||||
|
||||
public unsafe bool ContentEquals(string a)
|
||||
{
|
||||
if (a == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (this.stringValue != null)
|
||||
{
|
||||
return this.stringValue.Equals(a);
|
||||
}
|
||||
if (this.length != a.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.length > 0)
|
||||
{
|
||||
fixed (char* p = a)
|
||||
fixed (byte* b = &this.value[this.offset])
|
||||
for (int i = 0; i < this.length; ++i)
|
||||
{
|
||||
if (CharToByte(*(p + i)) != *(b + i) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ContentEquals(ICharSequence a)
|
||||
{
|
||||
if (a == null || a.Count != this.length)
|
||||
|
|
|
@ -0,0 +1,251 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace DotNetty.Common.Utilities
|
||||
{
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
public static class NetUtil
|
||||
{
|
||||
public static string ToSocketAddressString(string host, int port)
|
||||
{
|
||||
string portStr = Convert.ToString(port);
|
||||
return NewSocketAddressStringBuilder(host, portStr,
|
||||
!IsValidIpV6Address(host)).Append(':').Append(portStr).ToString();
|
||||
}
|
||||
|
||||
static StringBuilder NewSocketAddressStringBuilder(string host, string port, bool ipv4)
|
||||
{
|
||||
int hostLen = host.Length;
|
||||
if (ipv4)
|
||||
{
|
||||
// Need to include enough space for hostString:port.
|
||||
return new StringBuilder(hostLen + 1 + port.Length).Append(host);
|
||||
}
|
||||
|
||||
// Need to include enough space for [hostString]:port.
|
||||
var stringBuilder = new StringBuilder(hostLen + 3 + port.Length);
|
||||
if (hostLen > 1 && host[0] == '[' && host[hostLen - 1] == ']')
|
||||
{
|
||||
return stringBuilder.Append(host);
|
||||
}
|
||||
|
||||
return stringBuilder.Append('[').Append(host).Append(']');
|
||||
}
|
||||
|
||||
public static bool IsValidIpV6Address(string ip)
|
||||
{
|
||||
int end = ip.Length;
|
||||
if (end < 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// strip "[]"
|
||||
int start;
|
||||
char c = ip[0];
|
||||
if (c == '[')
|
||||
{
|
||||
end--;
|
||||
if (ip[end] != ']')
|
||||
{
|
||||
// must have a close ]
|
||||
return false;
|
||||
}
|
||||
|
||||
start = 1;
|
||||
c = ip[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
start = 0;
|
||||
}
|
||||
|
||||
int colons;
|
||||
int compressBegin;
|
||||
if (c == ':')
|
||||
{
|
||||
// an IPv6 address can start with "::" or with a number
|
||||
if (ip[start + 1] != ':')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
colons = 2;
|
||||
compressBegin = start;
|
||||
start += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
colons = 0;
|
||||
compressBegin = -1;
|
||||
}
|
||||
|
||||
int wordLen = 0;
|
||||
for (int i = start; i < end; i++)
|
||||
{
|
||||
c = ip[i];
|
||||
if (IsValidHexChar(c))
|
||||
{
|
||||
if (wordLen < 4)
|
||||
{
|
||||
wordLen++;
|
||||
continue;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (c)
|
||||
{
|
||||
case ':':
|
||||
if (colons > 7)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ip[i - 1] == ':')
|
||||
{
|
||||
if (compressBegin >= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
compressBegin = i - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
wordLen = 0;
|
||||
}
|
||||
|
||||
colons++;
|
||||
break;
|
||||
case '.':
|
||||
// case for the last 32-bits represented as IPv4 x:x:x:x:x:x:d.d.d.d
|
||||
|
||||
// check a normal case (6 single colons)
|
||||
if (compressBegin < 0 && colons != 6 ||
|
||||
// a special case ::1:2:3:4:5:d.d.d.d allows 7 colons with an
|
||||
// IPv4 ending, otherwise 7 :'s is bad
|
||||
(colons == 7 && compressBegin >= start || colons > 7))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify this address is of the correct structure to contain an IPv4 address.
|
||||
// It must be IPv4-Mapped or IPv4-Compatible
|
||||
// (see https://tools.ietf.org/html/rfc4291#section-2.5.5).
|
||||
int ipv4Start = i - wordLen;
|
||||
int j = ipv4Start - 2; // index of character before the previous ':'.
|
||||
if (IsValidIPv4MappedChar(ip[j]))
|
||||
{
|
||||
if (!IsValidIPv4MappedChar(ip[j - 1]) ||
|
||||
!IsValidIPv4MappedChar(ip[j - 2]) ||
|
||||
!IsValidIPv4MappedChar(ip[j - 3]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
j -= 5;
|
||||
}
|
||||
|
||||
for (; j >= start; --j)
|
||||
{
|
||||
char tmpChar = ip[j];
|
||||
if (tmpChar != '0' && tmpChar != ':')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 7 - is minimum IPv4 address length
|
||||
int ipv4End = ip.IndexOf('%', ipv4Start + 7);
|
||||
if (ipv4End < 0)
|
||||
{
|
||||
ipv4End = end;
|
||||
}
|
||||
|
||||
return IsValidIpV4Address(ip, ipv4Start, ipv4End);
|
||||
case '%':
|
||||
// strip the interface name/index after the percent sign
|
||||
end = i;
|
||||
goto loop;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
loop:
|
||||
// normal case without compression
|
||||
if (compressBegin < 0)
|
||||
{
|
||||
return colons == 7 && wordLen > 0;
|
||||
}
|
||||
|
||||
return compressBegin + 2 == end ||
|
||||
// 8 colons is valid only if compression in start or end
|
||||
wordLen > 0 && (colons < 8 || compressBegin <= start);
|
||||
}
|
||||
|
||||
// normal case without compression
|
||||
if (compressBegin < 0)
|
||||
{
|
||||
return colons == 7 && wordLen > 0;
|
||||
}
|
||||
|
||||
return compressBegin + 2 == end ||
|
||||
// 8 colons is valid only if compression in start or end
|
||||
wordLen > 0 && (colons < 8 || compressBegin <= start);
|
||||
}
|
||||
|
||||
static bool IsValidIpV4Address(string ip, int from, int toExcluded)
|
||||
{
|
||||
int len = toExcluded - from;
|
||||
int i;
|
||||
return len <= 15 && len >= 7 &&
|
||||
(i = ip.IndexOf('.', from + 1)) > 0 && IsValidIpV4Word(ip, from, i) &&
|
||||
(i = ip.IndexOf('.', from = i + 2)) > 0 && IsValidIpV4Word(ip, from - 1, i) &&
|
||||
(i = ip.IndexOf('.', from = i + 2)) > 0 && IsValidIpV4Word(ip, from - 1, i) &&
|
||||
IsValidIpV4Word(ip, i + 1, toExcluded);
|
||||
}
|
||||
|
||||
static bool IsValidIpV4Word(string word, int from, int toExclusive)
|
||||
{
|
||||
int len = toExclusive - from;
|
||||
char c0, c1, c2;
|
||||
if (len < 1 || len > 3 || (c0 = word[from]) < '0')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (len == 3)
|
||||
{
|
||||
return (c1 = word[from + 1]) >= '0'
|
||||
&& (c2 = word[from + 2]) >= '0'
|
||||
&& (c0 <= '1' && c1 <= '9' && c2 <= '9'
|
||||
|| c0 == '2' && c1 <= '5' && (c2 <= '5' || c1 < '5' && c2 <= '9'));
|
||||
}
|
||||
|
||||
return c0 <= '9' && (len == 1 || IsValidNumericChar(word[from + 1]));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static bool IsValidHexChar(char c)
|
||||
{
|
||||
return c >= '0' && c <= '9' || c >= 'A' && c <= 'F' || c >= 'a' && c <= 'f';
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static bool IsValidNumericChar(char c)
|
||||
{
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static bool IsValidIPv4MappedChar(char c)
|
||||
{
|
||||
return c == 'f' || c == 'F';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -66,7 +66,6 @@ namespace DotNetty.Transport.Channels
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
void CheckAdded()
|
||||
{
|
||||
if (!this.handlerAdded)
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||
<PackageReference Include="Moq" Version="4.7.99" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Multipart\file-01.txt" />
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace DotNetty.Codecs.Http.Tests
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Common.Concurrency;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels;
|
||||
using DotNetty.Transport.Channels.Embedded;
|
||||
using Xunit;
|
||||
|
||||
public class HttpServerUpgradeHandlerTest
|
||||
{
|
||||
sealed class TestUpgradeCodec : HttpServerUpgradeHandler.IUpgradeCodec
|
||||
{
|
||||
public ICollection<AsciiString> RequiredUpgradeHeaders => new List<AsciiString>();
|
||||
|
||||
public bool PrepareUpgradeResponse(IChannelHandlerContext ctx, IFullHttpRequest upgradeRequest, HttpHeaders upgradeHeaders) => true;
|
||||
|
||||
public void UpgradeTo(IChannelHandlerContext ctx, IFullHttpRequest upgradeRequest)
|
||||
{
|
||||
// Ensure that the HttpServerUpgradeHandler is still installed when this is called
|
||||
Assert.Equal(ctx.Channel.Pipeline.Context<HttpServerUpgradeHandler>(), ctx);
|
||||
Assert.NotNull(ctx.Channel.Pipeline.Get<HttpServerUpgradeHandler>());
|
||||
|
||||
// Add a marker handler to signal that the upgrade has happened
|
||||
ctx.Channel.Pipeline.AddAfter(ctx.Name, "marker", new ChannelHandlerAdapter());
|
||||
}
|
||||
}
|
||||
|
||||
sealed class UpgradeFactory : HttpServerUpgradeHandler.IUpgradeCodecFactory
|
||||
{
|
||||
public HttpServerUpgradeHandler.IUpgradeCodec NewUpgradeCodec(ICharSequence protocol) =>
|
||||
new TestUpgradeCodec();
|
||||
}
|
||||
|
||||
sealed class ChannelHandler : ChannelDuplexHandler
|
||||
{
|
||||
// marker boolean to signal that we're in the `channelRead` method
|
||||
bool inReadCall;
|
||||
bool writeUpgradeMessage;
|
||||
bool writeFlushed;
|
||||
|
||||
public override void ChannelRead(IChannelHandlerContext ctx, object msg)
|
||||
{
|
||||
Assert.False(this.inReadCall);
|
||||
Assert.False(this.writeUpgradeMessage);
|
||||
|
||||
this.inReadCall = true;
|
||||
try
|
||||
{
|
||||
base.ChannelRead(ctx, msg);
|
||||
// All in the same call stack, the upgrade codec should receive the message,
|
||||
// written the upgrade response, and upgraded the pipeline.
|
||||
Assert.True(this.writeUpgradeMessage);
|
||||
Assert.False(this.writeFlushed);
|
||||
//Assert.Null(ctx.Channel.Pipeline.Get<HttpServerCodec>());
|
||||
//Assert.NotNull(ctx.Channel.Pipeline.Get("marker"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.inReadCall = false;
|
||||
}
|
||||
}
|
||||
|
||||
public override Task WriteAsync(IChannelHandlerContext ctx, object msg)
|
||||
{
|
||||
// We ensure that we're in the read call and defer the write so we can
|
||||
// make sure the pipeline was reformed irrespective of the flush completing.
|
||||
Assert.True(this.inReadCall);
|
||||
this.writeUpgradeMessage = true;
|
||||
|
||||
var completion = new TaskCompletionSource();
|
||||
ctx.Channel.EventLoop.Execute(() =>
|
||||
{
|
||||
ctx.WriteAsync(msg)
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
if (t.Status == TaskStatus.RanToCompletion)
|
||||
{
|
||||
this.writeFlushed = true;
|
||||
completion.TryComplete();
|
||||
return;
|
||||
}
|
||||
completion.TrySetException(new InvalidOperationException($"Invalid WriteAsync task status {t.Status}"));
|
||||
},
|
||||
TaskContinuationOptions.ExecuteSynchronously);
|
||||
});
|
||||
return completion.Task;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpgradesPipelineInSameMethodInvocation()
|
||||
{
|
||||
var httpServerCodec = new HttpServerCodec();
|
||||
var factory = new UpgradeFactory();
|
||||
var testInStackFrame = new ChannelHandler();
|
||||
|
||||
var upgradeHandler = new HttpServerUpgradeHandler(httpServerCodec, factory);
|
||||
var channel = new EmbeddedChannel(testInStackFrame, httpServerCodec, upgradeHandler);
|
||||
|
||||
const string UpgradeString = "GET / HTTP/1.1\r\n" +
|
||||
"Host: example.com\r\n" +
|
||||
"Connection: Upgrade, HTTP2-Settings\r\n" +
|
||||
"Upgrade: nextprotocol\r\n" +
|
||||
"HTTP2-Settings: AAMAAABkAAQAAP__\r\n\r\n";
|
||||
IByteBuffer upgrade = Unpooled.CopiedBuffer(Encoding.ASCII.GetBytes(UpgradeString));
|
||||
|
||||
Assert.False(channel.WriteInbound(upgrade));
|
||||
//Assert.Null(channel.Pipeline.Get<HttpServerCodec>());
|
||||
//Assert.NotNull(channel.Pipeline.Get("marker"));
|
||||
|
||||
channel.Flush();
|
||||
Assert.Null(channel.Pipeline.Get<HttpServerCodec>());
|
||||
Assert.NotNull(channel.Pipeline.Get("marker"));
|
||||
|
||||
var upgradeMessage = channel.ReadOutbound<IByteBuffer>();
|
||||
const string ExpectedHttpResponse = "HTTP/1.1 101 Switching Protocols\r\n" +
|
||||
"connection: upgrade\r\n" +
|
||||
"upgrade: nextprotocol\r\n\r\n";
|
||||
Assert.Equal(ExpectedHttpResponse, upgradeMessage.ToString(Encoding.ASCII));
|
||||
Assert.True(upgradeMessage.Release());
|
||||
Assert.False(channel.FinishAndReleaseAll());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
||||
namespace DotNetty.Codecs.Http.Tests.WebSockets.Extensions.Compression
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions.Compression;
|
||||
using Xunit;
|
||||
|
||||
using static Http.WebSockets.Extensions.Compression.DeflateFrameServerExtensionHandshaker;
|
||||
|
||||
public sealed class DeflateFrameClientExtensionHandshakerTest
|
||||
{
|
||||
[Fact]
|
||||
public void WebkitDeflateFrameData()
|
||||
{
|
||||
var handshaker = new DeflateFrameClientExtensionHandshaker(true);
|
||||
|
||||
WebSocketExtensionData data = handshaker.NewRequestData();
|
||||
|
||||
Assert.Equal(XWebkitDeflateFrameExtension, data.Name);
|
||||
Assert.Empty(data.Parameters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeflateFrameData()
|
||||
{
|
||||
var handshaker = new DeflateFrameClientExtensionHandshaker(false);
|
||||
|
||||
WebSocketExtensionData data = handshaker.NewRequestData();
|
||||
|
||||
Assert.Equal(DeflateFrameExtension, data.Name);
|
||||
Assert.Empty(data.Parameters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalHandshake()
|
||||
{
|
||||
var handshaker = new DeflateFrameClientExtensionHandshaker(false);
|
||||
|
||||
IWebSocketClientExtension extension = handshaker.HandshakeExtension(
|
||||
new WebSocketExtensionData(DeflateFrameExtension, new Dictionary<string, string>()));
|
||||
|
||||
Assert.NotNull(extension);
|
||||
Assert.Equal(WebSocketRsv.Rsv1, extension.Rsv);
|
||||
Assert.IsType<PerFrameDeflateDecoder>(extension.NewExtensionDecoder());
|
||||
Assert.IsType<PerFrameDeflateEncoder>(extension.NewExtensionEncoder());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailedHandshake()
|
||||
{
|
||||
var handshaker = new DeflateFrameClientExtensionHandshaker(false);
|
||||
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
{ "invalid", "12" }
|
||||
};
|
||||
IWebSocketClientExtension extension = handshaker.HandshakeExtension(
|
||||
new WebSocketExtensionData(DeflateFrameExtension, parameters));
|
||||
|
||||
Assert.Null(extension);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.Tests.WebSockets.Extensions.Compression
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions.Compression;
|
||||
using Xunit;
|
||||
|
||||
using static Http.WebSockets.Extensions.Compression.DeflateFrameServerExtensionHandshaker;
|
||||
|
||||
public sealed class DeflateFrameServerExtensionHandshakerTest
|
||||
{
|
||||
[Fact]
|
||||
public void NormalHandshake()
|
||||
{
|
||||
var handshaker = new DeflateFrameServerExtensionHandshaker();
|
||||
IWebSocketServerExtension extension = handshaker.HandshakeExtension(
|
||||
new WebSocketExtensionData(DeflateFrameExtension, new Dictionary<string, string>()));
|
||||
|
||||
Assert.NotNull(extension);
|
||||
Assert.Equal(WebSocketRsv.Rsv1, extension.Rsv);
|
||||
Assert.IsType<PerFrameDeflateDecoder>(extension.NewExtensionDecoder());
|
||||
Assert.IsType<PerFrameDeflateEncoder>(extension.NewExtensionEncoder());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WebkitHandshake()
|
||||
{
|
||||
var handshaker = new DeflateFrameServerExtensionHandshaker();
|
||||
IWebSocketServerExtension extension = handshaker.HandshakeExtension(
|
||||
new WebSocketExtensionData(XWebkitDeflateFrameExtension, new Dictionary<string, string>()));
|
||||
|
||||
Assert.NotNull(extension);
|
||||
Assert.Equal(WebSocketRsv.Rsv1, extension.Rsv);
|
||||
Assert.IsType<PerFrameDeflateDecoder>(extension.NewExtensionDecoder());
|
||||
Assert.IsType<PerFrameDeflateEncoder>(extension.NewExtensionEncoder());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailedHandshake()
|
||||
{
|
||||
var handshaker = new DeflateFrameServerExtensionHandshaker();
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
{ "unknown", "11" }
|
||||
};
|
||||
IWebSocketServerExtension extension = handshaker.HandshakeExtension(
|
||||
new WebSocketExtensionData(DeflateFrameExtension, parameters));
|
||||
|
||||
Assert.Null(extension);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace DotNetty.Codecs.Http.Tests.WebSockets.Extensions.Compression
|
||||
{
|
||||
using System;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Codecs.Compression;
|
||||
using DotNetty.Codecs.Http.WebSockets;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions.Compression;
|
||||
using DotNetty.Transport.Channels.Embedded;
|
||||
using Xunit;
|
||||
|
||||
public sealed class PerFrameDeflateDecoderTest
|
||||
{
|
||||
readonly Random random;
|
||||
|
||||
public PerFrameDeflateDecoderTest()
|
||||
{
|
||||
this.random = new Random();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompressedFrame()
|
||||
{
|
||||
var encoderChannel = new EmbeddedChannel(
|
||||
ZlibCodecFactory.NewZlibEncoder(ZlibWrapper.None, 9, 15, 8));
|
||||
var decoderChannel = new EmbeddedChannel(new PerFrameDeflateDecoder(false));
|
||||
|
||||
var payload = new byte[300];
|
||||
this.random.NextBytes(payload);
|
||||
|
||||
encoderChannel.WriteOutbound(Unpooled.WrappedBuffer(payload));
|
||||
var compressedPayload = encoderChannel.ReadOutbound<IByteBuffer>();
|
||||
|
||||
var compressedFrame = new BinaryWebSocketFrame(true,
|
||||
WebSocketRsv.Rsv1 | WebSocketRsv.Rsv3,
|
||||
compressedPayload.Slice(0, compressedPayload.ReadableBytes - 4));
|
||||
|
||||
decoderChannel.WriteInbound(compressedFrame);
|
||||
var uncompressedFrame = decoderChannel.ReadInbound<BinaryWebSocketFrame>();
|
||||
|
||||
Assert.NotNull(uncompressedFrame);
|
||||
Assert.NotNull(uncompressedFrame.Content);
|
||||
Assert.IsType<BinaryWebSocketFrame>(uncompressedFrame);
|
||||
Assert.Equal(WebSocketRsv.Rsv3, uncompressedFrame.Rsv);
|
||||
Assert.Equal(300, uncompressedFrame.Content.ReadableBytes);
|
||||
|
||||
var finalPayload = new byte[300];
|
||||
uncompressedFrame.Content.ReadBytes(finalPayload);
|
||||
Assert.Equal(payload, finalPayload);
|
||||
uncompressedFrame.Release();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalFrame()
|
||||
{
|
||||
var decoderChannel = new EmbeddedChannel(new PerFrameDeflateDecoder(false));
|
||||
|
||||
var payload = new byte[300];
|
||||
this.random.NextBytes(payload);
|
||||
|
||||
var frame = new BinaryWebSocketFrame(true,
|
||||
WebSocketRsv.Rsv3, Unpooled.WrappedBuffer(payload));
|
||||
|
||||
decoderChannel.WriteInbound(frame);
|
||||
var newFrame = decoderChannel.ReadInbound<BinaryWebSocketFrame>();
|
||||
|
||||
Assert.NotNull(newFrame);
|
||||
Assert.NotNull(newFrame.Content);
|
||||
Assert.IsType<BinaryWebSocketFrame>(newFrame);
|
||||
Assert.Equal(WebSocketRsv.Rsv3, newFrame.Rsv);
|
||||
Assert.Equal(300, newFrame.Content.ReadableBytes);
|
||||
|
||||
var finalPayload = new byte[300];
|
||||
newFrame.Content.ReadBytes(finalPayload);
|
||||
Assert.Equal(payload, finalPayload);
|
||||
newFrame.Release();
|
||||
}
|
||||
|
||||
// See https://github.com/netty/netty/issues/4348
|
||||
[Fact]
|
||||
public void CompressedEmptyFrame()
|
||||
{
|
||||
var encoderChannel = new EmbeddedChannel(
|
||||
ZlibCodecFactory.NewZlibEncoder(ZlibWrapper.None, 9, 15, 8));
|
||||
var decoderChannel = new EmbeddedChannel(new PerFrameDeflateDecoder(false));
|
||||
|
||||
encoderChannel.WriteOutbound(Unpooled.Empty);
|
||||
var compressedPayload = encoderChannel.ReadOutbound<IByteBuffer>();
|
||||
var compressedFrame =
|
||||
new BinaryWebSocketFrame(true, WebSocketRsv.Rsv1 | WebSocketRsv.Rsv3, compressedPayload);
|
||||
|
||||
decoderChannel.WriteInbound(compressedFrame);
|
||||
var uncompressedFrame = decoderChannel.ReadInbound<BinaryWebSocketFrame>();
|
||||
|
||||
Assert.NotNull(uncompressedFrame);
|
||||
Assert.NotNull(uncompressedFrame.Content);
|
||||
Assert.IsType<BinaryWebSocketFrame>(uncompressedFrame);
|
||||
Assert.Equal(WebSocketRsv.Rsv3, uncompressedFrame.Rsv);
|
||||
Assert.Equal(0, uncompressedFrame.Content.ReadableBytes);
|
||||
uncompressedFrame.Release();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace DotNetty.Codecs.Http.Tests.WebSockets.Extensions.Compression
|
||||
{
|
||||
using System;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Codecs.Compression;
|
||||
using DotNetty.Codecs.Http.WebSockets;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions.Compression;
|
||||
using DotNetty.Transport.Channels.Embedded;
|
||||
using Xunit;
|
||||
|
||||
public sealed class PerFrameDeflateEncoderTest
|
||||
{
|
||||
readonly Random random;
|
||||
|
||||
public PerFrameDeflateEncoderTest()
|
||||
{
|
||||
this.random = new Random();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompressedFrame()
|
||||
{
|
||||
var encoderChannel = new EmbeddedChannel(new PerFrameDeflateEncoder(9, 15, false));
|
||||
var decoderChannel = new EmbeddedChannel(
|
||||
ZlibCodecFactory.NewZlibDecoder(ZlibWrapper.None));
|
||||
|
||||
var payload = new byte[300];
|
||||
this.random.NextBytes(payload);
|
||||
var frame = new BinaryWebSocketFrame(true,
|
||||
WebSocketRsv.Rsv3, Unpooled.WrappedBuffer(payload));
|
||||
|
||||
encoderChannel.WriteOutbound(frame);
|
||||
var compressedFrame = encoderChannel.ReadOutbound<BinaryWebSocketFrame>();
|
||||
|
||||
Assert.NotNull(compressedFrame);
|
||||
Assert.NotNull(compressedFrame.Content);
|
||||
Assert.IsType<BinaryWebSocketFrame>(compressedFrame);
|
||||
Assert.Equal(WebSocketRsv.Rsv1 | WebSocketRsv.Rsv3, compressedFrame.Rsv);
|
||||
|
||||
decoderChannel.WriteInbound(compressedFrame.Content);
|
||||
decoderChannel.WriteInbound(DeflateDecoder.FrameTail);
|
||||
var uncompressedPayload = decoderChannel.ReadInbound<IByteBuffer>();
|
||||
Assert.Equal(300, uncompressedPayload.ReadableBytes);
|
||||
|
||||
var finalPayload = new byte[300];
|
||||
uncompressedPayload.ReadBytes(finalPayload);
|
||||
|
||||
Assert.Equal(payload, finalPayload);
|
||||
uncompressedPayload.Release();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AlreadyCompressedFrame()
|
||||
{
|
||||
var encoderChannel = new EmbeddedChannel(new PerFrameDeflateEncoder(9, 15, false));
|
||||
var payload = new byte[300];
|
||||
this.random.NextBytes(payload);
|
||||
|
||||
var frame = new BinaryWebSocketFrame(true,
|
||||
WebSocketRsv.Rsv3 | WebSocketRsv.Rsv1, Unpooled.WrappedBuffer(payload));
|
||||
|
||||
encoderChannel.WriteOutbound(frame);
|
||||
var newFrame = encoderChannel.ReadOutbound<BinaryWebSocketFrame>();
|
||||
|
||||
Assert.NotNull(newFrame);
|
||||
Assert.NotNull(newFrame.Content);
|
||||
Assert.IsType<BinaryWebSocketFrame>(newFrame);
|
||||
Assert.Equal(WebSocketRsv.Rsv3 | WebSocketRsv.Rsv1, newFrame.Rsv);
|
||||
Assert.Equal(300, newFrame.Content.ReadableBytes);
|
||||
|
||||
var finalPayload = new byte[300];
|
||||
newFrame.Content.ReadBytes(finalPayload);
|
||||
Assert.Equal(payload, finalPayload);
|
||||
newFrame.Release();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FramementedFrame()
|
||||
{
|
||||
var encoderChannel = new EmbeddedChannel(new PerFrameDeflateEncoder(9, 15, false));
|
||||
var decoderChannel = new EmbeddedChannel(
|
||||
ZlibCodecFactory.NewZlibDecoder(ZlibWrapper.None));
|
||||
|
||||
var payload1 = new byte[100];
|
||||
this.random.NextBytes(payload1);
|
||||
var payload2 = new byte[100];
|
||||
this.random.NextBytes(payload2);
|
||||
var payload3 = new byte[100];
|
||||
this.random.NextBytes(payload3);
|
||||
|
||||
var frame1 = new BinaryWebSocketFrame(false,
|
||||
WebSocketRsv.Rsv3, Unpooled.WrappedBuffer(payload1));
|
||||
var frame2 = new ContinuationWebSocketFrame(false,
|
||||
WebSocketRsv.Rsv3, Unpooled.WrappedBuffer(payload2));
|
||||
var frame3 = new ContinuationWebSocketFrame(true,
|
||||
WebSocketRsv.Rsv3, Unpooled.WrappedBuffer(payload3));
|
||||
|
||||
encoderChannel.WriteOutbound(frame1);
|
||||
encoderChannel.WriteOutbound(frame2);
|
||||
encoderChannel.WriteOutbound(frame3);
|
||||
var compressedFrame1 = encoderChannel.ReadOutbound<BinaryWebSocketFrame>();
|
||||
var compressedFrame2 = encoderChannel.ReadOutbound<ContinuationWebSocketFrame>();
|
||||
var compressedFrame3 = encoderChannel.ReadOutbound<ContinuationWebSocketFrame>();
|
||||
|
||||
Assert.NotNull(compressedFrame1);
|
||||
Assert.NotNull(compressedFrame2);
|
||||
Assert.NotNull(compressedFrame3);
|
||||
Assert.Equal(WebSocketRsv.Rsv1 | WebSocketRsv.Rsv3, compressedFrame1.Rsv);
|
||||
Assert.Equal(WebSocketRsv.Rsv1 | WebSocketRsv.Rsv3, compressedFrame2.Rsv);
|
||||
Assert.Equal(WebSocketRsv.Rsv1 | WebSocketRsv.Rsv3, compressedFrame3.Rsv);
|
||||
Assert.False(compressedFrame1.IsFinalFragment);
|
||||
Assert.False(compressedFrame2.IsFinalFragment);
|
||||
Assert.True(compressedFrame3.IsFinalFragment);
|
||||
|
||||
decoderChannel.WriteInbound(compressedFrame1.Content);
|
||||
decoderChannel.WriteInbound(Unpooled.WrappedBuffer(DeflateDecoder.FrameTail));
|
||||
var uncompressedPayload1 = decoderChannel.ReadInbound<IByteBuffer>();
|
||||
var finalPayload1 = new byte[100];
|
||||
uncompressedPayload1.ReadBytes(finalPayload1);
|
||||
Assert.Equal(payload1, finalPayload1);
|
||||
uncompressedPayload1.Release();
|
||||
|
||||
decoderChannel.WriteInbound(compressedFrame2.Content);
|
||||
decoderChannel.WriteInbound(Unpooled.WrappedBuffer(DeflateDecoder.FrameTail));
|
||||
var uncompressedPayload2 = decoderChannel.ReadInbound<IByteBuffer>();
|
||||
var finalPayload2 = new byte[100];
|
||||
uncompressedPayload2.ReadBytes(finalPayload2);
|
||||
Assert.Equal(payload2, finalPayload2);
|
||||
uncompressedPayload2.Release();
|
||||
|
||||
decoderChannel.WriteInbound(compressedFrame3.Content);
|
||||
decoderChannel.WriteInbound(Unpooled.WrappedBuffer(DeflateDecoder.FrameTail));
|
||||
var uncompressedPayload3 = decoderChannel.ReadInbound<IByteBuffer>();
|
||||
var finalPayload3 = new byte[100];
|
||||
uncompressedPayload3.ReadBytes(finalPayload3);
|
||||
Assert.Equal(payload3, finalPayload3);
|
||||
uncompressedPayload3.Release();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace DotNetty.Codecs.Http.Tests.WebSockets.Extensions.Compression
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using DotNetty.Codecs.Compression;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions.Compression;
|
||||
using Xunit;
|
||||
|
||||
using static Http.WebSockets.Extensions.Compression.PerMessageDeflateServerExtensionHandshaker;
|
||||
|
||||
public sealed class PerMessageDeflateClientExtensionHandshakerTest
|
||||
{
|
||||
[Fact]
|
||||
public void NormalData()
|
||||
{
|
||||
var handshaker = new PerMessageDeflateClientExtensionHandshaker();
|
||||
WebSocketExtensionData data = handshaker.NewRequestData();
|
||||
|
||||
Assert.Equal(PerMessageDeflateExtension, data.Name);
|
||||
Assert.Equal(ZlibCodecFactory.IsSupportingWindowSizeAndMemLevel ? 1 : 0, data.Parameters.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CustomData()
|
||||
{
|
||||
var handshaker = new PerMessageDeflateClientExtensionHandshaker(6, true, 10, true, true);
|
||||
WebSocketExtensionData data = handshaker.NewRequestData();
|
||||
|
||||
Assert.Equal(PerMessageDeflateExtension, data.Name);
|
||||
Assert.Contains(ClientMaxWindow, data.Parameters.Keys);
|
||||
Assert.Contains(ServerMaxWindow, data.Parameters.Keys);
|
||||
Assert.Equal("10", data.Parameters[ServerMaxWindow]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalHandshake()
|
||||
{
|
||||
var handshaker = new PerMessageDeflateClientExtensionHandshaker();
|
||||
|
||||
IWebSocketClientExtension extension = handshaker.HandshakeExtension(
|
||||
new WebSocketExtensionData(PerMessageDeflateExtension, new Dictionary<string, string>()));
|
||||
|
||||
Assert.NotNull(extension);
|
||||
Assert.Equal(WebSocketRsv.Rsv1, extension.Rsv);
|
||||
Assert.IsType<PerMessageDeflateDecoder>(extension.NewExtensionDecoder());
|
||||
Assert.IsType<PerMessageDeflateEncoder>(extension.NewExtensionEncoder());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CustomHandshake()
|
||||
{
|
||||
var handshaker = new PerMessageDeflateClientExtensionHandshaker(6, true, 10, true, true);
|
||||
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
{ ClientMaxWindow, "12" },
|
||||
{ ServerMaxWindow, "10" },
|
||||
{ ClientNoContext, null },
|
||||
{ ServerNoContext, null }
|
||||
};
|
||||
IWebSocketClientExtension extension = handshaker.HandshakeExtension(
|
||||
new WebSocketExtensionData(PerMessageDeflateExtension, parameters));
|
||||
|
||||
Assert.NotNull(extension);
|
||||
Assert.Equal(WebSocketRsv.Rsv1, extension.Rsv);
|
||||
Assert.IsType<PerMessageDeflateDecoder>(extension.NewExtensionDecoder());
|
||||
Assert.IsType<PerMessageDeflateEncoder>(extension.NewExtensionEncoder());
|
||||
|
||||
parameters = new Dictionary<string, string>
|
||||
{
|
||||
{ ServerMaxWindow, "10" },
|
||||
{ ServerNoContext, null }
|
||||
};
|
||||
extension = handshaker.HandshakeExtension(
|
||||
new WebSocketExtensionData(PerMessageDeflateExtension, parameters));
|
||||
|
||||
Assert.NotNull(extension);
|
||||
Assert.Equal(WebSocketRsv.Rsv1, extension.Rsv);
|
||||
Assert.IsType<PerMessageDeflateDecoder>(extension.NewExtensionDecoder());
|
||||
Assert.IsType<PerMessageDeflateEncoder>(extension.NewExtensionEncoder());
|
||||
|
||||
parameters = new Dictionary<string, string>();
|
||||
extension = handshaker.HandshakeExtension(
|
||||
new WebSocketExtensionData(PerMessageDeflateExtension, parameters));
|
||||
|
||||
Assert.Null(extension);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace DotNetty.Codecs.Http.Tests.WebSockets.Extensions.Compression
|
||||
{
|
||||
using System;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Codecs.Compression;
|
||||
using DotNetty.Codecs.Http.WebSockets;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions.Compression;
|
||||
using DotNetty.Transport.Channels.Embedded;
|
||||
using Xunit;
|
||||
|
||||
public sealed class PerMessageDeflateDecoderTest
|
||||
{
|
||||
readonly Random random;
|
||||
|
||||
public PerMessageDeflateDecoderTest()
|
||||
{
|
||||
this.random = new Random();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompressedFrame()
|
||||
{
|
||||
var encoderChannel = new EmbeddedChannel(
|
||||
ZlibCodecFactory.NewZlibEncoder(ZlibWrapper.None, 9, 15, 8));
|
||||
var decoderChannel = new EmbeddedChannel(new PerMessageDeflateDecoder(false));
|
||||
|
||||
var payload = new byte[300];
|
||||
this.random.NextBytes(payload);
|
||||
|
||||
encoderChannel.WriteOutbound(Unpooled.WrappedBuffer(payload));
|
||||
var compressedPayload = encoderChannel.ReadOutbound<IByteBuffer>();
|
||||
|
||||
var compressedFrame = new BinaryWebSocketFrame(true,
|
||||
WebSocketRsv.Rsv1 | WebSocketRsv.Rsv3,
|
||||
compressedPayload.Slice(0, compressedPayload.ReadableBytes - 4));
|
||||
|
||||
decoderChannel.WriteInbound(compressedFrame);
|
||||
var uncompressedFrame = decoderChannel.ReadInbound<BinaryWebSocketFrame>();
|
||||
|
||||
Assert.NotNull(uncompressedFrame);
|
||||
Assert.NotNull(uncompressedFrame.Content);
|
||||
Assert.IsType<BinaryWebSocketFrame>(uncompressedFrame);
|
||||
Assert.Equal(WebSocketRsv.Rsv3, uncompressedFrame.Rsv);
|
||||
Assert.Equal(300, uncompressedFrame.Content.ReadableBytes);
|
||||
|
||||
var finalPayload = new byte[300];
|
||||
uncompressedFrame.Content.ReadBytes(finalPayload);
|
||||
Assert.Equal(payload, finalPayload);
|
||||
uncompressedFrame.Release();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalFrame()
|
||||
{
|
||||
var decoderChannel = new EmbeddedChannel(new PerMessageDeflateDecoder(false));
|
||||
|
||||
var payload = new byte[300];
|
||||
this.random.NextBytes(payload);
|
||||
|
||||
var frame = new BinaryWebSocketFrame(true,
|
||||
WebSocketRsv.Rsv3, Unpooled.WrappedBuffer(payload));
|
||||
|
||||
decoderChannel.WriteInbound(frame);
|
||||
var newFrame = decoderChannel.ReadInbound<BinaryWebSocketFrame>();
|
||||
|
||||
Assert.NotNull(newFrame);
|
||||
Assert.NotNull(newFrame.Content);
|
||||
Assert.IsType<BinaryWebSocketFrame>(newFrame);
|
||||
Assert.Equal(WebSocketRsv.Rsv3, newFrame.Rsv);
|
||||
Assert.Equal(300, newFrame.Content.ReadableBytes);
|
||||
|
||||
var finalPayload = new byte[300];
|
||||
newFrame.Content.ReadBytes(finalPayload);
|
||||
Assert.Equal(payload, finalPayload);
|
||||
newFrame.Release();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FramementedFrame()
|
||||
{
|
||||
var encoderChannel = new EmbeddedChannel(
|
||||
ZlibCodecFactory.NewZlibEncoder(ZlibWrapper.None, 9, 15, 8));
|
||||
var decoderChannel = new EmbeddedChannel(new PerMessageDeflateDecoder(false));
|
||||
|
||||
var payload = new byte[300];
|
||||
this.random.NextBytes(payload);
|
||||
|
||||
encoderChannel.WriteOutbound(Unpooled.WrappedBuffer(payload));
|
||||
var compressedPayload = encoderChannel.ReadOutbound<IByteBuffer>();
|
||||
compressedPayload = compressedPayload.Slice(0, compressedPayload.ReadableBytes - 4);
|
||||
|
||||
int oneThird = compressedPayload.ReadableBytes / 3;
|
||||
var compressedFrame1 = new BinaryWebSocketFrame(false,
|
||||
WebSocketRsv.Rsv1 | WebSocketRsv.Rsv3,
|
||||
compressedPayload.Slice(0, oneThird));
|
||||
var compressedFrame2 = new ContinuationWebSocketFrame(false,
|
||||
WebSocketRsv.Rsv3, compressedPayload.Slice(oneThird, oneThird));
|
||||
var compressedFrame3 = new ContinuationWebSocketFrame(true,
|
||||
WebSocketRsv.Rsv3, compressedPayload.Slice(oneThird * 2,
|
||||
compressedPayload.ReadableBytes - oneThird * 2));
|
||||
|
||||
decoderChannel.WriteInbound(compressedFrame1.Retain());
|
||||
decoderChannel.WriteInbound(compressedFrame2.Retain());
|
||||
decoderChannel.WriteInbound(compressedFrame3);
|
||||
var uncompressedFrame1 = decoderChannel.ReadInbound<BinaryWebSocketFrame>();
|
||||
var uncompressedFrame2 = decoderChannel.ReadInbound<ContinuationWebSocketFrame>();
|
||||
var uncompressedFrame3 = decoderChannel.ReadInbound<ContinuationWebSocketFrame>();
|
||||
|
||||
Assert.NotNull(uncompressedFrame1);
|
||||
Assert.NotNull(uncompressedFrame2);
|
||||
Assert.NotNull(uncompressedFrame3);
|
||||
Assert.Equal(WebSocketRsv.Rsv3, uncompressedFrame1.Rsv);
|
||||
Assert.Equal(WebSocketRsv.Rsv3, uncompressedFrame2.Rsv);
|
||||
Assert.Equal(WebSocketRsv.Rsv3, uncompressedFrame3.Rsv);
|
||||
|
||||
IByteBuffer finalPayloadWrapped = Unpooled.WrappedBuffer(uncompressedFrame1.Content,
|
||||
uncompressedFrame2.Content, uncompressedFrame3.Content);
|
||||
Assert.Equal(300, finalPayloadWrapped.ReadableBytes);
|
||||
|
||||
var finalPayload = new byte[300];
|
||||
finalPayloadWrapped.ReadBytes(finalPayload);
|
||||
Assert.Equal(payload, finalPayload);
|
||||
finalPayloadWrapped.Release();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultiCompressedPayloadWithinFrame()
|
||||
{
|
||||
var encoderChannel = new EmbeddedChannel(
|
||||
ZlibCodecFactory.NewZlibEncoder(ZlibWrapper.None, 9, 15, 8));
|
||||
var decoderChannel = new EmbeddedChannel(new PerMessageDeflateDecoder(false));
|
||||
|
||||
var payload1 = new byte[100];
|
||||
this.random.NextBytes(payload1);
|
||||
var payload2 = new byte[100];
|
||||
this.random.NextBytes(payload2);
|
||||
|
||||
encoderChannel.WriteOutbound(Unpooled.WrappedBuffer(payload1));
|
||||
var compressedPayload1 = encoderChannel.ReadOutbound<IByteBuffer>();
|
||||
encoderChannel.WriteOutbound(Unpooled.WrappedBuffer(payload2));
|
||||
var compressedPayload2 = encoderChannel.ReadOutbound<IByteBuffer>();
|
||||
|
||||
var compressedFrame = new BinaryWebSocketFrame(true,
|
||||
WebSocketRsv.Rsv1 | WebSocketRsv.Rsv3,
|
||||
Unpooled.WrappedBuffer(
|
||||
compressedPayload1,
|
||||
compressedPayload2.Slice(0, compressedPayload2.ReadableBytes - 4)));
|
||||
|
||||
decoderChannel.WriteInbound(compressedFrame);
|
||||
var uncompressedFrame = decoderChannel.ReadInbound<BinaryWebSocketFrame>();
|
||||
|
||||
Assert.NotNull(uncompressedFrame);
|
||||
Assert.NotNull(uncompressedFrame.Content);
|
||||
Assert.IsType<BinaryWebSocketFrame>(uncompressedFrame);
|
||||
Assert.Equal(WebSocketRsv.Rsv3, uncompressedFrame.Rsv);
|
||||
Assert.Equal(200, uncompressedFrame.Content.ReadableBytes);
|
||||
|
||||
var finalPayload1 = new byte[100];
|
||||
uncompressedFrame.Content.ReadBytes(finalPayload1);
|
||||
Assert.Equal(payload1, finalPayload1);
|
||||
var finalPayload2 = new byte[100];
|
||||
uncompressedFrame.Content.ReadBytes(finalPayload2);
|
||||
Assert.Equal(payload2, finalPayload2);
|
||||
uncompressedFrame.Release();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace DotNetty.Codecs.Http.Tests.WebSockets.Extensions.Compression
|
||||
{
|
||||
using System;
|
||||
using DotNetty.Buffers;
|
||||
using DotNetty.Codecs.Compression;
|
||||
using DotNetty.Codecs.Http.WebSockets;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions.Compression;
|
||||
using DotNetty.Transport.Channels.Embedded;
|
||||
using Xunit;
|
||||
|
||||
public sealed class PerMessageDeflateEncoderTest
|
||||
{
|
||||
readonly Random random;
|
||||
|
||||
public PerMessageDeflateEncoderTest()
|
||||
{
|
||||
this.random = new Random();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompressedFrame()
|
||||
{
|
||||
var encoderChannel = new EmbeddedChannel(new PerMessageDeflateEncoder(9, 15, false));
|
||||
var decoderChannel = new EmbeddedChannel(
|
||||
ZlibCodecFactory.NewZlibDecoder(ZlibWrapper.None));
|
||||
|
||||
var payload = new byte[300];
|
||||
this.random.NextBytes(payload);
|
||||
var frame = new BinaryWebSocketFrame(true,
|
||||
WebSocketRsv.Rsv3, Unpooled.WrappedBuffer(payload));
|
||||
|
||||
encoderChannel.WriteOutbound(frame);
|
||||
var compressedFrame = encoderChannel.ReadOutbound<BinaryWebSocketFrame>();
|
||||
|
||||
Assert.NotNull(compressedFrame);
|
||||
Assert.NotNull(compressedFrame.Content);
|
||||
Assert.IsType<BinaryWebSocketFrame>(compressedFrame);
|
||||
Assert.Equal(WebSocketRsv.Rsv1 | WebSocketRsv.Rsv3, compressedFrame.Rsv);
|
||||
|
||||
decoderChannel.WriteInbound(compressedFrame.Content);
|
||||
decoderChannel.WriteInbound(DeflateDecoder.FrameTail);
|
||||
var uncompressedPayload = decoderChannel.ReadInbound<IByteBuffer>();
|
||||
Assert.Equal(300, uncompressedPayload.ReadableBytes);
|
||||
|
||||
var finalPayload = new byte[300];
|
||||
uncompressedPayload.ReadBytes(finalPayload);
|
||||
Assert.Equal(payload, finalPayload);
|
||||
uncompressedPayload.Release();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AlreadyCompressedFrame()
|
||||
{
|
||||
var encoderChannel = new EmbeddedChannel(new PerMessageDeflateEncoder(9, 15, false));
|
||||
|
||||
var payload = new byte[300];
|
||||
this.random.NextBytes(payload);
|
||||
|
||||
var frame = new BinaryWebSocketFrame(true,
|
||||
WebSocketRsv.Rsv3 | WebSocketRsv.Rsv1, Unpooled.WrappedBuffer(payload));
|
||||
|
||||
encoderChannel.WriteOutbound(frame);
|
||||
var newFrame = encoderChannel.ReadOutbound<BinaryWebSocketFrame>();
|
||||
|
||||
Assert.NotNull(newFrame);
|
||||
Assert.NotNull(newFrame.Content);
|
||||
Assert.IsType<BinaryWebSocketFrame>(newFrame);
|
||||
Assert.Equal(WebSocketRsv.Rsv3 | WebSocketRsv.Rsv1, newFrame.Rsv);
|
||||
Assert.Equal(300, newFrame.Content.ReadableBytes);
|
||||
|
||||
var finalPayload = new byte[300];
|
||||
newFrame.Content.ReadBytes(finalPayload);
|
||||
Assert.Equal(payload, finalPayload);
|
||||
newFrame.Release();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FramementedFrame()
|
||||
{
|
||||
var encoderChannel = new EmbeddedChannel(new PerMessageDeflateEncoder(9, 15, false));
|
||||
var decoderChannel = new EmbeddedChannel(
|
||||
ZlibCodecFactory.NewZlibDecoder(ZlibWrapper.None));
|
||||
|
||||
var payload1 = new byte[100];
|
||||
this.random.NextBytes(payload1);
|
||||
var payload2 = new byte[100];
|
||||
this.random.NextBytes(payload2);
|
||||
var payload3 = new byte[100];
|
||||
this.random.NextBytes(payload3);
|
||||
|
||||
var frame1 = new BinaryWebSocketFrame(false,
|
||||
WebSocketRsv.Rsv3, Unpooled.WrappedBuffer(payload1));
|
||||
var frame2 = new ContinuationWebSocketFrame(false,
|
||||
WebSocketRsv.Rsv3, Unpooled.WrappedBuffer(payload2));
|
||||
var frame3 = new ContinuationWebSocketFrame(true,
|
||||
WebSocketRsv.Rsv3, Unpooled.WrappedBuffer(payload3));
|
||||
|
||||
encoderChannel.WriteOutbound(frame1);
|
||||
encoderChannel.WriteOutbound(frame2);
|
||||
encoderChannel.WriteOutbound(frame3);
|
||||
var compressedFrame1 = encoderChannel.ReadOutbound<BinaryWebSocketFrame>();
|
||||
var compressedFrame2 = encoderChannel.ReadOutbound<ContinuationWebSocketFrame>();
|
||||
var compressedFrame3 = encoderChannel.ReadOutbound<ContinuationWebSocketFrame>();
|
||||
|
||||
Assert.NotNull(compressedFrame1);
|
||||
Assert.NotNull(compressedFrame2);
|
||||
Assert.NotNull(compressedFrame3);
|
||||
Assert.Equal(WebSocketRsv.Rsv1 | WebSocketRsv.Rsv3, compressedFrame1.Rsv);
|
||||
Assert.Equal(WebSocketRsv.Rsv3, compressedFrame2.Rsv);
|
||||
Assert.Equal(WebSocketRsv.Rsv3, compressedFrame3.Rsv);
|
||||
Assert.False(compressedFrame1.IsFinalFragment);
|
||||
Assert.False(compressedFrame2.IsFinalFragment);
|
||||
Assert.True(compressedFrame3.IsFinalFragment);
|
||||
|
||||
decoderChannel.WriteInbound(compressedFrame1.Content);
|
||||
var uncompressedPayload1 = decoderChannel.ReadInbound<IByteBuffer>();
|
||||
var finalPayload1 = new byte[100];
|
||||
uncompressedPayload1.ReadBytes(finalPayload1);
|
||||
Assert.Equal(payload1, finalPayload1);
|
||||
uncompressedPayload1.Release();
|
||||
|
||||
decoderChannel.WriteInbound(compressedFrame2.Content);
|
||||
var uncompressedPayload2 = decoderChannel.ReadInbound<IByteBuffer>();
|
||||
var finalPayload2 = new byte[100];
|
||||
uncompressedPayload2.ReadBytes(finalPayload2);
|
||||
Assert.Equal(payload2, finalPayload2);
|
||||
uncompressedPayload2.Release();
|
||||
|
||||
decoderChannel.WriteInbound(compressedFrame3.Content);
|
||||
decoderChannel.WriteInbound(DeflateDecoder.FrameTail);
|
||||
var uncompressedPayload3 = decoderChannel.ReadInbound<IByteBuffer>();
|
||||
var finalPayload3 = new byte[100];
|
||||
uncompressedPayload3.ReadBytes(finalPayload3);
|
||||
Assert.Equal(payload3, finalPayload3);
|
||||
uncompressedPayload3.Release();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace DotNetty.Codecs.Http.Tests.WebSockets.Extensions.Compression
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions.Compression;
|
||||
using Xunit;
|
||||
|
||||
using static Http.WebSockets.Extensions.Compression.PerMessageDeflateServerExtensionHandshaker;
|
||||
|
||||
public sealed class PerMessageDeflateServerExtensionHandshakerTest
|
||||
{
|
||||
[Fact]
|
||||
public void NormalHandshake()
|
||||
{
|
||||
var handshaker = new PerMessageDeflateServerExtensionHandshaker();
|
||||
IWebSocketServerExtension extension = handshaker.HandshakeExtension(
|
||||
new WebSocketExtensionData(PerMessageDeflateExtension, new Dictionary<string, string>()));
|
||||
|
||||
Assert.NotNull(extension);
|
||||
Assert.Equal(WebSocketRsv.Rsv1, extension.Rsv);
|
||||
Assert.IsType<PerMessageDeflateDecoder>(extension.NewExtensionDecoder());
|
||||
Assert.IsType<PerMessageDeflateEncoder>(extension.NewExtensionEncoder());
|
||||
|
||||
WebSocketExtensionData data = extension.NewReponseData();
|
||||
|
||||
Assert.Equal(PerMessageDeflateExtension, data.Name);
|
||||
Assert.Empty(data.Parameters);
|
||||
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
{ ClientMaxWindow, null },
|
||||
{ ClientNoContext, null }
|
||||
};
|
||||
|
||||
extension = handshaker.HandshakeExtension(
|
||||
new WebSocketExtensionData(PerMessageDeflateExtension, parameters));
|
||||
|
||||
Assert.NotNull(extension);
|
||||
Assert.Equal(WebSocketRsv.Rsv1, extension.Rsv);
|
||||
Assert.IsType<PerMessageDeflateDecoder>(extension.NewExtensionDecoder());
|
||||
Assert.IsType<PerMessageDeflateEncoder>(extension.NewExtensionEncoder());
|
||||
|
||||
data = extension.NewReponseData();
|
||||
|
||||
Assert.Equal(PerMessageDeflateExtension, data.Name);
|
||||
Assert.Empty(data.Parameters);
|
||||
|
||||
parameters = new Dictionary<string, string>
|
||||
{
|
||||
{ ServerMaxWindow, "12" },
|
||||
{ ServerNoContext, null }
|
||||
};
|
||||
|
||||
extension = handshaker.HandshakeExtension(
|
||||
new WebSocketExtensionData(PerMessageDeflateExtension, parameters));
|
||||
Assert.Null(extension);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CustomHandshake()
|
||||
{
|
||||
var handshaker = new PerMessageDeflateServerExtensionHandshaker(6, true, 10, true, true);
|
||||
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
{ ClientMaxWindow, null },
|
||||
{ ServerMaxWindow, "12" },
|
||||
{ ClientNoContext, null },
|
||||
{ ServerNoContext, null }
|
||||
};
|
||||
|
||||
IWebSocketServerExtension extension = handshaker.HandshakeExtension(
|
||||
new WebSocketExtensionData(PerMessageDeflateExtension, parameters));
|
||||
|
||||
Assert.NotNull(extension);
|
||||
Assert.Equal(WebSocketRsv.Rsv1, extension.Rsv);
|
||||
Assert.IsType<PerMessageDeflateDecoder>(extension.NewExtensionDecoder());
|
||||
Assert.IsType<PerMessageDeflateEncoder>(extension.NewExtensionEncoder());
|
||||
|
||||
WebSocketExtensionData data = extension.NewReponseData();
|
||||
|
||||
Assert.Equal(PerMessageDeflateExtension, data.Name);
|
||||
Assert.Contains(ClientMaxWindow, data.Parameters.Keys);
|
||||
Assert.Equal("10", data.Parameters[ClientMaxWindow]);
|
||||
Assert.Contains(ServerMaxWindow, data.Parameters.Keys);
|
||||
Assert.Equal("12", data.Parameters[ServerMaxWindow]);
|
||||
|
||||
parameters = new Dictionary<string, string>
|
||||
{
|
||||
{ ServerMaxWindow, "12" },
|
||||
{ ServerNoContext, null }
|
||||
};
|
||||
extension = handshaker.HandshakeExtension(
|
||||
new WebSocketExtensionData(PerMessageDeflateExtension, parameters));
|
||||
|
||||
Assert.NotNull(extension);
|
||||
Assert.Equal(WebSocketRsv.Rsv1, extension.Rsv);
|
||||
Assert.IsType<PerMessageDeflateDecoder>(extension.NewExtensionDecoder());
|
||||
Assert.IsType<PerMessageDeflateEncoder>(extension.NewExtensionEncoder());
|
||||
|
||||
data = extension.NewReponseData();
|
||||
|
||||
Assert.Equal(PerMessageDeflateExtension, data.Name);
|
||||
Assert.Equal(2, data.Parameters.Count);
|
||||
Assert.Contains(ServerMaxWindow, data.Parameters.Keys);
|
||||
Assert.Equal("12", data.Parameters[ServerMaxWindow]);
|
||||
Assert.Contains(ServerNoContext, data.Parameters.Keys);
|
||||
|
||||
parameters = new Dictionary<string, string>();
|
||||
extension = handshaker.HandshakeExtension(
|
||||
new WebSocketExtensionData(PerMessageDeflateExtension, parameters));
|
||||
Assert.NotNull(extension);
|
||||
|
||||
data = extension.NewReponseData();
|
||||
Assert.Equal(PerMessageDeflateExtension, data.Name);
|
||||
Assert.Empty(data.Parameters);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace DotNetty.Codecs.Http.Tests.WebSockets.Extensions.Compression
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions.Compression;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels.Embedded;
|
||||
using Xunit;
|
||||
|
||||
using static Http.WebSockets.Extensions.Compression.PerMessageDeflateServerExtensionHandshaker;
|
||||
using static WebSocketExtensionTestUtil;
|
||||
|
||||
public sealed class WebSocketServerCompressionHandlerTest
|
||||
{
|
||||
[Fact]
|
||||
public void NormalSuccess()
|
||||
{
|
||||
var ch = new EmbeddedChannel(new WebSocketServerCompressionHandler());
|
||||
|
||||
IHttpRequest req = NewUpgradeRequest(PerMessageDeflateExtension);
|
||||
ch.WriteInbound(req);
|
||||
|
||||
IHttpResponse res = NewUpgradeResponse(null);
|
||||
ch.WriteOutbound(res);
|
||||
|
||||
var res2 = ch.ReadOutbound<IHttpResponse>();
|
||||
Assert.True(res2.Headers.TryGet(HttpHeaderNames.SecWebsocketExtensions, out ICharSequence value));
|
||||
List<WebSocketExtensionData> exts = WebSocketExtensionUtil.ExtractExtensions(value.ToString());
|
||||
|
||||
Assert.Equal(PerMessageDeflateExtension, exts[0].Name);
|
||||
Assert.Empty(exts[0].Parameters);
|
||||
Assert.NotNull(ch.Pipeline.Get<PerMessageDeflateDecoder>());
|
||||
Assert.NotNull(ch.Pipeline.Get<PerMessageDeflateEncoder>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClientWindowSizeSuccess()
|
||||
{
|
||||
var ch = new EmbeddedChannel(
|
||||
new WebSocketServerExtensionHandler(
|
||||
new PerMessageDeflateServerExtensionHandshaker(6, false, 10, false, false)));
|
||||
|
||||
IHttpRequest req = NewUpgradeRequest(PerMessageDeflateExtension + "; " + ClientMaxWindow);
|
||||
ch.WriteInbound(req);
|
||||
|
||||
IHttpResponse res = NewUpgradeResponse(null);
|
||||
ch.WriteOutbound(res);
|
||||
|
||||
var res2 = ch.ReadOutbound<IHttpResponse>();
|
||||
Assert.True(res2.Headers.TryGet(HttpHeaderNames.SecWebsocketExtensions, out ICharSequence value));
|
||||
List<WebSocketExtensionData> exts = WebSocketExtensionUtil.ExtractExtensions(value.ToString());
|
||||
|
||||
Assert.Equal(PerMessageDeflateExtension, exts[0].Name);
|
||||
Assert.Equal("10", exts[0].Parameters[ClientMaxWindow]);
|
||||
Assert.NotNull(ch.Pipeline.Get<PerMessageDeflateDecoder>());
|
||||
Assert.NotNull(ch.Pipeline.Get<PerMessageDeflateEncoder>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClientWindowSizeUnavailable()
|
||||
{
|
||||
var ch = new EmbeddedChannel(
|
||||
new WebSocketServerExtensionHandler(
|
||||
new PerMessageDeflateServerExtensionHandshaker(6, false, 10, false, false)));
|
||||
|
||||
IHttpRequest req = NewUpgradeRequest(PerMessageDeflateExtension);
|
||||
ch.WriteInbound(req);
|
||||
|
||||
IHttpResponse res = NewUpgradeResponse(null);
|
||||
ch.WriteOutbound(res);
|
||||
|
||||
var res2 = ch.ReadOutbound<IHttpResponse>();
|
||||
Assert.True(res2.Headers.TryGet(HttpHeaderNames.SecWebsocketExtensions, out ICharSequence value));
|
||||
List<WebSocketExtensionData> exts = WebSocketExtensionUtil.ExtractExtensions(value.ToString());
|
||||
|
||||
Assert.Equal(PerMessageDeflateExtension, exts[0].Name);
|
||||
Assert.Empty(exts[0].Parameters);
|
||||
Assert.NotNull(ch.Pipeline.Get<PerMessageDeflateDecoder>());
|
||||
Assert.NotNull(ch.Pipeline.Get<PerMessageDeflateEncoder>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServerWindowSizeSuccess()
|
||||
{
|
||||
var ch = new EmbeddedChannel(
|
||||
new WebSocketServerExtensionHandler(
|
||||
new PerMessageDeflateServerExtensionHandshaker(6, true, 15, false, false)));
|
||||
|
||||
IHttpRequest req = NewUpgradeRequest(PerMessageDeflateExtension + "; " + ServerMaxWindow + "=10");
|
||||
ch.WriteInbound(req);
|
||||
|
||||
IHttpResponse res = NewUpgradeResponse(null);
|
||||
ch.WriteOutbound(res);
|
||||
|
||||
var res2 = ch.ReadOutbound<IHttpResponse>();
|
||||
Assert.True(res2.Headers.TryGet(HttpHeaderNames.SecWebsocketExtensions, out ICharSequence value));
|
||||
List<WebSocketExtensionData> exts = WebSocketExtensionUtil.ExtractExtensions(value.ToString());
|
||||
|
||||
Assert.Equal(PerMessageDeflateExtension, exts[0].Name);
|
||||
Assert.Equal("10", exts[0].Parameters[ServerMaxWindow]);
|
||||
Assert.NotNull(ch.Pipeline.Get<PerMessageDeflateDecoder>());
|
||||
Assert.NotNull(ch.Pipeline.Get<PerMessageDeflateEncoder>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServerWindowSizeDisable()
|
||||
{
|
||||
var ch = new EmbeddedChannel(
|
||||
new WebSocketServerExtensionHandler(
|
||||
new PerMessageDeflateServerExtensionHandshaker(6, false, 15, false, false)));
|
||||
|
||||
IHttpRequest req = NewUpgradeRequest(PerMessageDeflateExtension + "; " + ServerMaxWindow + "=10");
|
||||
ch.WriteInbound(req);
|
||||
|
||||
IHttpResponse res = NewUpgradeResponse(null);
|
||||
ch.WriteOutbound(res);
|
||||
|
||||
var res2 = ch.ReadOutbound<IHttpResponse>();
|
||||
|
||||
Assert.False(res2.Headers.Contains(HttpHeaderNames.SecWebsocketExtensions));
|
||||
Assert.Null(ch.Pipeline.Get<PerMessageDeflateDecoder>());
|
||||
Assert.Null(ch.Pipeline.Get<PerMessageDeflateEncoder>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServerNoContext()
|
||||
{
|
||||
var ch = new EmbeddedChannel(new WebSocketServerCompressionHandler());
|
||||
|
||||
IHttpRequest req = NewUpgradeRequest(
|
||||
PerMessageDeflateExtension + "; "
|
||||
+ PerMessageDeflateServerExtensionHandshaker.ServerNoContext);
|
||||
ch.WriteInbound(req);
|
||||
|
||||
IHttpResponse res = NewUpgradeResponse(null);
|
||||
ch.WriteOutbound(res);
|
||||
|
||||
var res2 = ch.ReadOutbound<IHttpResponse>();
|
||||
|
||||
Assert.False(res2.Headers.Contains(HttpHeaderNames.SecWebsocketExtensions));
|
||||
Assert.Null(ch.Pipeline.Get<PerMessageDeflateDecoder>());
|
||||
Assert.Null(ch.Pipeline.Get<PerMessageDeflateEncoder>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClientNoContext()
|
||||
{
|
||||
var ch = new EmbeddedChannel(new WebSocketServerCompressionHandler());
|
||||
|
||||
IHttpRequest req = NewUpgradeRequest(
|
||||
PerMessageDeflateExtension + "; "
|
||||
+ PerMessageDeflateServerExtensionHandshaker.ClientNoContext);
|
||||
ch.WriteInbound(req);
|
||||
|
||||
IHttpResponse res = NewUpgradeResponse(null);
|
||||
ch.WriteOutbound(res);
|
||||
|
||||
var res2 = ch.ReadOutbound<IHttpResponse>();
|
||||
Assert.True(res2.Headers.TryGet(HttpHeaderNames.SecWebsocketExtensions, out ICharSequence value));
|
||||
List<WebSocketExtensionData> exts = WebSocketExtensionUtil.ExtractExtensions(value.ToString());
|
||||
|
||||
Assert.Equal(PerMessageDeflateExtension, exts[0].Name);
|
||||
Assert.Empty(exts[0].Parameters);
|
||||
Assert.NotNull(ch.Pipeline.Get<PerMessageDeflateDecoder>());
|
||||
Assert.NotNull(ch.Pipeline.Get<PerMessageDeflateEncoder>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServerWindowSizeDisableThenFallback()
|
||||
{
|
||||
var ch = new EmbeddedChannel(new WebSocketServerExtensionHandler(
|
||||
new PerMessageDeflateServerExtensionHandshaker(6, false, 15, false, false)));
|
||||
|
||||
IHttpRequest req = NewUpgradeRequest(
|
||||
PerMessageDeflateExtension + "; " + ServerMaxWindow + "=10, " +
|
||||
PerMessageDeflateExtension);
|
||||
ch.WriteInbound(req);
|
||||
|
||||
IHttpResponse res = NewUpgradeResponse(null);
|
||||
ch.WriteOutbound(res);
|
||||
|
||||
var res2 = ch.ReadOutbound<IHttpResponse>();
|
||||
Assert.True(res2.Headers.TryGet(HttpHeaderNames.SecWebsocketExtensions, out ICharSequence value));
|
||||
List<WebSocketExtensionData> exts = WebSocketExtensionUtil.ExtractExtensions(value.ToString());
|
||||
|
||||
Assert.Equal(PerMessageDeflateExtension, exts[0].Name);
|
||||
Assert.Empty(exts[0].Parameters);
|
||||
Assert.NotNull(ch.Pipeline.Get<PerMessageDeflateDecoder>());
|
||||
Assert.NotNull(ch.Pipeline.Get<PerMessageDeflateEncoder>());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
||||
namespace DotNetty.Codecs.Http.Tests.WebSockets.Extensions
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions;
|
||||
using DotNetty.Common.Utilities;
|
||||
using DotNetty.Transport.Channels.Embedded;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
using static WebSocketExtensionTestUtil;
|
||||
|
||||
public sealed class WebSocketClientExtensionHandlerTest
|
||||
{
|
||||
readonly Mock<IWebSocketClientExtensionHandshaker> mainHandshaker;
|
||||
readonly Mock<IWebSocketClientExtensionHandshaker> fallbackHandshaker;
|
||||
readonly Mock<IWebSocketClientExtension> mainExtension;
|
||||
readonly Mock<IWebSocketClientExtension> fallbackExtension;
|
||||
|
||||
public WebSocketClientExtensionHandlerTest()
|
||||
{
|
||||
this.mainHandshaker = new Mock<IWebSocketClientExtensionHandshaker>(MockBehavior.Strict);
|
||||
this.fallbackHandshaker = new Mock<IWebSocketClientExtensionHandshaker>(MockBehavior.Strict);
|
||||
this.mainExtension = new Mock<IWebSocketClientExtension>(MockBehavior.Strict);
|
||||
this.fallbackExtension = new Mock<IWebSocketClientExtension>(MockBehavior.Strict);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MainSuccess()
|
||||
{
|
||||
this.mainHandshaker.Setup(x => x.NewRequestData())
|
||||
.Returns(new WebSocketExtensionData("main", new Dictionary<string, string>()));
|
||||
this.mainHandshaker.Setup(x => x.HandshakeExtension(It.IsAny<WebSocketExtensionData>()))
|
||||
.Returns(this.mainExtension.Object);
|
||||
|
||||
this.fallbackHandshaker.Setup(x => x.NewRequestData())
|
||||
.Returns(new WebSocketExtensionData("fallback", new Dictionary<string, string>()));
|
||||
|
||||
this.mainExtension.Setup(x => x.Rsv).Returns(WebSocketRsv.Rsv1);
|
||||
this.mainExtension.Setup(x => x.NewExtensionEncoder()).Returns(new DummyEncoder());
|
||||
this.mainExtension.Setup(x => x.NewExtensionDecoder()).Returns(new DummyDecoder());
|
||||
|
||||
var ch = new EmbeddedChannel(
|
||||
new WebSocketClientExtensionHandler(
|
||||
this.mainHandshaker.Object,
|
||||
this.fallbackHandshaker.Object));
|
||||
|
||||
IHttpRequest req = NewUpgradeRequest(null);
|
||||
ch.WriteOutbound(req);
|
||||
|
||||
var req2 = ch.ReadOutbound<IHttpRequest>();
|
||||
Assert.True(req2.Headers.TryGet(HttpHeaderNames.SecWebsocketExtensions, out ICharSequence value));
|
||||
List<WebSocketExtensionData> reqExts = WebSocketExtensionUtil.ExtractExtensions(value.ToString());
|
||||
|
||||
IHttpResponse res = NewUpgradeResponse("main");
|
||||
ch.WriteInbound(res);
|
||||
|
||||
var res2 = ch.ReadInbound<IHttpResponse>();
|
||||
Assert.True(res2.Headers.TryGet(HttpHeaderNames.SecWebsocketExtensions, out value));
|
||||
List<WebSocketExtensionData> resExts = WebSocketExtensionUtil.ExtractExtensions(value.ToString());
|
||||
|
||||
Assert.Equal(2, reqExts.Count);
|
||||
Assert.Equal("main", reqExts[0].Name);
|
||||
Assert.Equal("fallback", reqExts[1].Name);
|
||||
|
||||
Assert.Single(resExts);
|
||||
Assert.Equal("main", resExts[0].Name);
|
||||
Assert.Empty(resExts[0].Parameters);
|
||||
Assert.NotNull(ch.Pipeline.Get<DummyDecoder>());
|
||||
Assert.NotNull(ch.Pipeline.Get<DummyEncoder>());
|
||||
|
||||
this.mainExtension.Verify(x => x.Rsv, Times.AtLeastOnce);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FallbackSuccess()
|
||||
{
|
||||
this.mainHandshaker.Setup(x => x.NewRequestData())
|
||||
.Returns(new WebSocketExtensionData("main", new Dictionary<string, string>()));
|
||||
this.mainHandshaker.Setup(x => x.HandshakeExtension(It.IsAny<WebSocketExtensionData>()))
|
||||
.Returns(default(IWebSocketClientExtension));
|
||||
|
||||
this.fallbackHandshaker.Setup(x => x.NewRequestData())
|
||||
.Returns(new WebSocketExtensionData("fallback", new Dictionary<string, string>()));
|
||||
this.fallbackHandshaker.Setup(x => x.HandshakeExtension(It.IsAny<WebSocketExtensionData>()))
|
||||
.Returns(this.fallbackExtension.Object);
|
||||
|
||||
this.fallbackExtension.Setup(x => x.Rsv).Returns(WebSocketRsv.Rsv1);
|
||||
this.fallbackExtension.Setup(x => x.NewExtensionEncoder()).Returns(new DummyEncoder());
|
||||
this.fallbackExtension.Setup(x => x.NewExtensionDecoder()).Returns(new DummyDecoder());
|
||||
|
||||
var ch = new EmbeddedChannel(
|
||||
new WebSocketClientExtensionHandler(
|
||||
this.mainHandshaker.Object,
|
||||
this.fallbackHandshaker.Object));
|
||||
|
||||
IHttpRequest req = NewUpgradeRequest(null);
|
||||
ch.WriteOutbound(req);
|
||||
|
||||
var req2 = ch.ReadOutbound<IHttpRequest>();
|
||||
Assert.True(req2.Headers.TryGet(HttpHeaderNames.SecWebsocketExtensions, out ICharSequence value));
|
||||
List<WebSocketExtensionData> reqExts = WebSocketExtensionUtil.ExtractExtensions(value.ToString());
|
||||
|
||||
IHttpResponse res = NewUpgradeResponse("fallback");
|
||||
ch.WriteInbound(res);
|
||||
|
||||
var res2 = ch.ReadInbound<IHttpResponse>();
|
||||
Assert.True(res2.Headers.TryGet(HttpHeaderNames.SecWebsocketExtensions, out value));
|
||||
List<WebSocketExtensionData> resExts = WebSocketExtensionUtil.ExtractExtensions(value.ToString());
|
||||
|
||||
Assert.Equal(2, reqExts.Count);
|
||||
Assert.Equal("main", reqExts[0].Name);
|
||||
Assert.Equal("fallback", reqExts[1].Name);
|
||||
|
||||
Assert.Single(resExts);
|
||||
Assert.Equal("fallback", resExts[0].Name);
|
||||
Assert.Empty(resExts[0].Parameters);
|
||||
Assert.NotNull(ch.Pipeline.Get<DummyDecoder>());
|
||||
Assert.NotNull(ch.Pipeline.Get<DummyEncoder>());
|
||||
|
||||
this.fallbackExtension.Verify(x => x.Rsv, Times.AtLeastOnce);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllSuccess()
|
||||
{
|
||||
this.mainHandshaker.Setup(x => x.NewRequestData())
|
||||
.Returns(new WebSocketExtensionData("main", new Dictionary<string, string>()));
|
||||
this.mainHandshaker.Setup(x => x.HandshakeExtension(
|
||||
It.Is<WebSocketExtensionData>(v => v.Name.Equals("main"))))
|
||||
.Returns(this.mainExtension.Object);
|
||||
this.mainHandshaker.Setup(x => x.HandshakeExtension(
|
||||
It.Is<WebSocketExtensionData>(v => v.Name.Equals("fallback"))))
|
||||
.Returns(default(IWebSocketClientExtension));
|
||||
this.fallbackHandshaker.Setup(x => x.NewRequestData())
|
||||
.Returns(new WebSocketExtensionData("fallback", new Dictionary<string, string>()));
|
||||
this.fallbackHandshaker.Setup(x => x.HandshakeExtension(
|
||||
It.Is<WebSocketExtensionData>(v => v.Name.Equals("main"))))
|
||||
.Returns(default(IWebSocketClientExtension));
|
||||
this.fallbackHandshaker.Setup(x => x.HandshakeExtension(
|
||||
It.Is<WebSocketExtensionData>(v => v.Name.Equals("fallback"))))
|
||||
.Returns(this.fallbackExtension.Object);
|
||||
|
||||
var mainEncoder = new DummyEncoder();
|
||||
var mainDecoder = new DummyDecoder();
|
||||
this.mainExtension.Setup(x => x.Rsv).Returns(WebSocketRsv.Rsv1);
|
||||
this.mainExtension.Setup(x => x.NewExtensionEncoder()).Returns(mainEncoder);
|
||||
this.mainExtension.Setup(x => x.NewExtensionDecoder()).Returns(mainDecoder);
|
||||
|
||||
var fallbackEncoder = new Dummy2Encoder();
|
||||
var fallbackDecoder = new Dummy2Decoder();
|
||||
this.fallbackExtension.Setup(x => x.Rsv).Returns(WebSocketRsv.Rsv2);
|
||||
this.fallbackExtension.Setup(x => x.NewExtensionEncoder()).Returns(fallbackEncoder);
|
||||
this.fallbackExtension.Setup(x => x.NewExtensionDecoder()).Returns(fallbackDecoder);
|
||||
|
||||
var ch = new EmbeddedChannel(new WebSocketClientExtensionHandler(
|
||||
this.mainHandshaker.Object, this.fallbackHandshaker.Object));
|
||||
|
||||
IHttpRequest req = NewUpgradeRequest(null);
|
||||
ch.WriteOutbound(req);
|
||||
|
||||
var req2 = ch.ReadOutbound<IHttpRequest>();
|
||||
Assert.True(req2.Headers.TryGet(HttpHeaderNames.SecWebsocketExtensions, out ICharSequence value));
|
||||
List<WebSocketExtensionData> reqExts = WebSocketExtensionUtil.ExtractExtensions(value.ToString());
|
||||
|
||||
IHttpResponse res = NewUpgradeResponse("main, fallback");
|
||||
ch.WriteInbound(res);
|
||||
|
||||
var res2 = ch.ReadInbound<IHttpResponse>();
|
||||
Assert.True(res2.Headers.TryGet(HttpHeaderNames.SecWebsocketExtensions, out value));
|
||||
List<WebSocketExtensionData> resExts = WebSocketExtensionUtil.ExtractExtensions(value.ToString());
|
||||
|
||||
Assert.Equal(2, reqExts.Count);
|
||||
Assert.Equal("main", reqExts[0].Name);
|
||||
Assert.Equal("fallback", reqExts[1].Name);
|
||||
|
||||
Assert.Equal(2, resExts.Count);
|
||||
Assert.Equal("main", resExts[0].Name);
|
||||
Assert.Equal("fallback", resExts[1].Name);
|
||||
Assert.NotNull(ch.Pipeline.Context(mainEncoder));
|
||||
Assert.NotNull(ch.Pipeline.Context(mainDecoder));
|
||||
Assert.NotNull(ch.Pipeline.Context(fallbackEncoder));
|
||||
Assert.NotNull(ch.Pipeline.Context(fallbackDecoder));
|
||||
|
||||
this.mainExtension.Verify(x => x.Rsv, Times.AtLeastOnce);
|
||||
this.fallbackExtension.Verify(x => x.Rsv, Times.AtLeastOnce);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MainAndFallbackUseRsv1WillFail()
|
||||
{
|
||||
this.mainHandshaker.Setup(x => x.NewRequestData())
|
||||
.Returns(new WebSocketExtensionData("main", new Dictionary<string, string>()));
|
||||
this.mainHandshaker.Setup(x => x.HandshakeExtension(
|
||||
It.Is<WebSocketExtensionData>(v => v.Name.Equals("main"))))
|
||||
.Returns(this.mainExtension.Object);
|
||||
this.mainHandshaker.Setup(x => x.HandshakeExtension(
|
||||
It.Is<WebSocketExtensionData>(v => v.Name.Equals("fallback"))))
|
||||
.Returns(default(IWebSocketClientExtension));
|
||||
this.fallbackHandshaker.Setup(x => x.NewRequestData())
|
||||
.Returns(new WebSocketExtensionData("fallback", new Dictionary<string, string>()));
|
||||
this.fallbackHandshaker.Setup(x => x.HandshakeExtension(
|
||||
It.Is<WebSocketExtensionData>(v => v.Name.Equals("fallback"))))
|
||||
.Returns(this.fallbackExtension.Object);
|
||||
this.mainExtension.Setup(x => x.Rsv).Returns(WebSocketRsv.Rsv1);
|
||||
this.fallbackExtension.Setup(x => x.Rsv).Returns(WebSocketRsv.Rsv1);
|
||||
|
||||
var ch = new EmbeddedChannel(new WebSocketClientExtensionHandler(
|
||||
this.mainHandshaker.Object, this.fallbackHandshaker.Object));
|
||||
|
||||
IHttpRequest req = NewUpgradeRequest(null);
|
||||
ch.WriteOutbound(req);
|
||||
|
||||
var req2 = ch.ReadOutbound<IHttpRequest>();
|
||||
Assert.True(req2.Headers.TryGet(HttpHeaderNames.SecWebsocketExtensions, out ICharSequence value));
|
||||
List<WebSocketExtensionData> reqExts = WebSocketExtensionUtil.ExtractExtensions(value.ToString());
|
||||
|
||||
IHttpResponse res = NewUpgradeResponse("main, fallback");
|
||||
Assert.Throws<CodecException>(() => ch.WriteInbound(res));
|
||||
|
||||
Assert.Equal(2, reqExts.Count);
|
||||
Assert.Equal("main", reqExts[0].Name);
|
||||
Assert.Equal("fallback", reqExts[1].Name);
|
||||
|
||||
this.mainHandshaker.Verify(x => x.HandshakeExtension(
|
||||
It.Is<WebSocketExtensionData>(v => v.Name.Equals("main"))), Times.AtLeastOnce);
|
||||
this.mainHandshaker.Verify(x => x.HandshakeExtension(
|
||||
It.Is<WebSocketExtensionData>(v => v.Name.Equals("fallback"))), Times.AtLeastOnce);
|
||||
|
||||
this.fallbackHandshaker.Verify(x => x.HandshakeExtension(
|
||||
It.Is<WebSocketExtensionData>(v => v.Name.Equals("fallback"))), Times.AtLeastOnce);
|
||||
|
||||
this.mainExtension.Verify(x => x.Rsv, Times.AtLeastOnce);
|
||||
this.fallbackExtension.Verify(x => x.Rsv, Times.AtLeastOnce);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace DotNetty.Codecs.Http.Tests.WebSockets.Extensions
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using DotNetty.Codecs.Http.WebSockets;
|
||||
using DotNetty.Codecs.Http.WebSockets.Extensions;
|
||||
using DotNetty.Transport.Channels;
|
||||
|
||||
static class WebSocketExtensionTestUtil
|
||||
{
|
||||
public static IHttpRequest NewUpgradeRequest(string ext)
|
||||
{
|
||||
var req = new DefaultHttpRequest(
|
||||
HttpVersion.Http11,
|
||||
HttpMethod.Get,
|
||||
"/chat");
|
||||
|
||||
req.Headers.Set(HttpHeaderNames.Host, "server.example.com");
|
||||
req.Headers.Set(HttpHeaderNames.Upgrade, HttpHeaderValues.Websocket.ToString().ToLower());
|
||||
req.Headers.Set(HttpHeaderNames.Connection, "Upgrade");
|
||||
req.Headers.Set(HttpHeaderNames.Origin, "http://example.com");
|
||||
if (ext != null)
|
||||
{
|
||||
req.Headers.Set(HttpHeaderNames.SecWebsocketExtensions, ext);
|
||||
}
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
public static IHttpResponse NewUpgradeResponse(string ext)
|
||||
{
|
||||
var res = new DefaultHttpResponse(
|
||||
HttpVersion.Http11,
|
||||
HttpResponseStatus.SwitchingProtocols);
|
||||
|
||||
res.Headers.Set(HttpHeaderNames.Host, "server.example.com");
|
||||
res.Headers.Set(HttpHeaderNames.Upgrade, HttpHeaderValues.Websocket.ToString().ToLower());
|
||||
res.Headers.Set(HttpHeaderNames.Connection, "Upgrade");
|
||||
res.Headers.Set(HttpHeaderNames.Origin, "http://example.com");
|
||||
if (ext != null)
|
||||
{
|
||||
res.Headers.Set(HttpHeaderNames.SecWebsocketExtensions, ext);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
internal class DummyEncoder : WebSocketExtensionEncoder
|
||||
{
|
||||
protected override void Encode(IChannelHandlerContext ctx, WebSocketFrame msg, List<object> ouput)
|
||||
{
|
||||
// unused
|
||||
}
|
||||
}
|
||||
|
||||
internal class DummyDecoder : WebSocketExtensionDecoder
|
||||
{
|
||||
protected override void Decode(IChannelHandlerContext ctx, WebSocketFrame msg, List<object> output)
|
||||
{
|
||||
// unused
|
||||
}
|
||||
}
|
||||
|
||||
internal class Dummy2Encoder : WebSocketExtensionEncoder
|
||||
{
|
||||
protected override void Encode(IChannelHandlerContext ctx, WebSocketFrame msg, List<object> ouput)
|
||||
{
|
||||
// unused
|
||||
}
|
||||
}
|
||||
|
||||
internal class Dummy2Decoder : WebSocketExtensionDecoder
|
||||
{
|
||||
protected override void Decode(IChannelHandlerContext ctx, WebSocketFrame msg, List<object> output)
|
||||
{
|
||||
// unused
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче