Skip to content

Commit

Permalink
Merge pull request #3 from Garados007/websocket
Browse files Browse the repository at this point in the history
Websocket
  • Loading branch information
Garados007 authored Feb 1, 2021
2 parents a1eece5 + 9e465cf commit 0fa8719
Show file tree
Hide file tree
Showing 15 changed files with 888 additions and 2 deletions.
111 changes: 111 additions & 0 deletions MaxLib.WebServer.Test/WebSocket/FrameParsing.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using MaxLib.WebServer.WebSocket;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;

namespace MaxLib.WebServer.Test.WebSocket
{
[TestClass]
public class FrameParsing
{
[TestMethod]
public async Task ReadSingleFrameUnmaskedTextMessage()
{
var m = new MemoryStream(new byte[] { 0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f });
var frame = await Frame.TryRead(m);
Assert.IsNotNull(frame);
Assert.IsTrue(frame!.FinalFrame);
Assert.AreEqual(OpCode.Text, frame.OpCode);
Assert.IsFalse(frame.HasMaskingKey);
Assert.AreEqual("Hello", frame.TextPayload);
}

[TestMethod]
public async Task ReadSingleFrameMaskedTextMessage()
{
var m = new MemoryStream(new byte[] { 0x81, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x7f, 0x9f, 0x4d, 0x51, 0x58 });
var frame = await Frame.TryRead(m);
Assert.IsNotNull(frame);
Assert.IsTrue(frame!.FinalFrame);
Assert.AreEqual(OpCode.Text, frame.OpCode);
Assert.IsTrue(frame.HasMaskingKey);
frame.UnapplyMask();
Assert.IsFalse(frame.HasMaskingKey);
Assert.AreEqual("Hello", frame.TextPayload);
}

[TestMethod]
public async Task ReadFragmentedUnmaskedTextMessage()
{
var m = new MemoryStream(new byte[] { 0x01, 0x03, 0x48, 0x65, 0x6c });
var frame = await Frame.TryRead(m);
Assert.IsNotNull(frame);
Assert.IsFalse(frame!.FinalFrame);
Assert.AreEqual(OpCode.Text, frame.OpCode);
Assert.IsFalse(frame.HasMaskingKey);
Assert.AreEqual("Hel", frame.TextPayload);

m = new MemoryStream(new byte[] { 0x80, 0x02, 0x6c, 0x6f });
frame = await Frame.TryRead(m);
Assert.IsNotNull(frame);
Assert.IsTrue(frame!.FinalFrame);
Assert.AreEqual(OpCode.Continuation, frame.OpCode);
Assert.IsFalse(frame.HasMaskingKey);
Assert.AreEqual("lo", frame.TextPayload);
}


[TestMethod]
public async Task ReadUnmaskedPingAndMaskedPongMessage()
{
var m = new MemoryStream(new byte[] { 0x89, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f });
var frame = await Frame.TryRead(m);
Assert.IsNotNull(frame);
Assert.IsTrue(frame!.FinalFrame);
Assert.AreEqual(OpCode.Ping, frame.OpCode);
Assert.IsFalse(frame.HasMaskingKey);
Assert.AreEqual("Hello", frame.TextPayload);

m = new MemoryStream(new byte[] { 0x8a, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x7f, 0x9f, 0x4d, 0x51, 0x58 });
frame = await Frame.TryRead(m);
Assert.IsNotNull(frame);
Assert.IsTrue(frame!.FinalFrame);
Assert.AreEqual(OpCode.Pong, frame.OpCode);
Assert.IsTrue(frame.HasMaskingKey);
frame.UnapplyMask();
Assert.IsFalse(frame.HasMaskingKey);
Assert.AreEqual("Hello", frame.TextPayload);
}

[TestMethod]
public async Task Read256ByteUnmaskedMessage()
{
Memory<byte> data = new byte[4 + 256];
(new byte[] { 0x82, 0x7E, 0x01, 0x00 }).CopyTo(data[..4]);
var m = new MemoryStream(data.ToArray());
var frame = await Frame.TryRead(m);
Assert.IsNotNull(frame);
Assert.IsTrue(frame!.FinalFrame);
Assert.AreEqual(OpCode.Binary, frame.OpCode);
Assert.IsFalse(frame.HasMaskingKey);
Assert.AreEqual(256, frame.Payload.Length);
}

[TestMethod]
public async Task Read64KiByteUnmaskedMessage()
{
Memory<byte> data = new byte[10 + 65536];
(new byte[] { 0x82, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00 }).CopyTo(data[..10]);
var m = new MemoryStream(data.ToArray());
var frame = await Frame.TryRead(m);
Assert.IsNotNull(frame);
Assert.IsTrue(frame!.FinalFrame);
Assert.AreEqual(OpCode.Binary, frame.OpCode);
Assert.IsFalse(frame.HasMaskingKey);
Assert.AreEqual(65536, frame.Payload.Length);
}
}
}
31 changes: 31 additions & 0 deletions MaxLib.WebServer.WebSocket.Echo/EchoConnection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.IO;
using System.Threading.Tasks;

#nullable enable

namespace MaxLib.WebServer.WebSocket.Echo
{
public class EchoConnection : WebSocketConnection
{
public EchoConnection(Stream networkStream)
: base(networkStream)
{
}

protected override async Task ReceiveClose(CloseReason? reason, string? info)
{
WebServerLog.Add(ServerLogType.Information, GetType(), "WebSocket", $"client close websocket ({reason}): {info}");
if (!SendCloseSignal)
await Close();
}

protected override async Task ReceivedFrame(Frame frame)
{
await SendFrame(new Frame
{
OpCode = frame.OpCode,
Payload = frame.Payload
});
}
}
}
16 changes: 16 additions & 0 deletions MaxLib.WebServer.WebSocket.Echo/EchoEndpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.IO;

#nullable enable

namespace MaxLib.WebServer.WebSocket.Echo
{
public class EchoEndpoint : WebSocketEndpoint<EchoConnection>
{
public override string? Protocol => null;

protected override EchoConnection CreateConnection(Stream stream, HttpRequestHeader header)
{
return new EchoConnection(stream);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\MaxLib.WebServer\MaxLib.WebServer.csproj" />
</ItemGroup>

</Project>
38 changes: 38 additions & 0 deletions MaxLib.WebServer.WebSocket.Echo/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using MaxLib.WebServer.Services;
using System;

#nullable enable

namespace MaxLib.WebServer.WebSocket.Echo
{
class Program
{
static void Main()
{
WebServerLog.LogAdded += WebServerLog_LogAdded;
var server = new Server(new WebServerSettings(8000, 5000));
// add services
server.AddWebService(new HttpRequestParser());
server.AddWebService(new HttpHeaderPostParser());
server.AddWebService(new HttpHeaderSpecialAction());
server.AddWebService(new HttpResponseCreator());
server.AddWebService(new HttpSender());
// setup web socket
var websocket = new WebSocketService();
websocket.Add(new EchoEndpoint());
server.AddWebService(websocket);
// start server
server.Start();
// wait for console quit
while (Console.ReadKey().Key != ConsoleKey.Q) ;
// close
server.Stop();
websocket.Dispose();
}

private static void WebServerLog_LogAdded(ServerLogItem item)
{
Console.WriteLine($"[{item.Date}] [{item.Type}] ({item.InfoType}) {item.SenderType}: {item.Information}");
}
}
}
17 changes: 16 additions & 1 deletion MaxLib.WebServer.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MaxLib.WebServer", "MaxLib.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MaxLib.WebServer.Test", "MaxLib.WebServer.Test\MaxLib.WebServer.Test.csproj", "{60225D92-5742-4BC0-A3A5-206123F2129D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaxLib.WebServer.Example", "example\MaxLib.WebServer.Example\MaxLib.WebServer.Example.csproj", "{6616448A-1AD7-4897-9124-F1560FB12461}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MaxLib.WebServer.Example", "example\MaxLib.WebServer.Example\MaxLib.WebServer.Example.csproj", "{6616448A-1AD7-4897-9124-F1560FB12461}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Example", "Example", "{39740782-6F07-470C-92B8-C0A07A5C0DFB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaxLib.WebServer.WebSocket.Echo", "MaxLib.WebServer.WebSocket.Echo\MaxLib.WebServer.WebSocket.Echo.csproj", "{01C302FF-8E99-4D54-8234-D4BA59862176}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -57,12 +59,25 @@ Global
{6616448A-1AD7-4897-9124-F1560FB12461}.Release|x64.Build.0 = Release|Any CPU
{6616448A-1AD7-4897-9124-F1560FB12461}.Release|x86.ActiveCfg = Release|Any CPU
{6616448A-1AD7-4897-9124-F1560FB12461}.Release|x86.Build.0 = Release|Any CPU
{01C302FF-8E99-4D54-8234-D4BA59862176}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{01C302FF-8E99-4D54-8234-D4BA59862176}.Debug|Any CPU.Build.0 = Debug|Any CPU
{01C302FF-8E99-4D54-8234-D4BA59862176}.Debug|x64.ActiveCfg = Debug|Any CPU
{01C302FF-8E99-4D54-8234-D4BA59862176}.Debug|x64.Build.0 = Debug|Any CPU
{01C302FF-8E99-4D54-8234-D4BA59862176}.Debug|x86.ActiveCfg = Debug|Any CPU
{01C302FF-8E99-4D54-8234-D4BA59862176}.Debug|x86.Build.0 = Debug|Any CPU
{01C302FF-8E99-4D54-8234-D4BA59862176}.Release|Any CPU.ActiveCfg = Release|Any CPU
{01C302FF-8E99-4D54-8234-D4BA59862176}.Release|Any CPU.Build.0 = Release|Any CPU
{01C302FF-8E99-4D54-8234-D4BA59862176}.Release|x64.ActiveCfg = Release|Any CPU
{01C302FF-8E99-4D54-8234-D4BA59862176}.Release|x64.Build.0 = Release|Any CPU
{01C302FF-8E99-4D54-8234-D4BA59862176}.Release|x86.ActiveCfg = Release|Any CPU
{01C302FF-8E99-4D54-8234-D4BA59862176}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{6616448A-1AD7-4897-9124-F1560FB12461} = {39740782-6F07-470C-92B8-C0A07A5C0DFB}
{01C302FF-8E99-4D54-8234-D4BA59862176} = {39740782-6F07-470C-92B8-C0A07A5C0DFB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E8A0EB3B-E5B8-4120-92DD-1C11CEE7017F}
Expand Down
2 changes: 1 addition & 1 deletion MaxLib.WebServer/MaxLib.WebServer.csproj.include
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project>

<PropertyGroup>
<Version>2.3.1</Version>
<Version>2.4.0</Version>
<AssemblyVersion>$(Version).0</AssemblyVersion>
<FileVersion>$(Version).0</FileVersion>
</PropertyGroup>
Expand Down
9 changes: 9 additions & 0 deletions MaxLib.WebServer/Server.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,15 @@ protected virtual async Task ClientStartListen(HttpConnection connection)

await ExecuteTaskChain(task);

if (task.SwitchProtocolHandler != null)
{
KeepAliveConnections.Remove(connection);
AllConnections.Remove(connection);
task.Dispose();
_ = task.SwitchProtocolHandler();
return;
}

if (task.Request.FieldConnection == HttpConnectionType.KeepAlive)
{
if (!KeepAliveConnections.Contains(connection))
Expand Down
19 changes: 19 additions & 0 deletions MaxLib.WebServer/WebProgressTask.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Net;
using System;
using System.Threading.Tasks;

#nullable enable

Expand Down Expand Up @@ -38,5 +39,23 @@ public void Dispose()
{
Document?.Dispose();
}

internal Func<Task>? SwitchProtocolHandler { get; private set; } = null;

/// <summary>
/// A call to this method notify the web server that this connection will switch protocols
/// after all steps are finished. The web server will remove this connection from its
/// watch list and call <paramref name="handler"/> after its finished.
/// <br />
/// You as the caller are responsible to safely cleanup the connection it is no more
/// used.
/// </summary>
/// <param name="handler">
/// This handler will be called after the server has no more control of this connection.
/// </param>
public void SwitchProtocols(Func<Task> handler)
{
SwitchProtocolHandler = handler;
}
}
}
67 changes: 67 additions & 0 deletions MaxLib.WebServer/WebSocket/CloseReason.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#nullable enable

namespace MaxLib.WebServer.WebSocket
{
public enum CloseReason : ushort
{
/// <summary>
/// 1000 indicates a normal closure, meaning that the purpose for
/// which the connection was established has been fulfilled.
/// </summary>
NormalClose = 1000,
/// <summary>
/// 1001 indicates that an endpoint is "going away", such as a server
/// going down or a browser having navigated away from a page.
/// </summary>
GoingAway = 1001,
/// <summary>
/// 1002 indicates that an endpoint is terminating the connection due
/// to a protocol error.
/// </summary>
ProtocolError = 1002,
/// <summary>
/// 1003 indicates that an endpoint is terminating the connection
/// because it has received a type of data it cannot accept (e.g., an
/// endpoint that understands only text data MAY send this if it
/// receives a binary message).
/// </summary>
CannotAccept = 1003,
/// <summary>
/// 1007 indicates that an endpoint is terminating the connection
/// because it has received data within a message that was not
/// consistent with the type of the message (e.g., non-UTF-8 [RFC3629]
/// data within a text message).
/// </summary>
InvalidMessageContent = 1007,
/// <summary>
/// 1008 indicates that an endpoint is terminating the connection
/// because it has received a message that violates its policy. This
/// is a generic status code that can be returned when there is no
/// other more suitable status code (e.g., 1003 or 1009) or if there
/// is a need to hide specific details about the policy.
/// </summary>
PolicyError = 1008,
/// <summary>
/// 1009 indicates that an endpoint is terminating the connection
/// because it has received a message that is too big for it to
/// process.
/// </summary>
TooBigMessage = 1009,
/// <summary>
/// 1010 indicates that an endpoint (client) is terminating the
/// connection because it has expected the server to negotiate one or
/// more extension, but the server didn't return them in the response
/// message of the WebSocket handshake. The list of extensions that
/// are needed SHOULD appear in the /reason/ part of the Close frame.
/// Note that this status code is not used by the server, because it
/// can fail the WebSocket handshake instead.
/// </summary>
MissingExtension = 1010,
/// <summary>
/// 1011 indicates that a server is terminating the connection because
/// it encountered an unexpected condition that prevented it from
/// fulfilling the request.
/// </summary>
UnexpectedCondition = 1011,
}
}
Loading

0 comments on commit 0fa8719

Please sign in to comment.