mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-30 17:30:40 +00:00
Permissions, RateControl and Auditing
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-and-audit:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install .NET SDK
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Restore solution and fail on vulnerable packages
|
||||
run: dotnet restore Esiur.sln -p:NuGetAudit=true -p:NuGetAuditMode=all -p:NuGetAuditLevel=low '-warnaserror:NU1901;NU1902;NU1903;NU1904'
|
||||
|
||||
- name: Build and run unit tests
|
||||
run: dotnet test Tests/Unit/Esiur.Tests.Unit.csproj --configuration Release --no-restore --verbosity minimal
|
||||
|
||||
- name: Build standalone web example
|
||||
run: dotnet build Examples/StandaloneWebServer/Esiur.Examples.StandaloneWebServer.csproj --configuration Release --no-restore --verbosity minimal
|
||||
|
||||
- name: Audit direct and transitive packages
|
||||
run: dotnet list Esiur.sln package --vulnerable --include-transitive
|
||||
@@ -24,11 +24,43 @@ internal class Program
|
||||
var service = await wh.Put("sys/demo", new Demo());
|
||||
|
||||
var http = await wh.Put<HttpServer>("sys/http", new HttpServer() { Port = 8888 });
|
||||
var webRoot = Path.GetFullPath("Web");
|
||||
var webRootPrefix = webRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
|
||||
+ Path.DirectorySeparatorChar;
|
||||
var pathComparison = Path.DirectorySeparatorChar == '\\'
|
||||
? StringComparison.OrdinalIgnoreCase
|
||||
: StringComparison.Ordinal;
|
||||
|
||||
|
||||
http.MapGet("{url}", (string url, HttpConnection sender) =>
|
||||
{
|
||||
var fn = "Web/" + (sender.Request.Filename == "/" ? "/index.html" : sender.Request.Filename);
|
||||
var requestedPath = sender.Request.Filename == "/"
|
||||
? "index.html"
|
||||
: sender.Request.Filename.TrimStart('/', '\\');
|
||||
string fn;
|
||||
|
||||
try
|
||||
{
|
||||
fn = Path.GetFullPath(Path.Combine(webRoot, requestedPath));
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is ArgumentException ||
|
||||
exception is NotSupportedException ||
|
||||
exception is PathTooLongException)
|
||||
{
|
||||
sender.Response.Number = HttpResponseCode.BadRequest;
|
||||
sender.Send("Invalid path");
|
||||
sender.Close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fn.StartsWith(webRootPrefix, pathComparison))
|
||||
{
|
||||
sender.Response.Number = HttpResponseCode.Forbidden;
|
||||
sender.Send("Forbidden");
|
||||
sender.Close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (File.Exists(fn))
|
||||
{
|
||||
@@ -44,7 +76,7 @@ internal class Program
|
||||
else
|
||||
{
|
||||
sender.Response.Number = HttpResponseCode.NotFound;
|
||||
sender.Send("`" + fn + "` Not Found");
|
||||
sender.Send("Not Found");
|
||||
sender.Close();
|
||||
}
|
||||
|
||||
@@ -57,4 +89,4 @@ internal class Program
|
||||
Console.WriteLine("Running on http://localhost:8888");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.1" />
|
||||
<PackageReference Include="System.Text.Encodings.Web" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,52 +1,67 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace Esiur.Core;
|
||||
|
||||
public class AsyncAwaiter : INotifyCompletion
|
||||
{
|
||||
Action callback = null;
|
||||
|
||||
AsyncException exception = null;
|
||||
|
||||
object result;
|
||||
private static readonly Action CompletedSentinel = () => { };
|
||||
|
||||
private Action continuation;
|
||||
private AsyncException exception;
|
||||
private object result;
|
||||
private readonly AsyncReply reply;
|
||||
|
||||
public AsyncAwaiter(AsyncReply reply)
|
||||
{
|
||||
reply.Then(x =>
|
||||
{
|
||||
this.IsCompleted = true;
|
||||
this.result = x;
|
||||
this.callback?.Invoke();
|
||||
}).Error(x =>
|
||||
{
|
||||
exception = x;
|
||||
this.IsCompleted = true;
|
||||
this.callback?.Invoke();
|
||||
});
|
||||
this.reply = reply;
|
||||
reply.Then(Complete).Error(Fail);
|
||||
}
|
||||
|
||||
public object GetResult()
|
||||
{
|
||||
if (exception != null)
|
||||
throw exception;
|
||||
throw reply.GetExceptionForAwait();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool IsCompleted { get; private set; }
|
||||
public bool IsCompleted
|
||||
=> ReferenceEquals(Volatile.Read(ref continuation), CompletedSentinel);
|
||||
|
||||
public void OnCompleted(Action continuation)
|
||||
{
|
||||
if (IsCompleted)
|
||||
continuation?.Invoke();
|
||||
else
|
||||
// Continue....
|
||||
callback = continuation;
|
||||
if (continuation == null)
|
||||
throw new ArgumentNullException(nameof(continuation));
|
||||
|
||||
var previous = Interlocked.CompareExchange(ref this.continuation, continuation, null);
|
||||
if (ReferenceEquals(previous, CompletedSentinel))
|
||||
{
|
||||
continuation();
|
||||
}
|
||||
else if (previous != null)
|
||||
{
|
||||
throw new InvalidOperationException("The awaiter already has a continuation.");
|
||||
}
|
||||
}
|
||||
|
||||
private void Complete(object value)
|
||||
{
|
||||
result = value;
|
||||
InvokeContinuation();
|
||||
}
|
||||
|
||||
private void Fail(AsyncException value)
|
||||
{
|
||||
exception = value;
|
||||
InvokeContinuation();
|
||||
}
|
||||
|
||||
private void InvokeContinuation()
|
||||
{
|
||||
var registeredContinuation = Interlocked.Exchange(ref continuation, CompletedSentinel);
|
||||
if (registeredContinuation != null && !ReferenceEquals(registeredContinuation, CompletedSentinel))
|
||||
registeredContinuation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,67 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace Esiur.Core;
|
||||
|
||||
public class AsyncAwaiter<T> : INotifyCompletion
|
||||
{
|
||||
Action callback = null;
|
||||
private static readonly Action CompletedSentinel = () => { };
|
||||
|
||||
AsyncException exception = null;
|
||||
|
||||
T result;
|
||||
private Action continuation;
|
||||
private AsyncException exception;
|
||||
private T result;
|
||||
private readonly AsyncReply<T> reply;
|
||||
|
||||
public AsyncAwaiter(AsyncReply<T> reply)
|
||||
{
|
||||
reply.Then(x =>
|
||||
{
|
||||
this.IsCompleted = true;
|
||||
this.result = (T)x;
|
||||
this.callback?.Invoke();
|
||||
}).Error(x =>
|
||||
{
|
||||
exception = x;
|
||||
this.IsCompleted = true;
|
||||
this.callback?.Invoke();
|
||||
});
|
||||
this.reply = reply;
|
||||
reply.Then(Complete).Error(Fail);
|
||||
}
|
||||
|
||||
public T GetResult()
|
||||
{
|
||||
if (exception != null)
|
||||
throw exception;
|
||||
throw reply.GetExceptionForAwait();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool IsCompleted { get; private set; }
|
||||
public bool IsCompleted
|
||||
=> ReferenceEquals(Volatile.Read(ref continuation), CompletedSentinel);
|
||||
|
||||
public void OnCompleted(Action continuation)
|
||||
{
|
||||
if (IsCompleted)
|
||||
continuation?.Invoke();
|
||||
else
|
||||
// Continue....
|
||||
callback = continuation;
|
||||
if (continuation == null)
|
||||
throw new ArgumentNullException(nameof(continuation));
|
||||
|
||||
var previous = Interlocked.CompareExchange(ref this.continuation, continuation, null);
|
||||
if (ReferenceEquals(previous, CompletedSentinel))
|
||||
{
|
||||
continuation();
|
||||
}
|
||||
else if (previous != null)
|
||||
{
|
||||
throw new InvalidOperationException("The awaiter already has a continuation.");
|
||||
}
|
||||
}
|
||||
|
||||
private void Complete(T value)
|
||||
{
|
||||
result = value;
|
||||
InvokeContinuation();
|
||||
}
|
||||
|
||||
private void Fail(AsyncException value)
|
||||
{
|
||||
exception = value;
|
||||
InvokeContinuation();
|
||||
}
|
||||
|
||||
private void InvokeContinuation()
|
||||
{
|
||||
var registeredContinuation = Interlocked.Exchange(ref continuation, CompletedSentinel);
|
||||
if (registeredContinuation != null && !ReferenceEquals(registeredContinuation, CompletedSentinel))
|
||||
registeredContinuation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Esiur.Core;
|
||||
@@ -44,6 +45,7 @@ public class AsyncBag<T> : AsyncReply, IAsyncBag
|
||||
|
||||
int count = 0;
|
||||
bool sealedBag = false;
|
||||
readonly object bagLock = new object();
|
||||
|
||||
|
||||
public virtual Type ArrayType { get; set; } = typeof(T);
|
||||
@@ -79,24 +81,28 @@ public class AsyncBag<T> : AsyncReply, IAsyncBag
|
||||
|
||||
public void Seal()
|
||||
{
|
||||
if (sealedBag)
|
||||
return;
|
||||
object[] pending;
|
||||
lock (bagLock)
|
||||
{
|
||||
if (sealedBag)
|
||||
return;
|
||||
|
||||
sealedBag = true;
|
||||
sealedBag = true;
|
||||
pending = replies.ToArray();
|
||||
}
|
||||
|
||||
var results = ArrayType == null ? new T[replies.Count]
|
||||
: Array.CreateInstance(ArrayType, replies.Count);
|
||||
var results = ArrayType == null ? new T[pending.Length]
|
||||
: Array.CreateInstance(ArrayType, pending.Length);
|
||||
|
||||
if (replies.Count == 0)
|
||||
if (pending.Length == 0)
|
||||
{
|
||||
Trigger(results);
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < replies.Count; i++)
|
||||
for (var i = 0; i < pending.Length; i++)
|
||||
{
|
||||
|
||||
var k = replies[i];
|
||||
var k = pending[i];
|
||||
var index = i;
|
||||
|
||||
if (k is AsyncReply reply)
|
||||
@@ -104,19 +110,17 @@ public class AsyncBag<T> : AsyncReply, IAsyncBag
|
||||
reply.Then((r) =>
|
||||
{
|
||||
results.SetValue(r, index);
|
||||
count++;
|
||||
if (count == replies.Count)
|
||||
if (Interlocked.Increment(ref count) == pending.Length)
|
||||
Trigger(results);
|
||||
}).Error(e => TriggerError(e));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ArrayType != null)
|
||||
replies[i] = RuntimeCaster.Cast(replies[i], ArrayType);
|
||||
|
||||
results.SetValue(replies[i], index);
|
||||
count++;
|
||||
if (count == replies.Count)
|
||||
k = RuntimeCaster.Cast(k, ArrayType);
|
||||
|
||||
results.SetValue(k, index);
|
||||
if (Interlocked.Increment(ref count) == pending.Length)
|
||||
Trigger(results);
|
||||
}
|
||||
}
|
||||
@@ -125,20 +129,24 @@ public class AsyncBag<T> : AsyncReply, IAsyncBag
|
||||
|
||||
public void Add(object valueOrReply)
|
||||
{
|
||||
if (!sealedBag)
|
||||
lock (bagLock)
|
||||
{
|
||||
//if (valueOrReply is AsyncReply)
|
||||
//{
|
||||
// results.Add(default(T));
|
||||
replies.Add(valueOrReply);
|
||||
//}
|
||||
if (!sealedBag)
|
||||
replies.Add(valueOrReply);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void AddBag(AsyncBag<T> bag)
|
||||
{
|
||||
foreach (var r in bag.replies)
|
||||
if (bag == null)
|
||||
throw new ArgumentNullException(nameof(bag));
|
||||
|
||||
object[] source;
|
||||
lock (bag.bagLock)
|
||||
source = bag.replies.ToArray();
|
||||
|
||||
foreach (var r in source)
|
||||
Add(r);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace Esiur.Core;
|
||||
|
||||
public class AsyncBagAwaiter<T> : INotifyCompletion
|
||||
{
|
||||
Action callback = null;
|
||||
private static readonly Action CompletedSentinel = () => { };
|
||||
|
||||
AsyncException exception = null;
|
||||
|
||||
T[] result;
|
||||
private Action continuation;
|
||||
private AsyncException exception;
|
||||
private T[] result;
|
||||
|
||||
public AsyncBagAwaiter(AsyncBag<T> reply)
|
||||
{
|
||||
reply.Then(x =>
|
||||
{
|
||||
this.IsCompleted = true;
|
||||
this.result = x;
|
||||
this.callback?.Invoke();
|
||||
}).Error(x =>
|
||||
{
|
||||
exception = x;
|
||||
this.IsCompleted = true;
|
||||
this.callback?.Invoke();
|
||||
});
|
||||
reply.Then(Complete).Error(Fail);
|
||||
}
|
||||
|
||||
public T[] GetResult()
|
||||
{
|
||||
if (exception != null)
|
||||
throw exception;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool IsCompleted { get; private set; }
|
||||
public bool IsCompleted
|
||||
=> ReferenceEquals(Volatile.Read(ref continuation), CompletedSentinel);
|
||||
|
||||
public void OnCompleted(Action continuation)
|
||||
{
|
||||
if (IsCompleted)
|
||||
continuation?.Invoke();
|
||||
else
|
||||
// Continue....
|
||||
callback = continuation;
|
||||
if (continuation == null)
|
||||
throw new ArgumentNullException(nameof(continuation));
|
||||
|
||||
var previous = Interlocked.CompareExchange(ref this.continuation, continuation, null);
|
||||
if (ReferenceEquals(previous, CompletedSentinel))
|
||||
{
|
||||
continuation();
|
||||
}
|
||||
else if (previous != null)
|
||||
{
|
||||
throw new InvalidOperationException("The awaiter already has a continuation.");
|
||||
}
|
||||
}
|
||||
|
||||
private void Complete(T[] value)
|
||||
{
|
||||
result = value;
|
||||
InvokeContinuation();
|
||||
}
|
||||
|
||||
private void Fail(AsyncException value)
|
||||
{
|
||||
exception = value;
|
||||
InvokeContinuation();
|
||||
}
|
||||
|
||||
private void InvokeContinuation()
|
||||
{
|
||||
var registeredContinuation = Interlocked.Exchange(ref continuation, CompletedSentinel);
|
||||
if (registeredContinuation != null && !ReferenceEquals(registeredContinuation, CompletedSentinel))
|
||||
registeredContinuation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,21 +108,22 @@ public class AsyncQueue<T> : AsyncReply<T>
|
||||
Arrival = DateTime.Now,
|
||||
HasResource = hasResource
|
||||
});
|
||||
resultReady = false;
|
||||
}
|
||||
|
||||
resultReady = false;
|
||||
if (reply.Ready)
|
||||
processQueue(default(T));
|
||||
else
|
||||
reply.Then(processQueue);
|
||||
reply.Then(processQueue).Error(TriggerError);
|
||||
}
|
||||
|
||||
public void Remove(AsyncReply<T> reply)
|
||||
{
|
||||
lock (queueLock)
|
||||
{
|
||||
var item = list.FirstOrDefault(i => i.Reply == reply);
|
||||
list.Remove(item);
|
||||
var index = list.FindIndex(item => ReferenceEquals(item.Reply, reply));
|
||||
if (index >= 0)
|
||||
list.RemoveAt(index);
|
||||
}
|
||||
|
||||
processQueue(default(T));
|
||||
@@ -145,34 +146,34 @@ public class AsyncQueue<T> : AsyncReply<T>
|
||||
}
|
||||
}
|
||||
|
||||
var flushId = currentFlushId++;
|
||||
if (batchSize > 0)
|
||||
{
|
||||
var flushId = currentFlushId++;
|
||||
var readyItems = list.GetRange(0, batchSize);
|
||||
|
||||
for (var i = 0; i < list.Count; i++)
|
||||
if (list[i].Reply.Ready)
|
||||
// Shift the remaining list once. The previous RemoveAt(0) loop shifted
|
||||
// it once per delivered item and became quadratic for large batches.
|
||||
list.RemoveRange(0, batchSize);
|
||||
|
||||
foreach (var item in readyItems)
|
||||
{
|
||||
Trigger(list[i].Reply.Result);
|
||||
Trigger(item.Reply.Result);
|
||||
resultReady = false;
|
||||
|
||||
if (captureProcessedItems)
|
||||
{
|
||||
var p = list[i];
|
||||
p.Delivered = DateTime.Now;
|
||||
p.Ready = p.Reply.ReadyTime;
|
||||
p.BatchSize = batchSize;
|
||||
p.FlushId = flushId;
|
||||
//p.HasResource = p.Reply. (p.Ready - p.Arrival).TotalMilliseconds > 5;
|
||||
processed.Add(p);
|
||||
var processedItem = item;
|
||||
processedItem.Delivered = DateTime.Now;
|
||||
processedItem.Ready = processedItem.Reply.ReadyTime;
|
||||
processedItem.BatchSize = batchSize;
|
||||
processedItem.FlushId = flushId;
|
||||
processed.Add(processedItem);
|
||||
}
|
||||
|
||||
list.RemoveAt(i);
|
||||
|
||||
i--;
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
resultReady = (list.Count == 0);
|
||||
resultReady = list.Count == 0;
|
||||
}
|
||||
}
|
||||
|
||||
public AsyncQueue()
|
||||
|
||||
+225
-265
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
|
||||
Copyright (c) 2017 Ahmed Kh. Zamil
|
||||
|
||||
@@ -22,408 +22,368 @@ SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
using Esiur.Resource;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Esiur.Resource;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Esiur.Core;
|
||||
|
||||
[AsyncMethodBuilder(typeof(AsyncReplyBuilder))]
|
||||
public class AsyncReply
|
||||
{
|
||||
|
||||
public DateTime ReadyTime;
|
||||
|
||||
protected List<Action<object>> callbacks = new List<Action<object>>();
|
||||
protected object result;
|
||||
// These lists are intentionally lazy. Completed replies are common and should not
|
||||
// allocate callback storage or a kernel-backed wait primitive unless it is needed.
|
||||
protected List<Action<object>> callbacks;
|
||||
protected List<Action<AsyncException>> errorCallbacks;
|
||||
protected List<Action<ProgressType, uint, uint>> progressCallbacks;
|
||||
protected List<Action<object>> chunkCallbacks;
|
||||
protected List<Action<object>> propagationCallbacks;
|
||||
protected List<Action<byte, string>> warningCallbacks;
|
||||
|
||||
protected List<Action<AsyncException>> errorCallbacks = null;
|
||||
protected volatile object result;
|
||||
protected volatile bool resultReady;
|
||||
|
||||
protected List<Action<ProgressType, uint, uint>> progressCallbacks = null;
|
||||
|
||||
protected List<Action<object>> chunkCallbacks = null;
|
||||
|
||||
protected List<Action<object>> propagationCallbacks = null;
|
||||
protected List<Action<byte, string>> warningCallbacks = null;
|
||||
|
||||
|
||||
object asyncLock = new object();
|
||||
|
||||
//public Timer timeout;// = new Timer()
|
||||
protected bool resultReady = false;
|
||||
AsyncException exception;
|
||||
// StackTrace trace;
|
||||
AutoResetEvent mutex = new AutoResetEvent(false);
|
||||
private readonly object asyncLock = new object();
|
||||
private volatile AsyncException exception;
|
||||
private Exception observedException;
|
||||
private ManualResetEventSlim completionEvent;
|
||||
|
||||
public static int MaxId;
|
||||
|
||||
public int Id;
|
||||
|
||||
public bool Ready
|
||||
{
|
||||
get { return resultReady; }
|
||||
}
|
||||
protected string codePath, codeMethod;
|
||||
protected int codeLine;
|
||||
|
||||
public bool Ready => resultReady;
|
||||
|
||||
public bool Failed => exception != null;
|
||||
|
||||
public Exception Exception => exception;
|
||||
|
||||
public object Result => result;
|
||||
|
||||
public static AsyncReply<T> FromResult<T>(T result) => new AsyncReply<T>(result);
|
||||
|
||||
public object Wait()
|
||||
{
|
||||
if (resultReady)
|
||||
return result;
|
||||
ManualResetEventSlim waiter;
|
||||
|
||||
mutex.WaitOne();
|
||||
|
||||
if (exception != null)
|
||||
throw exception;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
//int timeoutMilliseconds = 0;
|
||||
public void Timeout(int milliseconds, Action callback = null)
|
||||
{
|
||||
|
||||
//timeoutMilliseconds = milliseconds;
|
||||
|
||||
Task.Delay(milliseconds).ContinueWith(x =>
|
||||
lock (asyncLock)
|
||||
{
|
||||
if (!resultReady && exception == null)
|
||||
{
|
||||
TriggerError(new AsyncException(ErrorType.Management,
|
||||
(ushort)ExceptionCode.Timeout, "Execution timeout expired."));
|
||||
if (resultReady)
|
||||
return result;
|
||||
if (exception != null)
|
||||
throw observedException ?? exception;
|
||||
|
||||
callback?.Invoke();
|
||||
}
|
||||
});
|
||||
waiter = completionEvent ?? (completionEvent = new ManualResetEventSlim(false));
|
||||
}
|
||||
|
||||
waiter.Wait();
|
||||
return GetWaitResult();
|
||||
}
|
||||
|
||||
public object Wait(int millisecondsTimeout)
|
||||
{
|
||||
if (resultReady)
|
||||
return result;
|
||||
ManualResetEventSlim waiter;
|
||||
|
||||
//if (Debug)
|
||||
// Console.WriteLine($"AsyncReply: {Id} Wait");
|
||||
|
||||
if (!mutex.WaitOne(millisecondsTimeout))
|
||||
{
|
||||
var e = new Exception("AsyncReply timeout");
|
||||
TriggerError(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
//if (Debug)
|
||||
// Console.WriteLine($"AsyncReply: {Id} Wait ended");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public object Result
|
||||
{
|
||||
get { return result; }
|
||||
}
|
||||
|
||||
|
||||
protected string codePath, codeMethod;
|
||||
protected int codeLine;
|
||||
|
||||
public AsyncReply Then(Action<object> callback, [CallerMemberName] string methodName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = 0)
|
||||
{
|
||||
if (codeLine == 0)
|
||||
{
|
||||
codeLine = lineNumber; codeMethod = methodName; codePath = filePath;
|
||||
}
|
||||
//lock (callbacksLock)
|
||||
//{
|
||||
lock (asyncLock)
|
||||
{
|
||||
// trace = new StackTrace();
|
||||
if (resultReady)
|
||||
return result;
|
||||
if (exception != null)
|
||||
throw observedException ?? exception;
|
||||
|
||||
waiter = completionEvent ?? (completionEvent = new ManualResetEventSlim(false));
|
||||
}
|
||||
|
||||
if (!waiter.Wait(millisecondsTimeout))
|
||||
{
|
||||
var timeoutException = new Exception("AsyncReply timeout");
|
||||
|
||||
// If completion won the timeout race, return that terminal state. Otherwise
|
||||
// retain the timeout on the reply and preserve Wait's historical exception.
|
||||
if (TrySetException(timeoutException))
|
||||
throw timeoutException;
|
||||
}
|
||||
|
||||
return GetWaitResult();
|
||||
}
|
||||
|
||||
public void Timeout(int milliseconds, Action callback = null)
|
||||
{
|
||||
_ = Task.Delay(milliseconds).ContinueWith(_ =>
|
||||
{
|
||||
var timeoutException = new AsyncException(
|
||||
ErrorType.Management,
|
||||
(ushort)ExceptionCode.Timeout,
|
||||
"Execution timeout expired.");
|
||||
|
||||
if (TrySetException(timeoutException))
|
||||
callback?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
public AsyncReply Then(
|
||||
Action<object> callback,
|
||||
[CallerMemberName] string methodName = null,
|
||||
[CallerFilePath] string filePath = null,
|
||||
[CallerLineNumber] int lineNumber = 0)
|
||||
{
|
||||
object completedResult = null;
|
||||
var invokeImmediately = false;
|
||||
|
||||
lock (asyncLock)
|
||||
{
|
||||
if (codeLine == 0)
|
||||
{
|
||||
codeLine = lineNumber;
|
||||
codeMethod = methodName;
|
||||
codePath = filePath;
|
||||
}
|
||||
|
||||
if (resultReady)
|
||||
{
|
||||
//if (Debug)
|
||||
// Console.WriteLine($"AsyncReply: {Id} Then ready");
|
||||
|
||||
callback(result);
|
||||
return this;
|
||||
completedResult = result;
|
||||
invokeImmediately = true;
|
||||
}
|
||||
else if (exception == null)
|
||||
{
|
||||
(callbacks ?? (callbacks = new List<Action<object>>())).Add(callback);
|
||||
}
|
||||
|
||||
|
||||
//timeout = new Timer(x =>
|
||||
//{
|
||||
// // Get calling method name
|
||||
// Console.WriteLine(trace.GetFrame(1).GetMethod().Name);
|
||||
|
||||
// var tr = String.Join("\r\n", trace.GetFrames().Select(f => f.GetMethod().Name));
|
||||
// timeout.Dispose();
|
||||
|
||||
// tr = trace.ToString();
|
||||
// throw new Exception("Request timeout " + Id);
|
||||
//}, null, 15000, 0);
|
||||
|
||||
|
||||
//if (Debug)
|
||||
// Console.WriteLine($"AsyncReply: {Id} Then pending");
|
||||
|
||||
callbacks.Add(callback);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
if (invokeImmediately)
|
||||
callback(completedResult);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public AsyncReply Error(Action<AsyncException> callback)
|
||||
{
|
||||
AsyncException completedException = null;
|
||||
|
||||
if (errorCallbacks == null)
|
||||
errorCallbacks = new List<Action<AsyncException>>();
|
||||
lock (asyncLock)
|
||||
{
|
||||
if (exception != null)
|
||||
{
|
||||
completedException = exception;
|
||||
}
|
||||
else if (!resultReady)
|
||||
{
|
||||
(errorCallbacks ?? (errorCallbacks = new List<Action<AsyncException>>())).Add(callback);
|
||||
}
|
||||
}
|
||||
|
||||
errorCallbacks.Add(callback);
|
||||
|
||||
if (exception != null)
|
||||
callback(exception);
|
||||
if (completedException != null)
|
||||
callback(completedException);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public AsyncReply Progress(Action<ProgressType, uint, uint> callback)
|
||||
{
|
||||
if (progressCallbacks == null)
|
||||
progressCallbacks = new List<Action<ProgressType, uint, uint>>();
|
||||
lock (asyncLock)
|
||||
(progressCallbacks ?? (progressCallbacks = new List<Action<ProgressType, uint, uint>>())).Add(callback);
|
||||
|
||||
progressCallbacks.Add(callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AsyncReply Warning(Action<byte, string> callback)
|
||||
{
|
||||
if (warningCallbacks == null)
|
||||
warningCallbacks = new List<Action<byte, string>>();
|
||||
lock (asyncLock)
|
||||
(warningCallbacks ?? (warningCallbacks = new List<Action<byte, string>>())).Add(callback);
|
||||
|
||||
warningCallbacks.Add(callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AsyncReply Chunk(Action<object> callback)
|
||||
{
|
||||
if (chunkCallbacks == null)
|
||||
chunkCallbacks = new List<Action<object>>();
|
||||
lock (asyncLock)
|
||||
(chunkCallbacks ?? (chunkCallbacks = new List<Action<object>>())).Add(callback);
|
||||
|
||||
chunkCallbacks.Add(callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AsyncReply Propagation(Action<object> callback)
|
||||
{
|
||||
if (propagationCallbacks == null)
|
||||
propagationCallbacks = new List<Action<object>>();
|
||||
lock (asyncLock)
|
||||
(propagationCallbacks ?? (propagationCallbacks = new List<Action<object>>())).Add(callback);
|
||||
|
||||
propagationCallbacks.Add(callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void Trigger(object result)
|
||||
{
|
||||
Action<object> singleCallback = null;
|
||||
Action<object>[] registeredCallbacks = null;
|
||||
var callbackCount = 0;
|
||||
ManualResetEventSlim waiter;
|
||||
|
||||
lock (asyncLock)
|
||||
{
|
||||
if (exception != null || resultReady)
|
||||
return;
|
||||
|
||||
ReadyTime = DateTime.Now;
|
||||
|
||||
//timeout?.Dispose();
|
||||
|
||||
if (exception != null)
|
||||
return;
|
||||
|
||||
//if (Debug)
|
||||
// Console.WriteLine($"AsyncReply: {Id} Trigger");
|
||||
|
||||
if (resultReady)
|
||||
return;
|
||||
|
||||
this.result = result;
|
||||
|
||||
resultReady = true;
|
||||
|
||||
//if (mutex != null)
|
||||
mutex.Set();
|
||||
|
||||
foreach (var cb in callbacks)
|
||||
cb(result);
|
||||
|
||||
|
||||
//if (Debug)
|
||||
// Console.WriteLine($"AsyncReply: {Id} Trigger ended");
|
||||
// AsyncQueue deliberately resets resultReady between deliveries. Snapshot a
|
||||
// multi-callback list before unlocking, while keeping its usual one-callback
|
||||
// delivery path allocation-free.
|
||||
if (callbacks != null)
|
||||
{
|
||||
callbackCount = callbacks.Count;
|
||||
if (callbackCount == 1)
|
||||
singleCallback = callbacks[0];
|
||||
else if (callbackCount > 1)
|
||||
registeredCallbacks = callbacks.ToArray();
|
||||
}
|
||||
|
||||
waiter = completionEvent;
|
||||
}
|
||||
|
||||
return;
|
||||
waiter?.Set();
|
||||
|
||||
if (callbackCount == 1)
|
||||
singleCallback(result);
|
||||
else if (registeredCallbacks != null)
|
||||
foreach (var callback in registeredCallbacks)
|
||||
callback(result);
|
||||
}
|
||||
|
||||
public virtual void TriggerError(Exception exception)
|
||||
{
|
||||
//timeout?.Dispose();
|
||||
TrySetException(exception);
|
||||
}
|
||||
|
||||
if (resultReady)
|
||||
return;
|
||||
|
||||
if (exception is AsyncException)
|
||||
this.exception = exception as AsyncException;
|
||||
else
|
||||
this.exception = new AsyncException(exception);
|
||||
|
||||
if (errorCallbacks != null)
|
||||
{
|
||||
foreach (var cb in errorCallbacks)
|
||||
cb(this.exception);
|
||||
}
|
||||
else
|
||||
{
|
||||
// no error handlers found
|
||||
throw exception;
|
||||
}
|
||||
|
||||
mutex?.Set();
|
||||
internal void TriggerErrorFromBuilder(Exception exception, bool preserveSourceForAwait)
|
||||
{
|
||||
TrySetException(exception, preserveSourceForAwait);
|
||||
}
|
||||
|
||||
internal Exception GetExceptionForAwait()
|
||||
{
|
||||
lock (asyncLock)
|
||||
return observedException ?? exception;
|
||||
}
|
||||
|
||||
public void TriggerProgress(ProgressType type, uint value, uint max)
|
||||
{
|
||||
//timeout?.Dispose();
|
||||
Action<ProgressType, uint, uint>[] registeredCallbacks;
|
||||
|
||||
if (progressCallbacks != null)
|
||||
foreach (var cb in progressCallbacks)
|
||||
cb(type, value, max);
|
||||
lock (asyncLock)
|
||||
registeredCallbacks = progressCallbacks?.ToArray();
|
||||
|
||||
return;
|
||||
if (registeredCallbacks != null)
|
||||
foreach (var callback in registeredCallbacks)
|
||||
callback(type, value, max);
|
||||
}
|
||||
|
||||
public void TriggerWarning(byte level, string message)
|
||||
{
|
||||
//timeout?.Dispose();
|
||||
Action<byte, string>[] registeredCallbacks;
|
||||
|
||||
if (warningCallbacks != null)
|
||||
foreach (var cb in warningCallbacks)
|
||||
cb(level, message);
|
||||
lock (asyncLock)
|
||||
registeredCallbacks = warningCallbacks?.ToArray();
|
||||
|
||||
return ;
|
||||
if (registeredCallbacks != null)
|
||||
foreach (var callback in registeredCallbacks)
|
||||
callback(level, message);
|
||||
}
|
||||
|
||||
|
||||
public void TriggerPropagation(object value)
|
||||
{
|
||||
//timeout?.Dispose();
|
||||
Action<object>[] registeredCallbacks;
|
||||
|
||||
if (propagationCallbacks != null)
|
||||
foreach (var cb in propagationCallbacks)
|
||||
cb(value);
|
||||
lock (asyncLock)
|
||||
registeredCallbacks = propagationCallbacks?.ToArray();
|
||||
|
||||
return;
|
||||
if (registeredCallbacks != null)
|
||||
foreach (var callback in registeredCallbacks)
|
||||
callback(value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void TriggerChunk(object value)
|
||||
{
|
||||
Action<object>[] registeredCallbacks;
|
||||
|
||||
//timeout?.Dispose();
|
||||
|
||||
|
||||
if (chunkCallbacks != null)
|
||||
foreach (var cb in chunkCallbacks)
|
||||
cb(value);
|
||||
|
||||
lock (asyncLock)
|
||||
registeredCallbacks = chunkCallbacks?.ToArray();
|
||||
|
||||
if (registeredCallbacks != null)
|
||||
foreach (var callback in registeredCallbacks)
|
||||
callback(value);
|
||||
}
|
||||
|
||||
public AsyncAwaiter GetAwaiter()
|
||||
{
|
||||
return new AsyncAwaiter(this);
|
||||
}
|
||||
|
||||
|
||||
public AsyncAwaiter GetAwaiter() => new AsyncAwaiter(this);
|
||||
|
||||
public AsyncReply()
|
||||
{
|
||||
// this.Debug = true;
|
||||
Id = MaxId++;
|
||||
Id = Interlocked.Increment(ref MaxId) - 1;
|
||||
}
|
||||
|
||||
public AsyncReply(object result)
|
||||
{
|
||||
// this.Debug = true;
|
||||
|
||||
ReadyTime = DateTime.Now;
|
||||
|
||||
resultReady = true;
|
||||
this.result = result;
|
||||
|
||||
Id = MaxId++;
|
||||
Id = Interlocked.Increment(ref MaxId) - 1;
|
||||
}
|
||||
|
||||
/*
|
||||
public AsyncReply<T> Then(Action<T> callback)
|
||||
private object GetWaitResult()
|
||||
{
|
||||
base.Then(new Action<object>(o => callback((T)o)));
|
||||
return this;
|
||||
}
|
||||
|
||||
public void Trigger(T result)
|
||||
{
|
||||
Trigger((object)result);
|
||||
}
|
||||
|
||||
public Task<bool> MoveNext(CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public AsyncReply()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public new Task<T> Task
|
||||
{
|
||||
get
|
||||
lock (asyncLock)
|
||||
{
|
||||
return base.Task.ContinueWith<T>((t) =>
|
||||
{
|
||||
if (exception != null)
|
||||
throw observedException ?? exception;
|
||||
|
||||
#if NETSTANDARD
|
||||
return (T)t.GetType().GetTypeInfo().GetProperty("Result").GetValue(t);
|
||||
#else
|
||||
return (T)t.GetType().GetProperty("Result").GetValue(t);
|
||||
#endif
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public T Current => throw new NotImplementedException();
|
||||
|
||||
public AsyncReply(T result)
|
||||
: base(result)
|
||||
private bool TrySetException(Exception sourceException, bool preserveSourceForAwait = false)
|
||||
{
|
||||
AsyncException asyncException;
|
||||
Action<AsyncException> singleCallback = null;
|
||||
Action<AsyncException>[] registeredCallbacks = null;
|
||||
var callbackCount = 0;
|
||||
ManualResetEventSlim waiter;
|
||||
|
||||
lock (asyncLock)
|
||||
{
|
||||
if (resultReady || exception != null)
|
||||
return false;
|
||||
|
||||
asyncException = sourceException as AsyncException ?? new AsyncException(sourceException);
|
||||
observedException = preserveSourceForAwait ? sourceException : asyncException;
|
||||
exception = asyncException;
|
||||
|
||||
if (errorCallbacks != null)
|
||||
{
|
||||
callbackCount = errorCallbacks.Count;
|
||||
if (callbackCount == 1)
|
||||
singleCallback = errorCallbacks[0];
|
||||
else if (callbackCount > 1)
|
||||
registeredCallbacks = errorCallbacks.ToArray();
|
||||
}
|
||||
|
||||
waiter = completionEvent;
|
||||
}
|
||||
|
||||
waiter?.Set();
|
||||
|
||||
if (callbackCount == 1)
|
||||
singleCallback(asyncException);
|
||||
else if (registeredCallbacks != null)
|
||||
foreach (var callback in registeredCallbacks)
|
||||
callback(asyncException);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Esiur.Core;
|
||||
|
||||
public class AsyncReplyBuilder
|
||||
{
|
||||
AsyncReply reply;
|
||||
int hasSuspended;
|
||||
|
||||
AsyncReplyBuilder(AsyncReply reply)
|
||||
{
|
||||
@@ -33,7 +35,9 @@ public class AsyncReplyBuilder
|
||||
|
||||
public void SetException(Exception exception)
|
||||
{
|
||||
reply.TriggerError(exception);
|
||||
reply.TriggerErrorFromBuilder(
|
||||
exception,
|
||||
preserveSourceForAwait: Volatile.Read(ref hasSuspended) == 0);
|
||||
}
|
||||
|
||||
public void SetResult()
|
||||
@@ -46,6 +50,7 @@ public class AsyncReplyBuilder
|
||||
where TAwaiter : INotifyCompletion
|
||||
where TStateMachine : IAsyncStateMachine
|
||||
{
|
||||
Volatile.Write(ref hasSuspended, 1);
|
||||
awaiter.OnCompleted(stateMachine.MoveNext);
|
||||
}
|
||||
|
||||
@@ -54,6 +59,7 @@ public class AsyncReplyBuilder
|
||||
where TAwaiter : ICriticalNotifyCompletion
|
||||
where TStateMachine : IAsyncStateMachine
|
||||
{
|
||||
Volatile.Write(ref hasSuspended, 1);
|
||||
awaiter.UnsafeOnCompleted(stateMachine.MoveNext);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,14 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Esiur.Core;
|
||||
|
||||
public class AsyncReplyBuilder<T>
|
||||
{
|
||||
AsyncReply<T> reply;
|
||||
int hasSuspended;
|
||||
|
||||
AsyncReplyBuilder(AsyncReply<T> reply)
|
||||
{
|
||||
@@ -33,7 +35,9 @@ public class AsyncReplyBuilder<T>
|
||||
|
||||
public void SetException(Exception exception)
|
||||
{
|
||||
reply.TriggerError(exception);
|
||||
reply.TriggerErrorFromBuilder(
|
||||
exception,
|
||||
preserveSourceForAwait: Volatile.Read(ref hasSuspended) == 0);
|
||||
}
|
||||
|
||||
public void SetResult(T result)
|
||||
@@ -46,6 +50,7 @@ public class AsyncReplyBuilder<T>
|
||||
where TAwaiter : INotifyCompletion
|
||||
where TStateMachine : IAsyncStateMachine
|
||||
{
|
||||
Volatile.Write(ref hasSuspended, 1);
|
||||
awaiter.OnCompleted(stateMachine.MoveNext);
|
||||
}
|
||||
|
||||
@@ -54,6 +59,7 @@ public class AsyncReplyBuilder<T>
|
||||
where TAwaiter : ICriticalNotifyCompletion
|
||||
where TStateMachine : IAsyncStateMachine
|
||||
{
|
||||
Volatile.Write(ref hasSuspended, 1);
|
||||
awaiter.UnsafeOnCompleted(stateMachine.MoveNext);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
|
||||
Copyright (c) 2017 Ahmed Kh. Zamil
|
||||
|
||||
@@ -22,31 +22,22 @@ SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Esiur.Resource;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Esiur.Core;
|
||||
|
||||
[AsyncMethodBuilder(typeof(AsyncReplyBuilder<>))]
|
||||
public class AsyncReply<T> : AsyncReply
|
||||
{
|
||||
|
||||
public AsyncReply<T> Then(Action<T> callback, [CallerMemberName] string methodName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = 0)
|
||||
public AsyncReply<T> Then(
|
||||
Action<T> callback,
|
||||
[CallerMemberName] string methodName = null,
|
||||
[CallerFilePath] string filePath = null,
|
||||
[CallerLineNumber] int lineNumber = 0)
|
||||
{
|
||||
if (base.codeLine == 0)
|
||||
{
|
||||
base.codeLine = lineNumber; base.codeMethod = methodName; base.codePath = filePath;
|
||||
}
|
||||
|
||||
base.Then((x) => callback((T)x));
|
||||
base.Then(value => callback((T)value), methodName, filePath, lineNumber);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -56,329 +47,25 @@ public class AsyncReply<T> : AsyncReply
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public AsyncReply<T> Chunk(Action<T> callback)
|
||||
{
|
||||
chunkCallbacks.Add((x) => callback((T)x));
|
||||
base.Chunk(value => callback((T)value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AsyncReply(T result)
|
||||
: base(result)
|
||||
: base(result)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
public AsyncReply()
|
||||
: base()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public new AsyncAwaiter<T> GetAwaiter()
|
||||
{
|
||||
return new AsyncAwaiter<T>(this);
|
||||
}
|
||||
|
||||
public new T Wait()
|
||||
{
|
||||
return (T)base.Wait();
|
||||
}
|
||||
|
||||
public new T Wait(int millisecondsTimeout)
|
||||
{
|
||||
return (T)base.Wait(millisecondsTimeout);
|
||||
}
|
||||
|
||||
/*
|
||||
protected new List<Action> callbacks = new List<Action>();
|
||||
protected new object result;
|
||||
|
||||
protected new List<Action<AsyncException>> errorCallbacks = new List<Action<AsyncException>>();
|
||||
|
||||
protected new List<Action<ProgressType, int, int>> progressCallbacks = new List<Action<ProgressType, int, int>>();
|
||||
|
||||
protected new List<Action> chunkCallbacks = new List<Action>();
|
||||
|
||||
//List<AsyncAwaiter> awaiters = new List<AsyncAwaiter>();
|
||||
|
||||
object asyncLock = new object();
|
||||
|
||||
//public Timer timeout;// = new Timer()
|
||||
|
||||
AsyncException exception;
|
||||
// StackTrace trace;
|
||||
AutoResetEvent mutex = new AutoResetEvent(false);
|
||||
|
||||
public static int MaxId;
|
||||
|
||||
public int Id;
|
||||
|
||||
public bool Ready
|
||||
{
|
||||
get { return resultReady; }
|
||||
|
||||
}
|
||||
|
||||
|
||||
public T Wait()
|
||||
{
|
||||
|
||||
if (resultReady)
|
||||
return result;
|
||||
|
||||
if (Debug)
|
||||
Console.WriteLine($"AsyncReply: {Id} Wait");
|
||||
|
||||
//mutex = new AutoResetEvent(false);
|
||||
mutex.WaitOne();
|
||||
|
||||
if (Debug)
|
||||
Console.WriteLine($"AsyncReply: {Id} Wait ended");
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public object Result
|
||||
{
|
||||
get { return result; }
|
||||
}
|
||||
|
||||
|
||||
public IAsyncReply<T> Then(Action<T> callback)
|
||||
{
|
||||
//lock (callbacksLock)
|
||||
//{
|
||||
lock (asyncLock)
|
||||
{
|
||||
// trace = new StackTrace();
|
||||
|
||||
if (resultReady)
|
||||
{
|
||||
if (Debug)
|
||||
Console.WriteLine($"AsyncReply: {Id} Then ready");
|
||||
|
||||
callback(result);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
//timeout = new Timer(x =>
|
||||
//{
|
||||
// // Get calling method name
|
||||
// Console.WriteLine(trace.GetFrame(1).GetMethod().Name);
|
||||
|
||||
// var tr = String.Join("\r\n", trace.GetFrames().Select(f => f.GetMethod().Name));
|
||||
// timeout.Dispose();
|
||||
|
||||
// tr = trace.ToString();
|
||||
// throw new Exception("Request timeout " + Id);
|
||||
//}, null, 15000, 0);
|
||||
|
||||
|
||||
if (Debug)
|
||||
Console.WriteLine($"AsyncReply: {Id} Then pending");
|
||||
|
||||
|
||||
|
||||
callbacks.Add(callback);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public IAsyncReply<T> Error(Action<AsyncException> callback)
|
||||
{
|
||||
// lock (callbacksLock)
|
||||
// {
|
||||
errorCallbacks.Add(callback);
|
||||
|
||||
if (exception != null)
|
||||
callback(exception);
|
||||
|
||||
return this;
|
||||
//}
|
||||
}
|
||||
|
||||
public IAsyncReply<T> Progress(Action<ProgressType, int, int> callback)
|
||||
{
|
||||
//lock (callbacksLock)
|
||||
//{
|
||||
progressCallbacks.Add(callback);
|
||||
return this;
|
||||
//}
|
||||
}
|
||||
|
||||
|
||||
public IAsyncReply<T> Chunk(Action<T> callback)
|
||||
{
|
||||
// lock (callbacksLock)
|
||||
// {
|
||||
chunkCallbacks.Add(callback);
|
||||
return this;
|
||||
// }
|
||||
}
|
||||
|
||||
public void Trigger(object result)
|
||||
{
|
||||
lock (asyncLock)
|
||||
{
|
||||
//timeout?.Dispose();
|
||||
|
||||
if (Debug)
|
||||
Console.WriteLine($"AsyncReply: {Id} Trigger");
|
||||
|
||||
if (resultReady)
|
||||
return;
|
||||
|
||||
this.result = (T)result;
|
||||
|
||||
resultReady = true;
|
||||
|
||||
//if (mutex != null)
|
||||
mutex.Set();
|
||||
|
||||
foreach (var cb in callbacks)
|
||||
cb((T)result);
|
||||
|
||||
|
||||
if (Debug)
|
||||
Console.WriteLine($"AsyncReply: {Id} Trigger ended");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void TriggerError(Exception exception)
|
||||
{
|
||||
//timeout?.Dispose();
|
||||
|
||||
if (resultReady)
|
||||
return;
|
||||
|
||||
if (exception is AsyncException)
|
||||
this.exception = exception as AsyncException;
|
||||
else
|
||||
this.exception = new AsyncException(ErrorType.Management, 0, exception.Message);
|
||||
|
||||
|
||||
// lock (callbacksLock)
|
||||
// {
|
||||
foreach (var cb in errorCallbacks)
|
||||
cb(this.exception);
|
||||
// }
|
||||
|
||||
mutex?.Set();
|
||||
|
||||
}
|
||||
|
||||
public void TriggerProgress(ProgressType type, int value, int max)
|
||||
{
|
||||
//timeout?.Dispose();
|
||||
|
||||
if (resultReady)
|
||||
return;
|
||||
|
||||
//lock (callbacksLock)
|
||||
//{
|
||||
foreach (var cb in progressCallbacks)
|
||||
cb(type, value, max);
|
||||
|
||||
//}
|
||||
}
|
||||
|
||||
|
||||
public void TriggerChunk(object value)
|
||||
{
|
||||
|
||||
//timeout?.Dispose();
|
||||
|
||||
if (resultReady)
|
||||
return;
|
||||
|
||||
//lock (callbacksLock)
|
||||
//{
|
||||
foreach (var cb in chunkCallbacks)
|
||||
cb((T)value);
|
||||
|
||||
//}
|
||||
}
|
||||
|
||||
public AsyncAwaiter<T> GetAwaiter()
|
||||
{
|
||||
return new AsyncAwaiter<T>(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public AsyncReply()
|
||||
{
|
||||
// this.Debug = true;
|
||||
Id = MaxId++;
|
||||
}
|
||||
|
||||
public AsyncReply(T result)
|
||||
{
|
||||
// this.Debug = true;
|
||||
resultReady = true;
|
||||
this.result = result;
|
||||
|
||||
Id = MaxId++;
|
||||
}
|
||||
|
||||
/*
|
||||
public AsyncReply<T> Then(Action<T> callback)
|
||||
{
|
||||
base.Then(new Action<object>(o => callback((T)o)));
|
||||
return this;
|
||||
}
|
||||
|
||||
public void Trigger(T result)
|
||||
{
|
||||
Trigger((object)result);
|
||||
}
|
||||
|
||||
public Task<bool> MoveNext(CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public AsyncReply()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public new Task<T> Task
|
||||
{
|
||||
get
|
||||
{
|
||||
return base.Task.ContinueWith<T>((t) =>
|
||||
{
|
||||
|
||||
#if NETSTANDARD
|
||||
return (T)t.GetType().GetTypeInfo().GetProperty("Result").GetValue(t);
|
||||
#else
|
||||
return (T)t.GetType().GetProperty("Result").GetValue(t);
|
||||
#endif
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public T Current => throw new NotImplementedException();
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
|
||||
public new AsyncAwaiter<T> GetAwaiter() => new AsyncAwaiter<T>(this);
|
||||
|
||||
public new T Wait() => (T)base.Wait();
|
||||
|
||||
public new T Wait(int millisecondsTimeout) => (T)base.Wait(millisecondsTimeout);
|
||||
}
|
||||
|
||||
@@ -204,9 +204,7 @@ public class AsyncStreamReply : AsyncReply
|
||||
}
|
||||
|
||||
OnStreamError(exception);
|
||||
|
||||
if (errorCallbacks != null)
|
||||
base.TriggerError(exception);
|
||||
base.TriggerError(exception);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -56,6 +56,18 @@ internal static class ParserGuard
|
||||
"collection");
|
||||
}
|
||||
|
||||
internal static void EnsureTypeMetadataDepth(Warehouse? warehouse, int depth)
|
||||
{
|
||||
// Some compatibility entry points accept a null Warehouse. Keep those safe by
|
||||
// applying the built-in default rather than silently disabling this stack guard.
|
||||
var limit = warehouse?.Configuration.Parser.MaximumTypeMetadataDepth
|
||||
?? ParserConfiguration.DefaultMaximumTypeMetadataDepth;
|
||||
|
||||
if (limit > 0 && depth > limit)
|
||||
throw new ParserLimitException(
|
||||
$"TRU type metadata depth of {depth} exceeds the configured limit of {limit}.");
|
||||
}
|
||||
|
||||
internal static ulong MultiplySaturated(ulong value, ulong multiplier)
|
||||
=> value > ulong.MaxValue / multiplier ? ulong.MaxValue : value * multiplier;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Esiur.Data;
|
||||
public class StringKeyList : IEnumerable<KeyValuePair<string, string>>
|
||||
{
|
||||
|
||||
private List<KeyValuePair<string, string>> m_Variables = new List<KeyValuePair<string, string>>();
|
||||
private readonly List<KeyValuePair<string, string>> m_Variables = new List<KeyValuePair<string, string>>();
|
||||
|
||||
private bool allowMultiple;
|
||||
|
||||
@@ -54,44 +54,32 @@ public class StringKeyList : IEnumerable<KeyValuePair<string, string>>
|
||||
if (OnModified != null)
|
||||
OnModified(key, value);
|
||||
|
||||
key = key.ToLower();
|
||||
|
||||
if (!allowMultiple)
|
||||
{
|
||||
foreach (var kv in m_Variables)
|
||||
{
|
||||
if (kv.Key.ToLower() == key)
|
||||
{
|
||||
m_Variables.Remove(kv);
|
||||
break;
|
||||
}
|
||||
}
|
||||
var index = m_Variables.FindIndex(
|
||||
item => string.Equals(item.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||
if (index >= 0)
|
||||
m_Variables.RemoveAt(index);
|
||||
}
|
||||
|
||||
m_Variables.Add(new KeyValuePair<string, string>(key, value));
|
||||
m_Variables.Add(new KeyValuePair<string, string>(key.ToLowerInvariant(), value));
|
||||
}
|
||||
|
||||
public string this[string key]
|
||||
{
|
||||
get
|
||||
{
|
||||
key = key.ToLower();
|
||||
foreach (var kv in m_Variables)
|
||||
if (kv.Key.ToLower() == key)
|
||||
if (string.Equals(kv.Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
return kv.Value;
|
||||
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
key = key.ToLower();
|
||||
|
||||
var toRemove = m_Variables.Where(x => x.Key.ToLower() == key).ToArray();
|
||||
|
||||
foreach (var item in toRemove)
|
||||
m_Variables.Remove(item);
|
||||
|
||||
|
||||
key = key.ToLowerInvariant();
|
||||
m_Variables.RemoveAll(
|
||||
item => string.Equals(item.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||
m_Variables.Add(new KeyValuePair<string, string>(key, value));
|
||||
|
||||
OnModified?.Invoke(key, value);
|
||||
@@ -121,12 +109,10 @@ public class StringKeyList : IEnumerable<KeyValuePair<string, string>>
|
||||
|
||||
public List<string> GetValues(string Key)
|
||||
{
|
||||
var key = Key.ToLower();
|
||||
|
||||
List<string> values = new List<string>();
|
||||
|
||||
foreach (var kv in m_Variables)
|
||||
if (kv.Key.ToLower() == key)
|
||||
if (string.Equals(kv.Key, Key, StringComparison.OrdinalIgnoreCase))
|
||||
values.Add(kv.Value);
|
||||
|
||||
return values;
|
||||
@@ -134,22 +120,26 @@ public class StringKeyList : IEnumerable<KeyValuePair<string, string>>
|
||||
|
||||
public void RemoveAll(string key)
|
||||
{
|
||||
while (Remove(key)) { }
|
||||
for (var i = m_Variables.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (!string.Equals(m_Variables[i].Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
m_Variables.RemoveAt(i);
|
||||
OnModified?.Invoke(key, null);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(string key)
|
||||
{
|
||||
key = key.ToLower();
|
||||
|
||||
foreach (var kv in m_Variables)
|
||||
for (var i = 0; i < m_Variables.Count; i++)
|
||||
{
|
||||
if (kv.Key.ToLower() == key)
|
||||
{
|
||||
if (OnModified != null)
|
||||
OnModified(key, null);
|
||||
m_Variables.Remove(kv);
|
||||
return true;
|
||||
}
|
||||
if (!string.Equals(m_Variables[i].Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
m_Variables.RemoveAt(i);
|
||||
OnModified?.Invoke(key, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -162,9 +152,8 @@ public class StringKeyList : IEnumerable<KeyValuePair<string, string>>
|
||||
|
||||
public bool ContainsKey(string key)
|
||||
{
|
||||
key = key.ToLower();
|
||||
foreach (var kv in m_Variables)
|
||||
if (kv.Key.ToLower() == key)
|
||||
if (string.Equals(kv.Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
@@ -172,12 +161,11 @@ public class StringKeyList : IEnumerable<KeyValuePair<string, string>>
|
||||
|
||||
public bool ContainsValue(string value)
|
||||
{
|
||||
value = value.ToLower();
|
||||
foreach (var kv in m_Variables)
|
||||
if (kv.Value.ToLower() == value)
|
||||
if (string.Equals(kv.Value, value, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,7 +666,16 @@ namespace Esiur.Data
|
||||
//}
|
||||
|
||||
public static IParseResult<Tru> Parse(byte[] data, uint offset, Warehouse warehouse)
|
||||
=> Parse(data, offset, warehouse, 1);
|
||||
|
||||
private static IParseResult<Tru> Parse(
|
||||
byte[] data,
|
||||
uint offset,
|
||||
Warehouse warehouse,
|
||||
int depth)
|
||||
{
|
||||
ParserGuard.EnsureTypeMetadataDepth(warehouse, depth);
|
||||
|
||||
var oOffset = offset;
|
||||
|
||||
var header = data[offset++];
|
||||
@@ -753,7 +762,7 @@ namespace Esiur.Data
|
||||
var subTypes = new Tru[subsCount];
|
||||
for (var i = 0; i < subsCount; i++)
|
||||
{
|
||||
var pr = Tru.Parse(data, offset, warehouse);
|
||||
var pr = Parse(data, offset, warehouse, depth + 1);
|
||||
subTypes[i] = pr.Value;
|
||||
offset += pr.Size;
|
||||
}
|
||||
@@ -793,8 +802,18 @@ namespace Esiur.Data
|
||||
|
||||
|
||||
|
||||
public static async AsyncReply<IParseResult<Tru>> ParseAsync(byte[] data, uint offset, EpConnection connection, ulong[] requestSequence)
|
||||
public static AsyncReply<IParseResult<Tru>> ParseAsync(byte[] data, uint offset, EpConnection connection, ulong[] requestSequence)
|
||||
=> ParseAsync(data, offset, connection, requestSequence, 1);
|
||||
|
||||
private static async AsyncReply<IParseResult<Tru>> ParseAsync(
|
||||
byte[] data,
|
||||
uint offset,
|
||||
EpConnection connection,
|
||||
ulong[] requestSequence,
|
||||
int depth)
|
||||
{
|
||||
ParserGuard.EnsureTypeMetadataDepth(ParserGuard.GetWarehouse(connection), depth);
|
||||
|
||||
var oOffset = offset;
|
||||
|
||||
var header = data[offset++];
|
||||
@@ -891,7 +910,7 @@ namespace Esiur.Data
|
||||
var subTypes = new Tru[subsCount];
|
||||
for (var i = 0; i < subsCount; i++)
|
||||
{
|
||||
var pr = await Tru.ParseAsync(data, offset, connection, requestSequence);
|
||||
var pr = await ParseAsync(data, offset, connection, requestSequence, depth + 1);
|
||||
subTypes[i] = pr.Value;
|
||||
offset += pr.Size;
|
||||
}
|
||||
@@ -930,4 +949,4 @@ namespace Esiur.Data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,55 +42,87 @@ using Esiur.Net.Packets.Http;
|
||||
namespace Esiur.Net.Http;
|
||||
public class HttpConnection : NetworkConnection
|
||||
{
|
||||
const long InvalidPacket = long.MinValue;
|
||||
const string WebSocketMagicString = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
const string GenericInternalServerError = "An internal server error occurred.";
|
||||
|
||||
|
||||
MemoryStream websocketFragmentBuffer = new MemoryStream();
|
||||
WebsocketPacket.WSOpcode? websocketFragmentOpcode;
|
||||
ulong websocketFragmentLength;
|
||||
bool websocketCloseSent;
|
||||
uint parsedHttpPacketLength;
|
||||
|
||||
public bool WSMode { get; internal set; }
|
||||
public HttpServer Server { get; internal set; }
|
||||
|
||||
public WebsocketPacket WSRequest { get; set; }
|
||||
public string WebSocketSubprotocol { get; private set; }
|
||||
public HttpRequestPacket Request { get; set; }
|
||||
public HttpResponsePacket Response { get; } = new HttpResponsePacket();
|
||||
|
||||
HttpSession session;
|
||||
|
||||
public HttpSession Session => session;
|
||||
|
||||
public KeyList<string, object> Variables { get; } = new KeyList<string, object>();
|
||||
|
||||
|
||||
|
||||
internal long Parse(byte[] data)
|
||||
{
|
||||
if (WSMode)
|
||||
parsedHttpPacketLength = 0;
|
||||
try
|
||||
{
|
||||
// now parse WS protocol
|
||||
WebsocketPacket ws = new WebsocketPacket();
|
||||
|
||||
var pSize = ws.Parse(data, 0, (uint)data.Length);
|
||||
|
||||
|
||||
if (pSize > 0)
|
||||
if (WSMode)
|
||||
{
|
||||
WSRequest = ws;
|
||||
return 0;
|
||||
var ws = new WebsocketPacket
|
||||
{
|
||||
ExpectedMask = true,
|
||||
MaximumPayloadLength = Server?.MaximumWebSocketMessageLength
|
||||
?? WebsocketPacket.DefaultMaximumPayloadLength
|
||||
};
|
||||
var packetSize = ws.Parse(data, 0, (uint)data.Length);
|
||||
|
||||
if (packetSize > 0)
|
||||
{
|
||||
WSRequest = ws;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return packetSize == 0 ? InvalidPacket : packetSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
return pSize;
|
||||
var request = new HttpRequestPacket();
|
||||
if (Server != null)
|
||||
{
|
||||
request.MaximumContentLength = Server.MaxPost;
|
||||
request.MaximumHeaderLength = Server.MaximumHeaderLength;
|
||||
request.MaximumHeaderCount = Server.MaximumHeaderCount;
|
||||
request.MaximumFormFields = Server.MaximumFormFields;
|
||||
request.MaximumFormKeyLength = Server.MaximumFormKeyLength;
|
||||
request.MaximumFormValueLength = Server.MaximumFormValueLength;
|
||||
request.MaximumMultipartPartLength = Server.MaximumMultipartPartLength;
|
||||
}
|
||||
|
||||
var packetSize = request.Parse(data, 0, (uint)data.Length);
|
||||
if (packetSize > 0)
|
||||
{
|
||||
Request = request;
|
||||
parsedHttpPacketLength = (uint)packetSize;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return packetSize == 0 ? InvalidPacket : packetSize;
|
||||
}
|
||||
}
|
||||
else
|
||||
catch (Exception exception) when (
|
||||
exception is InvalidDataException ||
|
||||
exception is ParserLimitException ||
|
||||
exception is ArgumentException)
|
||||
{
|
||||
var rp = new HttpRequestPacket();
|
||||
var pSize = rp.Parse(data, 0, (uint)data.Length);
|
||||
if (pSize > 0)
|
||||
{
|
||||
Request = rp;
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
return pSize;
|
||||
}
|
||||
Global.Log(exception);
|
||||
return InvalidPacket;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,16 +130,26 @@ public class HttpConnection : NetworkConnection
|
||||
public void Flush()
|
||||
{
|
||||
// close the connection
|
||||
if (Request.Headers["connection"].ToLower() != "keep-alive" & IsConnected)
|
||||
if (!string.Equals(
|
||||
Request?.Headers?["connection"],
|
||||
"keep-alive",
|
||||
StringComparison.OrdinalIgnoreCase) && IsConnected)
|
||||
Close();
|
||||
}
|
||||
|
||||
public bool Upgrade()
|
||||
{
|
||||
var ok = Upgrade(Request, Response);
|
||||
var ok = Upgrade(
|
||||
Request,
|
||||
Response,
|
||||
Server?.WebSocketSubprotocols,
|
||||
out var selectedSubprotocol);
|
||||
|
||||
if (ok)
|
||||
{
|
||||
WebSocketSubprotocol = selectedSubprotocol;
|
||||
websocketCloseSent = false;
|
||||
ResetWebSocketFragment();
|
||||
WSMode = true;
|
||||
Send();
|
||||
}
|
||||
@@ -117,28 +159,44 @@ public class HttpConnection : NetworkConnection
|
||||
|
||||
public static bool Upgrade(HttpRequestPacket request, HttpResponsePacket response)
|
||||
{
|
||||
if (IsWebsocketRequest(request))
|
||||
{
|
||||
string magicString = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
string ret = request.Headers["Sec-WebSocket-Key"] + magicString;
|
||||
// Compute the SHA1 hash
|
||||
SHA1 sha = SHA1.Create();
|
||||
byte[] sha1Hash = sha.ComputeHash(Encoding.UTF8.GetBytes(ret));
|
||||
response.Headers["Upgrade"] = request.Headers["Upgrade"];
|
||||
response.Headers["Connection"] = request.Headers["Connection"];// "Upgrade";
|
||||
response.Headers["Sec-WebSocket-Accept"] = Convert.ToBase64String(sha1Hash);
|
||||
return Upgrade(request, response, null, out _);
|
||||
}
|
||||
|
||||
if (request.Headers.ContainsKey("Sec-WebSocket-Protocol"))
|
||||
response.Headers["Sec-WebSocket-Protocol"] = request.Headers["Sec-WebSocket-Protocol"];
|
||||
/// <summary>
|
||||
/// Validates a WebSocket handshake and selects at most one mutually supported
|
||||
/// subprotocol. Subprotocol names are case-sensitive as required by RFC 6455.
|
||||
/// </summary>
|
||||
public static bool Upgrade(
|
||||
HttpRequestPacket request,
|
||||
HttpResponsePacket response,
|
||||
IEnumerable<string> supportedSubprotocols,
|
||||
out string selectedSubprotocol)
|
||||
{
|
||||
selectedSubprotocol = null;
|
||||
response?.Headers.RemoveAll("Sec-WebSocket-Protocol");
|
||||
|
||||
if (response == null ||
|
||||
!TryValidateWebSocketRequest(request, out var requestedSubprotocols))
|
||||
return false;
|
||||
|
||||
response.Number = HttpResponseCode.Switching;
|
||||
response.Text = "Switching Protocols";
|
||||
selectedSubprotocol = SelectSubprotocol(
|
||||
requestedSubprotocols,
|
||||
supportedSubprotocols);
|
||||
|
||||
return true;
|
||||
}
|
||||
var challenge = request.Headers["Sec-WebSocket-Key"] + WebSocketMagicString;
|
||||
byte[] sha1Hash;
|
||||
using (var sha = SHA1.Create())
|
||||
sha1Hash = sha.ComputeHash(Encoding.ASCII.GetBytes(challenge));
|
||||
|
||||
return false;
|
||||
response.Headers["Upgrade"] = "websocket";
|
||||
response.Headers["Connection"] = "Upgrade";
|
||||
response.Headers["Sec-WebSocket-Accept"] = Convert.ToBase64String(sha1Hash);
|
||||
if (selectedSubprotocol != null)
|
||||
response.Headers["Sec-WebSocket-Protocol"] = selectedSubprotocol;
|
||||
|
||||
response.Number = HttpResponseCode.Switching;
|
||||
response.Text = "Switching Protocols";
|
||||
return true;
|
||||
}
|
||||
|
||||
public HttpServer Parent
|
||||
@@ -151,8 +209,22 @@ public class HttpConnection : NetworkConnection
|
||||
|
||||
public void Send(WebsocketPacket packet)
|
||||
{
|
||||
if (packet.Data != null)
|
||||
if (packet == null)
|
||||
return;
|
||||
|
||||
// This class is always the server side of the built-in WebSocket path.
|
||||
// Recompose even prebuilt packets so caller-supplied masked data cannot be sent.
|
||||
packet.Mask = false;
|
||||
packet.MaximumPayloadLength = IsControlOpcode(packet.Opcode)
|
||||
? 125
|
||||
: Server?.MaximumWebSocketMessageLength
|
||||
?? WebsocketPacket.DefaultMaximumPayloadLength;
|
||||
if (packet.Compose())
|
||||
{
|
||||
if (packet.Opcode == WebsocketPacket.WSOpcode.ConnectionClose)
|
||||
websocketCloseSent = true;
|
||||
base.Send(packet.Data);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Send(string data)
|
||||
@@ -215,6 +287,8 @@ public class HttpConnection : NetworkConnection
|
||||
cookie.Expires = DateTime.MaxValue;
|
||||
cookie.Path = "/";
|
||||
cookie.HttpOnly = true;
|
||||
cookie.Secure = Server.SSL;
|
||||
cookie.SameSite = HttpCookieSameSite.Lax;
|
||||
|
||||
Response.Cookies.Add(cookie);
|
||||
}
|
||||
@@ -228,33 +302,224 @@ public class HttpConnection : NetworkConnection
|
||||
|
||||
public static bool IsWebsocketRequest(HttpRequestPacket request)
|
||||
{
|
||||
if (request.Headers.ContainsKey("connection")
|
||||
&& request.Headers["connection"].ToLower().Contains("upgrade")
|
||||
&& request.Headers.ContainsKey("upgrade")
|
||||
&& request.Headers["upgrade"].ToLower() == "websocket"
|
||||
&& request.Headers.ContainsKey("Sec-WebSocket-Version")
|
||||
&& request.Headers["Sec-WebSocket-Version"] == "13"
|
||||
&& request.Headers.ContainsKey("Sec-WebSocket-Key"))
|
||||
//&& Request.Headers.ContainsKey("Sec-WebSocket-Protocol"))
|
||||
return TryValidateWebSocketRequest(request, out _);
|
||||
}
|
||||
|
||||
private static bool TryValidateWebSocketRequest(
|
||||
HttpRequestPacket request,
|
||||
out string[] requestedSubprotocols)
|
||||
{
|
||||
requestedSubprotocols = Array.Empty<string>();
|
||||
if (request == null ||
|
||||
request.Headers == null ||
|
||||
request.Method != Packets.Http.HttpMethod.GET ||
|
||||
(request.RawMethod != null &&
|
||||
!string.Equals(request.RawMethod, "GET", StringComparison.Ordinal)) ||
|
||||
!string.Equals(request.Version, "HTTP/1.1", StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
if (!TryParseTokenList(request.Headers["Connection"], out var connectionTokens) ||
|
||||
!ContainsToken(connectionTokens, "Upgrade", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
if (!TryParseUpgradeList(request.Headers["Upgrade"], out var hasWebSocket) ||
|
||||
!hasWebSocket)
|
||||
return false;
|
||||
|
||||
if (!string.Equals(
|
||||
request.Headers["Sec-WebSocket-Version"],
|
||||
"13",
|
||||
StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
var key = request.Headers["Sec-WebSocket-Key"];
|
||||
if (!IsCanonicalWebSocketKey(key))
|
||||
return false;
|
||||
|
||||
var protocols = request.Headers["Sec-WebSocket-Protocol"];
|
||||
if (protocols != null && !TryParseTokenList(protocols, out requestedSubprotocols))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsWebSocketUpgradeAttempt(HttpRequestPacket request)
|
||||
{
|
||||
var upgrade = request?.Headers?["Upgrade"];
|
||||
if (string.IsNullOrEmpty(upgrade))
|
||||
return false;
|
||||
|
||||
for (var index = 0; index < upgrade.Length;)
|
||||
{
|
||||
return true;
|
||||
while (index < upgrade.Length && !IsHttpTokenCharacter(upgrade[index]))
|
||||
index++;
|
||||
var start = index;
|
||||
while (index < upgrade.Length && IsHttpTokenCharacter(upgrade[index]))
|
||||
index++;
|
||||
|
||||
if (index > start &&
|
||||
string.Equals(
|
||||
upgrade.Substring(start, index - start),
|
||||
"websocket",
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
else
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsCanonicalWebSocketKey(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var decoded = Convert.FromBase64String(key);
|
||||
return decoded.Length == 16 &&
|
||||
string.Equals(
|
||||
Convert.ToBase64String(decoded),
|
||||
key,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseTokenList(string value, out string[] tokens)
|
||||
{
|
||||
tokens = Array.Empty<string>();
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return false;
|
||||
|
||||
var parts = value.Split(',');
|
||||
for (var i = 0; i < parts.Length; i++)
|
||||
{
|
||||
parts[i] = parts[i].Trim();
|
||||
if (!IsHttpToken(parts[i]))
|
||||
return false;
|
||||
}
|
||||
|
||||
tokens = parts;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseUpgradeList(string value, out bool hasWebSocket)
|
||||
{
|
||||
hasWebSocket = false;
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return false;
|
||||
|
||||
foreach (var entry in value.Split(','))
|
||||
{
|
||||
var protocol = entry.Trim();
|
||||
var slash = protocol.IndexOf('/');
|
||||
if (slash < 0)
|
||||
{
|
||||
if (!IsHttpToken(protocol))
|
||||
return false;
|
||||
|
||||
if (string.Equals(protocol, "websocket", StringComparison.OrdinalIgnoreCase))
|
||||
hasWebSocket = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (slash == 0 ||
|
||||
slash == protocol.Length - 1 ||
|
||||
protocol.IndexOf('/', slash + 1) >= 0 ||
|
||||
!IsHttpToken(protocol.Substring(0, slash)) ||
|
||||
!IsHttpToken(protocol.Substring(slash + 1)))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsHttpToken(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return false;
|
||||
|
||||
foreach (var character in value)
|
||||
{
|
||||
if (IsHttpTokenCharacter(character))
|
||||
continue;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsHttpTokenCharacter(char character)
|
||||
=> (character >= 'a' && character <= 'z') ||
|
||||
(character >= 'A' && character <= 'Z') ||
|
||||
(character >= '0' && character <= '9') ||
|
||||
character == '!' || character == '#' || character == '$' ||
|
||||
character == '%' || character == '&' || character == '\'' ||
|
||||
character == '*' || character == '+' || character == '-' ||
|
||||
character == '.' || character == '^' || character == '_' ||
|
||||
character == '`' || character == '|' || character == '~';
|
||||
|
||||
private static bool ContainsToken(
|
||||
string[] tokens,
|
||||
string expected,
|
||||
StringComparison comparison)
|
||||
{
|
||||
foreach (var token in tokens)
|
||||
if (string.Equals(token, expected, comparison))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string SelectSubprotocol(
|
||||
string[] requestedSubprotocols,
|
||||
IEnumerable<string> supportedSubprotocols)
|
||||
{
|
||||
if (supportedSubprotocols == null)
|
||||
return null;
|
||||
|
||||
foreach (var supported in supportedSubprotocols)
|
||||
{
|
||||
if (!IsHttpToken(supported))
|
||||
continue;
|
||||
|
||||
if (ContainsToken(
|
||||
requestedSubprotocols,
|
||||
supported,
|
||||
StringComparison.Ordinal))
|
||||
return supported;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override void DataReceived(NetworkBuffer data)
|
||||
{
|
||||
if (WSMode)
|
||||
{
|
||||
ProcessWebSocketData(data);
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] msg = data.Read();
|
||||
if (msg == null)
|
||||
return;
|
||||
|
||||
var BL = Parse(msg);
|
||||
|
||||
if (BL == 0)
|
||||
if (BL == InvalidPacket)
|
||||
{
|
||||
if (Request.Method == Packets.Http.HttpMethod.UNKNOWN)
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
else if (BL == 0)
|
||||
{
|
||||
if (Request == null || Request.Method == Packets.Http.HttpMethod.UNKNOWN)
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
@@ -272,47 +537,42 @@ public class HttpConnection : NetworkConnection
|
||||
}
|
||||
else if (BL < 0)
|
||||
{
|
||||
data.HoldFor(msg, (uint)(msg.Length - BL));
|
||||
return;
|
||||
}
|
||||
else if (BL > 0)
|
||||
{
|
||||
if (BL > Server.MaxPost)
|
||||
var requiredLength = (ulong)msg.Length + (ulong)(-BL);
|
||||
if (requiredLength > uint.MaxValue)
|
||||
{
|
||||
Send(
|
||||
"<html><body>POST method content is larger than "
|
||||
+ Server.MaxPost
|
||||
+ " bytes.</body></html>");
|
||||
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
data.HoldFor(msg, (uint)requiredLength);
|
||||
return;
|
||||
}
|
||||
|
||||
RestoreSessionFromRequest();
|
||||
|
||||
if (!WSMode && IsWebSocketUpgradeAttempt(Request))
|
||||
{
|
||||
if (!IsWebsocketRequest(Request) || !Upgrade())
|
||||
{
|
||||
data.HoldFor(msg, (uint)(msg.Length + BL));
|
||||
Response.Number = HttpResponseCode.BadRequest;
|
||||
Response.Text = "Bad Request";
|
||||
Response.Headers["Connection"] = "close";
|
||||
Send("Invalid WebSocket handshake.");
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if (BL < 0) // for security
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (IsWebsocketRequest(Request) & !WSMode)
|
||||
{
|
||||
Upgrade();
|
||||
//return;
|
||||
}
|
||||
|
||||
|
||||
//return;
|
||||
|
||||
try
|
||||
{
|
||||
if (!Server.Execute(this))
|
||||
if (Server == null || !Server.Execute(this))
|
||||
{
|
||||
if (WSMode)
|
||||
{
|
||||
FailWebSocket(1008, "No HTTP filter accepted the WebSocket connection.");
|
||||
return;
|
||||
}
|
||||
|
||||
Response.Number = HttpResponseCode.InternalServerError;
|
||||
Send("Bad Request");
|
||||
Close();
|
||||
@@ -325,23 +585,360 @@ public class HttpConnection : NetworkConnection
|
||||
|
||||
Global.Log("HTTPServer", LogType.Error, ex.ToString());
|
||||
|
||||
//Console.WriteLine(ex.ToString());
|
||||
//EventLog.WriteEntry("HttpServer", ex.ToString(), EventLogEntryType.Error);
|
||||
Send(Error500(ex.Message));
|
||||
if (WSMode)
|
||||
{
|
||||
FailWebSocket(1011, "A WebSocket filter failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
Response.Number = HttpResponseCode.InternalServerError;
|
||||
Response.Headers["Content-Type"] = "text/html; charset=utf-8";
|
||||
Send(FormatError500Page(ex));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (WSMode &&
|
||||
IsConnected &&
|
||||
parsedHttpPacketLength > 0 &&
|
||||
parsedHttpPacketLength < msg.Length)
|
||||
{
|
||||
data.Write(
|
||||
msg,
|
||||
parsedHttpPacketLength,
|
||||
(uint)msg.Length - parsedHttpPacketLength);
|
||||
ProcessWebSocketData(data);
|
||||
}
|
||||
}
|
||||
|
||||
private string Error500(string msg)
|
||||
internal void RestoreSessionFromRequest()
|
||||
{
|
||||
session = null;
|
||||
var sessionId = Request?.Cookies?["SID"];
|
||||
if (Server?.TryGetSession(sessionId, out var restored) != true)
|
||||
return;
|
||||
|
||||
session = restored;
|
||||
session.Refresh();
|
||||
}
|
||||
|
||||
private void ProcessWebSocketData(NetworkBuffer data)
|
||||
{
|
||||
var message = data.Read();
|
||||
if (message == null)
|
||||
return;
|
||||
|
||||
var offset = 0u;
|
||||
var ends = (uint)message.Length;
|
||||
|
||||
while (offset < ends && IsConnected)
|
||||
{
|
||||
var packet = new WebsocketPacket
|
||||
{
|
||||
ExpectedMask = true,
|
||||
MaximumPayloadLength = GetIncomingFrameLimit(message[offset])
|
||||
};
|
||||
|
||||
long packetLength;
|
||||
try
|
||||
{
|
||||
packetLength = packet.Parse(message, offset, ends);
|
||||
}
|
||||
catch (ParserLimitException exception)
|
||||
{
|
||||
FailWebSocket(1009, exception.Message);
|
||||
return;
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is InvalidDataException ||
|
||||
exception is ArgumentException)
|
||||
{
|
||||
FailWebSocket(
|
||||
IsInvalidUtf8(exception) ? (ushort)1007 : (ushort)1002,
|
||||
exception.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (packetLength < 0)
|
||||
{
|
||||
var remaining = ends - offset;
|
||||
var required = (ulong)remaining + (ulong)(-packetLength);
|
||||
if (required > int.MaxValue)
|
||||
{
|
||||
FailWebSocket(1009, "The incomplete WebSocket frame is too large to buffer.");
|
||||
return;
|
||||
}
|
||||
|
||||
data.HoldFor(message, offset, remaining, (uint)required);
|
||||
return;
|
||||
}
|
||||
|
||||
if (packetLength == 0 || (ulong)packetLength > ends - offset)
|
||||
{
|
||||
FailWebSocket(1002, "The WebSocket frame parser returned an invalid length.");
|
||||
return;
|
||||
}
|
||||
|
||||
offset += (uint)packetLength;
|
||||
if (!ProcessWebSocketFrame(packet))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private ulong GetIncomingFrameLimit(byte firstHeaderByte)
|
||||
{
|
||||
var opcode = (WebsocketPacket.WSOpcode)(firstHeaderByte & 0x0F);
|
||||
if (IsControlOpcode(opcode))
|
||||
return 125;
|
||||
|
||||
return Server?.MaximumWebSocketMessageLength
|
||||
?? WebsocketPacket.DefaultMaximumPayloadLength;
|
||||
}
|
||||
|
||||
private static bool IsControlOpcode(WebsocketPacket.WSOpcode opcode)
|
||||
=> opcode == WebsocketPacket.WSOpcode.ConnectionClose ||
|
||||
opcode == WebsocketPacket.WSOpcode.Ping ||
|
||||
opcode == WebsocketPacket.WSOpcode.Pong;
|
||||
|
||||
private bool ProcessWebSocketFrame(WebsocketPacket packet)
|
||||
{
|
||||
switch (packet.Opcode)
|
||||
{
|
||||
case WebsocketPacket.WSOpcode.Ping:
|
||||
SendWebSocketFrame(WebsocketPacket.WSOpcode.Pong, packet.Message);
|
||||
return IsConnected;
|
||||
|
||||
case WebsocketPacket.WSOpcode.Pong:
|
||||
return true;
|
||||
|
||||
case WebsocketPacket.WSOpcode.ConnectionClose:
|
||||
ResetWebSocketFragment();
|
||||
if (!websocketCloseSent)
|
||||
{
|
||||
websocketCloseSent = true;
|
||||
SendWebSocketCloseAndClose(packet.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Close();
|
||||
}
|
||||
return false;
|
||||
|
||||
case WebsocketPacket.WSOpcode.TextFrame:
|
||||
case WebsocketPacket.WSOpcode.BinaryFrame:
|
||||
if (websocketFragmentOpcode.HasValue)
|
||||
{
|
||||
FailWebSocket(
|
||||
1002,
|
||||
"A new WebSocket data frame arrived before the fragmented message completed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (packet.FIN)
|
||||
return DeliverWebSocketMessage(packet);
|
||||
|
||||
websocketFragmentOpcode = packet.Opcode;
|
||||
websocketFragmentLength = 0;
|
||||
websocketFragmentBuffer.SetLength(0);
|
||||
return AppendWebSocketFragment(packet.Message);
|
||||
|
||||
case WebsocketPacket.WSOpcode.ContinuationFrame:
|
||||
if (!websocketFragmentOpcode.HasValue)
|
||||
{
|
||||
FailWebSocket(
|
||||
1002,
|
||||
"A WebSocket continuation frame arrived without an active fragmented message.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!AppendWebSocketFragment(packet.Message))
|
||||
return false;
|
||||
|
||||
if (!packet.FIN)
|
||||
return true;
|
||||
|
||||
var opcode = websocketFragmentOpcode.Value;
|
||||
var completeMessage = websocketFragmentBuffer.ToArray();
|
||||
ResetWebSocketFragment();
|
||||
|
||||
try
|
||||
{
|
||||
if (opcode == WebsocketPacket.WSOpcode.TextFrame)
|
||||
WebsocketPacket.ValidateTextPayload(completeMessage);
|
||||
}
|
||||
catch (InvalidDataException exception)
|
||||
{
|
||||
FailWebSocket(1007, exception.Message);
|
||||
return false;
|
||||
}
|
||||
|
||||
return DeliverWebSocketMessage(new WebsocketPacket
|
||||
{
|
||||
FIN = true,
|
||||
Opcode = opcode,
|
||||
Mask = true,
|
||||
Message = completeMessage,
|
||||
PayloadLength = completeMessage.LongLength
|
||||
});
|
||||
|
||||
default:
|
||||
FailWebSocket(1002, "Unsupported WebSocket opcode.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool AppendWebSocketFragment(byte[] payload)
|
||||
{
|
||||
payload ??= Array.Empty<byte>();
|
||||
var payloadLength = (ulong)payload.LongLength;
|
||||
if (payloadLength > ulong.MaxValue - websocketFragmentLength)
|
||||
{
|
||||
FailWebSocket(1009, "The fragmented WebSocket message length overflowed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var nextLength = websocketFragmentLength + payloadLength;
|
||||
var maximumLength = Server?.MaximumWebSocketMessageLength
|
||||
?? WebsocketPacket.DefaultMaximumPayloadLength;
|
||||
if (nextLength > int.MaxValue ||
|
||||
(maximumLength > 0 && nextLength > maximumLength))
|
||||
{
|
||||
FailWebSocket(
|
||||
1009,
|
||||
$"The fragmented WebSocket message exceeds the {maximumLength}-byte limit.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload.Length > 0)
|
||||
websocketFragmentBuffer.Write(payload, 0, payload.Length);
|
||||
websocketFragmentLength = nextLength;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool DeliverWebSocketMessage(WebsocketPacket packet)
|
||||
{
|
||||
WSRequest = packet;
|
||||
try
|
||||
{
|
||||
if (Server == null)
|
||||
{
|
||||
FailWebSocket(1011, "The WebSocket connection is no longer assigned to a server.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Server.Execute(this))
|
||||
{
|
||||
FailWebSocket(1008, "No HTTP filter accepted the WebSocket message.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return IsConnected;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Global.Log("HTTPServer", LogType.Error, exception.ToString());
|
||||
FailWebSocket(1011, "A WebSocket filter failed.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SendWebSocketFrame(
|
||||
WebsocketPacket.WSOpcode opcode,
|
||||
byte[] payload)
|
||||
{
|
||||
var packet = new WebsocketPacket
|
||||
{
|
||||
FIN = true,
|
||||
Mask = false,
|
||||
Opcode = opcode,
|
||||
Message = payload ?? Array.Empty<byte>(),
|
||||
MaximumPayloadLength = 0
|
||||
};
|
||||
|
||||
if (packet.Compose())
|
||||
base.Send(packet.Data);
|
||||
}
|
||||
|
||||
private void FailWebSocket(ushort closeCode, string message)
|
||||
{
|
||||
Global.Log("HTTPServer", LogType.Warning, message);
|
||||
ResetWebSocketFragment();
|
||||
|
||||
if (IsConnected && !websocketCloseSent)
|
||||
{
|
||||
websocketCloseSent = true;
|
||||
SendWebSocketCloseAndClose(
|
||||
new[] { (byte)(closeCode >> 8), (byte)closeCode });
|
||||
return;
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
private void SendWebSocketCloseAndClose(byte[] payload)
|
||||
{
|
||||
var packet = new WebsocketPacket
|
||||
{
|
||||
FIN = true,
|
||||
Mask = false,
|
||||
Opcode = WebsocketPacket.WSOpcode.ConnectionClose,
|
||||
Message = payload ?? Array.Empty<byte>(),
|
||||
MaximumPayloadLength = 125
|
||||
};
|
||||
|
||||
if (!packet.Compose())
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
base.SendAsync(packet.Data, 0, packet.Data.Length)
|
||||
.Then(_ => Close())
|
||||
.Error(_ => Close());
|
||||
}
|
||||
|
||||
private void ResetWebSocketFragment()
|
||||
{
|
||||
websocketFragmentOpcode = null;
|
||||
websocketFragmentLength = 0;
|
||||
if (websocketFragmentBuffer.Capacity > 64 * 1024)
|
||||
{
|
||||
websocketFragmentBuffer.Dispose();
|
||||
websocketFragmentBuffer = new MemoryStream();
|
||||
}
|
||||
else
|
||||
{
|
||||
websocketFragmentBuffer.SetLength(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsInvalidUtf8(Exception exception)
|
||||
{
|
||||
for (var current = exception; current != null; current = current.InnerException)
|
||||
if (current is DecoderFallbackException)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static string FormatError500Page(string msg)
|
||||
{
|
||||
var encodedMessage = WebUtility.HtmlEncode(msg ?? string.Empty);
|
||||
return "<html><head><title>500 Internal Server Error</title></head><br>\r\n"
|
||||
+ "<body><br>\r\n"
|
||||
+ "<b>500</b> Internal Server Error<br>" + msg + "\r\n"
|
||||
+ "<b>500</b> Internal Server Error<br>" + encodedMessage + "\r\n"
|
||||
+ "</body><br>\r\n"
|
||||
+ "</html><br>\r\n";
|
||||
}
|
||||
|
||||
internal string FormatError500Page(Exception exception)
|
||||
{
|
||||
var message = Server?.ExposeExceptionDetails == true
|
||||
? exception?.Message
|
||||
: GenericInternalServerError;
|
||||
return FormatError500Page(message);
|
||||
}
|
||||
|
||||
public async AsyncReply<bool> SendFile(string filename)
|
||||
{
|
||||
if (Response.Handled == true)
|
||||
@@ -411,7 +1008,8 @@ public class HttpConnection : NetworkConnection
|
||||
|
||||
while (true)
|
||||
{
|
||||
var n = fs.Read(buffer, 0, 60000);
|
||||
var n = await fs.ReadAsync(buffer, 0, buffer.Length)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (n <= 0)
|
||||
break;
|
||||
@@ -447,6 +1045,6 @@ public class HttpConnection : NetworkConnection
|
||||
|
||||
protected override void Disconnected()
|
||||
{
|
||||
// do nothing
|
||||
ResetWebSocketFragment();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,11 +41,13 @@ using System.Text.RegularExpressions;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Esiur.Net.Packets.Http;
|
||||
using Esiur.Net.Packets.WebSocket;
|
||||
|
||||
namespace Esiur.Net.Http;
|
||||
public class HttpServer : NetworkServer<HttpConnection>, IResource
|
||||
{
|
||||
Dictionary<string, HttpSession> sessions = new Dictionary<string, HttpSession>();
|
||||
readonly object sessionsLock = new object();
|
||||
HttpFilter[] filters = new HttpFilter[0];
|
||||
|
||||
Dictionary<Packets.Http.HttpMethod, List<RouteInfo>> routes = new()
|
||||
@@ -156,6 +158,72 @@ public class HttpServer : NetworkServer<HttpConnection>, IResource
|
||||
|
||||
//[Attribute]
|
||||
public virtual uint MaxPost
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = HttpPacketHelpers.DefaultMaximumContentLength;
|
||||
|
||||
public virtual uint MaximumHeaderLength
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = HttpPacketHelpers.DefaultMaximumHeaderLength;
|
||||
|
||||
public virtual int MaximumHeaderCount
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = HttpPacketHelpers.DefaultMaximumHeaderCount;
|
||||
|
||||
public virtual int MaximumFormFields
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = HttpPacketHelpers.DefaultMaximumFormFields;
|
||||
|
||||
public virtual int MaximumFormKeyLength
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = HttpPacketHelpers.DefaultMaximumFormKeyLength;
|
||||
|
||||
public virtual int MaximumFormValueLength
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = HttpPacketHelpers.DefaultMaximumFormValueLength;
|
||||
|
||||
public virtual int MaximumMultipartPartLength
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = HttpPacketHelpers.DefaultMaximumMultipartPartLength;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum payload accumulated for one WebSocket application message, including
|
||||
/// all of its fragments. Set to zero to disable the configured limit.
|
||||
/// </summary>
|
||||
public virtual ulong MaximumWebSocketMessageLength
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = WebsocketPacket.DefaultMaximumPayloadLength;
|
||||
|
||||
/// <summary>
|
||||
/// WebSocket subprotocols supported by this server, in server preference order.
|
||||
/// A protocol is returned to the client only when it was also requested.
|
||||
/// </summary>
|
||||
public virtual string[] WebSocketSubprotocols
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether HTTP 500 responses may include exception messages. Disabled by default
|
||||
/// to avoid disclosing implementation details to remote clients.
|
||||
/// </summary>
|
||||
public virtual bool ExposeExceptionDetails
|
||||
{
|
||||
get;
|
||||
set;
|
||||
@@ -179,39 +247,99 @@ public class HttpServer : NetworkServer<HttpConnection>, IResource
|
||||
public HttpSession CreateSession(string id, int timeout)
|
||||
{
|
||||
var s = new HttpSession();
|
||||
s.OnEnd += SessionEnded;
|
||||
s.OnDestroy += SessionDestroyed;
|
||||
|
||||
s.Set(id, timeout);
|
||||
lock (sessionsLock)
|
||||
sessions.Add(id, s);
|
||||
|
||||
|
||||
sessions.Add(id, s);
|
||||
try
|
||||
{
|
||||
s.Set(id, timeout);
|
||||
}
|
||||
catch
|
||||
{
|
||||
lock (sessionsLock)
|
||||
sessions.Remove(id);
|
||||
s.Destroy();
|
||||
throw;
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
public static string MakeCookie(string Item, string Value, DateTime Expires, string Domain, string Path, bool HttpOnly)
|
||||
/// <summary>
|
||||
/// Looks up a live HTTP session by its cookie identifier.
|
||||
/// </summary>
|
||||
public bool TryGetSession(string id, out HttpSession session)
|
||||
{
|
||||
session = null;
|
||||
if (string.IsNullOrEmpty(id))
|
||||
return false;
|
||||
|
||||
//Set-Cookie: ckGeneric=CookieBody; expires=Sun, 30-Dec-2001 21:00:00 GMT; domain=.com.au; path=/
|
||||
//Set-Cookie: SessionID=another; expires=Fri, 29 Jun 2006 20:47:11 UTC; path=/
|
||||
string Cookie = Item + "=" + Value;
|
||||
lock (sessionsLock)
|
||||
{
|
||||
if (!sessions.TryGetValue(id, out var candidate) || candidate.IsDestroyed)
|
||||
return false;
|
||||
|
||||
if (Expires.Ticks != 0)
|
||||
{
|
||||
Cookie += "; expires=" + Expires.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss") + " GMT";
|
||||
session = candidate;
|
||||
return true;
|
||||
}
|
||||
if (Domain != null)
|
||||
}
|
||||
|
||||
private void SessionEnded(HttpSession session)
|
||||
{
|
||||
RemoveSession(session);
|
||||
session.Destroy();
|
||||
}
|
||||
|
||||
private void SessionDestroyed(object sender)
|
||||
{
|
||||
if (sender is HttpSession session)
|
||||
RemoveSession(session);
|
||||
}
|
||||
|
||||
private void RemoveSession(HttpSession session)
|
||||
{
|
||||
lock (sessionsLock)
|
||||
{
|
||||
Cookie += "; domain=" + Domain;
|
||||
if (session.Id != null &&
|
||||
sessions.TryGetValue(session.Id, out var current) &&
|
||||
ReferenceEquals(current, session))
|
||||
sessions.Remove(session.Id);
|
||||
}
|
||||
if (Path != null)
|
||||
}
|
||||
|
||||
public static string MakeCookie(string Item, string Value, DateTime Expires, string Domain, string Path, bool HttpOnly)
|
||||
=> MakeCookie(
|
||||
Item,
|
||||
Value,
|
||||
Expires,
|
||||
Domain,
|
||||
Path,
|
||||
HttpOnly,
|
||||
false,
|
||||
HttpCookieSameSite.Unspecified);
|
||||
|
||||
public static string MakeCookie(
|
||||
string Item,
|
||||
string Value,
|
||||
DateTime Expires,
|
||||
string Domain,
|
||||
string Path,
|
||||
bool HttpOnly,
|
||||
bool Secure,
|
||||
HttpCookieSameSite SameSite)
|
||||
{
|
||||
return new HttpCookie(Item, Value)
|
||||
{
|
||||
Cookie += "; path=" + Path;
|
||||
}
|
||||
if (HttpOnly)
|
||||
{
|
||||
Cookie += "; HttpOnly";
|
||||
}
|
||||
return Cookie;
|
||||
Expires = Expires,
|
||||
Domain = Domain,
|
||||
Path = Path,
|
||||
HttpOnly = HttpOnly,
|
||||
Secure = Secure,
|
||||
SameSite = SameSite,
|
||||
}.ToString();
|
||||
}
|
||||
|
||||
protected override void ClientDisconnected(HttpConnection connection)
|
||||
@@ -321,6 +449,7 @@ public class HttpServer : NetworkServer<HttpConnection>, IResource
|
||||
else if (operation == ResourceOperation.Terminate)
|
||||
{
|
||||
Stop();
|
||||
DisposeSessions();
|
||||
}
|
||||
else if (operation == ResourceOperation.SystemReloading)
|
||||
{
|
||||
@@ -336,6 +465,19 @@ public class HttpServer : NetworkServer<HttpConnection>, IResource
|
||||
|
||||
}
|
||||
|
||||
private void DisposeSessions()
|
||||
{
|
||||
HttpSession[] activeSessions;
|
||||
lock (sessionsLock)
|
||||
{
|
||||
activeSessions = sessions.Values.ToArray();
|
||||
sessions.Clear();
|
||||
}
|
||||
|
||||
foreach (var activeSession in activeSessions)
|
||||
activeSession.Destroy();
|
||||
}
|
||||
|
||||
|
||||
public override void Add(HttpConnection connection)
|
||||
{
|
||||
|
||||
@@ -44,6 +44,9 @@ public class HttpSession : IDestructible //<T> where T : TClient
|
||||
private string id;
|
||||
private Timer timer;
|
||||
private int timeout;
|
||||
private readonly object timerLock = new object();
|
||||
private long timerGeneration;
|
||||
private bool destroyed;
|
||||
DateTime creation;
|
||||
DateTime lastAction;
|
||||
|
||||
@@ -63,25 +66,44 @@ public class HttpSession : IDestructible //<T> where T : TClient
|
||||
variables = new KeyList<string, object>();
|
||||
variables.OnModified += new KeyList<string, object>.Modified(VariablesModified);
|
||||
creation = DateTime.Now;
|
||||
lastAction = creation;
|
||||
}
|
||||
|
||||
internal void Set(string id, int timeout)
|
||||
{
|
||||
//modified = sessionModifiedEvent;
|
||||
//ended = sessionEndEvent;
|
||||
this.id = id;
|
||||
if (timeout < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(timeout));
|
||||
|
||||
if (this.timeout != 0)
|
||||
lock (timerLock)
|
||||
{
|
||||
if (destroyed)
|
||||
throw new ObjectDisposedException(nameof(HttpSession));
|
||||
|
||||
this.id = id;
|
||||
this.timeout = timeout;
|
||||
timer = new Timer(OnSessionEndTimerCallback, null, TimeSpan.FromSeconds(timeout), TimeSpan.FromSeconds(0));
|
||||
creation = DateTime.Now;
|
||||
lastAction = creation;
|
||||
ScheduleTimerLocked();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSessionEndTimerCallback(object o)
|
||||
{
|
||||
OnEnd?.Invoke(this);
|
||||
SessionEndedEvent onEnd;
|
||||
|
||||
lock (timerLock)
|
||||
{
|
||||
if (destroyed || !(o is long generation) || generation != timerGeneration)
|
||||
return;
|
||||
|
||||
timer?.Dispose();
|
||||
timer = null;
|
||||
onEnd = OnEnd;
|
||||
}
|
||||
|
||||
onEnd?.Invoke(this);
|
||||
}
|
||||
|
||||
void VariablesModified(string key, object oldValue, object newValue, KeyList<string, object> sender)
|
||||
@@ -91,15 +113,54 @@ public class HttpSession : IDestructible //<T> where T : TClient
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
OnDestroy?.Invoke(this);
|
||||
timer.Dispose();
|
||||
timer = null;
|
||||
DestroyedEvent onDestroy;
|
||||
|
||||
lock (timerLock)
|
||||
{
|
||||
if (destroyed)
|
||||
return;
|
||||
|
||||
destroyed = true;
|
||||
timerGeneration++;
|
||||
timer?.Dispose();
|
||||
timer = null;
|
||||
variables.OnModified -= VariablesModified;
|
||||
onDestroy = OnDestroy;
|
||||
OnDestroy = null;
|
||||
OnEnd = null;
|
||||
OnModify = null;
|
||||
}
|
||||
|
||||
onDestroy?.Invoke(this);
|
||||
}
|
||||
|
||||
internal void Refresh()
|
||||
{
|
||||
lastAction = DateTime.Now;
|
||||
timer.Change(TimeSpan.FromSeconds(timeout), TimeSpan.FromSeconds(0));
|
||||
lock (timerLock)
|
||||
{
|
||||
if (destroyed)
|
||||
return;
|
||||
|
||||
lastAction = DateTime.Now;
|
||||
ScheduleTimerLocked();
|
||||
}
|
||||
}
|
||||
|
||||
private void ScheduleTimerLocked()
|
||||
{
|
||||
timerGeneration++;
|
||||
timer?.Dispose();
|
||||
timer = null;
|
||||
|
||||
if (timeout <= 0)
|
||||
return;
|
||||
|
||||
var generation = timerGeneration;
|
||||
timer = new Timer(
|
||||
OnSessionEndTimerCallback,
|
||||
generation,
|
||||
TimeSpan.FromSeconds(timeout),
|
||||
System.Threading.Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
|
||||
public int Timeout // Seconds
|
||||
@@ -110,8 +171,18 @@ public class HttpSession : IDestructible //<T> where T : TClient
|
||||
}
|
||||
set
|
||||
{
|
||||
timeout = value;
|
||||
Refresh();
|
||||
if (value < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
|
||||
lock (timerLock)
|
||||
{
|
||||
if (destroyed)
|
||||
return;
|
||||
|
||||
timeout = value;
|
||||
lastAction = DateTime.Now;
|
||||
ScheduleTimerLocked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,5 +195,14 @@ public class HttpSession : IDestructible //<T> where T : TClient
|
||||
{
|
||||
get { return lastAction; }
|
||||
}
|
||||
|
||||
internal bool IsDestroyed
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (timerLock)
|
||||
return destroyed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
|
||||
Copyright (c) 2017 Ahmed Kh. Zamil
|
||||
|
||||
@@ -23,31 +23,25 @@ SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Esiur.Data;
|
||||
using Esiur.Misc;
|
||||
|
||||
namespace Esiur.Net;
|
||||
|
||||
public class NetworkBuffer
|
||||
{
|
||||
byte[] data;
|
||||
private static readonly byte[] Empty = Array.Empty<byte>();
|
||||
|
||||
uint neededDataLength = 0;
|
||||
|
||||
object syncLock = new object();
|
||||
|
||||
public NetworkBuffer()
|
||||
{
|
||||
data = new byte[0];
|
||||
}
|
||||
private readonly object syncLock = new object();
|
||||
private byte[] buffer = Empty;
|
||||
private int start;
|
||||
private int length;
|
||||
private uint neededDataLength;
|
||||
|
||||
public bool Protected
|
||||
{
|
||||
get
|
||||
{
|
||||
return neededDataLength > data.Length;
|
||||
lock (syncLock)
|
||||
return (uint)length < neededDataLength;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,76 +49,88 @@ public class NetworkBuffer
|
||||
{
|
||||
get
|
||||
{
|
||||
return (uint)data.Length;
|
||||
lock (syncLock)
|
||||
return (uint)length;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void HoldForNextWrite(byte[] src)
|
||||
{
|
||||
HoldFor(src, (uint)src.Length + 1);
|
||||
if (src == null)
|
||||
throw new ArgumentNullException(nameof(src));
|
||||
|
||||
HoldFor(src, 0, (uint)src.Length, CheckedNextLength((uint)src.Length));
|
||||
}
|
||||
|
||||
public void HoldForNextWrite(byte[] src, uint offset, uint size)
|
||||
{
|
||||
HoldFor(src, offset, size, size + 1);
|
||||
}
|
||||
|
||||
=> HoldFor(src, offset, size, CheckedNextLength(size));
|
||||
|
||||
public void HoldFor(byte[] src, uint offset, uint size, uint needed)
|
||||
{
|
||||
ValidateRange(src, offset, size);
|
||||
ValidateNeededLength(needed);
|
||||
|
||||
if (size >= needed)
|
||||
// Preserve the historical exception contract for this semantic error.
|
||||
throw new Exception("Size >= Needed !");
|
||||
|
||||
lock (syncLock)
|
||||
{
|
||||
if (size >= needed)
|
||||
throw new Exception("Size >= Needed !");
|
||||
|
||||
//trim = true;
|
||||
data = DC.Combine(src, offset, size, data, 0, (uint)data.Length);
|
||||
Prepend(src, (int)offset, (int)size);
|
||||
neededDataLength = needed;
|
||||
}
|
||||
}
|
||||
|
||||
public void HoldFor(byte[] src, uint needed)
|
||||
{
|
||||
if (src == null)
|
||||
throw new ArgumentNullException(nameof(src));
|
||||
|
||||
HoldFor(src, 0, (uint)src.Length, needed);
|
||||
}
|
||||
|
||||
public bool Protect(byte[] data, uint offset, uint needed)
|
||||
{
|
||||
uint dataLength = (uint)data.Length - offset;
|
||||
if (data == null)
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
if (offset > (uint)data.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(offset));
|
||||
|
||||
// protection
|
||||
if (dataLength < needed)
|
||||
{
|
||||
HoldFor(data, offset, dataLength, needed);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
ValidateNeededLength(needed);
|
||||
|
||||
var dataLength = (uint)data.Length - offset;
|
||||
if (dataLength >= needed)
|
||||
return false;
|
||||
|
||||
// HoldFor validates that dataLength is strictly less than needed.
|
||||
HoldFor(data, offset, dataLength, needed);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Write(byte[] src)
|
||||
{
|
||||
if (src == null)
|
||||
throw new ArgumentNullException(nameof(src));
|
||||
|
||||
Write(src, 0, (uint)src.Length);
|
||||
}
|
||||
|
||||
public void Write(byte[] src, uint offset, uint length)
|
||||
{
|
||||
ValidateRange(src, offset, length);
|
||||
if (length == 0)
|
||||
return;
|
||||
|
||||
lock (syncLock)
|
||||
DC.Append(ref data, src, offset, length);
|
||||
Append(src, (int)offset, (int)length);
|
||||
}
|
||||
|
||||
public bool CanRead
|
||||
{
|
||||
get
|
||||
{
|
||||
if (data.Length == 0)
|
||||
return false;
|
||||
if (data.Length < neededDataLength)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
lock (syncLock)
|
||||
return length != 0 && (uint)length >= neededDataLength;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,34 +138,176 @@ public class NetworkBuffer
|
||||
{
|
||||
lock (syncLock)
|
||||
{
|
||||
if (data.Length == 0)
|
||||
if (length == 0 || (uint)length < neededDataLength)
|
||||
return null;
|
||||
|
||||
byte[] rt = null;
|
||||
byte[] result;
|
||||
|
||||
if (neededDataLength == 0)
|
||||
// A single write, and geometrically grown power-of-two payloads, can transfer
|
||||
// ownership without one final copy. Otherwise return an exact-size array, as the
|
||||
// historical API did, and release the working buffer.
|
||||
if (start == 0 && length == buffer.Length)
|
||||
{
|
||||
rt = data;
|
||||
data = new byte[0];
|
||||
return rt;
|
||||
result = buffer;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
if (data.Length >= neededDataLength)
|
||||
{
|
||||
rt = data;
|
||||
data = new byte[0];
|
||||
|
||||
neededDataLength = 0;
|
||||
return rt;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
result = new byte[length];
|
||||
Buffer.BlockCopy(buffer, start, result, 0, length);
|
||||
}
|
||||
|
||||
buffer = Empty;
|
||||
start = 0;
|
||||
length = 0;
|
||||
neededDataLength = 0;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private void Append(byte[] src, int offset, int count)
|
||||
{
|
||||
if (length == 0 && buffer.Length == 0)
|
||||
{
|
||||
// The overwhelmingly common path is one socket read followed by one Read().
|
||||
// Allocate exactly once so Read can transfer this array without copying it.
|
||||
buffer = new byte[count];
|
||||
Buffer.BlockCopy(src, offset, buffer, 0, count);
|
||||
start = 0;
|
||||
length = count;
|
||||
return;
|
||||
}
|
||||
|
||||
var requiredLength = CheckedCombinedLength(length, count);
|
||||
var writeOffset = start + length;
|
||||
|
||||
if (buffer.Length - writeOffset < count)
|
||||
{
|
||||
if (requiredLength <= buffer.Length)
|
||||
{
|
||||
// Reuse headroom left by a prepend operation.
|
||||
Buffer.BlockCopy(buffer, start, buffer, 0, length);
|
||||
start = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Grow(requiredLength, prependLength: 0);
|
||||
}
|
||||
|
||||
writeOffset = start + length;
|
||||
}
|
||||
|
||||
Buffer.BlockCopy(src, offset, buffer, writeOffset, count);
|
||||
length = requiredLength;
|
||||
}
|
||||
|
||||
private void Prepend(byte[] src, int offset, int count)
|
||||
{
|
||||
if (count == 0)
|
||||
return;
|
||||
|
||||
if (length == 0 && buffer.Length == 0)
|
||||
{
|
||||
buffer = new byte[count];
|
||||
Buffer.BlockCopy(src, offset, buffer, 0, count);
|
||||
start = 0;
|
||||
length = count;
|
||||
return;
|
||||
}
|
||||
|
||||
var requiredLength = CheckedCombinedLength(length, count);
|
||||
|
||||
if (start >= count)
|
||||
{
|
||||
start -= count;
|
||||
}
|
||||
else if (requiredLength <= buffer.Length)
|
||||
{
|
||||
// Re-center the live region once and leave any remaining spare capacity around it.
|
||||
var combinedStart = (buffer.Length - requiredLength) / 2;
|
||||
Buffer.BlockCopy(buffer, start, buffer, combinedStart + count, length);
|
||||
start = combinedStart;
|
||||
}
|
||||
else
|
||||
{
|
||||
Grow(requiredLength, count);
|
||||
}
|
||||
|
||||
Buffer.BlockCopy(src, offset, buffer, start, count);
|
||||
length = requiredLength;
|
||||
}
|
||||
|
||||
private void Grow(int requiredLength, int prependLength)
|
||||
{
|
||||
var newCapacity = GetExpandedCapacity(buffer.Length, requiredLength);
|
||||
var replacement = new byte[newCapacity];
|
||||
|
||||
if (prependLength == 0)
|
||||
{
|
||||
if (length > 0)
|
||||
Buffer.BlockCopy(buffer, start, replacement, 0, length);
|
||||
start = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var combinedStart = (newCapacity - requiredLength) / 2;
|
||||
if (length > 0)
|
||||
Buffer.BlockCopy(buffer, start, replacement, combinedStart + prependLength, length);
|
||||
start = combinedStart;
|
||||
}
|
||||
|
||||
buffer = replacement;
|
||||
}
|
||||
|
||||
private static int GetExpandedCapacity(int currentCapacity, int requiredLength)
|
||||
{
|
||||
var capacity = currentCapacity == 0 ? 256 : currentCapacity;
|
||||
|
||||
while (capacity < requiredLength)
|
||||
{
|
||||
if (capacity > int.MaxValue / 2)
|
||||
return requiredLength;
|
||||
|
||||
capacity *= 2;
|
||||
}
|
||||
|
||||
return capacity;
|
||||
}
|
||||
|
||||
private static int CheckedCombinedLength(int currentLength, int additionalLength)
|
||||
{
|
||||
if (additionalLength > int.MaxValue - currentLength)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(additionalLength),
|
||||
"The buffered data exceeds the maximum managed array length.");
|
||||
|
||||
return currentLength + additionalLength;
|
||||
}
|
||||
|
||||
private static uint CheckedNextLength(uint size)
|
||||
{
|
||||
if (size >= int.MaxValue)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(size),
|
||||
"The requested held length exceeds the maximum managed array length.");
|
||||
|
||||
return size + 1;
|
||||
}
|
||||
|
||||
private static void ValidateNeededLength(uint needed)
|
||||
{
|
||||
if (needed > int.MaxValue)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(needed),
|
||||
"The requested held length exceeds the maximum managed array length.");
|
||||
}
|
||||
|
||||
private static void ValidateRange(byte[] src, uint offset, uint count)
|
||||
{
|
||||
if (src == null)
|
||||
throw new ArgumentNullException(nameof(src));
|
||||
if (offset > (uint)src.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(offset));
|
||||
if (count > (uint)src.Length - offset)
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
@@ -39,13 +40,27 @@ namespace Esiur.Net;
|
||||
/// </summary>
|
||||
public abstract class NetworkConnection : IDestructible, INetworkReceiver<ISocket>
|
||||
{
|
||||
private ISocket sock;
|
||||
private volatile ISocket sock;
|
||||
private DateTime lastAction;
|
||||
|
||||
// Re-entrancy guard for NetworkReceive. 0 = idle, 1 = a thread is draining the buffer.
|
||||
// Interlocked is used instead of a plain bool so concurrent receive callbacks cannot
|
||||
// both enter the drain loop (which is not safe to run from two threads at once).
|
||||
private int receiving;
|
||||
private readonly object receiveLock = new object();
|
||||
private readonly Queue<PendingReceive> pendingReceives = new Queue<PendingReceive>();
|
||||
private bool receiving;
|
||||
private long socketGeneration;
|
||||
|
||||
private readonly struct PendingReceive
|
||||
{
|
||||
public PendingReceive(ISocket sender, NetworkBuffer buffer, long generation)
|
||||
{
|
||||
Sender = sender;
|
||||
Buffer = buffer;
|
||||
Generation = generation;
|
||||
}
|
||||
|
||||
public ISocket Sender { get; }
|
||||
public NetworkBuffer Buffer { get; }
|
||||
public long Generation { get; }
|
||||
}
|
||||
|
||||
public delegate void NetworkConnectionEvent(NetworkConnection connection);
|
||||
|
||||
@@ -69,9 +84,19 @@ public abstract class NetworkConnection : IDestructible, INetworkReceiver<ISocke
|
||||
|
||||
public virtual void Assign(ISocket socket)
|
||||
{
|
||||
lastAction = DateTime.Now;
|
||||
sock = socket;
|
||||
sock.Receiver = this;
|
||||
lock (receiveLock)
|
||||
{
|
||||
lastAction = DateTime.Now;
|
||||
|
||||
if (!ReferenceEquals(sock, socket))
|
||||
{
|
||||
socketGeneration++;
|
||||
pendingReceives.Clear();
|
||||
}
|
||||
|
||||
sock = socket;
|
||||
sock.Receiver = this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -80,14 +105,19 @@ public abstract class NetworkConnection : IDestructible, INetworkReceiver<ISocke
|
||||
/// </summary>
|
||||
public ISocket Unassign()
|
||||
{
|
||||
if (sock == null)
|
||||
return null;
|
||||
lock (receiveLock)
|
||||
{
|
||||
if (sock == null)
|
||||
return null;
|
||||
|
||||
sock.Receiver = null;
|
||||
sock.Receiver = null;
|
||||
|
||||
var detached = sock;
|
||||
sock = null;
|
||||
return detached;
|
||||
var detached = sock;
|
||||
sock = null;
|
||||
socketGeneration++;
|
||||
pendingReceives.Clear();
|
||||
return detached;
|
||||
}
|
||||
}
|
||||
|
||||
public void Close()
|
||||
@@ -163,12 +193,24 @@ public abstract class NetworkConnection : IDestructible, INetworkReceiver<ISocke
|
||||
|
||||
public void NetworkClose(ISocket socket)
|
||||
{
|
||||
lock (receiveLock)
|
||||
{
|
||||
if (!ReferenceEquals(socket, sock))
|
||||
return;
|
||||
|
||||
pendingReceives.Clear();
|
||||
}
|
||||
|
||||
Disconnected();
|
||||
OnClose?.Invoke(this);
|
||||
}
|
||||
|
||||
public void NetworkConnect(ISocket socket)
|
||||
{
|
||||
lock (receiveLock)
|
||||
if (!ReferenceEquals(socket, sock))
|
||||
return;
|
||||
|
||||
Connected();
|
||||
OnConnect?.Invoke(this);
|
||||
}
|
||||
@@ -181,30 +223,95 @@ public abstract class NetworkConnection : IDestructible, INetworkReceiver<ISocke
|
||||
{
|
||||
try
|
||||
{
|
||||
// Ignore callbacks once the socket is unassigned or closed.
|
||||
if (sock == null || sock.State == SocketState.Closed)
|
||||
return;
|
||||
|
||||
lastAction = DateTime.Now;
|
||||
|
||||
// Only one thread drains the buffer at a time; others return immediately and
|
||||
// rely on the active drainer to pick up the newly appended data.
|
||||
if (Interlocked.CompareExchange(ref receiving, 1, 0) != 0)
|
||||
return;
|
||||
|
||||
try
|
||||
bool drain;
|
||||
lock (receiveLock)
|
||||
{
|
||||
while (buffer.Available > 0 && !buffer.Protected)
|
||||
DataReceived(buffer);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref receiving, 0);
|
||||
// A callback can outlive Unassign/Assign. Only the socket that currently
|
||||
// owns this connection may enqueue work for its protocol parser.
|
||||
if (!ReferenceEquals(sender, sock) || sender.State == SocketState.Closed)
|
||||
return;
|
||||
|
||||
lastAction = DateTime.Now;
|
||||
pendingReceives.Enqueue(new PendingReceive(sender, buffer, socketGeneration));
|
||||
|
||||
if (receiving)
|
||||
return;
|
||||
|
||||
receiving = true;
|
||||
drain = true;
|
||||
}
|
||||
|
||||
if (drain)
|
||||
DrainReceiveQueue();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Global.Log("NetworkConnection:NetworkReceive", LogType.Warning, ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private void DrainReceiveQueue()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
PendingReceive pending = default;
|
||||
var found = false;
|
||||
|
||||
lock (receiveLock)
|
||||
{
|
||||
while (pendingReceives.Count > 0)
|
||||
{
|
||||
var candidate = pendingReceives.Dequeue();
|
||||
if (ReferenceEquals(candidate.Sender, sock)
|
||||
&& candidate.Generation == socketGeneration)
|
||||
{
|
||||
pending = candidate;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
receiving = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
while (IsCurrentReceive(pending)
|
||||
&& pending.Buffer.Available > 0
|
||||
&& !pending.Buffer.Protected)
|
||||
{
|
||||
DataReceived(pending.Buffer);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Keep the queue usable after a protocol parser fails. Any work already
|
||||
// queued for a replacement socket can still be drained safely.
|
||||
Global.Log("NetworkConnection:NetworkReceive", LogType.Warning, ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsCurrentReceive(PendingReceive pending)
|
||||
{
|
||||
lock (receiveLock)
|
||||
{
|
||||
if (!ReferenceEquals(pending.Sender, sock)
|
||||
|| pending.Generation != socketGeneration)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
return pending.Sender.State != SocketState.Closed;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,13 +37,20 @@ namespace Esiur.Net;
|
||||
|
||||
public abstract class NetworkServer<TConnection> : IDestructible where TConnection : NetworkConnection, new()
|
||||
{
|
||||
private Sockets.ISocket listener;
|
||||
private volatile Sockets.ISocket listener;
|
||||
private readonly object lifecycleLock = new object();
|
||||
public AutoList<TConnection, NetworkServer<TConnection>> Connections { get; internal set; }
|
||||
|
||||
private Thread thread;
|
||||
|
||||
private Timer timer;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum time allowed for an accepted socket to finish protocol initialization
|
||||
/// (for example, a TLS handshake). A zero or negative value disables the deadline.
|
||||
/// </summary>
|
||||
public TimeSpan ConnectionInitializationTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||
|
||||
public event DestroyedEvent OnDestroy;
|
||||
|
||||
|
||||
@@ -78,68 +85,142 @@ public abstract class NetworkServer<TConnection> : IDestructible where TConnecti
|
||||
|
||||
public void Start(Sockets.ISocket socket)//, uint timeout, uint clock)
|
||||
{
|
||||
if (listener != null)
|
||||
return;
|
||||
if (socket == null)
|
||||
throw new ArgumentNullException(nameof(socket));
|
||||
|
||||
|
||||
Connections = new AutoList<TConnection, NetworkServer<TConnection>>(this);
|
||||
|
||||
|
||||
if (Timeout > 0 & Clock > 0)
|
||||
lock (lifecycleLock)
|
||||
{
|
||||
timer = new Timer(MinuteThread, null, TimeSpan.FromMinutes(0), TimeSpan.FromSeconds(Clock));
|
||||
}
|
||||
if (listener != null)
|
||||
return;
|
||||
|
||||
Connections = new AutoList<TConnection, NetworkServer<TConnection>>(this);
|
||||
|
||||
listener = socket;
|
||||
|
||||
thread = new Thread(new ThreadStart(() =>
|
||||
{
|
||||
while (true)
|
||||
if (Timeout > 0 && Clock > 0)
|
||||
{
|
||||
timer = new Timer(MinuteThread, null, TimeSpan.FromMinutes(0), TimeSpan.FromSeconds(Clock));
|
||||
}
|
||||
|
||||
listener = socket;
|
||||
|
||||
// Bind this thread to this particular Start invocation. If the server is
|
||||
// stopped and restarted before the old Accept call unwinds, the old thread
|
||||
// must not begin accepting from the replacement listener.
|
||||
thread = new Thread(() => AcceptLoop(socket))
|
||||
{
|
||||
IsBackground = true
|
||||
};
|
||||
thread.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void AcceptLoop(ISocket activeListener)
|
||||
{
|
||||
while (ReferenceEquals(listener, activeListener))
|
||||
{
|
||||
try
|
||||
{
|
||||
var acceptedSocket = activeListener.Accept();
|
||||
|
||||
if (acceptedSocket == null)
|
||||
return;
|
||||
|
||||
TConnection connection = null;
|
||||
var stopped = false;
|
||||
|
||||
// Admission and Stop's connection snapshot share this gate. Therefore
|
||||
// either the connection is added before Stop snapshots it, or Stop wins
|
||||
// and this accepted socket is closed without being exposed to the server.
|
||||
lock (lifecycleLock)
|
||||
{
|
||||
if (!ReferenceEquals(listener, activeListener))
|
||||
{
|
||||
stopped = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
connection = new TConnection();
|
||||
connection.Assign(acceptedSocket);
|
||||
Add(connection);
|
||||
stopped = !ReferenceEquals(listener, activeListener);
|
||||
}
|
||||
}
|
||||
|
||||
if (stopped)
|
||||
{
|
||||
try { acceptedSocket.Close(); } catch { }
|
||||
return;
|
||||
}
|
||||
|
||||
// A derived server can reject admission (for example, due to a per-peer
|
||||
// connection quota) by not adding the connection and closing its socket.
|
||||
if (!Connections.Contains(connection))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
var s = listener.Accept();
|
||||
|
||||
if (s == null)
|
||||
{
|
||||
//Global.Log("NetworkServer", LogType.Error, "sock == null");
|
||||
return;
|
||||
}
|
||||
|
||||
var c = new TConnection();
|
||||
|
||||
c.Assign(s);
|
||||
Add(c);
|
||||
|
||||
// A derived server can reject admission (for example, due to a per-peer
|
||||
// connection quota) by not adding the connection and closing its socket.
|
||||
if (!Connections.Contains(c))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
ClientConnected(c);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// something wrong with the child.
|
||||
}
|
||||
|
||||
s.Begin();
|
||||
|
||||
ClientConnected(connection);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
Global.Log(ex);
|
||||
// something wrong with the child.
|
||||
}
|
||||
|
||||
if (!ReferenceEquals(listener, activeListener))
|
||||
{
|
||||
try { connection.Close(); } catch { }
|
||||
return;
|
||||
}
|
||||
|
||||
// Some socket implementations perform a protocol handshake in Begin
|
||||
// (notably SSLSocket). Never run that handshake on the single accept
|
||||
// thread: a peer that stops mid-handshake would otherwise prevent all
|
||||
// subsequent clients from being accepted.
|
||||
_ = BeginAcceptedSocketAsync(acceptedSocket);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!ReferenceEquals(listener, activeListener))
|
||||
return;
|
||||
|
||||
Global.Log(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BeginAcceptedSocketAsync(ISocket socket)
|
||||
{
|
||||
try
|
||||
{
|
||||
var beginTask = AwaitSocketBeginAsync(socket);
|
||||
var timeout = ConnectionInitializationTimeout;
|
||||
|
||||
if (timeout > TimeSpan.Zero)
|
||||
{
|
||||
var completed = await Task.WhenAny(beginTask, Task.Delay(timeout)).ConfigureAwait(false);
|
||||
if (!ReferenceEquals(completed, beginTask))
|
||||
{
|
||||
try { socket.Close(); } catch { }
|
||||
_ = beginTask.ContinueWith(
|
||||
completedTask => _ = completedTask.Exception,
|
||||
TaskContinuationOptions.OnlyOnFaulted);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
thread.Start();
|
||||
|
||||
if (!await beginTask.ConfigureAwait(false))
|
||||
try { socket.Close(); } catch { }
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Global.Log("NetworkServer", LogType.Warning,
|
||||
$"Accepted socket initialization failed: {ex.Message}");
|
||||
try { socket.Close(); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<bool> AwaitSocketBeginAsync(ISocket socket)
|
||||
=> await socket.BeginAsync();
|
||||
|
||||
|
||||
//[Attribute]
|
||||
public uint Timeout
|
||||
@@ -160,25 +241,39 @@ public abstract class NetworkServer<TConnection> : IDestructible where TConnecti
|
||||
public void Stop()
|
||||
{
|
||||
var port = 0;
|
||||
ISocket currentListener = null;
|
||||
TConnection[] connections = null;
|
||||
Timer currentTimer = null;
|
||||
|
||||
try
|
||||
{
|
||||
var currentListener = listener;
|
||||
lock (lifecycleLock)
|
||||
{
|
||||
currentListener = listener;
|
||||
listener = null;
|
||||
connections = Connections?.ToArray();
|
||||
currentTimer = timer;
|
||||
timer = null;
|
||||
}
|
||||
|
||||
if (currentListener != null)
|
||||
{
|
||||
// Reading the endpoint can throw if the socket is already disposed (e.g. a second
|
||||
// Stop or the finalizer after Destroy), so it is best-effort and only used for logging.
|
||||
try { port = currentListener.LocalEndPoint.Port; } catch { }
|
||||
try { currentListener.Close(); } catch { }
|
||||
listener = null; // make Stop idempotent
|
||||
}
|
||||
|
||||
foreach (TConnection con in Connections.ToArray())
|
||||
try { con.Close(); } catch { }
|
||||
if (connections != null)
|
||||
{
|
||||
foreach (TConnection con in connections)
|
||||
try { con.Close(); } catch { }
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Global.Log("NetworkServer", LogType.Warning, $"Server@{port} is down.");
|
||||
try { currentTimer?.Dispose(); } catch { }
|
||||
Global.Log("NetworkServer", LogType.Warning, $"Server on port {port} is down.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +295,8 @@ public abstract class NetworkServer<TConnection> : IDestructible where TConnecti
|
||||
{
|
||||
get
|
||||
{
|
||||
return listener.State == SocketState.Listening;
|
||||
var currentListener = listener;
|
||||
return currentListener != null && currentListener.State == SocketState.Listening;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -125,19 +125,6 @@ public class EpAuthPacket : Packet
|
||||
set;
|
||||
}
|
||||
|
||||
private uint dataLengthNeeded;
|
||||
|
||||
bool NotEnough(uint offset, uint ends, uint needed)
|
||||
{
|
||||
if (offset + needed > ends)
|
||||
{
|
||||
dataLengthNeeded = needed - (ends - offset);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Command.ToString() + " " + Method.ToString();
|
||||
@@ -145,12 +132,13 @@ public class EpAuthPacket : Packet
|
||||
|
||||
public override long Parse(byte[] data, uint offset, uint ends)
|
||||
{
|
||||
ValidateBounds(data, offset, ends);
|
||||
Tdu = null;
|
||||
|
||||
var oOffset = offset;
|
||||
|
||||
if (NotEnough(offset, ends, 1))
|
||||
return -dataLengthNeeded;
|
||||
if (TryGetMissingBytes(offset, ends, 1, out var incomplete))
|
||||
return incomplete;
|
||||
|
||||
Command = (EpAuthPacketCommand)(data[offset] >> 6);
|
||||
var hasTdu = (data[offset] & 0x20) != 0;
|
||||
@@ -183,18 +171,21 @@ public class EpAuthPacket : Packet
|
||||
|
||||
if (hasTdu)
|
||||
{
|
||||
if (NotEnough(offset, ends, 1))
|
||||
return -dataLengthNeeded;
|
||||
if (TryGetMissingBytes(offset, ends, 1, out incomplete))
|
||||
return incomplete;
|
||||
|
||||
var maximumPacketSize = _warehouse.Configuration.Parser.MaximumPacketSize;
|
||||
var maximumPayloadLength = maximumPacketSize == 0
|
||||
? (ulong)int.MaxValue
|
||||
: Math.Min(maximumPacketSize, (ulong)int.MaxValue);
|
||||
Tdu = PlainTdu.Parse(
|
||||
data,
|
||||
offset,
|
||||
ends,
|
||||
maximumPacketSize == 0 ? ulong.MaxValue : maximumPacketSize);
|
||||
maximumPayloadLength);
|
||||
|
||||
if (Tdu.Value.Class == TduClass.Invalid)
|
||||
return -(int)Tdu.Value.TotalLength;
|
||||
return -(long)Tdu.Value.TotalLength;
|
||||
|
||||
offset += (uint)Tdu.Value.TotalLength;
|
||||
|
||||
|
||||
@@ -51,9 +51,6 @@ class EpPacket : Packet
|
||||
|
||||
public PlainTdu? Tdu { get; set; }
|
||||
|
||||
private uint dataLengthNeeded;
|
||||
private uint originalOffset;
|
||||
|
||||
Warehouse _warehouse;
|
||||
|
||||
public EpPacket(Warehouse warehouse)
|
||||
@@ -61,11 +58,6 @@ class EpPacket : Packet
|
||||
_warehouse = warehouse;
|
||||
}
|
||||
|
||||
public override bool Compose()
|
||||
{
|
||||
return base.Compose();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Method switch
|
||||
@@ -78,24 +70,13 @@ class EpPacket : Packet
|
||||
};
|
||||
}
|
||||
|
||||
bool NotEnough(uint offset, uint ends, uint needed)
|
||||
{
|
||||
if (offset + needed > ends)
|
||||
{
|
||||
dataLengthNeeded = needed - (ends - offset);
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
public override long Parse(byte[] data, uint offset, uint ends)
|
||||
{
|
||||
originalOffset = offset;
|
||||
ValidateBounds(data, offset, ends);
|
||||
var originalOffset = offset;
|
||||
|
||||
if (NotEnough(offset, ends, 1))
|
||||
return -dataLengthNeeded;
|
||||
if (TryGetMissingBytes(offset, ends, 1, out var incomplete))
|
||||
return incomplete;
|
||||
|
||||
var hasDTU = (data[offset] & 0x20) == 0x20;
|
||||
|
||||
@@ -109,8 +90,8 @@ class EpPacket : Packet
|
||||
{
|
||||
Request = (EpPacketRequest)(data[offset++] & 0x1f);
|
||||
|
||||
if (NotEnough(offset, ends, 4))
|
||||
return -dataLengthNeeded;
|
||||
if (TryGetMissingBytes(offset, ends, 4, out incomplete))
|
||||
return incomplete;
|
||||
|
||||
CallbackId = data.GetUInt32(offset, Endian.Little);
|
||||
offset += 4;
|
||||
@@ -119,8 +100,8 @@ class EpPacket : Packet
|
||||
{
|
||||
Reply = (EpPacketReply)(data[offset++] & 0x1f);
|
||||
|
||||
if (NotEnough(offset, ends, 4))
|
||||
return -dataLengthNeeded;
|
||||
if (TryGetMissingBytes(offset, ends, 4, out incomplete))
|
||||
return incomplete;
|
||||
|
||||
CallbackId = data.GetUInt32(offset, Endian.Little);
|
||||
offset += 4;
|
||||
@@ -132,30 +113,26 @@ class EpPacket : Packet
|
||||
|
||||
if (hasDTU)
|
||||
{
|
||||
if (NotEnough(offset, ends, 1))
|
||||
return -dataLengthNeeded;
|
||||
if (TryGetMissingBytes(offset, ends, 1, out incomplete))
|
||||
return incomplete;
|
||||
|
||||
var maximumPacketSize = _warehouse.Configuration.Parser.MaximumPacketSize;
|
||||
var maximumPayloadLength = maximumPacketSize == 0
|
||||
? (ulong)int.MaxValue
|
||||
: Math.Min(maximumPacketSize, (ulong)int.MaxValue);
|
||||
Tdu = PlainTdu.Parse(
|
||||
data,
|
||||
offset,
|
||||
ends,
|
||||
maximumPacketSize == 0 ? ulong.MaxValue : maximumPacketSize);
|
||||
maximumPayloadLength);
|
||||
|
||||
if (Tdu.Value.Class == TduClass.Invalid)
|
||||
return -(int)Tdu.Value.TotalLength;
|
||||
return -(long)Tdu.Value.TotalLength;
|
||||
|
||||
//Tdu = ParsedTdu.ParseSync(data, offset, ends, _warehouse);
|
||||
|
||||
//if (Tdu.Value.Class == TduClass.Invalid)
|
||||
// return -(int)Tdu.Value.TotalLength;
|
||||
|
||||
//offset += (uint)Tdu.Value.TotalLength;
|
||||
offset += (uint)Tdu.Value.TotalLength;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Tdu = null;
|
||||
Tdu = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace Esiur.Net.Packets.Http
|
||||
{
|
||||
public enum HttpCookieSameSite
|
||||
{
|
||||
Unspecified,
|
||||
Lax,
|
||||
Strict,
|
||||
None,
|
||||
}
|
||||
|
||||
public struct HttpCookie
|
||||
{
|
||||
public string Name;
|
||||
@@ -11,6 +20,8 @@ namespace Esiur.Net.Packets.Http
|
||||
public DateTime Expires;
|
||||
public string Path;
|
||||
public bool HttpOnly;
|
||||
public bool Secure;
|
||||
public HttpCookieSameSite SameSite;
|
||||
public string Domain;
|
||||
|
||||
public HttpCookie(string name, string value)
|
||||
@@ -20,6 +31,8 @@ namespace Esiur.Net.Packets.Http
|
||||
Path = null;
|
||||
Expires = DateTime.MinValue;
|
||||
HttpOnly = false;
|
||||
Secure = false;
|
||||
SameSite = HttpCookieSameSite.Unspecified;
|
||||
Domain = null;
|
||||
}
|
||||
|
||||
@@ -29,6 +42,8 @@ namespace Esiur.Net.Packets.Http
|
||||
Value = value;
|
||||
Expires = expires;
|
||||
HttpOnly = false;
|
||||
Secure = false;
|
||||
SameSite = HttpCookieSameSite.Unspecified;
|
||||
Domain = null;
|
||||
Path = null;
|
||||
}
|
||||
@@ -40,7 +55,7 @@ namespace Esiur.Net.Packets.Http
|
||||
var cookie = Name + "=" + Value;
|
||||
|
||||
if (Expires.Ticks != 0)
|
||||
cookie += "; expires=" + Expires.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss") + " GMT";
|
||||
cookie += "; expires=" + Expires.ToUniversalTime().ToString("r", CultureInfo.InvariantCulture);
|
||||
|
||||
if (Domain != null)
|
||||
cookie += "; domain=" + Domain;
|
||||
@@ -51,6 +66,12 @@ namespace Esiur.Net.Packets.Http
|
||||
if (HttpOnly)
|
||||
cookie += "; HttpOnly";
|
||||
|
||||
if (Secure)
|
||||
cookie += "; Secure";
|
||||
|
||||
if (SameSite != HttpCookieSameSite.Unspecified)
|
||||
cookie += "; SameSite=" + SameSite;
|
||||
|
||||
return cookie;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
using Esiur.Data;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Esiur.Net.Packets.Http;
|
||||
|
||||
internal static class HttpPacketHelpers
|
||||
{
|
||||
internal const uint DefaultMaximumHeaderLength = 64 * 1024;
|
||||
internal const uint DefaultMaximumContentLength = 8 * 1024 * 1024;
|
||||
internal const int DefaultMaximumHeaderCount = 100;
|
||||
internal const int DefaultMaximumFormFields = 1_024;
|
||||
internal const int DefaultMaximumFormKeyLength = 2_048;
|
||||
internal const int DefaultMaximumFormValueLength = 1024 * 1024;
|
||||
internal const int DefaultMaximumMultipartPartLength = 4 * 1024 * 1024;
|
||||
|
||||
internal static bool TryFindHeaderEnd(
|
||||
byte[] data,
|
||||
uint offset,
|
||||
uint ends,
|
||||
uint maximumHeaderLength,
|
||||
out uint bodyOffset)
|
||||
{
|
||||
bodyOffset = 0;
|
||||
var available = ends - offset;
|
||||
var scanLength = maximumHeaderLength == 0
|
||||
? available
|
||||
: available < maximumHeaderLength ? available : maximumHeaderLength;
|
||||
|
||||
if (scanLength >= 4)
|
||||
{
|
||||
var scanEnds = offset + scanLength;
|
||||
for (var i = offset; i <= scanEnds - 4; i++)
|
||||
{
|
||||
if (data[i] == '\r' && data[i + 1] == '\n' &&
|
||||
data[i + 2] == '\r' && data[i + 3] == '\n')
|
||||
{
|
||||
bodyOffset = i + 4;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (maximumHeaderLength > 0 && available >= maximumHeaderLength)
|
||||
throw new ParserLimitException(
|
||||
$"HTTP header exceeds the {maximumHeaderLength}-byte limit.");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static string[] ReadHeaderLines(
|
||||
byte[] data,
|
||||
uint offset,
|
||||
uint bodyOffset,
|
||||
int maximumHeaderCount)
|
||||
{
|
||||
var headerContentLength = bodyOffset - offset - 4;
|
||||
var headerEnd = offset + headerContentLength;
|
||||
var headerCount = 0;
|
||||
|
||||
// Count before Split allocates its result so a header made of thousands of
|
||||
// tiny lines is rejected without creating thousands of strings first.
|
||||
for (var i = offset; i + 1 < headerEnd; i++)
|
||||
{
|
||||
if (data[i] != '\r' || data[i + 1] != '\n')
|
||||
continue;
|
||||
|
||||
headerCount++;
|
||||
if (maximumHeaderCount > 0 && headerCount > maximumHeaderCount)
|
||||
throw new ParserLimitException(
|
||||
$"HTTP header count exceeds the {maximumHeaderCount}-header limit.");
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return Encoding.ASCII
|
||||
.GetString(data, (int)offset, (int)headerContentLength)
|
||||
.Split(new[] { "\r\n" }, StringSplitOptions.None);
|
||||
}
|
||||
}
|
||||
@@ -1,303 +1,358 @@
|
||||
|
||||
/*
|
||||
|
||||
Copyright (c) 2017 Ahmed Kh. Zamil
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Esiur.Misc;
|
||||
using Esiur.Data;
|
||||
using Esiur.Misc;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text;
|
||||
|
||||
namespace Esiur.Net.Packets.Http;
|
||||
|
||||
public class HttpRequestPacket : Packet
|
||||
{
|
||||
|
||||
|
||||
public StringKeyList Query;
|
||||
public HttpMethod Method;
|
||||
public string RawMethod;
|
||||
public StringKeyList Headers;
|
||||
|
||||
public bool WSMode;
|
||||
|
||||
public string Version;
|
||||
public StringKeyList Cookies; // String
|
||||
public string URL; /// With query
|
||||
public string Filename; /// Without query
|
||||
|
||||
public StringKeyList Cookies;
|
||||
public string URL;
|
||||
public string Filename;
|
||||
public KeyList<string, object> PostForms;
|
||||
public byte[] Message;
|
||||
|
||||
|
||||
private HttpMethod GetMethod(string method)
|
||||
{
|
||||
switch (method.ToLower())
|
||||
{
|
||||
case "get":
|
||||
return HttpMethod.GET;
|
||||
case "post":
|
||||
return HttpMethod.POST;
|
||||
case "head":
|
||||
return HttpMethod.HEAD;
|
||||
case "put":
|
||||
return HttpMethod.PUT;
|
||||
case "delete":
|
||||
return HttpMethod.DELETE;
|
||||
case "options":
|
||||
return HttpMethod.OPTIONS;
|
||||
case "trace":
|
||||
return HttpMethod.TRACE;
|
||||
case "connect":
|
||||
return HttpMethod.CONNECT;
|
||||
default:
|
||||
return HttpMethod.UNKNOWN;
|
||||
}
|
||||
}
|
||||
public uint MaximumHeaderLength { get; set; } = HttpPacketHelpers.DefaultMaximumHeaderLength;
|
||||
public uint MaximumContentLength { get; set; } = HttpPacketHelpers.DefaultMaximumContentLength;
|
||||
public int MaximumHeaderCount { get; set; } = HttpPacketHelpers.DefaultMaximumHeaderCount;
|
||||
public int MaximumFormFields { get; set; } = HttpPacketHelpers.DefaultMaximumFormFields;
|
||||
public int MaximumFormKeyLength { get; set; } = HttpPacketHelpers.DefaultMaximumFormKeyLength;
|
||||
public int MaximumFormValueLength { get; set; } = HttpPacketHelpers.DefaultMaximumFormValueLength;
|
||||
public int MaximumMultipartPartLength { get; set; } = HttpPacketHelpers.DefaultMaximumMultipartPartLength;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "HTTPRequestPacket"
|
||||
+ "\n\tVersion: " + Version
|
||||
+ "\n\tMethod: " + Method
|
||||
+ "\n\tURL: " + URL
|
||||
+ "\n\tMessage: " + (Message != null ? Message.Length.ToString() : "NULL");
|
||||
}
|
||||
=> $"HTTPRequestPacket\n\tVersion: {Version}\n\tMethod: {Method}\n\tURL: {URL}" +
|
||||
$"\n\tMessage: {(Message == null ? "NULL" : Message.Length.ToString())}";
|
||||
|
||||
public override long Parse(byte[] data, uint offset, uint ends)
|
||||
{
|
||||
string[] sMethod = null;
|
||||
string[] sLines = null;
|
||||
ValidateBounds(data, offset, ends);
|
||||
var originalOffset = offset;
|
||||
|
||||
uint headerSize = 0;
|
||||
|
||||
for (uint i = offset; i < ends - 3; i++)
|
||||
{
|
||||
if (data[i] == '\r' && data[i + 1] == '\n'
|
||||
&& data[i + 2] == '\r' && data[i + 3] == '\n')
|
||||
{
|
||||
sLines = Encoding.ASCII.GetString(data, (int)offset, (int)(i - offset)).Split(new string[] { "\r\n" },
|
||||
StringSplitOptions.None);
|
||||
|
||||
headerSize = i + 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (headerSize == 0)
|
||||
if (!HttpPacketHelpers.TryFindHeaderEnd(
|
||||
data, offset, ends, MaximumHeaderLength, out var bodyOffset))
|
||||
return -1;
|
||||
|
||||
var lines = HttpPacketHelpers.ReadHeaderLines(
|
||||
data, offset, bodyOffset, MaximumHeaderCount);
|
||||
|
||||
if (lines.Length == 0)
|
||||
return 0;
|
||||
|
||||
Cookies = new StringKeyList();
|
||||
PostForms = new KeyList<string, object>();
|
||||
Query = new StringKeyList();
|
||||
Headers = new StringKeyList();
|
||||
Message = null;
|
||||
|
||||
sMethod = sLines[0].Split(' ');
|
||||
Method = GetMethod(sMethod[0].Trim());
|
||||
var requestLine = lines[0].Split(new[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (requestLine.Length != 3)
|
||||
return 0;
|
||||
|
||||
if (sMethod.Length == 3)
|
||||
RawMethod = requestLine[0];
|
||||
Method = GetMethod(RawMethod);
|
||||
Version = requestLine[2].Trim();
|
||||
|
||||
var target = requestLine[1].Trim();
|
||||
if (Uri.TryCreate(target, UriKind.Absolute, out var absoluteUri))
|
||||
target = absoluteUri.PathAndQuery;
|
||||
|
||||
var queryIndex = target.IndexOf('?');
|
||||
var rawFilename = queryIndex < 0 ? target : target.Substring(0, queryIndex);
|
||||
Filename = WebUtility.UrlDecode(rawFilename);
|
||||
URL = WebUtility.UrlDecode(target);
|
||||
|
||||
var hasContentLength = false;
|
||||
|
||||
for (var i = 1; i < lines.Length; i++)
|
||||
{
|
||||
sMethod[1] = WebUtility.UrlDecode(sMethod[1]);
|
||||
if (sMethod[1].Length >= 7)
|
||||
var separator = lines[i].IndexOf(':');
|
||||
if (separator <= 0)
|
||||
return 0;
|
||||
|
||||
var name = lines[i].Substring(0, separator).Trim();
|
||||
var value = lines[i].Substring(separator + 1).Trim();
|
||||
|
||||
if (string.Equals(name, "transfer-encoding", StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidDataException("HTTP Transfer-Encoding is not supported.");
|
||||
|
||||
if (string.Equals(name, "content-length", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (sMethod[1].StartsWith("http://"))
|
||||
{
|
||||
sMethod[1] = sMethod[1].Substring(sMethod[1].IndexOf("/", 7));
|
||||
}
|
||||
if (hasContentLength)
|
||||
throw new InvalidDataException("Duplicate HTTP Content-Length headers are not accepted.");
|
||||
|
||||
hasContentLength = true;
|
||||
}
|
||||
|
||||
URL = sMethod[1].Trim();
|
||||
Headers[name] = value;
|
||||
|
||||
if (URL.IndexOf("?", 0) != -1)
|
||||
if (string.Equals(name, "cookie", StringComparison.OrdinalIgnoreCase))
|
||||
ParseCookies(value);
|
||||
}
|
||||
|
||||
if (queryIndex >= 0 && queryIndex + 1 < target.Length)
|
||||
ParseQuery(target.Substring(queryIndex + 1));
|
||||
|
||||
var contentLength = 0u;
|
||||
if (hasContentLength && !uint.TryParse(
|
||||
Headers["content-length"],
|
||||
NumberStyles.None,
|
||||
CultureInfo.InvariantCulture,
|
||||
out contentLength))
|
||||
throw new InvalidDataException("HTTP Content-Length is invalid.");
|
||||
|
||||
if (MaximumContentLength > 0 && contentLength > MaximumContentLength)
|
||||
throw new ParserLimitException(
|
||||
$"HTTP content length of {contentLength} bytes exceeds the {MaximumContentLength}-byte limit.");
|
||||
|
||||
var availableBody = ends - bodyOffset;
|
||||
if (availableBody < contentLength)
|
||||
return -(long)(contentLength - availableBody);
|
||||
|
||||
var contentType = Headers["content-type"];
|
||||
if (Method == HttpMethod.POST &&
|
||||
(string.IsNullOrEmpty(contentType) ||
|
||||
contentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
ParseUrlEncodedForm(Encoding.UTF8.GetString(data, (int)bodyOffset, (int)contentLength));
|
||||
}
|
||||
else if (Method == HttpMethod.POST &&
|
||||
contentType.StartsWith("multipart/form-data", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!TryParseMultipart(data, bodyOffset, contentLength, contentType))
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Message = data.Clip(bodyOffset, contentLength);
|
||||
}
|
||||
|
||||
return bodyOffset - originalOffset + contentLength;
|
||||
}
|
||||
|
||||
private static HttpMethod GetMethod(string method)
|
||||
{
|
||||
if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase)) return HttpMethod.GET;
|
||||
if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase)) return HttpMethod.POST;
|
||||
if (string.Equals(method, "HEAD", StringComparison.OrdinalIgnoreCase)) return HttpMethod.HEAD;
|
||||
if (string.Equals(method, "PUT", StringComparison.OrdinalIgnoreCase)) return HttpMethod.PUT;
|
||||
if (string.Equals(method, "DELETE", StringComparison.OrdinalIgnoreCase)) return HttpMethod.DELETE;
|
||||
if (string.Equals(method, "OPTIONS", StringComparison.OrdinalIgnoreCase)) return HttpMethod.OPTIONS;
|
||||
if (string.Equals(method, "TRACE", StringComparison.OrdinalIgnoreCase)) return HttpMethod.TRACE;
|
||||
if (string.Equals(method, "CONNECT", StringComparison.OrdinalIgnoreCase)) return HttpMethod.CONNECT;
|
||||
return HttpMethod.UNKNOWN;
|
||||
}
|
||||
|
||||
private void ParseCookies(string header)
|
||||
{
|
||||
foreach (var segment in header.Split(';'))
|
||||
{
|
||||
var cookie = segment.Trim();
|
||||
if (cookie.Length == 0)
|
||||
continue;
|
||||
|
||||
var separator = cookie.IndexOf('=');
|
||||
var name = separator < 0 ? cookie : cookie.Substring(0, separator).Trim();
|
||||
var value = separator < 0 ? string.Empty : cookie.Substring(separator + 1).Trim();
|
||||
if (!Cookies.ContainsKey(name))
|
||||
Cookies.Add(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseQuery(string query)
|
||||
{
|
||||
foreach (var segment in query.Split('&'))
|
||||
{
|
||||
var separator = segment.IndexOf('=');
|
||||
var name = WebUtility.UrlDecode(separator < 0 ? segment : segment.Substring(0, separator));
|
||||
var value = separator < 0 ? null : WebUtility.UrlDecode(segment.Substring(separator + 1));
|
||||
if (!Query.ContainsKey(name))
|
||||
Query.Add(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseUrlEncodedForm(string form)
|
||||
{
|
||||
if (form.Length == 0)
|
||||
return;
|
||||
|
||||
var fieldCount = 0;
|
||||
var start = 0;
|
||||
StringBuilder unknown = null;
|
||||
var hasUnknownValue = false;
|
||||
|
||||
while (start <= form.Length)
|
||||
{
|
||||
fieldCount++;
|
||||
EnsureWithinLimit(fieldCount, MaximumFormFields, "form fields");
|
||||
|
||||
var end = form.IndexOf('&', start);
|
||||
if (end < 0)
|
||||
end = form.Length;
|
||||
|
||||
var separator = form.IndexOf('=', start, end - start);
|
||||
if (separator >= 0)
|
||||
{
|
||||
Filename = URL.Split(new char[] { '?' }, 2)[0];
|
||||
var key = DecodeFormComponent(form.Substring(start, separator - start));
|
||||
var value = DecodeFormComponent(form.Substring(separator + 1, end - separator - 1));
|
||||
EnsureStringWithinLimit(key, MaximumFormKeyLength, "form key");
|
||||
EnsureStringWithinLimit(value, MaximumFormValueLength, "form value");
|
||||
|
||||
if (string.Equals(key, "unknown", StringComparison.Ordinal))
|
||||
{
|
||||
if (unknown == null)
|
||||
unknown = new StringBuilder(value.Length);
|
||||
else
|
||||
unknown.Clear();
|
||||
unknown.Append(value);
|
||||
hasUnknownValue = true;
|
||||
}
|
||||
|
||||
PostForms[key] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Filename = URL;
|
||||
var value = DecodeFormComponent(form.Substring(start, end - start));
|
||||
EnsureStringWithinLimit(value, MaximumFormValueLength, "form value");
|
||||
|
||||
if (unknown == null)
|
||||
unknown = new StringBuilder(value.Length);
|
||||
|
||||
EnsureStringWithinLimit(
|
||||
unknown.Length + (hasUnknownValue ? 1 : 0) + value.Length,
|
||||
MaximumFormValueLength,
|
||||
"combined form value");
|
||||
|
||||
if (hasUnknownValue)
|
||||
unknown.Append('&');
|
||||
unknown.Append(value);
|
||||
hasUnknownValue = true;
|
||||
}
|
||||
|
||||
if (Filename.IndexOf("%", 0) != -1)
|
||||
{
|
||||
Filename = WebUtility.UrlDecode(Filename);
|
||||
}
|
||||
|
||||
Version = sMethod[2].Trim();
|
||||
if (end == form.Length)
|
||||
break;
|
||||
start = end + 1;
|
||||
}
|
||||
|
||||
// Read all headers
|
||||
if (unknown != null)
|
||||
PostForms["unknown"] = unknown.ToString();
|
||||
}
|
||||
|
||||
for (int i = 1; i < sLines.Length; i++)
|
||||
private bool TryParseMultipart(
|
||||
byte[] data,
|
||||
uint bodyOffset,
|
||||
uint contentLength,
|
||||
string contentType)
|
||||
{
|
||||
if (!TryGetMultipartBoundary(contentType, out var boundary))
|
||||
return false;
|
||||
|
||||
var delimiter = "--" + boundary;
|
||||
var body = Encoding.UTF8.GetString(data, (int)bodyOffset, (int)contentLength);
|
||||
var position = 0;
|
||||
var fieldCount = 0;
|
||||
|
||||
while (position < body.Length)
|
||||
{
|
||||
if (sLines[i] == string.Empty)
|
||||
{
|
||||
// Invalid header
|
||||
return 0;
|
||||
}
|
||||
var delimiterStart = body.IndexOf(delimiter, position, StringComparison.Ordinal);
|
||||
if (delimiterStart < 0)
|
||||
return false;
|
||||
|
||||
if (sLines[i].IndexOf(':') == -1)
|
||||
{
|
||||
// Invalid header
|
||||
return 0;
|
||||
}
|
||||
position = delimiterStart + delimiter.Length;
|
||||
if (position + 2 <= body.Length &&
|
||||
string.CompareOrdinal(body, position, "--", 0, 2) == 0)
|
||||
return true;
|
||||
|
||||
string[] header = sLines[i].Split(new char[] { ':' }, 2);
|
||||
if (position + 2 > body.Length ||
|
||||
string.CompareOrdinal(body, position, "\r\n", 0, 2) != 0)
|
||||
return false;
|
||||
position += 2;
|
||||
|
||||
header[0] = header[0].ToLower();
|
||||
Headers[header[0]] = header[1].Trim();
|
||||
var nextDelimiter = body.IndexOf("\r\n" + delimiter, position, StringComparison.Ordinal);
|
||||
if (nextDelimiter < 0)
|
||||
return false;
|
||||
|
||||
if (header[0] == "cookie")
|
||||
{
|
||||
string[] cookies = header[1].Split(';');
|
||||
var partLength = nextDelimiter - position;
|
||||
EnsureWithinLimit(partLength, MaximumMultipartPartLength, "multipart part length");
|
||||
|
||||
foreach (string cookie in cookies)
|
||||
{
|
||||
if (cookie.IndexOf('=') != -1)
|
||||
{
|
||||
string[] splitCookie = cookie.Split('=');
|
||||
splitCookie[0] = splitCookie[0].Trim();
|
||||
splitCookie[1] = splitCookie[1].Trim();
|
||||
if (!Cookies.ContainsKey(splitCookie[0].Trim()))
|
||||
Cookies.Add(splitCookie[0], splitCookie[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Cookies.ContainsKey(cookie.Trim()))
|
||||
{
|
||||
Cookies.Add(cookie.Trim(), string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var headerEnd = body.IndexOf("\r\n\r\n", position, partLength, StringComparison.Ordinal);
|
||||
if (headerEnd < 0)
|
||||
return false;
|
||||
|
||||
var nameStart = body.IndexOf("name=\"", position, headerEnd - position, StringComparison.OrdinalIgnoreCase);
|
||||
if (nameStart < 0)
|
||||
return false;
|
||||
nameStart += 6;
|
||||
|
||||
var nameEnd = body.IndexOf('"', nameStart, headerEnd - nameStart);
|
||||
if (nameEnd < 0 || nameEnd > headerEnd)
|
||||
return false;
|
||||
|
||||
fieldCount++;
|
||||
EnsureWithinLimit(fieldCount, MaximumFormFields, "form fields");
|
||||
|
||||
var name = body.Substring(nameStart, nameEnd - nameStart);
|
||||
EnsureStringWithinLimit(name, MaximumFormKeyLength, "form key");
|
||||
|
||||
var valueStart = headerEnd + 4;
|
||||
PostForms[name] = body.Substring(valueStart, nextDelimiter - valueStart);
|
||||
position = nextDelimiter + 2;
|
||||
}
|
||||
|
||||
// Query String
|
||||
if (URL.IndexOf("?", 0) != -1)
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string DecodeFormComponent(string value)
|
||||
=> WebUtility.HtmlDecode(WebUtility.UrlDecode(value));
|
||||
|
||||
private static bool TryGetMultipartBoundary(string contentType, out string boundary)
|
||||
{
|
||||
boundary = null;
|
||||
var parameterStart = contentType.IndexOf(';');
|
||||
while (parameterStart >= 0 && parameterStart + 1 < contentType.Length)
|
||||
{
|
||||
string[] SQ = URL.Split(new char[] { '?' }, 2)[1].Split('&');
|
||||
foreach (string S in SQ)
|
||||
{
|
||||
if (S.IndexOf("=", 0) != -1)
|
||||
{
|
||||
string[] qp = S.Split(new char[] { '=' }, 2);
|
||||
var parameterEnd = contentType.IndexOf(';', parameterStart + 1);
|
||||
if (parameterEnd < 0)
|
||||
parameterEnd = contentType.Length;
|
||||
|
||||
if (!Query.ContainsKey(WebUtility.UrlDecode(qp[0])))
|
||||
{
|
||||
Query.Add(WebUtility.UrlDecode(qp[0]), WebUtility.UrlDecode(qp[1]));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Query.ContainsKey(WebUtility.UrlDecode(S)))
|
||||
{
|
||||
Query.Add(WebUtility.UrlDecode(S), null);
|
||||
}
|
||||
}
|
||||
var parameter = contentType.Substring(
|
||||
parameterStart + 1,
|
||||
parameterEnd - parameterStart - 1).Trim();
|
||||
var separator = parameter.IndexOf('=');
|
||||
if (separator > 0 && string.Equals(
|
||||
parameter.Substring(0, separator).Trim(),
|
||||
"boundary",
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
boundary = parameter.Substring(separator + 1).Trim().Trim('"');
|
||||
return boundary.Length > 0 && boundary.IndexOfAny(new[] { '\r', '\n' }) < 0;
|
||||
}
|
||||
|
||||
parameterStart = parameterEnd < contentType.Length ? parameterEnd : -1;
|
||||
}
|
||||
|
||||
// Post Content-Length
|
||||
if (Method == HttpMethod.POST)
|
||||
{
|
||||
try
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint postSize = uint.Parse(Headers["content-length"]);
|
||||
private static void EnsureStringWithinLimit(string value, int limit, string kind)
|
||||
=> EnsureStringWithinLimit(value?.Length ?? 0, limit, kind);
|
||||
|
||||
// check limit
|
||||
if (postSize > data.Length - headerSize)
|
||||
return -(postSize - (data.Length - headerSize));
|
||||
private static void EnsureStringWithinLimit(int length, int limit, string kind)
|
||||
=> EnsureWithinLimit(length, limit, kind);
|
||||
|
||||
|
||||
if (
|
||||
Headers["content-type"] == null
|
||||
|| Headers["content-type"] == ""
|
||||
|| Headers["content-type"].StartsWith("application/x-www-form-urlencoded"))
|
||||
{
|
||||
string[] PostVars = null;
|
||||
PostVars = Encoding.UTF8.GetString(data, (int)headerSize, (int)postSize).Split('&');
|
||||
for (int J = 0; J < PostVars.Length; J++)
|
||||
{
|
||||
if (PostVars[J].IndexOf("=") != -1)
|
||||
{
|
||||
string key = WebUtility.HtmlDecode(
|
||||
WebUtility.UrlDecode(PostVars[J].Split(new char[] { '=' }, 2)[0]));
|
||||
if (PostForms.Contains(key))
|
||||
PostForms[key] = WebUtility.HtmlDecode(
|
||||
WebUtility.UrlDecode(PostVars[J].Split(new char[] { '=' }, 2)[1]));
|
||||
else
|
||||
PostForms.Add(key, WebUtility.HtmlDecode(
|
||||
WebUtility.UrlDecode(PostVars[J].Split(new char[] { '=' }, 2)[1])));
|
||||
}
|
||||
else
|
||||
if (PostForms.Contains("unknown"))
|
||||
PostForms["unknown"] = PostForms["unknown"]
|
||||
+ "&" + WebUtility.HtmlDecode(WebUtility.UrlDecode(PostVars[J]));
|
||||
else
|
||||
PostForms.Add("unknown", WebUtility.HtmlDecode(WebUtility.UrlDecode(PostVars[J])));
|
||||
}
|
||||
}
|
||||
else if (Headers["content-type"].StartsWith("multipart/form-data"))
|
||||
{
|
||||
int st = 1;
|
||||
int ed = 0;
|
||||
string strBoundry = "--" + Headers["content-type"].Substring(
|
||||
Headers["content-type"].IndexOf("boundary=", 0) + 9);
|
||||
|
||||
string[] sc = Encoding.UTF8.GetString(data, (int)headerSize, (int)postSize).Split(
|
||||
new string[] { strBoundry }, StringSplitOptions.None);
|
||||
|
||||
|
||||
for (int j = 1; j < sc.Length - 1; j++)
|
||||
{
|
||||
string[] ps = sc[j].Split(new string[] { "\r\n\r\n" }, 2, StringSplitOptions.None);
|
||||
ps[1] = ps[1].Substring(0, ps[1].Length - 2); // remove the empty line
|
||||
st = ps[0].IndexOf("name=", 0) + 6;
|
||||
ed = ps[0].IndexOf("\"", st);
|
||||
PostForms.Add(ps[0].Substring(st, ed - st), ps[1]);
|
||||
}
|
||||
}
|
||||
//else if (Headers["content-type"] == "application/json")
|
||||
//{
|
||||
// var json = DC.Clip(data, headerSize, postSize);
|
||||
//}
|
||||
else
|
||||
{
|
||||
//PostForms.Add(Headers["content-type"], Encoding.Default.GetString( ));
|
||||
Message = data.Clip(headerSize, postSize);
|
||||
}
|
||||
|
||||
return headerSize + postSize;
|
||||
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return headerSize;
|
||||
private static void EnsureWithinLimit(int value, int limit, string kind)
|
||||
{
|
||||
if (limit > 0 && value > limit)
|
||||
throw new ParserLimitException(
|
||||
$"HTTP {kind} of {value} exceeds the configured limit of {limit}.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,217 +1,210 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2017 Ahmed Kh. Zamil
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
using Esiur.Data;
|
||||
using Esiur.Misc;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Esiur.Misc;
|
||||
using Esiur.Data;
|
||||
|
||||
namespace Esiur.Net.Packets.Http;
|
||||
|
||||
public class HttpResponsePacket : Packet
|
||||
{
|
||||
|
||||
public StringKeyList Headers { get; } = new StringKeyList(true);
|
||||
public string Version { get; set; } = "HTTP/1.1";
|
||||
|
||||
public byte[] Message;
|
||||
public HttpResponseCode Number { get; set; } = HttpResponseCode.OK;
|
||||
public string Text;
|
||||
|
||||
public List<HttpCookie> Cookies { get; } = new List<HttpCookie>();
|
||||
public bool Handled;
|
||||
|
||||
public uint MaximumHeaderLength { get; set; } = HttpPacketHelpers.DefaultMaximumHeaderLength;
|
||||
public uint MaximumContentLength { get; set; } = HttpPacketHelpers.DefaultMaximumContentLength;
|
||||
public int MaximumHeaderCount { get; set; } = HttpPacketHelpers.DefaultMaximumHeaderCount;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum response body accepted by <see cref="Compose(HttpComposeOption)"/>.
|
||||
/// Zero preserves the legacy behavior of allowing any body that fits in a managed array.
|
||||
/// </summary>
|
||||
public uint MaximumComposedContentLength { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "HTTPResponsePacket"
|
||||
+ "\n\tVersion: " + Version
|
||||
//+ "\n\tMethod: " + Method
|
||||
//+ "\n\tURL: " + URL
|
||||
+ "\n\tMessage: " + (Message != null ? Message.Length.ToString() : "NULL");
|
||||
}
|
||||
=> $"HTTPResponsePacket\n\tVersion: {Version}" +
|
||||
$"\n\tMessage: {(Message == null ? "NULL" : Message.Length.ToString())}";
|
||||
|
||||
private string MakeHeader(HttpComposeOption options)
|
||||
private byte[] ComposeHeader(HttpComposeOption options)
|
||||
{
|
||||
string header = $"{Version} {(int)Number} {Text}\r\nServer: Esiur {Global.Version}\r\nDate: {DateTime.Now.ToUniversalTime().ToString("r")}\r\n";
|
||||
|
||||
if (options == HttpComposeOption.AllCalculateLength)
|
||||
Headers["Content-Length"] = Message?.Length.ToString() ?? "0";
|
||||
Headers["Content-Length"] = Message?.Length.ToString(CultureInfo.InvariantCulture) ?? "0";
|
||||
|
||||
foreach (var kv in Headers)
|
||||
header += kv.Key + ": " + kv.Value + "\r\n";
|
||||
var header = new StringBuilder(256);
|
||||
header.Append(Version)
|
||||
.Append(' ')
|
||||
.Append((int)Number)
|
||||
.Append(' ')
|
||||
.Append(Text ?? string.Empty)
|
||||
.Append("\r\nServer: Esiur ")
|
||||
.Append(Global.Version)
|
||||
.Append("\r\nDate: ")
|
||||
.Append(DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture))
|
||||
.Append("\r\n");
|
||||
|
||||
foreach (var entry in Headers)
|
||||
header.Append(entry.Key).Append(": ").Append(entry.Value).Append("\r\n");
|
||||
foreach (var cookie in Cookies)
|
||||
header.Append("Set-Cookie: ").Append(cookie).Append("\r\n");
|
||||
|
||||
// Set-Cookie: ckGeneric=CookieBody; expires=Sun, 30-Dec-2007 21:00:00 GMT; path=/
|
||||
// Set-Cookie: ASPSESSIONIDQABBDSQA=IPDPMMMALDGFLMICEJIOCIPM; path=/
|
||||
|
||||
foreach (var Cookie in Cookies)
|
||||
header += "Set-Cookie: " + Cookie.ToString() + "\r\n";
|
||||
|
||||
|
||||
header += "\r\n";
|
||||
|
||||
return header;
|
||||
header.Append("\r\n");
|
||||
return Encoding.ASCII.GetBytes(header.ToString());
|
||||
}
|
||||
|
||||
|
||||
public bool Compose(HttpComposeOption options)
|
||||
{
|
||||
List<byte> msg = new List<byte>();
|
||||
var header = options == HttpComposeOption.DataOnly
|
||||
? Array.Empty<byte>()
|
||||
: ComposeHeader(options);
|
||||
var body = options == HttpComposeOption.SpecifiedHeadersOnly || Message == null
|
||||
? Array.Empty<byte>()
|
||||
: Message;
|
||||
|
||||
if (options != HttpComposeOption.DataOnly)
|
||||
{
|
||||
msg.AddRange(Encoding.UTF8.GetBytes(MakeHeader(options)));
|
||||
}
|
||||
if (MaximumComposedContentLength > 0 && body.LongLength > MaximumComposedContentLength)
|
||||
throw new ParserLimitException(
|
||||
$"HTTP content length of {body.LongLength} bytes exceeds the {MaximumComposedContentLength}-byte limit.");
|
||||
|
||||
if (options != HttpComposeOption.SpecifiedHeadersOnly)
|
||||
{
|
||||
if (Message != null)
|
||||
msg.AddRange(Message);
|
||||
}
|
||||
|
||||
Data = msg.ToArray();
|
||||
Data = new byte[checked(header.Length + body.Length)];
|
||||
if (header.Length > 0)
|
||||
Buffer.BlockCopy(header, 0, Data, 0, header.Length);
|
||||
if (body.Length > 0)
|
||||
Buffer.BlockCopy(body, 0, Data, header.Length, body.Length);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool Compose()
|
||||
{
|
||||
return Compose(HttpComposeOption.AllDontCalculateLength);
|
||||
}
|
||||
public override bool Compose() => Compose(HttpComposeOption.AllDontCalculateLength);
|
||||
|
||||
public override long Parse(byte[] data, uint offset, uint ends)
|
||||
{
|
||||
string[] sMethod = null;
|
||||
string[] sLines = null;
|
||||
ValidateBounds(data, offset, ends);
|
||||
var originalOffset = offset;
|
||||
|
||||
uint headerSize = 0;
|
||||
|
||||
for (uint i = offset; i < ends - 3; i++)
|
||||
{
|
||||
if (data[i] == '\r' && data[i + 1] == '\n'
|
||||
&& data[i + 2] == '\r' && data[i + 3] == '\n')
|
||||
{
|
||||
sLines = Encoding.ASCII.GetString(data, (int)offset, (int)(i - offset)).Split(new string[] { "\r\n" },
|
||||
StringSplitOptions.None);
|
||||
|
||||
headerSize = i + 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (headerSize == 0)
|
||||
if (!HttpPacketHelpers.TryFindHeaderEnd(
|
||||
data, offset, ends, MaximumHeaderLength, out var bodyOffset))
|
||||
return -1;
|
||||
|
||||
var lines = HttpPacketHelpers.ReadHeaderLines(
|
||||
data, offset, bodyOffset, MaximumHeaderCount);
|
||||
|
||||
sMethod = sLines[0].Split(' ');
|
||||
|
||||
if (sMethod.Length == 3)
|
||||
{
|
||||
Version = sMethod[0].Trim();
|
||||
Number = (HttpResponseCode)Convert.ToInt32(sMethod[1].Trim());
|
||||
Text = sMethod[2];
|
||||
}
|
||||
|
||||
// Read all headers
|
||||
|
||||
for (int i = 1; i < sLines.Length; i++)
|
||||
{
|
||||
if (sLines[i] == string.Empty)
|
||||
{
|
||||
// Invalid header
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (sLines[i].IndexOf(':') == -1)
|
||||
{
|
||||
// Invalid header
|
||||
return 0;
|
||||
}
|
||||
|
||||
string[] header = sLines[i].Split(new char[] { ':' }, 2);
|
||||
|
||||
header[0] = header[0].ToLower();
|
||||
Headers[header[0]] = header[1].Trim();
|
||||
|
||||
//Set-Cookie: NAME=VALUE; expires=DATE;
|
||||
|
||||
if (header[0] == "set-cookie")
|
||||
{
|
||||
string[] cookie = header[1].Split(';');
|
||||
|
||||
if (cookie.Length >= 1)
|
||||
{
|
||||
string[] splitCookie = cookie[0].Split('=');
|
||||
HttpCookie c = new HttpCookie(splitCookie[0], splitCookie[1]);
|
||||
|
||||
for (int j = 1; j < cookie.Length; j++)
|
||||
{
|
||||
splitCookie = cookie[j].Split('=');
|
||||
switch (splitCookie[0].ToLower())
|
||||
{
|
||||
case "domain":
|
||||
c.Domain = splitCookie[1];
|
||||
break;
|
||||
case "path":
|
||||
c.Path = splitCookie[1];
|
||||
break;
|
||||
case "httponly":
|
||||
c.HttpOnly = true;
|
||||
break;
|
||||
case "expires":
|
||||
// Wed, 13-Jan-2021 22:23:01 GMT
|
||||
c.Expires = DateTime.Parse(splitCookie[1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Content-Length
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
uint contentLength = uint.Parse(Headers["content-length"]);
|
||||
|
||||
// check limit
|
||||
if (contentLength > data.Length - headerSize)
|
||||
{
|
||||
return contentLength - (data.Length - headerSize);
|
||||
}
|
||||
|
||||
Message = data.Clip(offset, contentLength);
|
||||
|
||||
return headerSize + contentLength;
|
||||
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (lines.Length == 0)
|
||||
return 0;
|
||||
|
||||
var statusLine = lines[0].Split(new[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (statusLine.Length < 2 ||
|
||||
!int.TryParse(statusLine[1], NumberStyles.None, CultureInfo.InvariantCulture, out var statusCode))
|
||||
return 0;
|
||||
|
||||
Version = statusLine[0];
|
||||
Number = (HttpResponseCode)statusCode;
|
||||
Text = statusLine.Length == 3 ? statusLine[2] : string.Empty;
|
||||
Headers.Clear();
|
||||
Cookies.Clear();
|
||||
Message = null;
|
||||
|
||||
var hasContentLength = false;
|
||||
|
||||
for (var i = 1; i < lines.Length; i++)
|
||||
{
|
||||
var separator = lines[i].IndexOf(':');
|
||||
if (separator <= 0)
|
||||
return 0;
|
||||
|
||||
var name = lines[i].Substring(0, separator).Trim();
|
||||
var value = lines[i].Substring(separator + 1).Trim();
|
||||
|
||||
if (string.Equals(name, "transfer-encoding", StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidDataException("HTTP Transfer-Encoding is not supported.");
|
||||
|
||||
if (string.Equals(name, "content-length", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (hasContentLength)
|
||||
throw new InvalidDataException("Duplicate HTTP Content-Length headers are not accepted.");
|
||||
|
||||
hasContentLength = true;
|
||||
}
|
||||
|
||||
Headers.Add(name, value);
|
||||
|
||||
if (string.Equals(name, "set-cookie", StringComparison.OrdinalIgnoreCase) &&
|
||||
TryParseCookie(value, out var cookie))
|
||||
Cookies.Add(cookie);
|
||||
}
|
||||
|
||||
var contentLengthHeader = Headers["content-length"];
|
||||
if (contentLengthHeader == null)
|
||||
{
|
||||
Message = Array.Empty<byte>();
|
||||
return bodyOffset - originalOffset;
|
||||
}
|
||||
|
||||
if (!uint.TryParse(
|
||||
contentLengthHeader,
|
||||
NumberStyles.None,
|
||||
CultureInfo.InvariantCulture,
|
||||
out var contentLength))
|
||||
return 0;
|
||||
|
||||
if (MaximumContentLength > 0 && contentLength > MaximumContentLength)
|
||||
throw new ParserLimitException(
|
||||
$"HTTP content length of {contentLength} bytes exceeds the {MaximumContentLength}-byte limit.");
|
||||
|
||||
var availableBody = ends - bodyOffset;
|
||||
if (availableBody < contentLength)
|
||||
return -(long)(contentLength - availableBody);
|
||||
|
||||
Message = data.Clip(bodyOffset, contentLength);
|
||||
return bodyOffset - originalOffset + contentLength;
|
||||
}
|
||||
|
||||
private static bool TryParseCookie(string header, out HttpCookie cookie)
|
||||
{
|
||||
cookie = default;
|
||||
var segments = header.Split(';');
|
||||
if (segments.Length == 0)
|
||||
return false;
|
||||
|
||||
var nameValueSeparator = segments[0].IndexOf('=');
|
||||
if (nameValueSeparator <= 0)
|
||||
return false;
|
||||
|
||||
cookie = new HttpCookie(
|
||||
segments[0].Substring(0, nameValueSeparator).Trim(),
|
||||
segments[0].Substring(nameValueSeparator + 1).Trim());
|
||||
|
||||
for (var i = 1; i < segments.Length; i++)
|
||||
{
|
||||
var segment = segments[i].Trim();
|
||||
var separator = segment.IndexOf('=');
|
||||
var name = separator < 0 ? segment : segment.Substring(0, separator).Trim();
|
||||
var value = separator < 0 ? string.Empty : segment.Substring(separator + 1).Trim();
|
||||
|
||||
if (string.Equals(name, "domain", StringComparison.OrdinalIgnoreCase))
|
||||
cookie.Domain = value;
|
||||
else if (string.Equals(name, "path", StringComparison.OrdinalIgnoreCase))
|
||||
cookie.Path = value;
|
||||
else if (string.Equals(name, "httponly", StringComparison.OrdinalIgnoreCase))
|
||||
cookie.HttpOnly = true;
|
||||
else if (string.Equals(name, "secure", StringComparison.OrdinalIgnoreCase))
|
||||
cookie.Secure = true;
|
||||
else if (string.Equals(name, "samesite", StringComparison.OrdinalIgnoreCase) &&
|
||||
Enum.TryParse(value, true, out HttpCookieSameSite sameSite))
|
||||
cookie.SameSite = sameSite;
|
||||
else if (string.Equals(name, "expires", StringComparison.OrdinalIgnoreCase) &&
|
||||
DateTime.TryParse(
|
||||
value,
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
|
||||
out var expires))
|
||||
cookie.Expires = expires;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,367 +1,42 @@
|
||||
|
||||
/********************************************************************************\
|
||||
* Uruky Project *
|
||||
* *
|
||||
* Copyright (C) 2006 Ahmed Zamil - ahmed@dijlh.com *
|
||||
* http://www.dijlh.com *
|
||||
* *
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy *
|
||||
* of this software and associated documentation files (the "Software"), to deal *
|
||||
* in the Software without restriction, including without limitation the rights *
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell *
|
||||
* copies of the Software, and to permit persons to whom the Software is *
|
||||
* furnished to do so, subject to the following conditions: *
|
||||
* *
|
||||
* The above copyright notice and this permission notice shall be included in all *
|
||||
* copies or substantial portions of the Software. *
|
||||
* *
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE *
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, *
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE *
|
||||
* SOFTWARE. *
|
||||
* *
|
||||
* File: Packet.cs *
|
||||
* Description: Ethernet/ARP/IPv4/TCP/UDP Packet Decoding & Encoding Class *
|
||||
* Compatibility: .Net Framework 2.0 / Mono 1.1.8 *
|
||||
* *
|
||||
\********************************************************************************/
|
||||
|
||||
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using Esiur.Misc;
|
||||
using Esiur.Net.DataLink;
|
||||
using System.Net.NetworkInformation;
|
||||
using Esiur.Data;
|
||||
|
||||
namespace Esiur.Net.Packets;
|
||||
internal static class Functions
|
||||
{
|
||||
public static void AddData(ref byte[] dest, byte[] src)
|
||||
{
|
||||
int I = 0;
|
||||
if (src == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (dest != null)
|
||||
{
|
||||
I = dest.Length;
|
||||
Array.Resize(ref dest, dest.Length + src.Length);
|
||||
//dest = (byte[])Resize(dest, dest.Length + src.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
dest = new byte[src.Length];
|
||||
}
|
||||
Array.Copy(src, 0, dest, I, src.Length);
|
||||
}
|
||||
|
||||
/*
|
||||
public static Array Resize(Array array, int newSize)
|
||||
{
|
||||
Type myType = Type.GetType(array.GetType().FullName.TrimEnd('[', ']'));
|
||||
Array nA = Array.CreateInstance(myType, newSize);
|
||||
Array.Copy(array, nA, (newSize > array.Length ? array.Length : newSize));
|
||||
return nA;
|
||||
} */
|
||||
|
||||
//Computes the checksum used in IP, ARP..., ie the
|
||||
// "The 16 bit one's complement of the one 's complement sum
|
||||
//of all 16 bit words" as seen in RFCs
|
||||
// Returns a 4 characters hex string
|
||||
// data's lenght must be multiple of 4, else zero padding
|
||||
public static ushort IP_CRC16(byte[] data)
|
||||
{
|
||||
ulong Sum = 0;
|
||||
bool Padding = false;
|
||||
/// * Padding if needed
|
||||
if (data.Length % 2 != 0)
|
||||
{
|
||||
Array.Resize(ref data, data.Length + 1);
|
||||
//data = (byte[])Resize(data, data.Length + 1);
|
||||
Padding = true;
|
||||
}
|
||||
int count = data.Length;
|
||||
///* add 16-bit words */
|
||||
while (count > 0) //1)
|
||||
{
|
||||
///* this is the inner loop */
|
||||
Sum += GetInteger(data[count - 2], data[count - 1]);
|
||||
///* Fold 32-bit sum to 16-bit */
|
||||
while (Sum >> 16 != 0)
|
||||
{
|
||||
Sum = (Sum & 0XFFFF) + (Sum >> 16);
|
||||
}
|
||||
count -= 2;
|
||||
}
|
||||
/// * reverse padding
|
||||
if (Padding)
|
||||
{
|
||||
Array.Resize(ref data, data.Length - 1);
|
||||
//data = (byte[])Resize(data, data.Length - 1);
|
||||
}
|
||||
///* Return one's compliment of final sum.
|
||||
//return (ushort)(ushort.MaxValue - (ushort)Sum);
|
||||
return (ushort)(~Sum);
|
||||
}
|
||||
|
||||
public static ushort GetInteger(byte B1, byte B2)
|
||||
{
|
||||
return BitConverter.ToUInt16(new byte[] { B2, B1 }, 0);
|
||||
//return System.Convert.ToUInt16("&h" + GetHex(B1) + GetHex(B2));
|
||||
}
|
||||
|
||||
public static uint GetLong(byte B1, byte B2, byte B3, byte B4)
|
||||
{
|
||||
return BitConverter.ToUInt32(new byte[] { B4, B3, B2, B1 }, 0);
|
||||
//return System.Convert.ToUInt32("&h" + GetHex(B1) + GetHex(B2) + GetHex(B3) + GetHex(B4));
|
||||
}
|
||||
|
||||
public static string GetHex(byte B)
|
||||
{
|
||||
return (((B < 15) ? 0 + System.Convert.ToString(B, 16).ToUpper() : System.Convert.ToString(B, 16).ToUpper()));
|
||||
}
|
||||
|
||||
public static bool GetBit(uint B, byte Pos)
|
||||
{
|
||||
//return BitConverter.ToBoolean(BitConverter.GetBytes(B), Pos + 1);
|
||||
return (B & (uint)(Math.Pow(2, (Pos - 1)))) == (Math.Pow(2, (Pos - 1)));
|
||||
}
|
||||
|
||||
public static ushort RemoveBit(ushort I, byte Pos)
|
||||
{
|
||||
return (ushort)RemoveBit((uint)I, Pos);
|
||||
}
|
||||
|
||||
public static uint RemoveBit(uint I, byte Pos)
|
||||
{
|
||||
if (GetBit(I, Pos))
|
||||
{
|
||||
return I - (uint)(Math.Pow(2, (Pos - 1)));
|
||||
}
|
||||
else
|
||||
{
|
||||
return I;
|
||||
}
|
||||
}
|
||||
|
||||
public static void SplitInteger(ushort I, ref byte BLeft, ref byte BRight)
|
||||
{
|
||||
byte[] b = BitConverter.GetBytes(I);
|
||||
BLeft = b[1];
|
||||
BRight = b[0];
|
||||
//BLeft = I >> 8;
|
||||
//BRight = (I << 8) >> 8;
|
||||
}
|
||||
|
||||
public static void SplitLong(uint I, ref byte BLeft, ref byte BLeftMiddle, ref byte BRightMiddle, ref byte BRight)
|
||||
{
|
||||
byte[] b = BitConverter.GetBytes(I);
|
||||
BLeft = b[3];
|
||||
BLeftMiddle = b[2];
|
||||
BRightMiddle = b[1];
|
||||
BRight = b[0];
|
||||
//BLeft = I >> 24;
|
||||
//BLeftMiddle = (I << 8) >> 24;
|
||||
//BRightMiddle = (I << 16) >> 24;
|
||||
//BRight = (I << 24) >> 24;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class PosixTime
|
||||
{
|
||||
ulong seconds;
|
||||
ulong microseconds;
|
||||
|
||||
PosixTime(ulong Seconds, ulong Microseconds)
|
||||
{
|
||||
seconds = Seconds;
|
||||
microseconds = Microseconds;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return seconds + "." + microseconds;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compatibility base for packet parsers and composers.
|
||||
/// </summary>
|
||||
public class Packet
|
||||
{
|
||||
//public EtherServer2.EthernetSource Source;
|
||||
|
||||
public PacketSource Source;
|
||||
|
||||
public DateTime Timestamp;
|
||||
|
||||
public enum PPPType : ushort
|
||||
{
|
||||
IP = 0x0021, // Internet Protocol version 4 [RFC1332]
|
||||
SDTP = 0x0049, // Serial Data Transport Protocol (PPP-SDTP) [RFC1963]
|
||||
IPv6HeaderCompression = 0x004f, // IPv6 Header Compression
|
||||
IPv6 = 0x0057, // Internet Protocol version 6 [RFC5072]
|
||||
W8021dHelloPacket = 0x0201, // 802.1d Hello Packets [RFC3518]
|
||||
IPv6ControlProtocol = 0x8057, // IPv6 Control Protocol [RFC5072]
|
||||
}
|
||||
|
||||
public enum ProtocolType : ushort
|
||||
{
|
||||
IP = 0x800, // IPv4
|
||||
ARP = 0x806, // Address Resolution Protocol
|
||||
IPv6 = 0x86DD, // IPv6
|
||||
FrameRelayARP = 0x0808, // Frame Relay ARP [RFC1701]
|
||||
VINESLoopback = 0x0BAE, // VINES Loopback [RFC1701]
|
||||
VINESEcho = 0x0BAF, // VINES ECHO [RFC1701]
|
||||
TransEtherBridging = 0x6558, // TransEther Bridging [RFC1701]
|
||||
RawFrameRelay = 0x6559, // Raw Frame Relay [RFC1701]
|
||||
IEE8021QVLAN = 0x8100, // IEEE 802.1Q VLAN-tagged frames (initially Wellfleet)
|
||||
SNMP = 0x814C, // SNMP [JKR1]
|
||||
TCPIP_Compression = 0x876B, // TCP/IP Compression [RFC1144]
|
||||
IPAutonomousSystems = 0x876C, // IP Autonomous Systems [RFC1701]
|
||||
SecureData = 0x876D, // Secure Data [RFC1701]
|
||||
PPP = 0x880B, // PPP [IANA]
|
||||
MPLS = 0x8847, // MPLS [RFC5332]
|
||||
MPLS_UpstreamAssignedLabel = 0x8848, // MPLS with upstream-assigned label [RFC5332]
|
||||
PPPoEDiscoveryStage = 0x8863, // PPPoE Discovery Stage [RFC2516]
|
||||
PPPoESessionStage = 0x8864, // PPPoE Session Stage [RFC2516]
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
public static void GetPacketMACAddresses(Packet packet, out byte[] srcMAC, out byte[] dstMAC)
|
||||
{
|
||||
|
||||
// get the node address
|
||||
Packet root = packet.RootPacket;
|
||||
if (root is TZSPPacket)
|
||||
{
|
||||
|
||||
TZSPPacket tp = (TZSPPacket)root;
|
||||
if (tp.Protocol == TZSPPacket.TZSPEncapsulatedProtocol.Ethernet)
|
||||
{
|
||||
EthernetPacket ep = (EthernetPacket)tp.SubPacket;
|
||||
srcMAC = ep.SourceMAC;
|
||||
dstMAC = ep.DestinationMAC;
|
||||
}
|
||||
else if (tp.Protocol == TZSPPacket.TZSPEncapsulatedProtocol.IEEE802_11)
|
||||
{
|
||||
W802_11Packet wp = (W802_11Packet)tp.SubPacket;
|
||||
srcMAC = wp.SA;
|
||||
dstMAC = wp.DA;
|
||||
}
|
||||
else
|
||||
{
|
||||
srcMAC = null;
|
||||
dstMAC = null;
|
||||
}
|
||||
}
|
||||
else if (root is EthernetPacket)
|
||||
{
|
||||
EthernetPacket ep = (EthernetPacket)root;
|
||||
srcMAC = ep.SourceMAC;
|
||||
dstMAC = ep.DestinationMAC;
|
||||
}
|
||||
else if (root is W802_11Packet)
|
||||
{
|
||||
W802_11Packet wp = (W802_11Packet)root;
|
||||
srcMAC = wp.SA;
|
||||
dstMAC = wp.DA;
|
||||
}
|
||||
else
|
||||
{
|
||||
srcMAC = null;
|
||||
dstMAC = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static void GetPacketAddresses(Packet packet, ref string srcMAC, ref string dstMAC, ref string srcIP, ref string dstIP)
|
||||
{
|
||||
|
||||
if (packet is TCPv4Packet)
|
||||
{
|
||||
if (packet.ParentPacket is IPv4Packet)
|
||||
{
|
||||
IPv4Packet ip = (IPv4Packet)packet.ParentPacket;
|
||||
srcIP = ip.SourceIP.ToString();
|
||||
dstIP = ip.DestinationIP.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
// get the node address
|
||||
Packet root = packet.RootPacket;
|
||||
if (root is TZSPPacket)
|
||||
{
|
||||
|
||||
TZSPPacket tp = (TZSPPacket)root;
|
||||
if (tp.Protocol == TZSPPacket.TZSPEncapsulatedProtocol.Ethernet)
|
||||
{
|
||||
EthernetPacket ep = (EthernetPacket)tp.SubPacket;
|
||||
srcMAC = DC.GetPhysicalAddress(ep.SourceMAC, 0).ToString();
|
||||
dstMAC = DC.GetPhysicalAddress(ep.DestinationMAC, 0).ToString();
|
||||
}
|
||||
else if (tp.Protocol == TZSPPacket.TZSPEncapsulatedProtocol.IEEE802_11)
|
||||
{
|
||||
W802_11Packet wp = (W802_11Packet)tp.SubPacket;
|
||||
srcMAC = DC.GetPhysicalAddress(wp.SA, 0).ToString();
|
||||
dstMAC = DC.GetPhysicalAddress(wp.DA, 0).ToString();
|
||||
}
|
||||
}
|
||||
else if (root is EthernetPacket)
|
||||
{
|
||||
EthernetPacket ep = (EthernetPacket)root;
|
||||
srcMAC = DC.GetPhysicalAddress(ep.SourceMAC, 0).ToString();
|
||||
dstMAC = DC.GetPhysicalAddress(ep.DestinationMAC, 0).ToString();
|
||||
}
|
||||
else if (root is W802_11Packet)
|
||||
{
|
||||
W802_11Packet wp = (W802_11Packet)root;
|
||||
srcMAC = DC.GetPhysicalAddress(wp.SA, 0).ToString();
|
||||
dstMAC = DC.GetPhysicalAddress(wp.DA, 0).ToString();
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
//PosixTime Timeval;
|
||||
public byte[] Header;
|
||||
public byte[] Preamble;
|
||||
//public byte[] Payload;
|
||||
public byte[] Data;
|
||||
|
||||
public Packet SubPacket;
|
||||
public Packet ParentPacket;
|
||||
public virtual long Parse(byte[] data, uint offset, uint ends) { return 0; }
|
||||
public virtual bool Compose() { return false; }
|
||||
public virtual long Parse(byte[] data, uint offset, uint ends) => 0;
|
||||
|
||||
public Packet RootPacket
|
||||
public virtual bool Compose() => false;
|
||||
|
||||
protected static void ValidateBounds(byte[] data, uint offset, uint ends)
|
||||
{
|
||||
get
|
||||
{
|
||||
Packet root = this;
|
||||
while (root.ParentPacket != null)
|
||||
root = root.ParentPacket;
|
||||
return root;
|
||||
}
|
||||
if (data == null)
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
if (ends > data.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(ends));
|
||||
if (offset > ends)
|
||||
throw new ArgumentOutOfRangeException(nameof(offset));
|
||||
}
|
||||
|
||||
public Packet LeafPacket
|
||||
protected static bool TryGetMissingBytes(
|
||||
uint offset,
|
||||
uint ends,
|
||||
uint needed,
|
||||
out long parseResult)
|
||||
{
|
||||
get
|
||||
var available = ends - offset;
|
||||
if (available < needed)
|
||||
{
|
||||
Packet leaf = this;
|
||||
while (leaf.SubPacket != null)
|
||||
leaf = leaf.SubPacket;
|
||||
return leaf;
|
||||
parseResult = -(long)(needed - available);
|
||||
return true;
|
||||
}
|
||||
|
||||
parseResult = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/************************************ EOF *************************************/
|
||||
|
||||
|
||||
@@ -1,56 +1,26 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2017 Ahmed Kh. Zamil
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Esiur.Misc;
|
||||
using Esiur.Data;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Esiur.Net.Packets.WebSocket;
|
||||
|
||||
public class WebsocketPacket : Packet
|
||||
{
|
||||
private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(false, true);
|
||||
|
||||
public enum WSOpcode : byte
|
||||
{
|
||||
ContinuationFrame = 0x0, // %x0 denotes a continuation frame
|
||||
|
||||
TextFrame = 0x1, // %x1 denotes a text frame
|
||||
|
||||
BinaryFrame = 0x2, // %x2 denotes a binary frame
|
||||
|
||||
// %x3-7 are reserved for further non-control frames
|
||||
|
||||
ConnectionClose = 0x8, // %x8 denotes a connection close
|
||||
|
||||
Ping = 0x9, // %x9 denotes a ping
|
||||
|
||||
Pong = 0xA, // %xA denotes a pong
|
||||
|
||||
//* %xB-F are reserved for further control frames
|
||||
ContinuationFrame = 0x0,
|
||||
TextFrame = 0x1,
|
||||
BinaryFrame = 0x2,
|
||||
ConnectionClose = 0x8,
|
||||
Ping = 0x9,
|
||||
Pong = 0xA,
|
||||
}
|
||||
|
||||
public const ulong DefaultMaximumPayloadLength = 8 * 1024 * 1024;
|
||||
|
||||
public bool FIN;
|
||||
public bool RSV1;
|
||||
@@ -59,157 +29,243 @@ public class WebsocketPacket : Packet
|
||||
public WSOpcode Opcode;
|
||||
public bool Mask;
|
||||
public long PayloadLength;
|
||||
// public UInt32 MaskKey;
|
||||
public byte[] MaskKey;
|
||||
|
||||
public byte[] Message;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum accepted or composed payload. Set to zero to disable the limit.
|
||||
/// </summary>
|
||||
public ulong MaximumPayloadLength { get; set; } = DefaultMaximumPayloadLength;
|
||||
|
||||
/// <summary>
|
||||
/// Expected mask bit for an incoming frame. Servers set this to <c>true</c>,
|
||||
/// clients set it to <c>false</c>, and standalone packet parsing can leave it
|
||||
/// <c>null</c> to accept either direction.
|
||||
/// </summary>
|
||||
public bool? ExpectedMask { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "WebsocketPacket"
|
||||
+ "\n\tFIN: " + FIN
|
||||
+ "\n\tOpcode: " + Opcode
|
||||
+ "\n\tPayload: " + PayloadLength
|
||||
+ "\n\tMaskKey: " + MaskKey
|
||||
+ "\n\tMessage: " + (Message != null ? Message.Length.ToString() : "NULL");
|
||||
}
|
||||
=> $"WebsocketPacket\n\tFIN: {FIN}\n\tOpcode: {Opcode}\n\tPayload: {PayloadLength}" +
|
||||
$"\n\tMaskKey: {MaskKey}\n\tMessage: {(Message == null ? "NULL" : Message.Length.ToString())}";
|
||||
|
||||
public override bool Compose()
|
||||
{
|
||||
var pkt = new List<byte>();
|
||||
pkt.Add((byte)((FIN ? 0x80 : 0x0) |
|
||||
(RSV1 ? 0x40 : 0x0) |
|
||||
(RSV2 ? 0x20 : 0x0) |
|
||||
(RSV3 ? 0x10 : 0x0) |
|
||||
(byte)Opcode));
|
||||
var message = Message ?? Array.Empty<byte>();
|
||||
ValidateFrame(Opcode, FIN, (ulong)message.LongLength);
|
||||
ValidateApplicationPayload(Opcode, FIN, message);
|
||||
EnsureWithinLimit((ulong)message.LongLength);
|
||||
|
||||
// calculate length
|
||||
if (Message.Length > ushort.MaxValue)
|
||||
// 4 bytes
|
||||
var extendedLengthSize = message.Length <= 125
|
||||
? 0
|
||||
: message.Length <= ushort.MaxValue ? 2 : 8;
|
||||
var headerLength = 2 + extendedLengthSize + (Mask ? 4 : 0);
|
||||
Data = new byte[checked(headerLength + message.Length)];
|
||||
|
||||
var offset = 0;
|
||||
Data[offset++] = (byte)((FIN ? 0x80 : 0) |
|
||||
(RSV1 ? 0x40 : 0) |
|
||||
(RSV2 ? 0x20 : 0) |
|
||||
(RSV3 ? 0x10 : 0) |
|
||||
(byte)Opcode);
|
||||
|
||||
if (extendedLengthSize == 0)
|
||||
{
|
||||
pkt.Add((byte)((Mask ? 0x80 : 0x0) | 127));
|
||||
pkt.AddRange(((ulong)Message.LongCount()).ToBytes(Endian.Big));
|
||||
Data[offset++] = (byte)((Mask ? 0x80 : 0) | message.Length);
|
||||
}
|
||||
else if (Message.Length > 125)
|
||||
// 2 bytes
|
||||
else if (extendedLengthSize == 2)
|
||||
{
|
||||
pkt.Add((byte)((Mask ? 0x80 : 0x0) | 126));
|
||||
pkt.AddRange(((ushort)Message.Length).ToBytes(Endian.Big));
|
||||
Data[offset++] = (byte)((Mask ? 0x80 : 0) | 126);
|
||||
Data[offset++] = (byte)(message.Length >> 8);
|
||||
Data[offset++] = (byte)message.Length;
|
||||
}
|
||||
else
|
||||
{
|
||||
pkt.Add((byte)((Mask ? 0x80 : 0x0) | Message.Length));
|
||||
Data[offset++] = (byte)((Mask ? 0x80 : 0) | 127);
|
||||
var length = (ulong)message.LongLength;
|
||||
for (var shift = 56; shift >= 0; shift -= 8)
|
||||
Data[offset++] = (byte)(length >> shift);
|
||||
}
|
||||
|
||||
if (Mask)
|
||||
{
|
||||
pkt.AddRange(MaskKey);
|
||||
if (MaskKey == null || MaskKey.Length != 4)
|
||||
{
|
||||
MaskKey = new byte[4];
|
||||
using (var random = RandomNumberGenerator.Create())
|
||||
random.GetBytes(MaskKey);
|
||||
}
|
||||
|
||||
Buffer.BlockCopy(MaskKey, 0, Data, offset, MaskKey.Length);
|
||||
offset += MaskKey.Length;
|
||||
|
||||
for (var i = 0; i < message.Length; i++)
|
||||
Data[offset + i] = (byte)(message[i] ^ MaskKey[i & 3]);
|
||||
}
|
||||
else if (message.Length > 0)
|
||||
{
|
||||
Buffer.BlockCopy(message, 0, Data, offset, message.Length);
|
||||
}
|
||||
|
||||
pkt.AddRange(Message);
|
||||
|
||||
Data = pkt.ToArray();
|
||||
|
||||
PayloadLength = message.LongLength;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override long Parse(byte[] data, uint offset, uint ends)
|
||||
{
|
||||
ValidateBounds(data, offset, ends);
|
||||
var originalOffset = offset;
|
||||
|
||||
if (TryGetMissingBytes(offset, ends, 2, out var incomplete))
|
||||
return incomplete;
|
||||
|
||||
var first = data[offset++];
|
||||
var second = data[offset++];
|
||||
|
||||
FIN = (first & 0x80) != 0;
|
||||
RSV1 = (first & 0x40) != 0;
|
||||
RSV2 = (first & 0x20) != 0;
|
||||
RSV3 = (first & 0x10) != 0;
|
||||
Opcode = (WSOpcode)(first & 0x0F);
|
||||
Mask = (second & 0x80) != 0;
|
||||
|
||||
if (ExpectedMask.HasValue && Mask != ExpectedMask.Value)
|
||||
throw new InvalidDataException(ExpectedMask.Value
|
||||
? "WebSocket clients must mask every frame sent to a server."
|
||||
: "WebSocket servers must not mask frames sent to a client.");
|
||||
|
||||
if (RSV1 || RSV2 || RSV3)
|
||||
throw new InvalidDataException("WebSocket extensions are not enabled for this connection.");
|
||||
|
||||
ulong payloadLength = (byte)(second & 0x7F);
|
||||
if (payloadLength == 126)
|
||||
{
|
||||
if (TryGetMissingBytes(offset, ends, 2, out incomplete))
|
||||
return incomplete;
|
||||
|
||||
payloadLength = (uint)(data[offset] << 8 | data[offset + 1]);
|
||||
offset += 2;
|
||||
|
||||
if (payloadLength < 126)
|
||||
throw new InvalidDataException("WebSocket payload length is not minimally encoded.");
|
||||
}
|
||||
else if (payloadLength == 127)
|
||||
{
|
||||
if (TryGetMissingBytes(offset, ends, 8, out incomplete))
|
||||
return incomplete;
|
||||
if ((data[offset] & 0x80) != 0)
|
||||
throw new InvalidDataException("WebSocket payload length exceeds the protocol limit.");
|
||||
|
||||
payloadLength = 0;
|
||||
for (var i = 0; i < 8; i++)
|
||||
payloadLength = payloadLength << 8 | data[offset++];
|
||||
|
||||
if (payloadLength <= ushort.MaxValue)
|
||||
throw new InvalidDataException("WebSocket payload length is not minimally encoded.");
|
||||
}
|
||||
|
||||
ValidateFrame(Opcode, FIN, payloadLength);
|
||||
EnsureWithinLimit(payloadLength);
|
||||
if (payloadLength > int.MaxValue)
|
||||
throw new ParserLimitException("WebSocket payload cannot fit in a managed byte array.");
|
||||
|
||||
if (Mask)
|
||||
{
|
||||
if (TryGetMissingBytes(offset, ends, 4, out incomplete))
|
||||
return incomplete;
|
||||
|
||||
MaskKey = new byte[4];
|
||||
Buffer.BlockCopy(data, (int)offset, MaskKey, 0, MaskKey.Length);
|
||||
offset += 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
MaskKey = null;
|
||||
}
|
||||
|
||||
var availablePayload = ends - offset;
|
||||
if ((ulong)availablePayload < payloadLength)
|
||||
return -(long)(payloadLength - availablePayload);
|
||||
|
||||
Message = new byte[(int)payloadLength];
|
||||
if (Mask)
|
||||
{
|
||||
for (var i = 0; i < Message.Length; i++)
|
||||
Message[i] = (byte)(data[offset + i] ^ MaskKey[i & 3]);
|
||||
}
|
||||
else if (Message.Length > 0)
|
||||
{
|
||||
Buffer.BlockCopy(data, (int)offset, Message, 0, Message.Length);
|
||||
}
|
||||
|
||||
offset += (uint)payloadLength;
|
||||
PayloadLength = (long)payloadLength;
|
||||
ValidateApplicationPayload(Opcode, FIN, Message);
|
||||
return offset - originalOffset;
|
||||
}
|
||||
|
||||
private void EnsureWithinLimit(ulong payloadLength)
|
||||
{
|
||||
if (MaximumPayloadLength > 0 && payloadLength > MaximumPayloadLength)
|
||||
throw new ParserLimitException(
|
||||
$"WebSocket payload of {payloadLength} bytes exceeds the {MaximumPayloadLength}-byte limit.");
|
||||
}
|
||||
|
||||
private static void ValidateFrame(WSOpcode opcode, bool final, ulong payloadLength)
|
||||
{
|
||||
var isControl = opcode == WSOpcode.ConnectionClose ||
|
||||
opcode == WSOpcode.Ping ||
|
||||
opcode == WSOpcode.Pong;
|
||||
var isData = opcode == WSOpcode.ContinuationFrame ||
|
||||
opcode == WSOpcode.TextFrame ||
|
||||
opcode == WSOpcode.BinaryFrame;
|
||||
|
||||
if (!isControl && !isData)
|
||||
throw new InvalidDataException($"Unsupported WebSocket opcode: 0x{(byte)opcode:X}.");
|
||||
if (isControl && (!final || payloadLength > 125))
|
||||
throw new InvalidDataException("WebSocket control frames must be final and at most 125 bytes.");
|
||||
if (opcode == WSOpcode.ConnectionClose && payloadLength == 1)
|
||||
throw new InvalidDataException("A WebSocket close frame cannot contain a one-byte payload.");
|
||||
}
|
||||
|
||||
private static void ValidateApplicationPayload(WSOpcode opcode, bool final, byte[] payload)
|
||||
{
|
||||
if (opcode == WSOpcode.TextFrame && final)
|
||||
ValidateTextPayload(payload);
|
||||
else if (opcode == WSOpcode.ConnectionClose)
|
||||
ValidateClosePayload(payload);
|
||||
}
|
||||
|
||||
internal static void ValidateTextPayload(byte[] payload)
|
||||
=> ValidateTextPayload(payload ?? Array.Empty<byte>(), 0, payload?.Length ?? 0);
|
||||
|
||||
private static void ValidateTextPayload(byte[] payload, int offset, int count)
|
||||
{
|
||||
try
|
||||
{
|
||||
long needed = 2;
|
||||
var length = ends - offset;
|
||||
if (length < needed)
|
||||
{
|
||||
//Console.WriteLine("stage 1 " + needed);
|
||||
return length - needed;
|
||||
}
|
||||
|
||||
uint oOffset = offset;
|
||||
FIN = (data[offset] & 0x80) == 0x80;
|
||||
RSV1 = (data[offset] & 0x40) == 0x40;
|
||||
RSV2 = (data[offset] & 0x20) == 0x20;
|
||||
RSV3 = (data[offset] & 0x10) == 0x10;
|
||||
Opcode = (WSOpcode)(data[offset++] & 0xF);
|
||||
Mask = (data[offset] & 0x80) == 0x80;
|
||||
PayloadLength = data[offset++] & 0x7F;
|
||||
|
||||
if (Mask)
|
||||
needed += 4;
|
||||
|
||||
if (PayloadLength == 126)
|
||||
{
|
||||
needed += 2;
|
||||
if (length < needed)
|
||||
{
|
||||
//Console.WriteLine("stage 2 " + needed);
|
||||
return length - needed;
|
||||
}
|
||||
PayloadLength = data.GetUInt16(offset, Endian.Big);
|
||||
offset += 2;
|
||||
}
|
||||
else if (PayloadLength == 127)
|
||||
{
|
||||
needed += 8;
|
||||
if (length < needed)
|
||||
{
|
||||
//Console.WriteLine("stage 3 " + needed);
|
||||
return length - needed;
|
||||
}
|
||||
|
||||
PayloadLength = data.GetInt64(offset, Endian.Big);
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
/*
|
||||
if (Mask)
|
||||
{
|
||||
MaskKey = new byte[4];
|
||||
MaskKey[0] = data[offset++];
|
||||
MaskKey[1] = data[offset++];
|
||||
MaskKey[2] = data[offset++];
|
||||
MaskKey[3] = data[offset++];
|
||||
|
||||
//MaskKey = DC.GetUInt32(data, offset);
|
||||
//offset += 4;
|
||||
}
|
||||
*/
|
||||
|
||||
needed += PayloadLength;
|
||||
if (length < needed)
|
||||
{
|
||||
//Console.WriteLine("stage 4");
|
||||
return length - needed;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
if (Mask)
|
||||
{
|
||||
MaskKey = new byte[4];
|
||||
MaskKey[0] = data[offset++];
|
||||
MaskKey[1] = data[offset++];
|
||||
MaskKey[2] = data[offset++];
|
||||
MaskKey[3] = data[offset++];
|
||||
|
||||
Message = data.Clip(offset, (uint)PayloadLength);
|
||||
|
||||
//var aMask = BitConverter.GetBytes(MaskKey);
|
||||
for (int i = 0; i < Message.Length; i++)
|
||||
Message[i] = (byte)(Message[i] ^ MaskKey[i % 4]);
|
||||
}
|
||||
else
|
||||
Message = data.Clip(offset, (uint)PayloadLength);
|
||||
|
||||
|
||||
return offset - oOffset + (int)PayloadLength;
|
||||
}
|
||||
_ = StrictUtf8.GetCharCount(payload, offset, count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (DecoderFallbackException exception)
|
||||
{
|
||||
Global.Log(ex);
|
||||
Global.Log("WebsocketPacket", Core.LogType.Debug, offset + "::" + data.ToHex());
|
||||
throw ex;
|
||||
throw new InvalidDataException("WebSocket text payload is not valid UTF-8.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateClosePayload(byte[] payload)
|
||||
{
|
||||
if (payload == null || payload.Length < 2)
|
||||
return;
|
||||
|
||||
var statusCode = payload[0] << 8 | payload[1];
|
||||
var isDefinedProtocolCode = statusCode >= 1000 && statusCode <= 1014
|
||||
&& statusCode != 1004
|
||||
&& statusCode != 1005
|
||||
&& statusCode != 1006;
|
||||
var isApplicationCode = statusCode >= 3000 && statusCode <= 4999;
|
||||
|
||||
if (!isDefinedProtocolCode && !isApplicationCode)
|
||||
throw new InvalidDataException($"Invalid WebSocket close status code: {statusCode}.");
|
||||
|
||||
if (payload.Length > 2)
|
||||
ValidateTextPayload(payload, 2, payload.Length - 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,12 @@ namespace Esiur.Net.Sockets;
|
||||
public class SSLSocket : ISocket
|
||||
{
|
||||
|
||||
private sealed class PendingSend
|
||||
{
|
||||
public AsyncReply<bool> Reply;
|
||||
public byte[] Buffer;
|
||||
}
|
||||
|
||||
public INetworkReceiver<ISocket> Receiver { get; set; }
|
||||
|
||||
Socket sock;
|
||||
@@ -54,7 +60,10 @@ public class SSLSocket : ISocket
|
||||
|
||||
readonly object sendLock = new object();
|
||||
|
||||
Queue<KeyValuePair<AsyncReply<bool>, byte[]>> sendBufferQueue = new Queue<KeyValuePair<AsyncReply<bool>, byte[]>>();// Queue<byte[]>();
|
||||
readonly Queue<PendingSend> sendBufferQueue = new Queue<PendingSend>();
|
||||
PendingSend currentSend;
|
||||
long pendingSendBytes;
|
||||
long maximumPendingSendBytes = 16 * 1024 * 1024;
|
||||
|
||||
bool asyncSending;
|
||||
bool began = false;
|
||||
@@ -72,33 +81,50 @@ public class SSLSocket : ISocket
|
||||
bool server;
|
||||
string hostname;
|
||||
|
||||
public long PendingSendBytes => Interlocked.Read(ref pendingSendBytes);
|
||||
|
||||
/// <summary>Maximum number of unsent plaintext bytes retained for a slow TLS peer.</summary>
|
||||
public long MaximumPendingSendBytes
|
||||
{
|
||||
get => Interlocked.Read(ref maximumPendingSendBytes);
|
||||
set
|
||||
{
|
||||
if (value <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
|
||||
Interlocked.Exchange(ref maximumPendingSendBytes, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async AsyncReply<bool> Connect(string hostname, ushort port)
|
||||
{
|
||||
var rt = new AsyncReply<bool>();
|
||||
|
||||
this.hostname = hostname;
|
||||
this.server = false;
|
||||
|
||||
state = SocketState.Connecting;
|
||||
await sock.ConnectAsync(hostname, port);
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
await BeginAsync();
|
||||
await sock.ConnectAsync(hostname, port);
|
||||
ssl = new SslStream(new NetworkStream(sock));
|
||||
state = SocketState.Established;
|
||||
|
||||
if (!await BeginAsync())
|
||||
{
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
//OnConnect?.Invoke();
|
||||
Receiver?.NetworkConnect(this);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
state = SocketState.Closed;// .Terminated;
|
||||
Close();
|
||||
Global.Log(ex);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//private void DataSent(Task task)
|
||||
@@ -132,64 +158,55 @@ public class SSLSocket : ISocket
|
||||
//}
|
||||
|
||||
|
||||
private void SendCallback(IAsyncResult ar)
|
||||
private async Task ProcessSendQueueAsync()
|
||||
{
|
||||
if (ar != null)
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
PendingSend pending;
|
||||
|
||||
lock (sendLock)
|
||||
{
|
||||
ssl.EndWrite(ar);
|
||||
|
||||
if (ar.AsyncState != null)
|
||||
((AsyncReply<bool>)ar.AsyncState).Trigger(true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (state != SocketState.Closed && !sock.Connected)
|
||||
{
|
||||
//state = SocketState.Closed;//.Terminated;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lock (sendLock)
|
||||
{
|
||||
|
||||
if (sendBufferQueue.Count > 0)
|
||||
{
|
||||
var kv = sendBufferQueue.Dequeue();
|
||||
|
||||
try
|
||||
{
|
||||
ssl.BeginWrite(kv.Value, 0, kv.Value.Length, SendCallback, kv.Key);
|
||||
}
|
||||
catch //(Exception ex)
|
||||
if (held || state == SocketState.Closed || sendBufferQueue.Count == 0)
|
||||
{
|
||||
asyncSending = false;
|
||||
try
|
||||
{
|
||||
if (kv.Key != null)
|
||||
kv.Key.Trigger(false);
|
||||
|
||||
if (state != SocketState.Closed && !sock.Connected)
|
||||
{
|
||||
//state = SocketState.Terminated;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
catch //(Exception ex2)
|
||||
{
|
||||
//state = SocketState.Closed;// .Terminated;
|
||||
Close();
|
||||
}
|
||||
|
||||
//Global.Log("TCPSocket", LogType.Error, ex.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
pending = sendBufferQueue.Dequeue();
|
||||
currentSend = pending;
|
||||
}
|
||||
else
|
||||
|
||||
try
|
||||
{
|
||||
asyncSending = false;
|
||||
await ssl.WriteAsync(pending.Buffer, 0, pending.Buffer.Length).ConfigureAwait(false);
|
||||
|
||||
lock (sendLock)
|
||||
{
|
||||
if (ReferenceEquals(currentSend, pending))
|
||||
{
|
||||
currentSend = null;
|
||||
Interlocked.Add(ref pendingSendBytes, -pending.Buffer.Length);
|
||||
}
|
||||
}
|
||||
|
||||
TryCompleteSend(pending, true, null);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
lock (sendLock)
|
||||
{
|
||||
if (ReferenceEquals(currentSend, pending))
|
||||
{
|
||||
currentSend = null;
|
||||
Interlocked.Add(ref pendingSendBytes, -pending.Buffer.Length);
|
||||
}
|
||||
|
||||
asyncSending = false;
|
||||
}
|
||||
|
||||
TryCompleteSend(pending, false, exception);
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,25 +274,52 @@ public class SSLSocket : ISocket
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (state != SocketState.Closed)// && state != SocketState.Terminated)
|
||||
List<PendingSend> abandoned;
|
||||
lock (sendLock)
|
||||
{
|
||||
state = SocketState.Closed;
|
||||
if (state == SocketState.Closed)
|
||||
return;
|
||||
|
||||
if (sock.Connected)
|
||||
state = SocketState.Closed;
|
||||
abandoned = sendBufferQueue.ToList();
|
||||
sendBufferQueue.Clear();
|
||||
if (currentSend != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
sock.Shutdown(SocketShutdown.Both);
|
||||
}
|
||||
catch
|
||||
{
|
||||
//state = SocketState.Terminated;
|
||||
}
|
||||
abandoned.Insert(0, currentSend);
|
||||
currentSend = null;
|
||||
}
|
||||
|
||||
Receiver?.NetworkClose(this);
|
||||
//OnClose?.Invoke();
|
||||
foreach (var pending in abandoned)
|
||||
Interlocked.Add(ref pendingSendBytes, -pending.Buffer.Length);
|
||||
asyncSending = false;
|
||||
}
|
||||
|
||||
if (sock != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (sock.Connected)
|
||||
sock.Shutdown(SocketShutdown.Both);
|
||||
}
|
||||
catch
|
||||
{
|
||||
//state = SocketState.Terminated;
|
||||
}
|
||||
|
||||
// Closing the underlying socket is what aborts an in-progress TLS
|
||||
// handshake. Shutdown alone can leave AuthenticateAsServerAsync waiting
|
||||
// forever for a peer that never sends a ClientHello.
|
||||
try { sock.Close(); } catch { }
|
||||
}
|
||||
|
||||
try { ssl?.Dispose(); } catch { }
|
||||
|
||||
foreach (var pending in abandoned)
|
||||
TryCompleteSend(pending, false, null);
|
||||
|
||||
try { Receiver?.NetworkClose(this); }
|
||||
catch (Exception exception) { Global.Log(exception); }
|
||||
//OnClose?.Invoke();
|
||||
}
|
||||
|
||||
|
||||
@@ -287,35 +331,26 @@ public class SSLSocket : ISocket
|
||||
|
||||
public void Send(byte[] message, int offset, int size)
|
||||
{
|
||||
ValidateRange(message, offset, size);
|
||||
if (size == 0)
|
||||
return;
|
||||
|
||||
|
||||
var msg = message.Clip((uint)offset, (uint)size);
|
||||
|
||||
bool startPump = false;
|
||||
lock (sendLock)
|
||||
{
|
||||
|
||||
if (!sock.Connected)
|
||||
if (state != SocketState.Established)
|
||||
return;
|
||||
|
||||
if (asyncSending || held)
|
||||
{
|
||||
sendBufferQueue.Enqueue(new KeyValuePair<AsyncReply<bool>, byte[]>(null, msg));// message.Clip((uint)offset, (uint)size));
|
||||
}
|
||||
else
|
||||
{
|
||||
asyncSending = true;
|
||||
try
|
||||
{
|
||||
ssl.BeginWrite(msg, 0, msg.Length, SendCallback, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
asyncSending = false;
|
||||
//state = SocketState.Terminated;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
EnsureSendCapacity_NoLock(size);
|
||||
var msg = new byte[size];
|
||||
Buffer.BlockCopy(message, offset, msg, 0, size);
|
||||
sendBufferQueue.Enqueue(new PendingSend { Buffer = msg });
|
||||
Interlocked.Add(ref pendingSendBytes, size);
|
||||
startPump = TryStartSendPump_NoLock();
|
||||
}
|
||||
|
||||
if (startPump)
|
||||
_ = ProcessSendQueueAsync();
|
||||
}
|
||||
|
||||
//public void Send(byte[] message)
|
||||
@@ -438,15 +473,16 @@ public class SSLSocket : ISocket
|
||||
ssl.BeginRead(receiveBuffer, 0, receiveBuffer.Length, ReceiveCallback, this);
|
||||
|
||||
}
|
||||
catch //(Exception ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (state != SocketState.Closed && !sock.Connected)
|
||||
{
|
||||
//state = SocketState.Terminated;
|
||||
// Socket.Connected reports the state of the last operation and can
|
||||
// remain true after a TLS read failure. Any read exception ends this
|
||||
// receive loop, so close deterministically instead of leaving a
|
||||
// half-open connection that will never read again.
|
||||
if (state != SocketState.Closed)
|
||||
Close();
|
||||
}
|
||||
|
||||
//Global.Log("SSLSocket", LogType.Error, ex.ToString());
|
||||
Global.Log("SSLSocket", LogType.Warning, ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,59 +525,101 @@ public class SSLSocket : ISocket
|
||||
|
||||
public void Hold()
|
||||
{
|
||||
held = true;
|
||||
lock (sendLock)
|
||||
held = true;
|
||||
}
|
||||
|
||||
public void Unhold()
|
||||
{
|
||||
try
|
||||
{
|
||||
SendCallback(null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Global.Log(ex);
|
||||
}
|
||||
finally
|
||||
bool startPump;
|
||||
lock (sendLock)
|
||||
{
|
||||
held = false;
|
||||
startPump = TryStartSendPump_NoLock();
|
||||
}
|
||||
|
||||
if (startPump)
|
||||
_ = ProcessSendQueueAsync();
|
||||
}
|
||||
|
||||
|
||||
public AsyncReply<bool> SendAsync(byte[] message, int offset, int length)
|
||||
{
|
||||
ValidateRange(message, offset, length);
|
||||
if (length == 0)
|
||||
return new AsyncReply<bool>(true);
|
||||
|
||||
var msg = message.Clip((uint)offset, (uint)length);
|
||||
|
||||
var rt = new AsyncReply<bool>();
|
||||
bool startPump = false;
|
||||
Exception capacityError = null;
|
||||
lock (sendLock)
|
||||
{
|
||||
if (!sock.Connected)
|
||||
if (state != SocketState.Established)
|
||||
return new AsyncReply<bool>(false);
|
||||
|
||||
var rt = new AsyncReply<bool>();
|
||||
|
||||
if (asyncSending || held)
|
||||
try
|
||||
{
|
||||
sendBufferQueue.Enqueue(new KeyValuePair<AsyncReply<bool>, byte[]>(rt, msg));
|
||||
EnsureSendCapacity_NoLock(length);
|
||||
var msg = new byte[length];
|
||||
Buffer.BlockCopy(message, offset, msg, 0, length);
|
||||
sendBufferQueue.Enqueue(new PendingSend { Reply = rt, Buffer = msg });
|
||||
Interlocked.Add(ref pendingSendBytes, length);
|
||||
startPump = TryStartSendPump_NoLock();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
capacityError = exception;
|
||||
}
|
||||
}
|
||||
|
||||
if (capacityError != null)
|
||||
rt.TriggerError(capacityError);
|
||||
else if (startPump)
|
||||
_ = ProcessSendQueueAsync();
|
||||
|
||||
return rt;
|
||||
}
|
||||
|
||||
private bool TryStartSendPump_NoLock()
|
||||
{
|
||||
if (asyncSending || held || state != SocketState.Established || sendBufferQueue.Count == 0)
|
||||
return false;
|
||||
|
||||
asyncSending = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void EnsureSendCapacity_NoLock(int length)
|
||||
{
|
||||
var limit = Interlocked.Read(ref maximumPendingSendBytes);
|
||||
var pending = Interlocked.Read(ref pendingSendBytes);
|
||||
if (length > limit - pending)
|
||||
throw new InvalidOperationException($"The TLS send queue exceeded its {limit}-byte limit.");
|
||||
}
|
||||
|
||||
private static void ValidateRange(byte[] message, int offset, int length)
|
||||
{
|
||||
if (message == null)
|
||||
throw new ArgumentNullException(nameof(message));
|
||||
if (offset < 0 || length < 0 || offset > message.Length - length)
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
private static void TryCompleteSend(PendingSend pending, bool succeeded, Exception exception)
|
||||
{
|
||||
if (pending?.Reply == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (exception != null)
|
||||
pending.Reply.TriggerError(exception);
|
||||
else
|
||||
{
|
||||
asyncSending = true;
|
||||
try
|
||||
{
|
||||
ssl.BeginWrite(msg, 0, msg.Length, SendCallback, rt);// null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
rt.TriggerError(ex);
|
||||
asyncSending = false;
|
||||
//state = SocketState.Terminated;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
return rt;
|
||||
pending.Reply.Trigger(succeeded);
|
||||
}
|
||||
catch (Exception callbackException)
|
||||
{
|
||||
Global.Log(callbackException);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,20 @@ public class TcpSocket : ISocket
|
||||
public AsyncReply<bool> Reply;
|
||||
}
|
||||
|
||||
private readonly struct SendReplyCompletion
|
||||
{
|
||||
public SendReplyCompletion(AsyncReply<bool> reply, bool result, Exception error)
|
||||
{
|
||||
Reply = reply;
|
||||
Result = result;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
public AsyncReply<bool> Reply { get; }
|
||||
public bool Result { get; }
|
||||
public Exception Error { get; }
|
||||
}
|
||||
|
||||
public INetworkReceiver<ISocket> Receiver { get; set; }
|
||||
public event DestroyedEvent OnDestroy;
|
||||
|
||||
@@ -37,6 +51,8 @@ public class TcpSocket : ISocket
|
||||
private SocketAsyncEventArgs sendArgs;
|
||||
|
||||
private PendingSend currentSend;
|
||||
private long pendingSendBytes;
|
||||
private long maximumPendingSendBytes = 16 * 1024 * 1024;
|
||||
private bool sendInProgress;
|
||||
private bool began;
|
||||
private bool held;
|
||||
@@ -52,6 +68,24 @@ public class TcpSocket : ISocket
|
||||
public SocketState State => state;
|
||||
public int BytesSent => bytesSent;
|
||||
public int BytesReceived => bytesReceived;
|
||||
public long PendingSendBytes => Interlocked.Read(ref pendingSendBytes);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of unsent bytes retained by this socket. This bounds the
|
||||
/// copies made by <see cref="Send(byte[], int, int)"/> and
|
||||
/// <see cref="SendAsync(byte[], int, int)"/> when a peer is slow.
|
||||
/// </summary>
|
||||
public long MaximumPendingSendBytes
|
||||
{
|
||||
get => Interlocked.Read(ref maximumPendingSendBytes);
|
||||
set
|
||||
{
|
||||
if (value <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
|
||||
Interlocked.Exchange(ref maximumPendingSendBytes, value);
|
||||
}
|
||||
}
|
||||
|
||||
public IPEndPoint LocalEndPoint => sock.LocalEndPoint as IPEndPoint;
|
||||
public IPEndPoint RemoteEndPoint => sock.RemoteEndPoint as IPEndPoint;
|
||||
@@ -227,14 +261,19 @@ public class TcpSocket : ISocket
|
||||
if (destroyed || state != SocketState.Established)
|
||||
return;
|
||||
|
||||
var copy = new byte[length];
|
||||
Buffer.BlockCopy(message, offset, copy, 0, length);
|
||||
List<SendReplyCompletion> completions = null;
|
||||
Exception sendError = null;
|
||||
|
||||
lock (sendLock)
|
||||
{
|
||||
if (destroyed || state != SocketState.Established)
|
||||
return;
|
||||
|
||||
EnsureSendCapacity_NoLock(length);
|
||||
|
||||
var copy = new byte[length];
|
||||
Buffer.BlockCopy(message, offset, copy, 0, length);
|
||||
|
||||
sendQueue.Enqueue(new PendingSend
|
||||
{
|
||||
Buffer = copy,
|
||||
@@ -242,9 +281,12 @@ public class TcpSocket : ISocket
|
||||
Count = copy.Length,
|
||||
Reply = null
|
||||
});
|
||||
Interlocked.Add(ref pendingSendBytes, length);
|
||||
|
||||
TryStartNextSend_NoLock();
|
||||
TryStartNextSend_NoLock(ref completions, ref sendError);
|
||||
}
|
||||
|
||||
FinishSendWork(completions, sendError);
|
||||
}
|
||||
|
||||
public AsyncReply<bool> SendAsync(byte[] message, int offset, int length)
|
||||
@@ -268,8 +310,8 @@ public class TcpSocket : ISocket
|
||||
return rt;
|
||||
}
|
||||
|
||||
var copy = new byte[length];
|
||||
Buffer.BlockCopy(message, offset, copy, 0, length);
|
||||
List<SendReplyCompletion> completions = null;
|
||||
Exception sendError = null;
|
||||
|
||||
lock (sendLock)
|
||||
{
|
||||
@@ -279,6 +321,19 @@ public class TcpSocket : ISocket
|
||||
return rt;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
EnsureSendCapacity_NoLock(length);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
rt.TriggerError(ex);
|
||||
return rt;
|
||||
}
|
||||
|
||||
var copy = new byte[length];
|
||||
Buffer.BlockCopy(message, offset, copy, 0, length);
|
||||
|
||||
sendQueue.Enqueue(new PendingSend
|
||||
{
|
||||
Buffer = copy,
|
||||
@@ -286,10 +341,13 @@ public class TcpSocket : ISocket
|
||||
Count = copy.Length,
|
||||
Reply = rt
|
||||
});
|
||||
Interlocked.Add(ref pendingSendBytes, length);
|
||||
|
||||
TryStartNextSend_NoLock();
|
||||
TryStartNextSend_NoLock(ref completions, ref sendError);
|
||||
}
|
||||
|
||||
FinishSendWork(completions, sendError);
|
||||
|
||||
return rt;
|
||||
}
|
||||
|
||||
@@ -323,15 +381,22 @@ public class TcpSocket : ISocket
|
||||
|
||||
public void Hold()
|
||||
{
|
||||
held = true;
|
||||
lock (sendLock)
|
||||
held = true;
|
||||
}
|
||||
|
||||
public void Unhold()
|
||||
{
|
||||
held = false;
|
||||
List<SendReplyCompletion> completions = null;
|
||||
Exception sendError = null;
|
||||
|
||||
lock (sendLock)
|
||||
TryStartNextSend_NoLock();
|
||||
{
|
||||
held = false;
|
||||
TryStartNextSend_NoLock(ref completions, ref sendError);
|
||||
}
|
||||
|
||||
FinishSendWork(completions, sendError);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
@@ -444,16 +509,20 @@ public class TcpSocket : ISocket
|
||||
}
|
||||
}
|
||||
|
||||
private void TryStartNextSend_NoLock()
|
||||
private void TryStartNextSend_NoLock(
|
||||
ref List<SendReplyCompletion> completions,
|
||||
ref Exception sendError)
|
||||
{
|
||||
if (held || destroyed || state != SocketState.Established || sendInProgress)
|
||||
return;
|
||||
|
||||
sendInProgress = true;
|
||||
PumpSendQueue_NoLock();
|
||||
PumpSendQueue_NoLock(ref completions, ref sendError);
|
||||
}
|
||||
|
||||
private void PumpSendQueue_NoLock()
|
||||
private void PumpSendQueue_NoLock(
|
||||
ref List<SendReplyCompletion> completions,
|
||||
ref Exception sendError)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
@@ -492,9 +561,9 @@ public class TcpSocket : ISocket
|
||||
var reply = currentSend?.Reply;
|
||||
currentSend = null;
|
||||
sendInProgress = false;
|
||||
reply?.TriggerError(ex);
|
||||
FailPendingSends_NoLock(ex);
|
||||
CloseDueToSendError_NoLock(ex);
|
||||
QueueSendCompletion(ref completions, reply, false, ex);
|
||||
FailPendingSends_NoLock(ex, ref completions);
|
||||
sendError = ex;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -503,23 +572,35 @@ public class TcpSocket : ISocket
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ProcessSendCompletion_NoLock(sendArgs))
|
||||
if (!ProcessSendCompletion_NoLock(
|
||||
sendArgs,
|
||||
ref completions,
|
||||
ref sendError))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessSend(SocketAsyncEventArgs e)
|
||||
{
|
||||
List<SendReplyCompletion> completions = null;
|
||||
Exception sendError = null;
|
||||
|
||||
lock (sendLock)
|
||||
{
|
||||
if (!ProcessSendCompletion_NoLock(e))
|
||||
return;
|
||||
|
||||
PumpSendQueue_NoLock();
|
||||
if (ProcessSendCompletion_NoLock(
|
||||
e,
|
||||
ref completions,
|
||||
ref sendError))
|
||||
PumpSendQueue_NoLock(ref completions, ref sendError);
|
||||
}
|
||||
|
||||
FinishSendWork(completions, sendError);
|
||||
}
|
||||
|
||||
private bool ProcessSendCompletion_NoLock(SocketAsyncEventArgs e)
|
||||
private bool ProcessSendCompletion_NoLock(
|
||||
SocketAsyncEventArgs e,
|
||||
ref List<SendReplyCompletion> completions,
|
||||
ref Exception sendError)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -532,26 +613,27 @@ public class TcpSocket : ISocket
|
||||
if (e.SocketError != SocketError.Success)
|
||||
{
|
||||
var ex = new SocketException((int)e.SocketError);
|
||||
currentSend.Reply?.TriggerError(ex);
|
||||
QueueSendCompletion(ref completions, currentSend.Reply, false, ex);
|
||||
currentSend = null;
|
||||
sendInProgress = false;
|
||||
FailPendingSends_NoLock(ex);
|
||||
CloseDueToSendError_NoLock(ex);
|
||||
FailPendingSends_NoLock(ex, ref completions);
|
||||
sendError = ex;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.BytesTransferred <= 0)
|
||||
{
|
||||
var ex = new SocketException((int)SocketError.ConnectionReset);
|
||||
currentSend.Reply?.TriggerError(ex);
|
||||
QueueSendCompletion(ref completions, currentSend.Reply, false, ex);
|
||||
currentSend = null;
|
||||
sendInProgress = false;
|
||||
FailPendingSends_NoLock(ex);
|
||||
CloseDueToSendError_NoLock(ex);
|
||||
FailPendingSends_NoLock(ex, ref completions);
|
||||
sendError = ex;
|
||||
return false;
|
||||
}
|
||||
|
||||
Interlocked.Add(ref bytesSent, e.BytesTransferred);
|
||||
Interlocked.Add(ref pendingSendBytes, -e.BytesTransferred);
|
||||
|
||||
currentSend.Offset += e.BytesTransferred;
|
||||
currentSend.Count -= e.BytesTransferred;
|
||||
@@ -561,42 +643,42 @@ public class TcpSocket : ISocket
|
||||
return true;
|
||||
}
|
||||
|
||||
currentSend.Reply?.Trigger(true);
|
||||
QueueSendCompletion(ref completions, currentSend.Reply, true, null);
|
||||
currentSend = null;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
currentSend?.Reply?.TriggerError(ex);
|
||||
QueueSendCompletion(ref completions, currentSend?.Reply, false, ex);
|
||||
currentSend = null;
|
||||
sendInProgress = false;
|
||||
FailPendingSends_NoLock(ex);
|
||||
CloseDueToSendError_NoLock(ex);
|
||||
FailPendingSends_NoLock(ex, ref completions);
|
||||
sendError = ex;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void FailPendingSends_NoLock(Exception ex)
|
||||
private void FailPendingSends_NoLock(
|
||||
Exception ex,
|
||||
ref List<SendReplyCompletion> completions)
|
||||
{
|
||||
while (sendQueue.Count > 0)
|
||||
{
|
||||
var item = sendQueue.Dequeue();
|
||||
try
|
||||
{
|
||||
item.Reply?.TriggerError(ex);
|
||||
}
|
||||
catch { }
|
||||
QueueSendCompletion(ref completions, item.Reply, false, ex);
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref pendingSendBytes, 0);
|
||||
}
|
||||
|
||||
private void CloseDueToSendError_NoLock(Exception ex)
|
||||
private bool CloseDueToSendError(Exception ex)
|
||||
{
|
||||
bool notify = false;
|
||||
|
||||
lock (stateLock)
|
||||
{
|
||||
if (state == SocketState.Closed)
|
||||
return;
|
||||
return false;
|
||||
|
||||
state = SocketState.Closed;
|
||||
notify = !closeNotified;
|
||||
@@ -609,6 +691,17 @@ public class TcpSocket : ISocket
|
||||
|
||||
Global.Log(ex);
|
||||
|
||||
return notify;
|
||||
}
|
||||
|
||||
private void FinishSendWork(
|
||||
List<SendReplyCompletion> completions,
|
||||
Exception sendError)
|
||||
{
|
||||
var notify = sendError != null && CloseDueToSendError(sendError);
|
||||
|
||||
CompleteSendReplies(completions);
|
||||
|
||||
if (notify)
|
||||
{
|
||||
try { Receiver?.NetworkClose(this); }
|
||||
@@ -616,9 +709,44 @@ public class TcpSocket : ISocket
|
||||
}
|
||||
}
|
||||
|
||||
private static void QueueSendCompletion(
|
||||
ref List<SendReplyCompletion> completions,
|
||||
AsyncReply<bool> reply,
|
||||
bool result,
|
||||
Exception error)
|
||||
{
|
||||
if (reply == null)
|
||||
return;
|
||||
|
||||
completions ??= new List<SendReplyCompletion>();
|
||||
completions.Add(new SendReplyCompletion(reply, result, error));
|
||||
}
|
||||
|
||||
private static void CompleteSendReplies(List<SendReplyCompletion> completions)
|
||||
{
|
||||
if (completions == null)
|
||||
return;
|
||||
|
||||
foreach (var completion in completions)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (completion.Error != null)
|
||||
completion.Reply.TriggerError(completion.Error);
|
||||
else
|
||||
completion.Reply.Trigger(completion.Result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Global.Log(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SafeClose(Exception ex, bool notifyReceiver)
|
||||
{
|
||||
bool notify = false;
|
||||
List<SendReplyCompletion> completions = null;
|
||||
|
||||
lock (stateLock)
|
||||
{
|
||||
@@ -636,25 +764,30 @@ public class TcpSocket : ISocket
|
||||
|
||||
if (ex != null)
|
||||
{
|
||||
try { currentSend?.Reply?.TriggerError(ex); } catch { }
|
||||
QueueSendCompletion(ref completions, currentSend?.Reply, false, ex);
|
||||
currentSend = null;
|
||||
FailPendingSends_NoLock(ex);
|
||||
FailPendingSends_NoLock(ex, ref completions);
|
||||
}
|
||||
else
|
||||
{
|
||||
QueueSendCompletion(ref completions, currentSend?.Reply, false, null);
|
||||
currentSend = null;
|
||||
while (sendQueue.Count > 0)
|
||||
{
|
||||
var item = sendQueue.Dequeue();
|
||||
try { item.Reply?.Trigger(false); } catch { }
|
||||
QueueSendCompletion(ref completions, item.Reply, false, null);
|
||||
}
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref pendingSendBytes, 0);
|
||||
}
|
||||
|
||||
try { sock.Shutdown(SocketShutdown.Both); } catch { }
|
||||
try { sock.Close(); } catch { }
|
||||
try { sock.Dispose(); } catch { }
|
||||
|
||||
CompleteSendReplies(completions);
|
||||
|
||||
if (ex != null)
|
||||
Global.Log(ex);
|
||||
|
||||
@@ -667,7 +800,19 @@ public class TcpSocket : ISocket
|
||||
|
||||
private static void ValidateRange(byte[] message, int offset, int length)
|
||||
{
|
||||
if (offset < 0 || length < 0 || offset + length > message.Length)
|
||||
if (offset < 0 || length < 0 || offset > message.Length - length)
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureSendCapacity_NoLock(int length)
|
||||
{
|
||||
var limit = Interlocked.Read(ref maximumPendingSendBytes);
|
||||
var pending = Interlocked.Read(ref pendingSendBytes);
|
||||
|
||||
if (length > limit - pending)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The socket send queue exceeded its {limit}-byte limit.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,9 +45,16 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
ISocket sock;
|
||||
NetworkBuffer receiveNetworkBuffer = new NetworkBuffer();
|
||||
NetworkBuffer sendNetworkBuffer = new NetworkBuffer();
|
||||
NetworkBuffer fragmentedMessageBuffer = new NetworkBuffer();
|
||||
|
||||
bool fragmentedMessage;
|
||||
WebsocketPacket.WSOpcode fragmentedMessageOpcode;
|
||||
ulong fragmentedMessageLength;
|
||||
|
||||
object sendLock = new object();
|
||||
bool held;
|
||||
bool destroyed;
|
||||
ulong maximumMessageLength = WebsocketPacket.DefaultMaximumPayloadLength;
|
||||
|
||||
//public event ISocketReceiveEvent OnReceive;
|
||||
//public event ISocketConnectEvent OnConnect;
|
||||
@@ -79,11 +86,32 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
|
||||
public INetworkReceiver<ISocket> Receiver { get; set; }
|
||||
|
||||
public WSocket(ISocket socket)
|
||||
/// <summary>Whether this endpoint receives client frames and sends server frames.</summary>
|
||||
public bool IsServer { get; }
|
||||
|
||||
/// <summary>Maximum payload accepted for one complete message.</summary>
|
||||
public ulong MaximumMessageLength
|
||||
{
|
||||
get => maximumMessageLength;
|
||||
set
|
||||
{
|
||||
maximumMessageLength = value;
|
||||
pkt_receive.MaximumPayloadLength = value;
|
||||
}
|
||||
}
|
||||
|
||||
public WSocket(ISocket socket)
|
||||
: this(socket, true)
|
||||
{
|
||||
}
|
||||
|
||||
public WSocket(ISocket socket, bool isServer)
|
||||
{
|
||||
IsServer = isServer;
|
||||
pkt_send.FIN = true;
|
||||
pkt_send.Mask = false;
|
||||
pkt_send.Mask = !isServer;
|
||||
pkt_send.Opcode = WebsocketPacket.WSOpcode.BinaryFrame;
|
||||
pkt_receive.ExpectedMask = isServer;
|
||||
sock = socket;
|
||||
|
||||
sock.Receiver = this;
|
||||
@@ -111,8 +139,11 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
public void Send(WebsocketPacket packet)
|
||||
{
|
||||
lock (sendLock)
|
||||
{
|
||||
PrepareOutboundPacket(packet);
|
||||
if (packet.Compose())
|
||||
sock.Send(packet.Data);
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(byte[] message)
|
||||
@@ -131,6 +162,7 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
|
||||
pkt_send.Message = message;
|
||||
|
||||
PrepareOutboundPacket(pkt_send);
|
||||
if (pkt_send.Compose())
|
||||
sock?.Send(pkt_send.Data);
|
||||
|
||||
@@ -154,6 +186,7 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
|
||||
pkt_send.Message = new byte[size];
|
||||
Buffer.BlockCopy(message, offset, pkt_send.Message, 0, size);
|
||||
PrepareOutboundPacket(pkt_send);
|
||||
if (pkt_send.Compose())
|
||||
sock.Send(pkt_send.Data);
|
||||
}
|
||||
@@ -184,18 +217,36 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
Close();
|
||||
//OnClose = null;
|
||||
//OnConnect = null;
|
||||
//OnReceive = null;
|
||||
receiveNetworkBuffer = null;
|
||||
//sock.OnReceive -= Sock_OnReceive;
|
||||
//sock.OnClose -= Sock_OnClose;
|
||||
//sock.OnConnect -= Sock_OnConnect;
|
||||
sock.Receiver = null;
|
||||
sock = null;
|
||||
OnDestroy?.Invoke(this);
|
||||
OnDestroy = null;
|
||||
ISocket socket;
|
||||
DestroyedEvent onDestroy;
|
||||
|
||||
lock (sendLock)
|
||||
{
|
||||
if (destroyed)
|
||||
return;
|
||||
|
||||
destroyed = true;
|
||||
socket = sock;
|
||||
onDestroy = OnDestroy;
|
||||
OnDestroy = null;
|
||||
}
|
||||
|
||||
// Close can synchronously re-enter Destroy through NetworkClose. The
|
||||
// guard above keeps that path idempotent while the captured socket is
|
||||
// still valid for the outer cleanup.
|
||||
try { socket?.Close(); } catch (Exception ex) { Global.Log(ex); }
|
||||
|
||||
lock (sendLock)
|
||||
{
|
||||
if (socket != null && ReferenceEquals(socket.Receiver, this))
|
||||
socket.Receiver = null;
|
||||
|
||||
sock = null;
|
||||
receiveNetworkBuffer = null;
|
||||
fragmentedMessageBuffer = null;
|
||||
}
|
||||
|
||||
onDestroy?.Invoke(this);
|
||||
}
|
||||
|
||||
public AsyncReply<ISocket> AcceptAsync()
|
||||
@@ -205,8 +256,8 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
|
||||
public void Hold()
|
||||
{
|
||||
//Console.WriteLine("WS Hold ");
|
||||
held = true;
|
||||
lock (sendLock)
|
||||
held = true;
|
||||
}
|
||||
|
||||
public void Unhold()
|
||||
@@ -225,6 +276,7 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
totalSent += message.Length;
|
||||
|
||||
pkt_send.Message = message;
|
||||
PrepareOutboundPacket(pkt_send);
|
||||
if (pkt_send.Compose())
|
||||
sock.Send(pkt_send.Data);
|
||||
|
||||
@@ -248,7 +300,7 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
|
||||
public void NetworkClose(ISocket sender)
|
||||
{
|
||||
Receiver?.NetworkClose(sender);
|
||||
Receiver?.NetworkClose(this);
|
||||
}
|
||||
|
||||
public void NetworkReceive(ISocket sender, NetworkBuffer buffer)
|
||||
@@ -267,7 +319,8 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
if (msg == null)
|
||||
return;
|
||||
|
||||
var wsPacketLength = pkt_receive.Parse(msg, 0, (uint)msg.Length);
|
||||
if (!TryParseFrame(msg, 0, out var wsPacketLength))
|
||||
return;
|
||||
|
||||
if (wsPacketLength < 0)
|
||||
{
|
||||
@@ -289,45 +342,33 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
var pkt_pong = new WebsocketPacket()
|
||||
{
|
||||
FIN = true,
|
||||
Mask = false,
|
||||
Mask = !IsServer,
|
||||
Opcode = WebsocketPacket.WSOpcode.Pong,
|
||||
Message = pkt_receive.Message
|
||||
};
|
||||
|
||||
offset += (uint)wsPacketLength;
|
||||
|
||||
Send(pkt_pong);
|
||||
}
|
||||
else if (pkt_receive.Opcode == WebsocketPacket.WSOpcode.Pong)
|
||||
{
|
||||
offset += (uint)wsPacketLength;
|
||||
}
|
||||
else if (pkt_receive.Opcode == WebsocketPacket.WSOpcode.BinaryFrame
|
||||
|| pkt_receive.Opcode == WebsocketPacket.WSOpcode.TextFrame
|
||||
|| pkt_receive.Opcode == WebsocketPacket.WSOpcode.ContinuationFrame)
|
||||
{
|
||||
totalReceived += pkt_receive.Message.Length;
|
||||
//Console.WriteLine("RX " + pkt_receive.Message.Length + "/" + totalReceived);// + " " + DC.ToHex(message, 0, (uint)size));
|
||||
|
||||
receiveNetworkBuffer.Write(pkt_receive.Message);
|
||||
offset += (uint)wsPacketLength;
|
||||
|
||||
//Console.WriteLine("WS IN: " + pkt_receive.Opcode.ToString() + " " + pkt_receive.Message.Length + " | " + offset + " " + string.Join(" ", pkt_receive.Message));// DC.ToHex(pkt_receive.Message));
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Global.Log("WSocket", LogType.Debug, "Unknown WS opcode:" + pkt_receive.Opcode);
|
||||
if (!ProcessDataFrame(pkt_receive))
|
||||
return;
|
||||
}
|
||||
|
||||
// Pong frames need no further processing. All successfully handled frames
|
||||
// advance by the same parsed length.
|
||||
offset += (uint)wsPacketLength;
|
||||
|
||||
if (offset == msg.Length)
|
||||
{
|
||||
|
||||
Receiver?.NetworkReceive(this, receiveNetworkBuffer);
|
||||
DeliverReceivedData();
|
||||
return;
|
||||
}
|
||||
|
||||
wsPacketLength = pkt_receive.Parse(msg, offset, (uint)msg.Length);
|
||||
if (!TryParseFrame(msg, offset, out wsPacketLength))
|
||||
return;
|
||||
}
|
||||
|
||||
if (wsPacketLength < 0)
|
||||
@@ -339,13 +380,128 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
|
||||
//Console.WriteLine("WS IN: " + receiveNetworkBuffer.Available);
|
||||
|
||||
Receiver?.NetworkReceive(this, receiveNetworkBuffer);
|
||||
DeliverReceivedData();
|
||||
|
||||
|
||||
if (buffer.Available > 0 && !buffer.Protected)
|
||||
NetworkReceive(this, buffer);
|
||||
}
|
||||
|
||||
private bool ProcessDataFrame(WebsocketPacket packet)
|
||||
{
|
||||
var payload = packet.Message ?? Array.Empty<byte>();
|
||||
|
||||
if (MaximumMessageLength > 0 && (ulong)payload.LongLength > MaximumMessageLength)
|
||||
return RejectProtocol($"WebSocket message exceeds the {MaximumMessageLength}-byte limit.");
|
||||
|
||||
if (packet.Opcode == WebsocketPacket.WSOpcode.TextFrame
|
||||
|| packet.Opcode == WebsocketPacket.WSOpcode.BinaryFrame)
|
||||
{
|
||||
if (fragmentedMessage)
|
||||
return RejectProtocol("A new WebSocket data frame arrived before the fragmented message completed.");
|
||||
|
||||
if (packet.FIN)
|
||||
{
|
||||
receiveNetworkBuffer.Write(payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
fragmentedMessage = true;
|
||||
fragmentedMessageOpcode = packet.Opcode;
|
||||
fragmentedMessageLength = 0;
|
||||
fragmentedMessageBuffer.Read();
|
||||
return AppendFragment(payload);
|
||||
}
|
||||
|
||||
if (!fragmentedMessage)
|
||||
return RejectProtocol("A WebSocket continuation frame arrived without an active fragmented message.");
|
||||
|
||||
if (!AppendFragment(payload))
|
||||
return false;
|
||||
|
||||
if (!packet.FIN)
|
||||
return true;
|
||||
|
||||
var message = fragmentedMessageBuffer.Read() ?? Array.Empty<byte>();
|
||||
try
|
||||
{
|
||||
if (fragmentedMessageOpcode == WebsocketPacket.WSOpcode.TextFrame)
|
||||
WebsocketPacket.ValidateTextPayload(message);
|
||||
}
|
||||
catch (InvalidDataException exception)
|
||||
{
|
||||
return RejectProtocol(exception.Message);
|
||||
}
|
||||
|
||||
receiveNetworkBuffer.Write(message);
|
||||
ResetFragmentedMessage();
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool AppendFragment(byte[] payload)
|
||||
{
|
||||
var nextLength = fragmentedMessageLength + (ulong)payload.LongLength;
|
||||
if (nextLength < fragmentedMessageLength
|
||||
|| nextLength > int.MaxValue
|
||||
|| (MaximumMessageLength > 0 && nextLength > MaximumMessageLength))
|
||||
return RejectProtocol($"WebSocket fragmented message exceeds the {MaximumMessageLength}-byte limit.");
|
||||
|
||||
fragmentedMessageBuffer.Write(payload);
|
||||
fragmentedMessageLength = nextLength;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void DeliverReceivedData()
|
||||
{
|
||||
if (receiveNetworkBuffer != null && receiveNetworkBuffer.Available > 0)
|
||||
Receiver?.NetworkReceive(this, receiveNetworkBuffer);
|
||||
}
|
||||
|
||||
private bool RejectProtocol(string message)
|
||||
{
|
||||
Global.Log("WSocket", LogType.Warning, message);
|
||||
ResetFragmentedMessage();
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ResetFragmentedMessage()
|
||||
{
|
||||
fragmentedMessage = false;
|
||||
fragmentedMessageLength = 0;
|
||||
fragmentedMessageOpcode = default;
|
||||
fragmentedMessageBuffer?.Read();
|
||||
}
|
||||
|
||||
private void PrepareOutboundPacket(WebsocketPacket packet)
|
||||
{
|
||||
packet.Mask = !IsServer;
|
||||
|
||||
// RFC 6455 requires a fresh unpredictable masking key for every client
|
||||
// frame. pkt_send is intentionally reused, so discard its previous key.
|
||||
if (packet.Mask)
|
||||
packet.MaskKey = null;
|
||||
}
|
||||
|
||||
private bool TryParseFrame(byte[] message, uint offset, out long packetLength)
|
||||
{
|
||||
try
|
||||
{
|
||||
packetLength = pkt_receive.Parse(message, offset, (uint)message.Length);
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is InvalidDataException ||
|
||||
exception is ParserLimitException ||
|
||||
exception is ArgumentException)
|
||||
{
|
||||
Global.Log(exception);
|
||||
packetLength = 0;
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void NetworkConnect(ISocket sender)
|
||||
{
|
||||
|
||||
@@ -1053,13 +1053,38 @@ public partial class EpConnection : NetworkConnection, IStore
|
||||
// check if the request through Websockets
|
||||
if (_initialPacket)
|
||||
{
|
||||
var available = ends - offset;
|
||||
var matchesGetPrefix = available > 0 && msg[offset] == 'G' &&
|
||||
(available < 2 || msg[offset + 1] == 'E') &&
|
||||
(available < 3 || msg[offset + 2] == 'T');
|
||||
|
||||
if (matchesGetPrefix && available < 3)
|
||||
{
|
||||
data.HoldFor(msg, offset, available, 3);
|
||||
return ends;
|
||||
}
|
||||
|
||||
_initialPacket = false;
|
||||
|
||||
if (msg.Length > 3 && Encoding.Default.GetString(msg, 0, 3) == "GET")
|
||||
if (available >= 3 &&
|
||||
msg[offset] == 'G' && msg[offset + 1] == 'E' && msg[offset + 2] == 'T')
|
||||
{
|
||||
// Parse with http packet
|
||||
var req = new HttpRequestPacket();
|
||||
var pSize = req.Parse(msg, 0, (uint)msg.Length);
|
||||
long pSize;
|
||||
try
|
||||
{
|
||||
pSize = req.Parse(msg, offset, ends);
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is InvalidDataException ||
|
||||
exception is ParserLimitException ||
|
||||
exception is ArgumentException)
|
||||
{
|
||||
Global.Log(exception);
|
||||
pSize = 0;
|
||||
}
|
||||
|
||||
if (pSize > 0)
|
||||
{
|
||||
// check for WS upgrade
|
||||
@@ -1090,10 +1115,24 @@ public partial class EpConnection : NetworkConnection, IStore
|
||||
//@TODO: kill the connection
|
||||
}
|
||||
}
|
||||
else if (pSize < 0)
|
||||
{
|
||||
_initialPacket = true;
|
||||
var requiredLength = (ulong)available + (ulong)(-pSize);
|
||||
if (requiredLength > uint.MaxValue)
|
||||
throw new ParserLimitException("HTTP upgrade request is too large.");
|
||||
|
||||
data.HoldFor(msg, offset, available, (uint)requiredLength);
|
||||
return ends;
|
||||
}
|
||||
else
|
||||
{
|
||||
// packet incomplete
|
||||
return (uint)pSize;
|
||||
var res = new HttpResponsePacket
|
||||
{
|
||||
Number = HttpResponseCode.BadRequest
|
||||
};
|
||||
res.Compose(HttpComposeOption.AllCalculateLength);
|
||||
Send(res.Data);
|
||||
}
|
||||
|
||||
// switching completed
|
||||
@@ -1130,7 +1169,9 @@ public partial class EpConnection : NetworkConnection, IStore
|
||||
|
||||
if (_authPacket.Tdu != null)
|
||||
{
|
||||
remoteHeaders = Codec.ParseIndexedType<SessionHeaders>(_authPacket.Tdu.Value, null);
|
||||
remoteHeaders = Codec.ParseIndexedType<SessionHeaders>(
|
||||
_authPacket.Tdu.Value,
|
||||
_serverWarehouse);
|
||||
remoteAuthData = remoteHeaders.AuthenticationData;
|
||||
remoteHeaders.AuthenticationData = null;
|
||||
}
|
||||
@@ -1162,7 +1203,17 @@ public partial class EpConnection : NetworkConnection, IStore
|
||||
return offset;
|
||||
}
|
||||
|
||||
if (_session.RemoteHeaders.AuthenticationProtocol == null)
|
||||
var authenticationProtocol = _session.RemoteHeaders.AuthenticationProtocol;
|
||||
var allowedAuthenticationProviders =
|
||||
Server?.AllowedAuthenticationProviders ?? Array.Empty<string>();
|
||||
var provider = !string.IsNullOrWhiteSpace(authenticationProtocol)
|
||||
&& allowedAuthenticationProviders.Contains(
|
||||
authenticationProtocol,
|
||||
StringComparer.Ordinal)
|
||||
? _serverWarehouse?.TryGetAuthenticationProvider(authenticationProtocol)
|
||||
: null;
|
||||
|
||||
if (provider == null)
|
||||
{
|
||||
SendAuthHeaders(EpAuthPacketMethod.NotSupported, localHeaders);
|
||||
_invalidCredentials = true;
|
||||
@@ -1170,8 +1221,6 @@ public partial class EpConnection : NetworkConnection, IStore
|
||||
return offset;
|
||||
}
|
||||
|
||||
var provider = _serverWarehouse.GetAuthenticationProvider(_session.RemoteHeaders.AuthenticationProtocol);
|
||||
|
||||
var handler = provider.CreateAuthenticationHandler(new AuthenticationContext()
|
||||
{
|
||||
Direction = AuthenticationDirection.Responder,
|
||||
|
||||
@@ -56,6 +56,20 @@ partial class EpConnection
|
||||
|
||||
internal bool IsRateControlBlocked => Volatile.Read(ref _rateControlBlocked) != 0;
|
||||
|
||||
IEnumerable<IResourceManager> ResolveTypeDefManagers(TypeDef typeDef)
|
||||
{
|
||||
var definedType = typeDef switch
|
||||
{
|
||||
LocalTypeDef localTypeDef => localTypeDef.DefinedType,
|
||||
RemoteTypeDef remoteTypeDef => remoteTypeDef.ProxyType,
|
||||
_ => null
|
||||
};
|
||||
|
||||
return definedType == null
|
||||
? Array.Empty<IResourceManager>()
|
||||
: Instance.Warehouse.ResolveResourceManagers(definedType, null);
|
||||
}
|
||||
|
||||
bool TryApplyManagers(
|
||||
MemberDef member,
|
||||
IResource? resource,
|
||||
@@ -1359,7 +1373,7 @@ partial class EpConnection
|
||||
|
||||
var classNames = (string[])value;
|
||||
|
||||
var typeDefs = new List<LocalTypeDef>();
|
||||
var typeDefs = new List<TypeDef>();
|
||||
|
||||
foreach (var className in classNames)
|
||||
{
|
||||
@@ -1372,7 +1386,7 @@ partial class EpConnection
|
||||
if (typeDefs.Count > 0)
|
||||
{
|
||||
var managers = typeDefs
|
||||
.SelectMany(typeDef => Instance.Warehouse.ResolveResourceManagers(typeDef.DefinedType, null));
|
||||
.SelectMany(ResolveTypeDefManagers);
|
||||
|
||||
if (!TryApplyManagers(
|
||||
null,
|
||||
@@ -1405,7 +1419,7 @@ partial class EpConnection
|
||||
|
||||
if (t != null)
|
||||
{
|
||||
var managers = Instance.Warehouse.ResolveResourceManagers(t.DefinedType, null);
|
||||
var managers = ResolveTypeDefManagers(t);
|
||||
if (!TryApplyManagers(
|
||||
null,
|
||||
null,
|
||||
@@ -1670,7 +1684,6 @@ partial class EpConnection
|
||||
ErrorType.Management,
|
||||
ExceptionCode.InvokeDenied,
|
||||
out var managerDelay,
|
||||
managers: Instance.Warehouse.ResolveResourceManagers(typeDef.DefinedType, null),
|
||||
supportsDelay: true))
|
||||
return;
|
||||
|
||||
|
||||
@@ -56,8 +56,13 @@ public class EpServer : NetworkServer<EpConnection>, IResource
|
||||
set;
|
||||
}
|
||||
|
||||
//[Attribute]
|
||||
public string[] AllowedAuthenticationProviders { get; set; }
|
||||
/// <summary>
|
||||
/// Authentication provider protocol names that incoming connections may negotiate.
|
||||
/// Providers must also be registered with the server Warehouse. An empty list denies
|
||||
/// authenticated negotiation; anonymous access remains controlled separately by
|
||||
/// <see cref="AllowUnauthorizedAccess"/>.
|
||||
/// </summary>
|
||||
public string[] AllowedAuthenticationProviders { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Encryption provider protocol names that incoming connections may negotiate.
|
||||
@@ -124,12 +129,6 @@ public class EpServer : NetworkServer<EpConnection>, IResource
|
||||
| ExceptionLevel.Message
|
||||
| ExceptionLevel.Trace;
|
||||
|
||||
|
||||
|
||||
public void RegisterAuthenticationHandler<T>(string[] domains, AuthenticationMode[] modes) where T : class, IAuthenticationHandler
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public Instance Instance
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Esiur.Proxy;
|
||||
|
||||
public static class TypeDefGenerator
|
||||
{
|
||||
private const string GeneratedFilesManifestName = ".esiur-generated-files";
|
||||
internal static Regex urlRegex = new Regex(@"^(?:([\S]*)://([^/]*)/?)");
|
||||
|
||||
//public static string ToLiteral(string valueTextForCompiler)
|
||||
@@ -238,16 +239,25 @@ public static class TypeDefGenerator
|
||||
// no longer needed
|
||||
Warehouse.Default.Remove(con);
|
||||
|
||||
var generatedFiles = typeDefs
|
||||
.Where(td => td.Kind == TypeDefKind.Resource
|
||||
|| td.Kind == TypeDefKind.Record
|
||||
|| td.Kind == TypeDefKind.Enum)
|
||||
.Select(td => GetGeneratedFileName(td.Name))
|
||||
.Append("Initialization.g.cs")
|
||||
.ToArray();
|
||||
|
||||
if (generatedFiles.Distinct(StringComparer.OrdinalIgnoreCase).Count()
|
||||
!= generatedFiles.Length)
|
||||
throw new InvalidDataException("The remote type definitions produce duplicate filenames.");
|
||||
|
||||
var dstDir = new DirectoryInfo(tempDir ? Path.GetTempPath() + Path.DirectorySeparatorChar
|
||||
+ Misc.Global.GenerateCode(20) + Path.DirectorySeparatorChar + dir : dir);
|
||||
|
||||
if (!dstDir.Exists)
|
||||
dstDir.Create();
|
||||
else
|
||||
{
|
||||
foreach (FileInfo file in dstDir.GetFiles())
|
||||
file.Delete();
|
||||
}
|
||||
DeletePreviouslyGeneratedFiles(dstDir);
|
||||
|
||||
// make sources
|
||||
foreach (var td in typeDefs)
|
||||
@@ -255,17 +265,23 @@ public static class TypeDefGenerator
|
||||
if (td.Kind == TypeDefKind.Resource)
|
||||
{
|
||||
var source = GenerateClass(td, typeDefs, asyncSetters);
|
||||
File.WriteAllText(dstDir.FullName + Path.DirectorySeparatorChar + td.Name + ".g.cs", source);
|
||||
File.WriteAllText(
|
||||
Path.Combine(dstDir.FullName, GetGeneratedFileName(td.Name)),
|
||||
source);
|
||||
}
|
||||
else if (td.Kind == TypeDefKind.Record)
|
||||
{
|
||||
var source = GenerateRecord(td, typeDefs);
|
||||
File.WriteAllText(dstDir.FullName + Path.DirectorySeparatorChar + td.Name + ".g.cs", source);
|
||||
File.WriteAllText(
|
||||
Path.Combine(dstDir.FullName, GetGeneratedFileName(td.Name)),
|
||||
source);
|
||||
}
|
||||
else if (td.Kind == TypeDefKind.Enum)
|
||||
{
|
||||
var source = GenerateEnum(td, typeDefs);
|
||||
File.WriteAllText(dstDir.FullName + Path.DirectorySeparatorChar + td.Name + ".g.cs", source);
|
||||
File.WriteAllText(
|
||||
Path.Combine(dstDir.FullName, GetGeneratedFileName(td.Name)),
|
||||
source);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,15 +317,52 @@ public static class TypeDefGenerator
|
||||
\r\n } \r\n}";
|
||||
|
||||
|
||||
File.WriteAllText(dstDir.FullName + Path.DirectorySeparatorChar + "Initialization.g.cs", typesFile);
|
||||
File.WriteAllText(Path.Combine(dstDir.FullName, "Initialization.g.cs"), typesFile);
|
||||
File.WriteAllLines(
|
||||
Path.Combine(dstDir.FullName, GeneratedFilesManifestName),
|
||||
generatedFiles);
|
||||
|
||||
|
||||
return dstDir.FullName;
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
throw ex;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
internal static string GetGeneratedFileName(string typeName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(typeName))
|
||||
throw new InvalidDataException("A remote type has no name.");
|
||||
|
||||
var fileName = typeName + ".g.cs";
|
||||
if (!string.Equals(Path.GetFileName(fileName), fileName, StringComparison.Ordinal)
|
||||
|| fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
|
||||
throw new InvalidDataException(
|
||||
$"Remote type name '{typeName}' cannot be used as a safe generated filename.");
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
internal static void DeletePreviouslyGeneratedFiles(DirectoryInfo destination)
|
||||
{
|
||||
var manifestPath = Path.Combine(destination.FullName, GeneratedFilesManifestName);
|
||||
if (!File.Exists(manifestPath))
|
||||
return;
|
||||
|
||||
foreach (var entry in File.ReadAllLines(manifestPath))
|
||||
{
|
||||
var fileName = entry.Trim();
|
||||
if (!fileName.EndsWith(".g.cs", StringComparison.OrdinalIgnoreCase)
|
||||
|| !string.Equals(Path.GetFileName(fileName), fileName, StringComparison.Ordinal)
|
||||
|| fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
|
||||
continue;
|
||||
|
||||
var generatedPath = Path.Combine(destination.FullName, fileName);
|
||||
if (File.Exists(generatedPath))
|
||||
File.Delete(generatedPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -847,10 +847,10 @@ public class Warehouse
|
||||
else
|
||||
return (T)store;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
Remove(store);
|
||||
throw ex;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -946,10 +946,10 @@ public class Warehouse
|
||||
StoreConnected?.Invoke(resource as IStore);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
Remove(resource);
|
||||
throw ex;
|
||||
throw;
|
||||
}
|
||||
|
||||
return resource;
|
||||
|
||||
@@ -29,6 +29,9 @@ public sealed class EncryptionConfiguration
|
||||
/// </summary>
|
||||
public sealed class ParserConfiguration
|
||||
{
|
||||
/// <summary>Default maximum nesting depth for TRU type metadata.</summary>
|
||||
public const int DefaultMaximumTypeMetadataDepth = 64;
|
||||
|
||||
/// <summary>Maximum declared TDU payload retained for one packet.</summary>
|
||||
public uint MaximumPacketSize { get; set; } = 8 * 1024 * 1024;
|
||||
|
||||
@@ -37,6 +40,12 @@ public sealed class ParserConfiguration
|
||||
|
||||
/// <summary>Maximum number of values decoded into one collection.</summary>
|
||||
public int MaximumCollectionItems { get; set; } = 65_536;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of nested TRU type-metadata nodes, including the root node.
|
||||
/// A value of zero disables the limit.
|
||||
/// </summary>
|
||||
public int MaximumTypeMetadataDepth { get; set; } = DefaultMaximumTypeMetadataDepth;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -35,6 +35,23 @@ namespace Esiur.Security.Permissions;
|
||||
|
||||
public class UserPermissionsManager : IPermissionsManager
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<ActionType, string> ResourcePermissionKeys =
|
||||
new Dictionary<ActionType, string>
|
||||
{
|
||||
[ActionType.Attach] = "_attach",
|
||||
[ActionType.Detach] = "_detach",
|
||||
[ActionType.Delete] = "_delete",
|
||||
[ActionType.CreateResource] = "_create_resource",
|
||||
[ActionType.InquireAttributes] = "_get_attributes",
|
||||
[ActionType.UpdateAttributes] = "_set_attributes",
|
||||
[ActionType.AddChild] = "_add_child",
|
||||
[ActionType.RemoveChild] = "_remove_child",
|
||||
[ActionType.AddParent] = "_add_parent",
|
||||
[ActionType.RemoveParent] = "_remove_parent",
|
||||
[ActionType.Rename] = "_rename",
|
||||
[ActionType.ViewTypeDef] = "_view_typedef"
|
||||
};
|
||||
|
||||
IResource resource;
|
||||
Map<string, object> settings;
|
||||
|
||||
@@ -42,70 +59,39 @@ public class UserPermissionsManager : IPermissionsManager
|
||||
|
||||
public Ruling Applicable(IResource resource, Session session, ActionType action, MemberDef member, object inquirer)
|
||||
{
|
||||
Map<string,object> userPermissions = null;
|
||||
|
||||
if (settings.ContainsKey(session.RemoteIdentity))
|
||||
userPermissions = settings[session.RemoteIdentity] as Map<string, object>;
|
||||
else if (settings.ContainsKey("public"))
|
||||
userPermissions = settings["public"] as Map<string,object>;
|
||||
else
|
||||
if (settings == null || session == null)
|
||||
return Ruling.Denied;
|
||||
|
||||
if (action == ActionType.Attach)// || action == ActionType.Delete)
|
||||
{
|
||||
if ((string)userPermissions["_attach"] != "yes")
|
||||
return Ruling.Denied;
|
||||
}
|
||||
else if (action == ActionType.Delete)
|
||||
{
|
||||
if ((string)userPermissions["_delete"] != "yes")
|
||||
return Ruling.Denied;
|
||||
}
|
||||
else if (action == ActionType.InquireAttributes)
|
||||
{
|
||||
if ((string)userPermissions["_get_attributes"] == "yes")
|
||||
return Ruling.Denied;
|
||||
}
|
||||
else if (action == ActionType.UpdateAttributes)
|
||||
{
|
||||
if ((string)userPermissions["_set_attributes"] != "yes")
|
||||
return Ruling.Denied;
|
||||
}
|
||||
else if (action == ActionType.AddChild)
|
||||
{
|
||||
if ((string)userPermissions["_add_child"] != "yes")
|
||||
return Ruling.Denied;
|
||||
}
|
||||
else if (action == ActionType.RemoveChild)
|
||||
{
|
||||
if ((string)userPermissions["_remove_child"] != "yes")
|
||||
return Ruling.Denied;
|
||||
}
|
||||
else if (action == ActionType.AddParent)
|
||||
{
|
||||
if ((string)userPermissions["_add_parent"] != "yes")
|
||||
return Ruling.Denied;
|
||||
}
|
||||
else if (action == ActionType.RemoveParent)
|
||||
{
|
||||
if ((string)userPermissions["_remove_parent"] != "yes")
|
||||
return Ruling.Denied;
|
||||
}
|
||||
else if (action == ActionType.Rename)
|
||||
{
|
||||
if ((string)userPermissions["_rename"] != "yes")
|
||||
return Ruling.Denied;
|
||||
}
|
||||
else if (userPermissions.ContainsKey(member?.Name))
|
||||
{
|
||||
Map<string,object> methodPermissions = userPermissions[member.Name] as Map<string,object>;
|
||||
if ((string)methodPermissions[action.ToString()] != "yes")
|
||||
return Ruling.Denied;
|
||||
}
|
||||
Map<string, object> userPermissions = null;
|
||||
if (!string.IsNullOrEmpty(session.RemoteIdentity)
|
||||
&& settings.TryGetValue(session.RemoteIdentity, out var identityPermissions))
|
||||
userPermissions = identityPermissions as Map<string, object>;
|
||||
else if (settings.TryGetValue("public", out var publicPermissions))
|
||||
userPermissions = publicPermissions as Map<string, object>;
|
||||
|
||||
return Ruling.DontCare;
|
||||
if (userPermissions == null)
|
||||
return Ruling.Denied;
|
||||
|
||||
if (ResourcePermissionKeys.TryGetValue(action, out var resourcePermissionKey))
|
||||
return IsAllowed(userPermissions, resourcePermissionKey);
|
||||
|
||||
// Member-level access is fail closed: an absent member/action entry must
|
||||
// never fall through to Warehouse's compatibility defaults.
|
||||
if (member == null
|
||||
|| !userPermissions.TryGetValue(member.Name, out var rawMemberPermissions)
|
||||
|| rawMemberPermissions is not Map<string, object> memberPermissions)
|
||||
return Ruling.Denied;
|
||||
|
||||
return IsAllowed(memberPermissions, action.ToString());
|
||||
}
|
||||
|
||||
private static Ruling IsAllowed(Map<string, object> permissions, string key)
|
||||
=> permissions.TryGetValue(key, out var value)
|
||||
&& value is string text
|
||||
&& string.Equals(text, "yes", StringComparison.Ordinal)
|
||||
? Ruling.Allowed
|
||||
: Ruling.Denied;
|
||||
|
||||
public UserPermissionsManager()
|
||||
{
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
|
||||
<PackageReference Include="System.Collections" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -19,10 +19,15 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MongoDB.Bson" Version="2.29.0" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="2.29.0" />
|
||||
<!-- MongoDB.Driver 2.x permits older vulnerable compression packages.
|
||||
Keep the compatible driver API while enforcing patched dependency floors. -->
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.10" />
|
||||
<PackageReference Include="SharpCompress" Version="0.50.0" />
|
||||
<PackageReference Include="Snappier" Version="1.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Libraries\Esiur\Esiur.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -15,14 +15,12 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MessagePack" Version="3.1.4" />
|
||||
<PackageReference Include="MessagePack" Version="3.1.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.28" />
|
||||
<PackageReference Include="MQTTnet" Version="5.0.1.1416" />
|
||||
<PackageReference Include="protobuf-net.Core" Version="3.2.56" />
|
||||
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="10.0.0" />
|
||||
<PackageReference Include="System.Diagnostics.Tracing" Version="4.3.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
|
||||
<!-- ASP.NET Core 10 permits Microsoft.OpenApi 2.0.0; enforce a patched 2.x floor. -->
|
||||
<PackageReference Include="Microsoft.OpenApi" Version="2.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ApacheThrift" Version="0.22.0" />
|
||||
<PackageReference Include="Thrift" Version="0.9.1.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
<PackageReference Include="AvroConvert" Version="3.4.16" />
|
||||
<PackageReference Include="FlatSharp" Version="6.3.5" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.34.0" />
|
||||
<PackageReference Include="MessagePack" Version="3.1.4" />
|
||||
<PackageReference Include="MessagePack" Version="3.1.8" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" PrivateAssets="all" />
|
||||
<PackageReference Include="MongoDB.Bson" Version="3.7.1" />
|
||||
<PackageReference Include="PeterO.Cbor" Version="4.5.5" />
|
||||
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
<PackageReference Include="AvroConvert" Version="3.4.16" />
|
||||
<PackageReference Include="FlatSharp" Version="6.3.5" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.34.0" />
|
||||
<PackageReference Include="MessagePack" Version="3.1.4" />
|
||||
<PackageReference Include="MessagePack" Version="3.1.8" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" PrivateAssets="all" />
|
||||
<PackageReference Include="MongoDB.Bson" Version="3.7.1" />
|
||||
<PackageReference Include="PeterO.Cbor" Version="4.5.5" />
|
||||
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using Esiur.Core;
|
||||
|
||||
namespace Esiur.Tests.Unit;
|
||||
|
||||
public class AsyncBagTests
|
||||
{
|
||||
[Fact]
|
||||
public void Seal_CollectsConcurrentReplyCompletionsExactlyOnce()
|
||||
{
|
||||
const int count = 2_000;
|
||||
var replies = Enumerable.Range(0, count)
|
||||
.Select(_ => new AsyncReply<int>())
|
||||
.ToArray();
|
||||
var bag = new AsyncBag<int>();
|
||||
foreach (var reply in replies)
|
||||
bag.Add(reply);
|
||||
|
||||
bag.Seal();
|
||||
Parallel.For(0, replies.Length, index => replies[index].Trigger(index));
|
||||
|
||||
Assert.Equal(Enumerable.Range(0, count), bag.Wait(5_000));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddBag_UsesAStableSnapshot()
|
||||
{
|
||||
var source = new AsyncBag<int>();
|
||||
source.Add(1);
|
||||
source.Add(new AsyncReply<int>(2));
|
||||
|
||||
var destination = new AsyncBag<int>();
|
||||
destination.AddBag(source);
|
||||
destination.Seal();
|
||||
|
||||
Assert.Equal(new[] { 1, 2 }, destination.Wait(1_000));
|
||||
}
|
||||
}
|
||||
@@ -94,4 +94,27 @@ public class AsyncQueueTests
|
||||
|
||||
Assert.Equal(Enumerable.Range(1, itemCount), sequences.OrderBy(x => x));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeReadyBatch_IsDeliveredInOrder()
|
||||
{
|
||||
const int itemCount = 5_000;
|
||||
var queue = new AsyncQueue<int>();
|
||||
var replies = Enumerable.Range(0, itemCount)
|
||||
.Select(_ => new AsyncReply<int>())
|
||||
.ToArray();
|
||||
var delivered = new List<int>(itemCount);
|
||||
queue.Then(delivered.Add);
|
||||
|
||||
foreach (var reply in replies)
|
||||
queue.Add(reply);
|
||||
|
||||
// Keep the head pending while every later item becomes ready, then release
|
||||
// the whole batch at once. This exercises the bulk-removal path.
|
||||
for (var i = 1; i < replies.Length; i++)
|
||||
replies[i].Trigger(i);
|
||||
replies[0].Trigger(0);
|
||||
|
||||
Assert.Equal(Enumerable.Range(0, itemCount), delivered);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
using Esiur.Core;
|
||||
using Esiur.Data.Types;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Esiur.Tests.Unit;
|
||||
|
||||
public class AsyncReplyConcurrencyTests
|
||||
{
|
||||
[Fact]
|
||||
public void CompletedRepliesKeepCallbackAndWaiterStorageLazy()
|
||||
{
|
||||
var reply = new AsyncReply<int>(42);
|
||||
var awaiter = reply.GetAwaiter();
|
||||
|
||||
Assert.True(reply.Ready);
|
||||
Assert.True(awaiter.IsCompleted);
|
||||
Assert.Equal(42, awaiter.GetResult());
|
||||
Assert.Null(GetBaseField(reply, "callbacks"));
|
||||
Assert.Null(GetBaseField(reply, "errorCallbacks"));
|
||||
Assert.Null(GetBaseField(reply, "completionEvent"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompletedReplyAllocationStaysWithinObjectAndLockBudget()
|
||||
{
|
||||
const int count = 4096;
|
||||
const int maximumBytesPerReply = 224;
|
||||
var replies = new AsyncReply<int>[count];
|
||||
|
||||
// Warm up constructors and property access before measuring this thread.
|
||||
for (var i = 0; i < 32; i++)
|
||||
_ = new AsyncReply<int>(i).Ready;
|
||||
|
||||
var before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (var i = 0; i < replies.Length; i++)
|
||||
replies[i] = new AsyncReply<int>(i);
|
||||
|
||||
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
GC.KeepAlive(replies);
|
||||
|
||||
Assert.True(
|
||||
allocated < count * (long)maximumBytesPerReply,
|
||||
$"Completed replies allocated {allocated / (double)count:N1} bytes each.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TriggerErrorWithoutAHandlerIsRetainedAndObservedByWaiters()
|
||||
{
|
||||
var reply = new AsyncReply<int>();
|
||||
var source = new InvalidOperationException("synchronous failure");
|
||||
|
||||
var triggerException = Record.Exception(() => reply.TriggerError(source));
|
||||
|
||||
Assert.Null(triggerException);
|
||||
Assert.False(reply.Ready);
|
||||
Assert.True(reply.Failed);
|
||||
var stored = Assert.IsType<AsyncException>(reply.Exception);
|
||||
Assert.Same(source, stored.InnerException);
|
||||
Assert.Same(stored, Assert.Throws<AsyncException>(() => reply.Wait()));
|
||||
Assert.Same(stored, Assert.Throws<AsyncException>(() => reply.Wait(10)));
|
||||
|
||||
AsyncException? observed = null;
|
||||
reply.Error(error => observed = error);
|
||||
Assert.Same(stored, observed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CustomAsyncBuilderRetainsAnErrorThrownBeforeTheFirstAwait()
|
||||
{
|
||||
AsyncReply<int>? reply = null;
|
||||
|
||||
var creationException = Record.Exception(() => reply = FailBeforeFirstAwait(fail: true));
|
||||
|
||||
Assert.Null(creationException);
|
||||
Assert.NotNull(reply);
|
||||
Assert.True(reply!.Failed);
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => _ = await reply);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CustomAsyncBuilderPreservesAsyncExceptionAfterSuspension()
|
||||
{
|
||||
var gate = new AsyncReply();
|
||||
var reply = FailAfterFirstAwait(gate);
|
||||
var observation = AwaitAsTask(reply);
|
||||
|
||||
gate.Trigger(null!);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<AsyncException>(() => observation);
|
||||
Assert.IsType<InvalidOperationException>(exception.InnerException);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CustomAsyncBuilderPublishesSuspensionBeforeInlineContinuation()
|
||||
{
|
||||
AsyncReply<int>? reply = null;
|
||||
|
||||
var creationException = Record.Exception(() => reply = FailAfterInlineCompletion());
|
||||
|
||||
Assert.Null(creationException);
|
||||
var exception = await Assert.ThrowsAsync<AsyncException>(async () => _ = await reply!);
|
||||
Assert.IsType<InvalidOperationException>(exception.InnerException);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamErrorsReachTheBaseTerminalStateWithoutAnErrorHandler()
|
||||
{
|
||||
static AsyncReply CompletedControl() => new AsyncReply((object)null!);
|
||||
|
||||
var stream = new AsyncStreamReply<int>(
|
||||
StreamMode.Push,
|
||||
CompletedControl,
|
||||
CompletedControl,
|
||||
CompletedControl,
|
||||
CompletedControl);
|
||||
var expected = new AsyncException(ErrorType.Exception, 0, "stream failed");
|
||||
|
||||
stream.TriggerError(expected);
|
||||
|
||||
Assert.True(stream.Failed);
|
||||
Assert.Same(expected, stream.Exception);
|
||||
Assert.Same(expected, Assert.Throws<AsyncException>(() => stream.Wait()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void TerminalStateWakesEverySynchronousWaiter(bool fail)
|
||||
{
|
||||
const int waiterCount = 8;
|
||||
var reply = new AsyncReply<int>();
|
||||
var results = new int[waiterCount];
|
||||
var errors = new Exception?[waiterCount];
|
||||
var threads = new Thread[waiterCount];
|
||||
|
||||
for (var i = 0; i < threads.Length; i++)
|
||||
{
|
||||
var index = i;
|
||||
threads[i] = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
results[index] = reply.Wait();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
errors[index] = exception;
|
||||
}
|
||||
})
|
||||
{
|
||||
IsBackground = true
|
||||
};
|
||||
threads[i].Start();
|
||||
}
|
||||
|
||||
var allWaiting = SpinWait.SpinUntil(
|
||||
() => threads.All(thread =>
|
||||
(thread.ThreadState & System.Threading.ThreadState.WaitSleepJoin) != 0),
|
||||
millisecondsTimeout: 5000);
|
||||
|
||||
if (fail)
|
||||
reply.TriggerError(new InvalidOperationException("failed"));
|
||||
else
|
||||
reply.Trigger(73);
|
||||
|
||||
var allJoined = threads.Select(thread => thread.Join(2000)).ToArray();
|
||||
|
||||
Assert.True(allWaiting, "Not all test threads reached the blocking wait.");
|
||||
Assert.All(allJoined, joined => Assert.True(joined, "A synchronous waiter was not released."));
|
||||
|
||||
if (fail)
|
||||
Assert.All(errors, error => Assert.IsType<AsyncException>(error));
|
||||
else
|
||||
{
|
||||
Assert.All(results, value => Assert.Equal(73, value));
|
||||
Assert.All(errors, Assert.Null);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompletionCallbacksRunOutsideTheReplyLock()
|
||||
{
|
||||
var reply = new AsyncReply<int>();
|
||||
reply.Then(_ =>
|
||||
{
|
||||
var concurrentRegistration = Task.Run(() => reply.Then(__ => { }));
|
||||
if (!concurrentRegistration.Wait(2000))
|
||||
throw new TimeoutException("Concurrent callback registration was blocked by the completion callback.");
|
||||
});
|
||||
|
||||
Assert.Null(Record.Exception(() => reply.Trigger(1)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ErrorRegistrationRacingWithFailureInvokesExactlyOnce()
|
||||
{
|
||||
for (var iteration = 0; iteration < 500; iteration++)
|
||||
{
|
||||
var reply = new AsyncReply<int>();
|
||||
var expected = new AsyncException(ErrorType.Exception, 0, "failed");
|
||||
AsyncException? observed = null;
|
||||
var calls = 0;
|
||||
|
||||
Parallel.Invoke(
|
||||
() => reply.Error(error =>
|
||||
{
|
||||
observed = error;
|
||||
Interlocked.Increment(ref calls);
|
||||
}),
|
||||
() => reply.TriggerError(expected));
|
||||
|
||||
Assert.Equal(1, Volatile.Read(ref calls));
|
||||
Assert.Same(expected, observed);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AwaiterContinuationIsNotLostDuringCompletionRace()
|
||||
{
|
||||
for (var iteration = 0; iteration < 500; iteration++)
|
||||
{
|
||||
var reply = new AsyncReply<int>();
|
||||
var awaiter = reply.GetAwaiter();
|
||||
using var continued = new ManualResetEventSlim(false);
|
||||
|
||||
Parallel.Invoke(
|
||||
() => awaiter.OnCompleted(continued.Set),
|
||||
() => reply.Trigger(iteration));
|
||||
|
||||
Assert.True(continued.Wait(2000), "The awaiter continuation was lost.");
|
||||
Assert.True(awaiter.IsCompleted);
|
||||
Assert.Equal(iteration, awaiter.GetResult());
|
||||
}
|
||||
}
|
||||
|
||||
private static object? GetBaseField(AsyncReply reply, string name)
|
||||
=> typeof(AsyncReply)
|
||||
.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
?.GetValue(reply);
|
||||
|
||||
private static async AsyncReply<int> FailBeforeFirstAwait(bool fail)
|
||||
{
|
||||
if (fail)
|
||||
throw new InvalidOperationException("failed before await");
|
||||
|
||||
await Task.Yield();
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static async AsyncReply<int> FailAfterFirstAwait(AsyncReply gate)
|
||||
{
|
||||
await gate;
|
||||
throw new InvalidOperationException("failed after await");
|
||||
}
|
||||
|
||||
private static async Task<int> AwaitAsTask(AsyncReply<int> reply) => await reply;
|
||||
|
||||
private static async AsyncReply<int> FailAfterInlineCompletion()
|
||||
{
|
||||
await new InlineCompletion();
|
||||
throw new InvalidOperationException("failed after inline continuation");
|
||||
}
|
||||
|
||||
private readonly struct InlineCompletion
|
||||
{
|
||||
public Awaiter GetAwaiter() => new Awaiter();
|
||||
|
||||
public readonly struct Awaiter : INotifyCompletion
|
||||
{
|
||||
public bool IsCompleted => false;
|
||||
|
||||
public void GetResult()
|
||||
{
|
||||
}
|
||||
|
||||
public void OnCompleted(Action continuation) => continuation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Esiur.Data;
|
||||
using Esiur.Net.Http;
|
||||
using Esiur.Net.Packets.Http;
|
||||
|
||||
namespace Esiur.Tests.Unit;
|
||||
|
||||
public class HttpHardeningTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("Content-Length: 1\r\nContent-Length: 1\r\n")]
|
||||
[InlineData("Content-Length: 1\r\nContent-Length: 2\r\n")]
|
||||
public void Request_RejectsDuplicateContentLength(string framingHeaders)
|
||||
{
|
||||
var data = RequestBytes("POST", framingHeaders, "x");
|
||||
|
||||
Assert.Throws<InvalidDataException>(() =>
|
||||
new HttpRequestPacket().Parse(data, 0, (uint)data.Length));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Transfer-Encoding: chunked\r\n")]
|
||||
[InlineData("Transfer-Encoding: identity\r\nContent-Length: 1\r\n")]
|
||||
public void Request_RejectsUnsupportedTransferEncoding(string framingHeaders)
|
||||
{
|
||||
var data = RequestBytes("POST", framingHeaders, "x");
|
||||
|
||||
Assert.Throws<InvalidDataException>(() =>
|
||||
new HttpRequestPacket().Parse(data, 0, (uint)data.Length));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("PUT")]
|
||||
[InlineData("GET")]
|
||||
[InlineData("DELETE")]
|
||||
public void Request_ConsumesDeclaredBodiesForEveryMethod(string method)
|
||||
{
|
||||
var data = RequestBytes(
|
||||
method,
|
||||
"Content-Type: application/octet-stream\r\nContent-Length: 3\r\n",
|
||||
"abc");
|
||||
var packet = new HttpRequestPacket();
|
||||
|
||||
Assert.Equal(data.Length, packet.Parse(data, 0, (uint)data.Length));
|
||||
Assert.Equal("abc", Encoding.ASCII.GetString(packet.Message));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Request_AllowsPostWithoutABodyOrContentLength()
|
||||
{
|
||||
var data = RequestBytes("POST", string.Empty, string.Empty);
|
||||
var packet = new HttpRequestPacket();
|
||||
|
||||
Assert.Equal(data.Length, packet.Parse(data, 0, (uint)data.Length));
|
||||
Assert.Empty(packet.PostForms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Request_EnforcesHeaderCountBeforeSplittingHeaders()
|
||||
{
|
||||
var data = RequestBytes("GET", "X-One: 1\r\nX-Two: 2\r\n", string.Empty);
|
||||
var packet = new HttpRequestPacket { MaximumHeaderCount = 1 };
|
||||
|
||||
Assert.Throws<ParserLimitException>(() =>
|
||||
packet.Parse(data, 0, (uint)data.Length));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UrlEncodedForm_EnforcesFieldKeyAndValueLimits()
|
||||
{
|
||||
AssertFormLimit("a=1&b=2", packet => packet.MaximumFormFields = 1);
|
||||
AssertFormLimit("long=1", packet => packet.MaximumFormKeyLength = 3);
|
||||
AssertFormLimit("a=long", packet => packet.MaximumFormValueLength = 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UrlEncodedForm_CombinesUnkeyedFieldsWithinTheConfiguredBound()
|
||||
{
|
||||
const string body = "first&second&third";
|
||||
var data = RequestBytes(
|
||||
"POST",
|
||||
$"Content-Type: application/x-www-form-urlencoded\r\nContent-Length: {body.Length}\r\n",
|
||||
body);
|
||||
var packet = new HttpRequestPacket { MaximumFormValueLength = body.Length };
|
||||
|
||||
Assert.Equal(data.Length, packet.Parse(data, 0, (uint)data.Length));
|
||||
Assert.Equal(body, packet.PostForms["unknown"]);
|
||||
|
||||
packet = new HttpRequestPacket { MaximumFormValueLength = body.Length - 1 };
|
||||
Assert.Throws<ParserLimitException>(() =>
|
||||
packet.Parse(data, 0, (uint)data.Length));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipartForm_EnforcesPartLength()
|
||||
{
|
||||
const string boundary = "test-boundary";
|
||||
const string body =
|
||||
"--test-boundary\r\n" +
|
||||
"Content-Disposition: form-data; name=\"value\"\r\n\r\n" +
|
||||
"payload\r\n" +
|
||||
"--test-boundary--\r\n";
|
||||
var data = RequestBytes(
|
||||
"POST",
|
||||
$"Content-Type: multipart/form-data; boundary=\"{boundary}\"\r\n" +
|
||||
$"Content-Length: {Encoding.UTF8.GetByteCount(body)}\r\n",
|
||||
body);
|
||||
var packet = new HttpRequestPacket { MaximumMultipartPartLength = 8 };
|
||||
|
||||
Assert.Throws<ParserLimitException>(() =>
|
||||
packet.Parse(data, 0, (uint)data.Length));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipartForm_ParsesQuotedBoundaryIncrementally()
|
||||
{
|
||||
const string boundary = "test-boundary";
|
||||
const string body =
|
||||
"--test-boundary\r\n" +
|
||||
"Content-Disposition: form-data; name=\"first\"\r\n\r\n" +
|
||||
"one\r\n" +
|
||||
"--test-boundary\r\n" +
|
||||
"Content-Disposition: form-data; name=\"second\"\r\n\r\n" +
|
||||
"two\r\n" +
|
||||
"--test-boundary--\r\n";
|
||||
var data = RequestBytes(
|
||||
"POST",
|
||||
$"Content-Type: multipart/form-data; boundary=\"{boundary}\"\r\n" +
|
||||
$"Content-Length: {Encoding.UTF8.GetByteCount(body)}\r\n",
|
||||
body);
|
||||
var packet = new HttpRequestPacket();
|
||||
|
||||
Assert.Equal(data.Length, packet.Parse(data, 0, (uint)data.Length));
|
||||
Assert.Equal("one", packet.PostForms["first"]);
|
||||
Assert.Equal("two", packet.PostForms["second"]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Content-Length: 1\r\nContent-Length: 1\r\n")]
|
||||
[InlineData("Transfer-Encoding: chunked\r\n")]
|
||||
[InlineData("Transfer-Encoding: identity\r\nContent-Length: 1\r\n")]
|
||||
public void Response_RejectsAmbiguousOrUnsupportedFraming(string framingHeaders)
|
||||
{
|
||||
var data = Encoding.ASCII.GetBytes(
|
||||
"HTTP/1.1 200 OK\r\n" + framingHeaders + "\r\nx");
|
||||
|
||||
Assert.Throws<InvalidDataException>(() =>
|
||||
new HttpResponsePacket().Parse(data, 0, (uint)data.Length));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cookie_RoundTripsSecureAndSameSiteAttributes()
|
||||
{
|
||||
var cookie = new HttpCookie("sid", "value")
|
||||
{
|
||||
Path = "/",
|
||||
HttpOnly = true,
|
||||
Secure = true,
|
||||
SameSite = HttpCookieSameSite.Strict,
|
||||
};
|
||||
var serialized = cookie.ToString();
|
||||
Assert.Contains("; Secure", serialized, StringComparison.Ordinal);
|
||||
Assert.Contains("; SameSite=Strict", serialized, StringComparison.Ordinal);
|
||||
|
||||
var data = Encoding.ASCII.GetBytes(
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: 0\r\nSet-Cookie: " + serialized + "\r\n\r\n");
|
||||
var response = new HttpResponsePacket();
|
||||
|
||||
Assert.Equal(data.Length, response.Parse(data, 0, (uint)data.Length));
|
||||
Assert.True(response.Cookies[0].Secure);
|
||||
Assert.Equal(HttpCookieSameSite.Strict, response.Cookies[0].SameSite);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Response_ComposeUsesAnExplicitOutboundLimitOnly()
|
||||
{
|
||||
var response = new HttpResponsePacket
|
||||
{
|
||||
MaximumContentLength = 1,
|
||||
Message = new byte[] { 1, 2 },
|
||||
};
|
||||
|
||||
Assert.True(response.Compose(HttpComposeOption.DataOnly));
|
||||
Assert.Equal(response.Message, response.Data);
|
||||
|
||||
response.MaximumComposedContentLength = 1;
|
||||
Assert.Throws<ParserLimitException>(() =>
|
||||
response.Compose(HttpComposeOption.DataOnly));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Session_TimerStartsAndServerRemovesExpiredSession()
|
||||
{
|
||||
var server = new HttpServer();
|
||||
var ended = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var session = server.CreateSession("expiring", 1);
|
||||
session.OnEnd += _ => ended.TrySetResult(true);
|
||||
|
||||
await ended.Task.WaitAsync(TimeSpan.FromSeconds(3));
|
||||
|
||||
var sessions = (IDictionary)typeof(HttpServer)
|
||||
.GetField("sessions", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.GetValue(server)!;
|
||||
Assert.Empty(sessions);
|
||||
|
||||
// Destruction remains safe after the expiry callback has already disposed the timer.
|
||||
session.Destroy();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Session_WithoutATimerCanBeDestroyedIdempotently()
|
||||
{
|
||||
var session = new HttpSession();
|
||||
|
||||
session.Destroy();
|
||||
session.Destroy();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExplicitlyDestroyedSession_IsRemovedFromServer()
|
||||
{
|
||||
var server = new HttpServer();
|
||||
var session = server.CreateSession("destroyed", 0);
|
||||
|
||||
session.Destroy();
|
||||
|
||||
var sessions = (IDictionary)typeof(HttpServer)
|
||||
.GetField("sessions", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.GetValue(server)!;
|
||||
Assert.Empty(sessions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InternalServerErrorPage_HtmlEncodesExceptionText()
|
||||
{
|
||||
var page = HttpConnection.FormatError500Page("<script>alert('x')</script>&");
|
||||
|
||||
Assert.DoesNotContain("<script>", page, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("<script>", page, StringComparison.Ordinal);
|
||||
Assert.Contains("&", page, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static void AssertFormLimit(string body, Action<HttpRequestPacket> configure)
|
||||
{
|
||||
var data = RequestBytes(
|
||||
"POST",
|
||||
$"Content-Type: application/x-www-form-urlencoded\r\nContent-Length: {body.Length}\r\n",
|
||||
body);
|
||||
var packet = new HttpRequestPacket();
|
||||
configure(packet);
|
||||
|
||||
Assert.Throws<ParserLimitException>(() =>
|
||||
packet.Parse(data, 0, (uint)data.Length));
|
||||
}
|
||||
|
||||
private static byte[] RequestBytes(string method, string headers, string body)
|
||||
=> Encoding.ASCII.GetBytes($"{method} / HTTP/1.1\r\n{headers}\r\n{body}");
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Esiur.Core;
|
||||
using Esiur.Data;
|
||||
using Esiur.Net;
|
||||
using Esiur.Net.Http;
|
||||
using Esiur.Net.Packets.Http;
|
||||
using Esiur.Net.Packets.WebSocket;
|
||||
using Esiur.Net.Sockets;
|
||||
using Esiur.Resource;
|
||||
|
||||
namespace Esiur.Tests.Unit;
|
||||
|
||||
public class HttpWebSocketConnectionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Handshake_RequiresGetHttp11TokensVersionAndCanonical16ByteKey()
|
||||
{
|
||||
Assert.True(HttpConnection.IsWebsocketRequest(ValidHandshake()));
|
||||
|
||||
var request = ValidHandshake();
|
||||
request.Method = Esiur.Net.Packets.Http.HttpMethod.POST;
|
||||
Assert.False(HttpConnection.IsWebsocketRequest(request));
|
||||
|
||||
request = ValidHandshake();
|
||||
request.Version = "HTTP/1.0";
|
||||
Assert.False(HttpConnection.IsWebsocketRequest(request));
|
||||
|
||||
request = ValidHandshake();
|
||||
request.RawMethod = "get";
|
||||
Assert.False(HttpConnection.IsWebsocketRequest(request));
|
||||
|
||||
request = ValidHandshake();
|
||||
request.Headers["Connection"] = "keep-alive, not-an-upgrade";
|
||||
Assert.False(HttpConnection.IsWebsocketRequest(request));
|
||||
|
||||
request = ValidHandshake();
|
||||
request.Headers["Upgrade"] = "notwebsocket";
|
||||
Assert.False(HttpConnection.IsWebsocketRequest(request));
|
||||
|
||||
request = ValidHandshake();
|
||||
request.Headers["Sec-WebSocket-Version"] = "12";
|
||||
Assert.False(HttpConnection.IsWebsocketRequest(request));
|
||||
|
||||
request = ValidHandshake();
|
||||
request.Headers["Sec-WebSocket-Key"] = "not-base64";
|
||||
Assert.False(HttpConnection.IsWebsocketRequest(request));
|
||||
|
||||
request = ValidHandshake();
|
||||
request.Headers["Sec-WebSocket-Key"] = Convert.ToBase64String(new byte[15]);
|
||||
Assert.False(HttpConnection.IsWebsocketRequest(request));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Upgrade_SelectsOnlyAnExplicitlySupportedSubprotocol()
|
||||
{
|
||||
var request = ValidHandshake();
|
||||
request.Headers["Sec-WebSocket-Protocol"] = "chat, superchat";
|
||||
var response = new HttpResponsePacket();
|
||||
response.Headers["Sec-WebSocket-Protocol"] = "untrusted";
|
||||
|
||||
Assert.True(HttpConnection.Upgrade(request, response));
|
||||
Assert.Null(response.Headers["Sec-WebSocket-Protocol"]);
|
||||
Assert.Equal("websocket", response.Headers["Upgrade"]);
|
||||
Assert.Equal("Upgrade", response.Headers["Connection"]);
|
||||
|
||||
response = new HttpResponsePacket();
|
||||
Assert.True(HttpConnection.Upgrade(
|
||||
request,
|
||||
response,
|
||||
new[] { "superchat", "chat" },
|
||||
out var selected));
|
||||
Assert.Equal("superchat", selected);
|
||||
Assert.Equal("superchat", response.Headers["Sec-WebSocket-Protocol"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MalformedAdvertisedUpgrade_IsRejectedBeforeFiltersOrRoutesRun()
|
||||
{
|
||||
var server = new HttpServer();
|
||||
var (connection, socket, filter) = CreateHttpConnection(server);
|
||||
var request = Encoding.ASCII.GetBytes(
|
||||
"GET / HTTP/1.1\r\n" +
|
||||
"Connection: Upgrade\r\n" +
|
||||
"Upgrade: websocket\r\n" +
|
||||
"Sec-WebSocket-Version: 13\r\n" +
|
||||
"Sec-WebSocket-Key: invalid\r\n\r\n");
|
||||
|
||||
Receive(connection, socket, request);
|
||||
|
||||
Assert.Equal(SocketState.Closed, socket.State);
|
||||
Assert.False(connection.WSMode);
|
||||
Assert.Equal(0, filter.ExecutionCount);
|
||||
Assert.StartsWith("HTTP/1.1 400 Bad Request", Encoding.ASCII.GetString(Assert.Single(socket.Sent)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidUpgrade_UsesServerSubprotocolConfiguration()
|
||||
{
|
||||
var server = new HttpServer
|
||||
{
|
||||
WebSocketSubprotocols = new[] { "superchat" }
|
||||
};
|
||||
var (connection, socket, filter) = CreateHttpConnection(server);
|
||||
var request = Encoding.ASCII.GetBytes(
|
||||
"GET / HTTP/1.1\r\n" +
|
||||
"Connection: keep-alive, Upgrade\r\n" +
|
||||
"Upgrade: websocket\r\n" +
|
||||
"Sec-WebSocket-Version: 13\r\n" +
|
||||
$"Sec-WebSocket-Key: {Convert.ToBase64String(new byte[16])}\r\n" +
|
||||
"Sec-WebSocket-Protocol: chat, superchat\r\n\r\n");
|
||||
|
||||
Receive(connection, socket, request);
|
||||
|
||||
Assert.Equal(SocketState.Established, socket.State);
|
||||
Assert.True(connection.WSMode);
|
||||
Assert.Equal("superchat", connection.WebSocketSubprotocol);
|
||||
Assert.Equal(1, filter.ExecutionCount);
|
||||
Assert.Contains(
|
||||
"sec-websocket-protocol: superchat\r\n",
|
||||
Encoding.ASCII.GetString(Assert.Single(socket.Sent)),
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidUpgrade_DrainsFramesCoalescedWithTheHttpHandshake()
|
||||
{
|
||||
var (connection, socket, filter) = CreateHttpConnection();
|
||||
var handshake = Encoding.ASCII.GetBytes(
|
||||
"GET / HTTP/1.1\r\n" +
|
||||
"Connection: Upgrade\r\n" +
|
||||
"Upgrade: websocket\r\n" +
|
||||
"Sec-WebSocket-Version: 13\r\n" +
|
||||
$"Sec-WebSocket-Key: {Convert.ToBase64String(new byte[16])}\r\n\r\n");
|
||||
var input = Concat(
|
||||
handshake,
|
||||
Frame(WebsocketPacket.WSOpcode.BinaryFrame, true, true, new byte[] { 1 }),
|
||||
Frame(WebsocketPacket.WSOpcode.BinaryFrame, true, true, new byte[] { 2 }));
|
||||
|
||||
Receive(connection, socket, input);
|
||||
|
||||
Assert.True(connection.WSMode);
|
||||
Assert.Equal(3, filter.ExecutionCount);
|
||||
Assert.Collection(
|
||||
filter.Messages,
|
||||
message => Assert.Equal(new byte[] { 1 }, message.Payload),
|
||||
message => Assert.Equal(new byte[] { 2 }, message.Payload));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuiltInWebSocket_SendAlwaysRecomposesServerFramesAsUnmasked()
|
||||
{
|
||||
var (connection, socket, _) = CreateWebSocketConnection();
|
||||
var packet = new WebsocketPacket
|
||||
{
|
||||
FIN = true,
|
||||
Opcode = WebsocketPacket.WSOpcode.BinaryFrame,
|
||||
Mask = true,
|
||||
MaskKey = new byte[] { 1, 2, 3, 4 },
|
||||
Message = new byte[] { 7, 8, 9 }
|
||||
};
|
||||
packet.Compose();
|
||||
|
||||
connection.Send(packet);
|
||||
|
||||
var sent = ParseServerFrame(Assert.Single(socket.Sent));
|
||||
Assert.False(sent.Mask);
|
||||
Assert.Equal(new byte[] { 7, 8, 9 }, sent.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuiltInWebSocket_ReassemblesMessagesHandlesPingAndDrainsCoalescedFrames()
|
||||
{
|
||||
var (connection, socket, filter) = CreateWebSocketConnection();
|
||||
var frames = Concat(
|
||||
Frame(WebsocketPacket.WSOpcode.TextFrame, false, true, Encoding.UTF8.GetBytes("hel")),
|
||||
Frame(WebsocketPacket.WSOpcode.Ping, true, true, Encoding.ASCII.GetBytes("p")),
|
||||
Frame(WebsocketPacket.WSOpcode.ContinuationFrame, true, true, Encoding.UTF8.GetBytes("lo")),
|
||||
Frame(WebsocketPacket.WSOpcode.BinaryFrame, true, true, new byte[] { 1, 2 }));
|
||||
|
||||
Receive(connection, socket, frames);
|
||||
|
||||
Assert.Equal(SocketState.Established, socket.State);
|
||||
Assert.Collection(
|
||||
filter.Messages,
|
||||
message =>
|
||||
{
|
||||
Assert.Equal(WebsocketPacket.WSOpcode.TextFrame, message.Opcode);
|
||||
Assert.Equal("hello", Encoding.UTF8.GetString(message.Payload));
|
||||
},
|
||||
message =>
|
||||
{
|
||||
Assert.Equal(WebsocketPacket.WSOpcode.BinaryFrame, message.Opcode);
|
||||
Assert.Equal(new byte[] { 1, 2 }, message.Payload);
|
||||
});
|
||||
|
||||
var pong = Assert.Single(socket.Sent);
|
||||
var parsedPong = ParseServerFrame(pong);
|
||||
Assert.Equal(WebsocketPacket.WSOpcode.Pong, parsedPong.Opcode);
|
||||
Assert.Equal("p", Encoding.ASCII.GetString(parsedPong.Message));
|
||||
Assert.Equal(WebsocketPacket.WSOpcode.BinaryFrame, connection.WSRequest.Opcode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuiltInWebSocket_RetainsAnIncompleteFrameWithoutLosingTheNextFrame()
|
||||
{
|
||||
var (connection, socket, filter) = CreateWebSocketConnection();
|
||||
var first = Frame(
|
||||
WebsocketPacket.WSOpcode.BinaryFrame,
|
||||
true,
|
||||
true,
|
||||
new byte[] { 1, 2, 3, 4 });
|
||||
var second = Frame(
|
||||
WebsocketPacket.WSOpcode.BinaryFrame,
|
||||
true,
|
||||
true,
|
||||
new byte[] { 5, 6 });
|
||||
var buffer = new NetworkBuffer();
|
||||
var split = first.Length - 2;
|
||||
|
||||
buffer.Write(first, 0, (uint)split);
|
||||
connection.NetworkReceive(socket, buffer);
|
||||
Assert.True(buffer.Protected);
|
||||
Assert.Empty(filter.Messages);
|
||||
|
||||
buffer.Write(Concat(first.Skip(split).ToArray(), second));
|
||||
connection.NetworkReceive(socket, buffer);
|
||||
|
||||
Assert.False(buffer.Protected);
|
||||
Assert.Collection(
|
||||
filter.Messages,
|
||||
message => Assert.Equal(new byte[] { 1, 2, 3, 4 }, message.Payload),
|
||||
message => Assert.Equal(new byte[] { 5, 6 }, message.Payload));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuiltInWebSocket_RejectsUnmaskedClientFramesWithProtocolClose()
|
||||
{
|
||||
var (connection, socket, filter) = CreateWebSocketConnection();
|
||||
|
||||
Receive(connection, socket, Frame(
|
||||
WebsocketPacket.WSOpcode.BinaryFrame,
|
||||
true,
|
||||
false,
|
||||
new byte[] { 1 }));
|
||||
|
||||
Assert.Equal(SocketState.Closed, socket.State);
|
||||
Assert.Empty(filter.Messages);
|
||||
AssertCloseCode(socket, 1002);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuiltInWebSocket_RejectsInvalidUtf8AcrossFragments()
|
||||
{
|
||||
var (connection, socket, filter) = CreateWebSocketConnection();
|
||||
|
||||
Receive(connection, socket, Concat(
|
||||
Frame(WebsocketPacket.WSOpcode.TextFrame, false, true, new byte[] { 0xC3 }),
|
||||
Frame(WebsocketPacket.WSOpcode.ContinuationFrame, true, true, new byte[] { 0x28 })));
|
||||
|
||||
Assert.Equal(SocketState.Closed, socket.State);
|
||||
Assert.Empty(filter.Messages);
|
||||
AssertCloseCode(socket, 1007);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuiltInWebSocket_EnforcesAggregateFragmentLimit()
|
||||
{
|
||||
var server = new HttpServer { MaximumWebSocketMessageLength = 4 };
|
||||
var (connection, socket, filter) = CreateWebSocketConnection(server);
|
||||
|
||||
Receive(connection, socket, Concat(
|
||||
Frame(WebsocketPacket.WSOpcode.BinaryFrame, false, true, new byte[] { 1, 2, 3 }),
|
||||
Frame(WebsocketPacket.WSOpcode.ContinuationFrame, true, true, new byte[] { 4, 5 })));
|
||||
|
||||
Assert.Equal(SocketState.Closed, socket.State);
|
||||
Assert.Empty(filter.Messages);
|
||||
AssertCloseCode(socket, 1009);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuiltInWebSocket_EchoesAValidClosePayloadThenCloses()
|
||||
{
|
||||
var (connection, socket, _) = CreateWebSocketConnection();
|
||||
var payload = new byte[] { 0x03, 0xE8 };
|
||||
|
||||
Receive(connection, socket, Frame(
|
||||
WebsocketPacket.WSOpcode.ConnectionClose,
|
||||
true,
|
||||
true,
|
||||
payload));
|
||||
|
||||
Assert.Equal(SocketState.Closed, socket.State);
|
||||
var close = ParseServerFrame(Assert.Single(socket.Sent));
|
||||
Assert.Equal(WebsocketPacket.WSOpcode.ConnectionClose, close.Opcode);
|
||||
Assert.Equal(payload, close.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuiltInWebSocket_WaitsForCloseFrameSendBeforeClosingTransport()
|
||||
{
|
||||
var pendingSend = new AsyncReply<bool>();
|
||||
var server = new HttpServer();
|
||||
var filter = new CaptureFilter();
|
||||
SetFilters(server, filter);
|
||||
var socket = new TestSocket(pendingSend);
|
||||
var connection = new HttpConnection { Server = server, WSMode = true };
|
||||
connection.Assign(socket);
|
||||
|
||||
Receive(connection, socket, Frame(
|
||||
WebsocketPacket.WSOpcode.ConnectionClose,
|
||||
true,
|
||||
true,
|
||||
new byte[] { 0x03, 0xE8 }));
|
||||
|
||||
Assert.Equal(SocketState.Established, socket.State);
|
||||
Assert.Single(socket.Sent);
|
||||
|
||||
pendingSend.Trigger(true);
|
||||
Assert.Equal(SocketState.Closed, socket.State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExceptionDetails_AreGenericByDefaultAndEncodedWhenOptedIn()
|
||||
{
|
||||
var server = new HttpServer();
|
||||
var connection = new HttpConnection { Server = server };
|
||||
var exception = new InvalidOperationException("<script>secret</script>");
|
||||
|
||||
var genericPage = connection.FormatError500Page(exception);
|
||||
Assert.Contains("An internal server error occurred.", genericPage);
|
||||
Assert.DoesNotContain("secret", genericPage, StringComparison.Ordinal);
|
||||
|
||||
server.ExposeExceptionDetails = true;
|
||||
var detailedPage = connection.FormatError500Page(exception);
|
||||
Assert.Contains("<script>secret</script>", detailedPage);
|
||||
Assert.DoesNotContain("<script>", detailedPage, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SessionLookup_RestoresRefreshesAndStopsReturningDestroyedSessions()
|
||||
{
|
||||
var server = new HttpServer();
|
||||
var session = server.CreateSession("known-session", 0);
|
||||
var initialAction = session.LastAction;
|
||||
var request = new HttpRequestPacket { Cookies = new StringKeyList() };
|
||||
request.Cookies["SID"] = session.Id;
|
||||
var connection = new HttpConnection
|
||||
{
|
||||
Server = server,
|
||||
Request = request
|
||||
};
|
||||
|
||||
Assert.True(server.TryGetSession(session.Id, out var found));
|
||||
Assert.Same(session, found);
|
||||
|
||||
connection.RestoreSessionFromRequest();
|
||||
Assert.Same(session, connection.Session);
|
||||
Assert.True(session.LastAction >= initialAction);
|
||||
|
||||
session.Destroy();
|
||||
Assert.False(server.TryGetSession(session.Id, out _));
|
||||
connection.RestoreSessionFromRequest();
|
||||
Assert.Null(connection.Session);
|
||||
}
|
||||
|
||||
private static HttpRequestPacket ValidHandshake()
|
||||
{
|
||||
var headers = new StringKeyList();
|
||||
headers["Connection"] = "keep-alive, Upgrade";
|
||||
headers["Upgrade"] = "websocket";
|
||||
headers["Sec-WebSocket-Version"] = "13";
|
||||
headers["Sec-WebSocket-Key"] = Convert.ToBase64String(new byte[16]);
|
||||
return new HttpRequestPacket
|
||||
{
|
||||
Method = Esiur.Net.Packets.Http.HttpMethod.GET,
|
||||
Version = "HTTP/1.1",
|
||||
Headers = headers,
|
||||
URL = "/"
|
||||
};
|
||||
}
|
||||
|
||||
private static (HttpConnection Connection, TestSocket Socket, CaptureFilter Filter)
|
||||
CreateWebSocketConnection(HttpServer? server = null)
|
||||
{
|
||||
var result = CreateHttpConnection(server);
|
||||
result.Connection.WSMode = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static (HttpConnection Connection, TestSocket Socket, CaptureFilter Filter)
|
||||
CreateHttpConnection(HttpServer? server = null)
|
||||
{
|
||||
server ??= new HttpServer();
|
||||
var filter = new CaptureFilter();
|
||||
SetFilters(server, filter);
|
||||
|
||||
var socket = new TestSocket();
|
||||
var connection = new HttpConnection
|
||||
{
|
||||
Server = server
|
||||
};
|
||||
connection.Assign(socket);
|
||||
return (connection, socket, filter);
|
||||
}
|
||||
|
||||
private static void SetFilters(HttpServer server, params HttpFilter[] filters)
|
||||
=> typeof(HttpServer)
|
||||
.GetField("filters", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.SetValue(server, filters);
|
||||
|
||||
private static void Receive(
|
||||
HttpConnection connection,
|
||||
TestSocket socket,
|
||||
byte[] message)
|
||||
{
|
||||
var buffer = new NetworkBuffer();
|
||||
buffer.Write(message);
|
||||
connection.NetworkReceive(socket, buffer);
|
||||
}
|
||||
|
||||
private static byte[] Frame(
|
||||
WebsocketPacket.WSOpcode opcode,
|
||||
bool final,
|
||||
bool masked,
|
||||
byte[] payload)
|
||||
{
|
||||
var packet = new WebsocketPacket
|
||||
{
|
||||
FIN = final,
|
||||
Opcode = opcode,
|
||||
Mask = masked,
|
||||
MaskKey = new byte[] { 1, 2, 3, 4 },
|
||||
Message = payload
|
||||
};
|
||||
packet.Compose();
|
||||
return packet.Data;
|
||||
}
|
||||
|
||||
private static byte[] Concat(params byte[][] messages)
|
||||
{
|
||||
var length = messages.Sum(message => message.Length);
|
||||
var result = new byte[length];
|
||||
var offset = 0;
|
||||
foreach (var message in messages)
|
||||
{
|
||||
Buffer.BlockCopy(message, 0, result, offset, message.Length);
|
||||
offset += message.Length;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static WebsocketPacket ParseServerFrame(byte[] data)
|
||||
{
|
||||
var packet = new WebsocketPacket { ExpectedMask = false };
|
||||
Assert.Equal(data.Length, packet.Parse(data, 0, (uint)data.Length));
|
||||
return packet;
|
||||
}
|
||||
|
||||
private static void AssertCloseCode(TestSocket socket, int expected)
|
||||
{
|
||||
var close = ParseServerFrame(Assert.Single(socket.Sent));
|
||||
Assert.Equal(WebsocketPacket.WSOpcode.ConnectionClose, close.Opcode);
|
||||
Assert.Equal(expected, close.Message[0] << 8 | close.Message[1]);
|
||||
}
|
||||
|
||||
private sealed class CaptureFilter : HttpFilter
|
||||
{
|
||||
public List<(WebsocketPacket.WSOpcode Opcode, byte[] Payload)> Messages { get; } = new();
|
||||
public int ExecutionCount { get; private set; }
|
||||
|
||||
public override AsyncReply<bool> Execute(HttpConnection sender)
|
||||
{
|
||||
ExecutionCount++;
|
||||
var packet = sender.WSRequest;
|
||||
if (packet != null)
|
||||
Messages.Add((packet.Opcode, packet.Message.ToArray()));
|
||||
return new AsyncReply<bool>(true);
|
||||
}
|
||||
|
||||
public override AsyncReply<bool> Handle(
|
||||
ResourceOperation operation,
|
||||
IResourceContext? context = null) => new(true);
|
||||
}
|
||||
|
||||
private sealed class TestSocket : ISocket
|
||||
{
|
||||
private readonly AsyncReply<bool>? sendReply;
|
||||
|
||||
public TestSocket(AsyncReply<bool>? sendReply = null)
|
||||
=> this.sendReply = sendReply;
|
||||
|
||||
public event DestroyedEvent? OnDestroy;
|
||||
public SocketState State { get; private set; } = SocketState.Established;
|
||||
public INetworkReceiver<ISocket> Receiver { get; set; } = null!;
|
||||
public IPEndPoint RemoteEndPoint { get; } = new(IPAddress.Loopback, 12345);
|
||||
public IPEndPoint LocalEndPoint { get; } = new(IPAddress.Loopback, 54321);
|
||||
public List<byte[]> Sent { get; } = new();
|
||||
|
||||
public void Send(byte[] message) => Send(message, 0, message.Length);
|
||||
|
||||
public void Send(byte[] message, int offset, int length)
|
||||
{
|
||||
var copy = new byte[length];
|
||||
Buffer.BlockCopy(message, offset, copy, 0, length);
|
||||
Sent.Add(copy);
|
||||
}
|
||||
|
||||
public AsyncReply<bool> SendAsync(byte[] message, int offset, int length)
|
||||
{
|
||||
Send(message, offset, length);
|
||||
return sendReply ?? new AsyncReply<bool>(true);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (State == SocketState.Closed)
|
||||
return;
|
||||
|
||||
State = SocketState.Closed;
|
||||
Receiver?.NetworkClose(this);
|
||||
}
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
Close();
|
||||
OnDestroy?.Invoke(this);
|
||||
OnDestroy = null;
|
||||
}
|
||||
|
||||
public AsyncReply<bool> Connect(string hostname, ushort port) => new(false);
|
||||
public bool Begin() => true;
|
||||
public AsyncReply<bool> BeginAsync() => new(true);
|
||||
public AsyncReply<ISocket> AcceptAsync() => new((ISocket)null!);
|
||||
public ISocket Accept() => null!;
|
||||
public void Hold() { }
|
||||
public void Unhold() { }
|
||||
}
|
||||
}
|
||||
@@ -114,14 +114,17 @@ internal sealed class IntegrationCluster : IAsyncDisposable
|
||||
bool allowEncryption = true,
|
||||
bool oneStepAuthentication = false,
|
||||
bool useWebSocket = false,
|
||||
bool mismatchedSessionKeys = false)
|
||||
bool mismatchedSessionKeys = false,
|
||||
bool allowAuthentication = true,
|
||||
bool registerServerAuthenticationProvider = true)
|
||||
{
|
||||
var port = Interlocked.Increment(ref _portCounter);
|
||||
|
||||
var serverWh = new Warehouse();
|
||||
serverWh.RegisterAuthenticationProvider(oneStepAuthentication
|
||||
? new OneStepAuthenticationProvider()
|
||||
: new TestServerAuthProvider());
|
||||
if (registerServerAuthenticationProvider)
|
||||
serverWh.RegisterAuthenticationProvider(oneStepAuthentication
|
||||
? new OneStepAuthenticationProvider()
|
||||
: new TestServerAuthProvider());
|
||||
if (encrypted || requireEncryption)
|
||||
serverWh.RegisterEncryptionProvider(new AesEncryptionProvider());
|
||||
|
||||
@@ -129,10 +132,9 @@ internal sealed class IntegrationCluster : IAsyncDisposable
|
||||
var server = await serverWh.Put("sys/server", new EpServer
|
||||
{
|
||||
Port = (ushort)port,
|
||||
AllowedAuthenticationProviders = new[]
|
||||
{
|
||||
oneStepAuthentication ? "one-step" : "hash",
|
||||
},
|
||||
AllowedAuthenticationProviders = allowAuthentication
|
||||
? new[] { oneStepAuthentication ? "one-step" : "hash" }
|
||||
: Array.Empty<string>(),
|
||||
AllowedEncryptionProviders = (encrypted || requireEncryption) && allowEncryption
|
||||
? new[] { AesEncryptionProvider.Name }
|
||||
: Array.Empty<string>(),
|
||||
|
||||
@@ -70,6 +70,32 @@ public class SessionHeadersIntegrationTests
|
||||
.WaitAsync(TimeSpan.FromSeconds(10)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisallowedAuthenticationProvider_FailsClosedPromptly()
|
||||
{
|
||||
var exception = await Assert.ThrowsAnyAsync<Exception>(async () =>
|
||||
await IntegrationCluster
|
||||
.StartAsync(
|
||||
_ => Task.CompletedTask,
|
||||
allowAuthentication: false)
|
||||
.WaitAsync(TimeSpan.FromSeconds(5)));
|
||||
|
||||
Assert.IsNotType<TimeoutException>(exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AllowlistedButUnregisteredAuthenticationProvider_FailsClosedPromptly()
|
||||
{
|
||||
var exception = await Assert.ThrowsAnyAsync<Exception>(async () =>
|
||||
await IntegrationCluster
|
||||
.StartAsync(
|
||||
_ => Task.CompletedTask,
|
||||
registerServerAuthenticationProvider: false)
|
||||
.WaitAsync(TimeSpan.FromSeconds(5)));
|
||||
|
||||
Assert.IsNotType<TimeoutException>(exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddressBoundEncryption_DerivesMatchingCiphersAcrossPeers()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
using Esiur.Net;
|
||||
|
||||
namespace Esiur.Tests.Unit;
|
||||
|
||||
public class NetworkBufferTests
|
||||
{
|
||||
[Fact]
|
||||
public void FragmentedWritesAreConcatenatedAndReadResetsState()
|
||||
{
|
||||
var buffer = new NetworkBuffer();
|
||||
|
||||
buffer.Write(new byte[] { 1, 2 });
|
||||
buffer.Write(new byte[] { 99, 3, 4, 100 }, 1, 2);
|
||||
|
||||
Assert.Equal(4u, buffer.Available);
|
||||
Assert.True(buffer.CanRead);
|
||||
Assert.False(buffer.Protected);
|
||||
Assert.Equal(new byte[] { 1, 2, 3, 4 }, buffer.Read());
|
||||
Assert.Equal(0u, buffer.Available);
|
||||
Assert.False(buffer.CanRead);
|
||||
Assert.False(buffer.Protected);
|
||||
Assert.Null(buffer.Read());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HoldForRetainsPartialPacketUntilThreshold()
|
||||
{
|
||||
var buffer = new NetworkBuffer();
|
||||
|
||||
buffer.HoldFor(new byte[] { 1, 2 }, 5);
|
||||
Assert.True(buffer.Protected);
|
||||
Assert.False(buffer.CanRead);
|
||||
Assert.Null(buffer.Read());
|
||||
|
||||
buffer.Write(new byte[] { 3, 4 });
|
||||
Assert.True(buffer.Protected);
|
||||
Assert.Null(buffer.Read());
|
||||
|
||||
buffer.Write(new byte[] { 5 });
|
||||
Assert.False(buffer.Protected);
|
||||
Assert.True(buffer.CanRead);
|
||||
Assert.Equal(new byte[] { 1, 2, 3, 4, 5 }, buffer.Read());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HoldForPrependsPartialPacketToBufferedData()
|
||||
{
|
||||
var buffer = new NetworkBuffer();
|
||||
|
||||
buffer.Write(new byte[] { 3, 4 });
|
||||
buffer.HoldFor(new byte[] { 0, 1, 2, 9 }, 1, 2, 5);
|
||||
buffer.Write(new byte[] { 5 });
|
||||
|
||||
Assert.Equal(new byte[] { 1, 2, 3, 4, 5 }, buffer.Read());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProtectRetainsOnlyTheRemainingSlice()
|
||||
{
|
||||
var buffer = new NetworkBuffer();
|
||||
var source = new byte[] { 99, 1, 2 };
|
||||
|
||||
Assert.True(buffer.Protect(source, 1, 4));
|
||||
Assert.Equal(2u, buffer.Available);
|
||||
Assert.True(buffer.Protected);
|
||||
|
||||
buffer.Write(new byte[] { 3, 4 });
|
||||
Assert.Equal(new byte[] { 1, 2, 3, 4 }, buffer.Read());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProtectDoesNotRetainACompleteSlice()
|
||||
{
|
||||
var buffer = new NetworkBuffer();
|
||||
|
||||
Assert.False(buffer.Protect(new byte[] { 0, 1, 2, 3 }, 1, 3));
|
||||
Assert.Equal(0u, buffer.Available);
|
||||
Assert.Null(buffer.Read());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidRangesAndImpossibleThresholdsAreRejected()
|
||||
{
|
||||
var buffer = new NetworkBuffer();
|
||||
var source = new byte[4];
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => buffer.Write(null!));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => buffer.Write(source, 5, 0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => buffer.Write(source, 3, 2));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => buffer.Protect(source, 5, 6));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => buffer.Protect(source, 0, uint.MaxValue));
|
||||
Assert.Throws<Exception>(() => buffer.HoldFor(source, 0, 2, 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FragmentedGrowthHasLinearAllocationBound()
|
||||
{
|
||||
const int payloadLength = 512 * 1024;
|
||||
const int chunkLength = 1024;
|
||||
var chunk = new byte[chunkLength];
|
||||
|
||||
// Warm up the methods used below so JIT allocation is outside the measurement.
|
||||
var warmup = new NetworkBuffer();
|
||||
warmup.Write(chunk);
|
||||
warmup.Write(chunk);
|
||||
_ = warmup.Read();
|
||||
|
||||
var before = GC.GetAllocatedBytesForCurrentThread();
|
||||
var buffer = new NetworkBuffer();
|
||||
for (var offset = 0; offset < payloadLength; offset += chunkLength)
|
||||
buffer.Write(chunk);
|
||||
|
||||
var result = buffer.Read();
|
||||
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(payloadLength, result.Length);
|
||||
Assert.True(
|
||||
allocated < payloadLength * 8L,
|
||||
$"Fragmented buffering allocated {allocated:N0} bytes for a {payloadLength:N0}-byte payload.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using Esiur.Core;
|
||||
using Esiur.Data;
|
||||
using Esiur.Net;
|
||||
using Esiur.Net.Sockets;
|
||||
|
||||
namespace Esiur.Tests.Unit;
|
||||
|
||||
public class NetworkLifecycleConcurrencyTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task NetworkConnection_DrainsDistinctBuffersQueuedWhileParserIsBusy()
|
||||
{
|
||||
using var parserEntered = new ManualResetEventSlim();
|
||||
using var releaseParser = new ManualResetEventSlim();
|
||||
var connection = new BlockingConnection(parserEntered, releaseParser);
|
||||
var socket = new TestSocket();
|
||||
connection.Assign(socket);
|
||||
|
||||
var firstBuffer = BufferWith(1);
|
||||
var secondBuffer = BufferWith(2);
|
||||
var firstReceive = Task.Run(() => connection.NetworkReceive(socket, firstBuffer));
|
||||
|
||||
try
|
||||
{
|
||||
Assert.True(parserEntered.Wait(TimeSpan.FromSeconds(2)));
|
||||
|
||||
var secondReceive = Task.Run(() => connection.NetworkReceive(socket, secondBuffer));
|
||||
Assert.Same(secondReceive, await Task.WhenAny(
|
||||
secondReceive,
|
||||
Task.Delay(TimeSpan.FromSeconds(2))));
|
||||
|
||||
releaseParser.Set();
|
||||
var bothReceives = Task.WhenAll(firstReceive, secondReceive);
|
||||
Assert.Same(bothReceives, await Task.WhenAny(
|
||||
bothReceives,
|
||||
Task.Delay(TimeSpan.FromSeconds(2))));
|
||||
await bothReceives;
|
||||
|
||||
Assert.Equal(new byte[] { 1, 2 }, connection.Received.ToArray());
|
||||
Assert.Equal(0u, secondBuffer.Available);
|
||||
}
|
||||
finally
|
||||
{
|
||||
releaseParser.Set();
|
||||
await firstReceive;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NetworkConnection_DrainsNewSocketBufferAfterReplacementDuringReceive()
|
||||
{
|
||||
using var parserEntered = new ManualResetEventSlim();
|
||||
using var releaseParser = new ManualResetEventSlim();
|
||||
var connection = new BlockingConnection(parserEntered, releaseParser);
|
||||
var oldSocket = new TestSocket();
|
||||
var newSocket = new TestSocket();
|
||||
connection.Assign(oldSocket);
|
||||
|
||||
var firstReceive = Task.Run(() =>
|
||||
connection.NetworkReceive(oldSocket, BufferWith(1)));
|
||||
|
||||
try
|
||||
{
|
||||
Assert.True(parserEntered.Wait(TimeSpan.FromSeconds(2)));
|
||||
|
||||
var replacementBuffer = BufferWith(2);
|
||||
var replacementReceive = Task.Run(() =>
|
||||
{
|
||||
Assert.Same(oldSocket, connection.Unassign());
|
||||
connection.Assign(newSocket);
|
||||
connection.NetworkReceive(newSocket, replacementBuffer);
|
||||
});
|
||||
|
||||
Assert.Same(replacementReceive, await Task.WhenAny(
|
||||
replacementReceive,
|
||||
Task.Delay(TimeSpan.FromSeconds(2))));
|
||||
|
||||
releaseParser.Set();
|
||||
var bothReceives = Task.WhenAll(firstReceive, replacementReceive);
|
||||
Assert.Same(bothReceives, await Task.WhenAny(
|
||||
bothReceives,
|
||||
Task.Delay(TimeSpan.FromSeconds(2))));
|
||||
await bothReceives;
|
||||
|
||||
Assert.Equal(new byte[] { 1, 2 }, connection.Received.ToArray());
|
||||
Assert.Equal(0u, replacementBuffer.Available);
|
||||
}
|
||||
finally
|
||||
{
|
||||
releaseParser.Set();
|
||||
await firstReceive;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NetworkConnection_IgnoresCallbacksFromUnassignedSocket()
|
||||
{
|
||||
var connection = new RecordingConnection();
|
||||
var staleSocket = new TestSocket();
|
||||
var currentSocket = new TestSocket();
|
||||
var connectEvents = 0;
|
||||
var closeEvents = 0;
|
||||
connection.OnConnect += _ => connectEvents++;
|
||||
connection.OnClose += _ => closeEvents++;
|
||||
|
||||
connection.Assign(staleSocket);
|
||||
Assert.Same(staleSocket, connection.Unassign());
|
||||
connection.Assign(currentSocket);
|
||||
|
||||
var staleBuffer = BufferWith(9);
|
||||
connection.NetworkReceive(staleSocket, staleBuffer);
|
||||
connection.NetworkConnect(staleSocket);
|
||||
connection.NetworkClose(staleSocket);
|
||||
|
||||
Assert.Equal(1u, staleBuffer.Available);
|
||||
Assert.Empty(connection.Received);
|
||||
Assert.Equal(0, connection.ConnectedCalls);
|
||||
Assert.Equal(0, connection.DisconnectedCalls);
|
||||
Assert.Equal(0, connectEvents);
|
||||
Assert.Equal(0, closeEvents);
|
||||
|
||||
connection.NetworkConnect(currentSocket);
|
||||
connection.NetworkReceive(currentSocket, BufferWith(1));
|
||||
connection.NetworkClose(currentSocket);
|
||||
|
||||
Assert.Equal(new byte[] { 1 }, connection.Received.ToArray());
|
||||
Assert.Equal(1, connection.ConnectedCalls);
|
||||
Assert.Equal(1, connection.DisconnectedCalls);
|
||||
Assert.Equal(1, connectEvents);
|
||||
Assert.Equal(1, closeEvents);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NetworkServer_ClosesSocketAcceptedAfterStopSnapshot()
|
||||
{
|
||||
var acceptedSocket = new TestSocket();
|
||||
using var listener = new BlockingListener(acceptedSocket);
|
||||
var server = new TestServer();
|
||||
|
||||
try
|
||||
{
|
||||
server.Start(listener);
|
||||
Assert.True(listener.AcceptEntered.Wait(TimeSpan.FromSeconds(2)));
|
||||
|
||||
server.Stop();
|
||||
listener.ReleaseAccept.Set();
|
||||
|
||||
Assert.True(SpinWait.SpinUntil(
|
||||
() => acceptedSocket.State == SocketState.Closed,
|
||||
TimeSpan.FromSeconds(2)));
|
||||
Assert.Empty(server.Connections);
|
||||
Assert.Equal(0, Volatile.Read(ref acceptedSocket.BeginCalls));
|
||||
Assert.Equal(0, Volatile.Read(ref server.ClientConnectedCalls));
|
||||
}
|
||||
finally
|
||||
{
|
||||
listener.ReleaseAccept.Set();
|
||||
server.Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private static NetworkBuffer BufferWith(byte value)
|
||||
{
|
||||
var buffer = new NetworkBuffer();
|
||||
buffer.Write(new[] { value });
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private class RecordingConnection : NetworkConnection
|
||||
{
|
||||
public ConcurrentQueue<byte> Received { get; } = new();
|
||||
public int ConnectedCalls;
|
||||
public int DisconnectedCalls;
|
||||
|
||||
protected override void DataReceived(NetworkBuffer buffer)
|
||||
{
|
||||
var data = buffer.Read();
|
||||
if (data == null)
|
||||
return;
|
||||
|
||||
foreach (var value in data)
|
||||
Received.Enqueue(value);
|
||||
}
|
||||
|
||||
protected override void Connected()
|
||||
=> Interlocked.Increment(ref ConnectedCalls);
|
||||
|
||||
protected override void Disconnected()
|
||||
=> Interlocked.Increment(ref DisconnectedCalls);
|
||||
}
|
||||
|
||||
private sealed class BlockingConnection : RecordingConnection
|
||||
{
|
||||
private readonly ManualResetEventSlim parserEntered;
|
||||
private readonly ManualResetEventSlim releaseParser;
|
||||
private int blocked;
|
||||
|
||||
public BlockingConnection(
|
||||
ManualResetEventSlim parserEntered,
|
||||
ManualResetEventSlim releaseParser)
|
||||
{
|
||||
this.parserEntered = parserEntered;
|
||||
this.releaseParser = releaseParser;
|
||||
}
|
||||
|
||||
protected override void DataReceived(NetworkBuffer buffer)
|
||||
{
|
||||
base.DataReceived(buffer);
|
||||
|
||||
if (Interlocked.CompareExchange(ref blocked, 1, 0) == 0)
|
||||
{
|
||||
parserEntered.Set();
|
||||
releaseParser.Wait(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ServerConnection : NetworkConnection
|
||||
{
|
||||
protected override void DataReceived(NetworkBuffer buffer) { }
|
||||
protected override void Connected() { }
|
||||
protected override void Disconnected() { }
|
||||
}
|
||||
|
||||
private sealed class TestServer : NetworkServer<ServerConnection>
|
||||
{
|
||||
public int ClientConnectedCalls;
|
||||
|
||||
protected override void ClientConnected(ServerConnection connection)
|
||||
=> Interlocked.Increment(ref ClientConnectedCalls);
|
||||
|
||||
protected override void ClientDisconnected(ServerConnection connection) { }
|
||||
}
|
||||
|
||||
private class TestSocket : ISocket
|
||||
{
|
||||
public event DestroyedEvent? OnDestroy;
|
||||
|
||||
public SocketState State { get; protected set; } = SocketState.Established;
|
||||
public INetworkReceiver<ISocket> Receiver { get; set; } = null!;
|
||||
public IPEndPoint RemoteEndPoint { get; } = new(IPAddress.Loopback, 12345);
|
||||
public IPEndPoint LocalEndPoint { get; } = new(IPAddress.Loopback, 54321);
|
||||
public int BeginCalls;
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (State == SocketState.Closed)
|
||||
return;
|
||||
|
||||
State = SocketState.Closed;
|
||||
Receiver?.NetworkClose(this);
|
||||
}
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
Close();
|
||||
OnDestroy?.Invoke(this);
|
||||
OnDestroy = null;
|
||||
}
|
||||
|
||||
public bool Begin()
|
||||
{
|
||||
Interlocked.Increment(ref BeginCalls);
|
||||
return State == SocketState.Established;
|
||||
}
|
||||
|
||||
public AsyncReply<bool> BeginAsync() => new(Begin());
|
||||
public AsyncReply<bool> Connect(string hostname, ushort port) => new(true);
|
||||
public virtual ISocket Accept() => null!;
|
||||
public AsyncReply<ISocket> AcceptAsync() => new((ISocket)null!);
|
||||
public void Send(byte[] message) { }
|
||||
public void Send(byte[] message, int offset, int length) { }
|
||||
public AsyncReply<bool> SendAsync(byte[] message, int offset, int length) => new(true);
|
||||
public void Hold() { }
|
||||
public void Unhold() { }
|
||||
}
|
||||
|
||||
private sealed class BlockingListener : TestSocket, IDisposable
|
||||
{
|
||||
private readonly ISocket acceptedSocket;
|
||||
private int acceptCalls;
|
||||
|
||||
public BlockingListener(ISocket acceptedSocket)
|
||||
{
|
||||
this.acceptedSocket = acceptedSocket;
|
||||
State = SocketState.Listening;
|
||||
}
|
||||
|
||||
public ManualResetEventSlim AcceptEntered { get; } = new();
|
||||
public ManualResetEventSlim ReleaseAccept { get; } = new();
|
||||
|
||||
public override ISocket Accept()
|
||||
{
|
||||
if (Interlocked.Increment(ref acceptCalls) != 1)
|
||||
return null!;
|
||||
|
||||
AcceptEntered.Set();
|
||||
ReleaseAccept.Wait(TimeSpan.FromSeconds(5));
|
||||
return acceptedSocket;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ReleaseAccept.Set();
|
||||
AcceptEntered.Dispose();
|
||||
ReleaseAccept.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using Esiur.Data;
|
||||
using Esiur.Net.Packets;
|
||||
using Esiur.Net.Packets.Http;
|
||||
using Esiur.Net.Packets.WebSocket;
|
||||
using Esiur.Resource;
|
||||
using System.Text;
|
||||
|
||||
namespace Esiur.Tests.Unit;
|
||||
|
||||
public class PacketCodecTests
|
||||
{
|
||||
[Fact]
|
||||
public void EpParser_ValidatesSliceBounds()
|
||||
{
|
||||
var packet = new EpAuthPacket(new Warehouse());
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => packet.Parse(Array.Empty<byte>(), 1, 0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => packet.Parse(Array.Empty<byte>(), 0, 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WebSocket_ComposesAndParsesUnmaskedFrame()
|
||||
{
|
||||
var packet = new WebsocketPacket
|
||||
{
|
||||
FIN = true,
|
||||
Opcode = WebsocketPacket.WSOpcode.BinaryFrame,
|
||||
Message = new byte[] { 1, 2, 3 }
|
||||
};
|
||||
|
||||
Assert.True(packet.Compose());
|
||||
Assert.Equal(new byte[] { 0x82, 0x03, 1, 2, 3 }, packet.Data);
|
||||
|
||||
var parsed = new WebsocketPacket();
|
||||
Assert.Equal(packet.Data.Length, parsed.Parse(packet.Data, 0, (uint)packet.Data.Length));
|
||||
Assert.Equal(packet.Message, parsed.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WebSocket_MasksOutboundPayloadAndRestoresItWhenParsed()
|
||||
{
|
||||
var message = Encoding.UTF8.GetBytes("hello");
|
||||
var packet = new WebsocketPacket
|
||||
{
|
||||
FIN = true,
|
||||
Mask = true,
|
||||
MaskKey = new byte[] { 1, 2, 3, 4 },
|
||||
Opcode = WebsocketPacket.WSOpcode.TextFrame,
|
||||
Message = message
|
||||
};
|
||||
|
||||
packet.Compose();
|
||||
|
||||
Assert.Equal(0x81, packet.Data[0]);
|
||||
Assert.Equal(0x85, packet.Data[1]);
|
||||
Assert.Equal(packet.MaskKey, packet.Data.Skip(2).Take(4));
|
||||
Assert.NotEqual(message, packet.Data.Skip(6).ToArray());
|
||||
|
||||
var parsed = new WebsocketPacket();
|
||||
Assert.Equal(packet.Data.Length, parsed.Parse(packet.Data, 0, (uint)packet.Data.Length));
|
||||
Assert.Equal(message, parsed.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WebSocket_RejectsOversizedDeclarationBeforePayloadArrives()
|
||||
{
|
||||
var packet = new WebsocketPacket { MaximumPayloadLength = 1_024 };
|
||||
var header = new byte[] { 0x82, 126, 0x04, 0x01 };
|
||||
|
||||
Assert.Throws<ParserLimitException>(() => packet.Parse(header, 0, (uint)header.Length));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HttpRequest_ParsesOffsetBodyQueryCookiesAndForm()
|
||||
{
|
||||
const string request =
|
||||
"POST /submit?a=1 HTTP/1.1\r\n" +
|
||||
"Host: example.test\r\n" +
|
||||
"Cookie: sid=abc; flag\r\n" +
|
||||
"Content-Type: application/x-www-form-urlencoded\r\n" +
|
||||
"Content-Length: 7\r\n\r\n" +
|
||||
"x=hello";
|
||||
var packetBytes = Encoding.ASCII.GetBytes(request);
|
||||
var data = new byte[packetBytes.Length + 2];
|
||||
Buffer.BlockCopy(packetBytes, 0, data, 2, packetBytes.Length);
|
||||
|
||||
var packet = new HttpRequestPacket();
|
||||
var consumed = packet.Parse(data, 2, (uint)data.Length);
|
||||
|
||||
Assert.Equal(packetBytes.Length, consumed);
|
||||
Assert.Equal(Esiur.Net.Packets.Http.HttpMethod.POST, packet.Method);
|
||||
Assert.Equal("/submit", packet.Filename);
|
||||
Assert.Equal("1", packet.Query["A"]);
|
||||
Assert.Equal("abc", packet.Cookies["SID"]);
|
||||
Assert.Equal(string.Empty, packet.Cookies["flag"]);
|
||||
Assert.Equal("hello", packet.PostForms["x"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HttpRequest_RejectsOversizedContentBeforeBuffering()
|
||||
{
|
||||
var data = Encoding.ASCII.GetBytes(
|
||||
"POST / HTTP/1.1\r\nContent-Length: 5\r\n\r\n");
|
||||
var packet = new HttpRequestPacket { MaximumContentLength = 4 };
|
||||
|
||||
Assert.Throws<ParserLimitException>(() => packet.Parse(data, 0, (uint)data.Length));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HttpResponse_ParsesBodyFromHeaderBoundaryAndUsesNegativeMissingCount()
|
||||
{
|
||||
var complete = Encoding.ASCII.GetBytes(
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nSet-Cookie: sid=abc; Path=/; HttpOnly\r\n\r\nhello");
|
||||
var prefixed = new byte[complete.Length + 2];
|
||||
Buffer.BlockCopy(complete, 0, prefixed, 2, complete.Length);
|
||||
|
||||
var packet = new HttpResponsePacket();
|
||||
Assert.Equal(complete.Length, packet.Parse(prefixed, 2, (uint)prefixed.Length));
|
||||
Assert.Equal("hello", Encoding.ASCII.GetString(packet.Message));
|
||||
Assert.Single(packet.Cookies);
|
||||
Assert.True(packet.Cookies[0].HttpOnly);
|
||||
|
||||
var incomplete = Encoding.ASCII.GetBytes(
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhe");
|
||||
Assert.Equal(-3, new HttpResponsePacket().Parse(incomplete, 0, (uint)incomplete.Length));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HttpResponse_ComposeCalculatesLengthAndAppendsBody()
|
||||
{
|
||||
var packet = new HttpResponsePacket
|
||||
{
|
||||
Text = "OK",
|
||||
Message = Encoding.ASCII.GetBytes("hello")
|
||||
};
|
||||
|
||||
packet.Compose(HttpComposeOption.AllCalculateLength);
|
||||
var response = Encoding.ASCII.GetString(packet.Data);
|
||||
|
||||
Assert.Contains("Content-Length: 5\r\n", response, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.EndsWith("\r\n\r\nhello", response, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StringKeyList_LookupsRemainCaseInsensitiveWithoutDuplicates()
|
||||
{
|
||||
var values = new StringKeyList();
|
||||
values.Add("Content-Type", "text/plain");
|
||||
values["CONTENT-TYPE"] = "application/json";
|
||||
|
||||
Assert.Equal(1, values.Count);
|
||||
Assert.Equal("application/json", values["content-type"]);
|
||||
Assert.True(values.ContainsKey("Content-Type"));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Esiur.Data;
|
||||
using Esiur.Net.Packets;
|
||||
using Esiur.Protocol;
|
||||
using Esiur.Resource;
|
||||
|
||||
namespace Esiur.Tests.Unit;
|
||||
@@ -52,4 +53,74 @@ public class ParserSecurityTests
|
||||
|
||||
Assert.Throws<ParserLimitException>(() => Codec.ParseSync(data, 0, warehouse));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TruMetadataParser_EnforcesConfiguredDepthInSyncParsing()
|
||||
{
|
||||
var warehouse = new Warehouse();
|
||||
warehouse.Configuration.Parser.MaximumTypeMetadataDepth = 3;
|
||||
|
||||
var accepted = ParsedTdu.ParseSync(
|
||||
ComposeTypedTdu(ComposeNestedTypeMetadata(3)),
|
||||
0,
|
||||
5,
|
||||
warehouse);
|
||||
|
||||
Assert.Equal(TduClass.Typed, accepted.Class);
|
||||
Assert.Throws<ParserLimitException>(() => ParsedTdu.ParseSync(
|
||||
ComposeTypedTdu(ComposeNestedTypeMetadata(4)),
|
||||
0,
|
||||
6,
|
||||
warehouse));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TruMetadataParser_EnforcesConnectionWarehouseDepthInAsyncParsing()
|
||||
{
|
||||
var warehouse = new Warehouse();
|
||||
warehouse.Configuration.Parser.MaximumTypeMetadataDepth = 2;
|
||||
var connection = new EpConnection();
|
||||
await connection.Handle(
|
||||
ResourceOperation.Configure,
|
||||
new EpServerConnectionContext
|
||||
{
|
||||
Server = new EpServer(),
|
||||
Warehouse = warehouse,
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
var data = ComposeTypedTdu(ComposeNestedTypeMetadata(3));
|
||||
|
||||
await Assert.ThrowsAsync<ParserLimitException>(async () =>
|
||||
await ParsedTdu.ParseAsync(data, connection));
|
||||
}
|
||||
finally
|
||||
{
|
||||
connection.Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] ComposeNestedTypeMetadata(int depth)
|
||||
{
|
||||
if (depth < 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(depth));
|
||||
|
||||
var metadata = new byte[depth];
|
||||
Array.Fill(metadata, (byte)TruIdentifier.TypedList, 0, depth - 1);
|
||||
metadata[^1] = (byte)TruIdentifier.Bool;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private static byte[] ComposeTypedTdu(byte[] metadata)
|
||||
{
|
||||
if (metadata.Length > byte.MaxValue)
|
||||
throw new ArgumentOutOfRangeException(nameof(metadata));
|
||||
|
||||
var data = new byte[metadata.Length + 2];
|
||||
data[0] = 0x88; // Typed TDU with a one-byte payload-length prefix.
|
||||
data[1] = (byte)metadata.Length;
|
||||
Buffer.BlockCopy(metadata, 0, data, 2, metadata.Length);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Esiur.Core;
|
||||
using Esiur.Data;
|
||||
using Esiur.Net;
|
||||
using Esiur.Net.Packets.WebSocket;
|
||||
using Esiur.Net.Sockets;
|
||||
|
||||
namespace Esiur.Tests.Unit;
|
||||
|
||||
public class SocketProtocolSecurityTests
|
||||
{
|
||||
[Fact]
|
||||
public void NetworkServer_DoesNotBlockAcceptLoopOnSocketInitialization()
|
||||
{
|
||||
var neverCompletes = new AsyncReply<bool>();
|
||||
var stalled = new TestSocket(neverCompletes);
|
||||
var ready = new TestSocket(new AsyncReply<bool>(true));
|
||||
var listener = new TestListener(stalled, ready);
|
||||
var server = new TestServer
|
||||
{
|
||||
ConnectionInitializationTimeout = TimeSpan.FromMilliseconds(100)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
server.Start(listener);
|
||||
|
||||
Assert.True(SpinWait.SpinUntil(
|
||||
() => Volatile.Read(ref ready.BeginCalls) == 1,
|
||||
TimeSpan.FromSeconds(2)));
|
||||
Assert.True(SpinWait.SpinUntil(
|
||||
() => stalled.State == SocketState.Closed,
|
||||
TimeSpan.FromSeconds(2)));
|
||||
Assert.Equal(SocketState.Established, ready.State);
|
||||
}
|
||||
finally
|
||||
{
|
||||
server.Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WebSocketServer_RejectsUnmaskedClientFrames()
|
||||
{
|
||||
var transport = new TestSocket();
|
||||
var socket = new WSocket(transport, isServer: true)
|
||||
{
|
||||
Receiver = new TestReceiver()
|
||||
};
|
||||
var frame = ComposeFrame(
|
||||
WebsocketPacket.WSOpcode.BinaryFrame,
|
||||
final: true,
|
||||
masked: false,
|
||||
new byte[] { 1, 2, 3 });
|
||||
|
||||
var input = new NetworkBuffer();
|
||||
input.Write(frame);
|
||||
socket.NetworkReceive(transport, input);
|
||||
|
||||
Assert.Equal(SocketState.Closed, transport.State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WebSocket_ReassemblesFragmentsBeforeDeliveringThem()
|
||||
{
|
||||
var transport = new TestSocket();
|
||||
var receiver = new TestReceiver();
|
||||
var socket = new WSocket(transport, isServer: true)
|
||||
{
|
||||
Receiver = receiver
|
||||
};
|
||||
var first = ComposeFrame(
|
||||
WebsocketPacket.WSOpcode.BinaryFrame,
|
||||
final: false,
|
||||
masked: true,
|
||||
new byte[] { 1, 2 });
|
||||
var second = ComposeFrame(
|
||||
WebsocketPacket.WSOpcode.ContinuationFrame,
|
||||
final: true,
|
||||
masked: true,
|
||||
new byte[] { 3, 4 });
|
||||
|
||||
var input = new NetworkBuffer();
|
||||
input.Write(first.Concat(second).ToArray());
|
||||
socket.NetworkReceive(transport, input);
|
||||
|
||||
Assert.Equal(SocketState.Established, transport.State);
|
||||
Assert.Single(receiver.Messages);
|
||||
Assert.Equal(new byte[] { 1, 2, 3, 4 }, receiver.Messages[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WebSocket_RejectsInvalidUtf8AcrossFragments()
|
||||
{
|
||||
var transport = new TestSocket();
|
||||
var receiver = new TestReceiver();
|
||||
var socket = new WSocket(transport, isServer: true)
|
||||
{
|
||||
Receiver = receiver
|
||||
};
|
||||
var first = ComposeFrame(
|
||||
WebsocketPacket.WSOpcode.TextFrame,
|
||||
final: false,
|
||||
masked: true,
|
||||
new byte[] { 0xC3 });
|
||||
var second = ComposeFrame(
|
||||
WebsocketPacket.WSOpcode.ContinuationFrame,
|
||||
final: true,
|
||||
masked: true,
|
||||
new byte[] { 0x28 });
|
||||
|
||||
var input = new NetworkBuffer();
|
||||
input.Write(first.Concat(second).ToArray());
|
||||
socket.NetworkReceive(transport, input);
|
||||
|
||||
Assert.Equal(SocketState.Closed, transport.State);
|
||||
Assert.Empty(receiver.Messages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WebSocketClient_MasksEveryOutgoingFrameWithAFreshKey()
|
||||
{
|
||||
var transport = new TestSocket();
|
||||
var socket = new WSocket(transport, isServer: false);
|
||||
|
||||
socket.Send(new byte[] { 5, 6, 7 });
|
||||
socket.Send(new byte[] { 8, 9 });
|
||||
|
||||
Assert.Equal(2, transport.Sent.Count);
|
||||
var frame = transport.Sent[0];
|
||||
var secondFrame = transport.Sent[1];
|
||||
Assert.False(
|
||||
frame.Skip(2).Take(4)
|
||||
.SequenceEqual(secondFrame.Skip(2).Take(4)),
|
||||
"Each client frame must use a fresh masking key.");
|
||||
|
||||
var parsed = new WebsocketPacket { ExpectedMask = true };
|
||||
Assert.Equal(frame.Length, parsed.Parse(frame, 0, (uint)frame.Length));
|
||||
Assert.Equal(new byte[] { 5, 6, 7 }, parsed.Message);
|
||||
|
||||
parsed = new WebsocketPacket { ExpectedMask = true };
|
||||
Assert.Equal(
|
||||
secondFrame.Length,
|
||||
parsed.Parse(secondFrame, 0, (uint)secondFrame.Length));
|
||||
Assert.Equal(new byte[] { 8, 9 }, parsed.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WebSocket_MessageLimitAlsoAppliesToUnfragmentedFrames()
|
||||
{
|
||||
var transport = new TestSocket();
|
||||
var socket = new WSocket(transport, isServer: true)
|
||||
{
|
||||
Receiver = new TestReceiver(),
|
||||
MaximumMessageLength = 2
|
||||
};
|
||||
var frame = ComposeFrame(
|
||||
WebsocketPacket.WSOpcode.BinaryFrame,
|
||||
final: true,
|
||||
masked: true,
|
||||
new byte[] { 1, 2, 3 });
|
||||
|
||||
var input = new NetworkBuffer();
|
||||
input.Write(frame);
|
||||
socket.NetworkReceive(transport, input);
|
||||
|
||||
Assert.Equal(SocketState.Closed, transport.State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WebSocket_DestroyIsReentrantAndForwardsTheWrapperOnClose()
|
||||
{
|
||||
var transport = new TestSocket();
|
||||
var receiver = new ReentrantCloseReceiver();
|
||||
var socket = new WSocket(transport, isServer: true)
|
||||
{
|
||||
Receiver = receiver
|
||||
};
|
||||
receiver.OnClose = socket.Destroy;
|
||||
|
||||
socket.Destroy();
|
||||
socket.Destroy();
|
||||
|
||||
Assert.Equal(SocketState.Closed, transport.State);
|
||||
Assert.Same(socket, receiver.ClosedSender);
|
||||
Assert.Null(transport.Receiver);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TcpSocket_BoundsCopiedPendingSendData()
|
||||
{
|
||||
using var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
|
||||
using var client = new Socket(
|
||||
AddressFamily.InterNetwork,
|
||||
SocketType.Stream,
|
||||
ProtocolType.Tcp);
|
||||
var endpoint = (IPEndPoint)listener.LocalEndpoint;
|
||||
var connectTask = client.ConnectAsync(endpoint);
|
||||
var accepted = await listener.AcceptSocketAsync();
|
||||
await connectTask;
|
||||
|
||||
var socket = new TcpSocket(accepted)
|
||||
{
|
||||
MaximumPendingSendBytes = 4
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
socket.Hold();
|
||||
socket.Send(new byte[] { 1, 2, 3, 4 });
|
||||
|
||||
Assert.Equal(4, socket.PendingSendBytes);
|
||||
Assert.Throws<InvalidOperationException>(() => socket.Send(new byte[] { 5 }));
|
||||
|
||||
var rejected = socket.SendAsync(new byte[] { 6 }, 0, 1);
|
||||
var exception = await Assert.ThrowsAsync<AsyncException>(async () => await rejected);
|
||||
Assert.IsType<InvalidOperationException>(exception.InnerException);
|
||||
Assert.Equal(4, socket.PendingSendBytes);
|
||||
}
|
||||
finally
|
||||
{
|
||||
socket.Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TcpSocket_SendCallbackFailureDoesNotCloseTheTransport()
|
||||
{
|
||||
using var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
|
||||
using var client = new Socket(
|
||||
AddressFamily.InterNetwork,
|
||||
SocketType.Stream,
|
||||
ProtocolType.Tcp);
|
||||
var endpoint = (IPEndPoint)listener.LocalEndpoint;
|
||||
var connectTask = client.ConnectAsync(endpoint);
|
||||
using var accepted = await listener.AcceptSocketAsync();
|
||||
await connectTask;
|
||||
|
||||
var socket = new TcpSocket(accepted);
|
||||
|
||||
try
|
||||
{
|
||||
socket.Hold();
|
||||
var reply = socket.SendAsync(new byte[] { 42 }, 0, 1);
|
||||
_ = reply.Then(_ => throw new InvalidOperationException("Consumer callback failure."));
|
||||
|
||||
socket.Unhold();
|
||||
|
||||
Assert.True(await reply);
|
||||
Assert.Equal(SocketState.Established, socket.State);
|
||||
Assert.True(SpinWait.SpinUntil(
|
||||
() => socket.PendingSendBytes == 0,
|
||||
TimeSpan.FromSeconds(2)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
socket.Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] ComposeFrame(
|
||||
WebsocketPacket.WSOpcode opcode,
|
||||
bool final,
|
||||
bool masked,
|
||||
byte[] payload)
|
||||
{
|
||||
var packet = new WebsocketPacket
|
||||
{
|
||||
FIN = final,
|
||||
Opcode = opcode,
|
||||
Mask = masked,
|
||||
MaskKey = new byte[] { 1, 2, 3, 4 },
|
||||
Message = payload
|
||||
};
|
||||
packet.Compose();
|
||||
return packet.Data;
|
||||
}
|
||||
|
||||
private sealed class TestReceiver : INetworkReceiver<ISocket>
|
||||
{
|
||||
public List<byte[]> Messages { get; } = new();
|
||||
|
||||
public void NetworkReceive(ISocket sender, NetworkBuffer buffer)
|
||||
{
|
||||
var message = buffer.Read();
|
||||
if (message != null)
|
||||
Messages.Add(message);
|
||||
}
|
||||
|
||||
public void NetworkConnect(ISocket sender) { }
|
||||
public void NetworkClose(ISocket sender) { }
|
||||
}
|
||||
|
||||
private sealed class ReentrantCloseReceiver : INetworkReceiver<ISocket>
|
||||
{
|
||||
public Action? OnClose { get; set; }
|
||||
public ISocket? ClosedSender { get; private set; }
|
||||
|
||||
public void NetworkReceive(ISocket sender, NetworkBuffer buffer) { }
|
||||
public void NetworkConnect(ISocket sender) { }
|
||||
|
||||
public void NetworkClose(ISocket sender)
|
||||
{
|
||||
ClosedSender = sender;
|
||||
OnClose?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestSocket : ISocket
|
||||
{
|
||||
private readonly AsyncReply<bool>? beginReply;
|
||||
|
||||
public event DestroyedEvent? OnDestroy;
|
||||
|
||||
public SocketState State { get; private set; } = SocketState.Established;
|
||||
public INetworkReceiver<ISocket> Receiver { get; set; } = null!;
|
||||
public IPEndPoint RemoteEndPoint { get; } = new(IPAddress.Loopback, 12345);
|
||||
public IPEndPoint LocalEndPoint { get; } = new(IPAddress.Loopback, 54321);
|
||||
public List<byte[]> Sent { get; } = new();
|
||||
public int BeginCalls;
|
||||
|
||||
public TestSocket(AsyncReply<bool>? beginReply = null)
|
||||
=> this.beginReply = beginReply;
|
||||
|
||||
public void Send(byte[] message) => Send(message, 0, message.Length);
|
||||
|
||||
public void Send(byte[] message, int offset, int length)
|
||||
{
|
||||
var copy = new byte[length];
|
||||
Buffer.BlockCopy(message, offset, copy, 0, length);
|
||||
Sent.Add(copy);
|
||||
}
|
||||
|
||||
public AsyncReply<bool> SendAsync(byte[] message, int offset, int length)
|
||||
{
|
||||
Send(message, offset, length);
|
||||
return new AsyncReply<bool>(true);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (State == SocketState.Closed)
|
||||
return;
|
||||
|
||||
State = SocketState.Closed;
|
||||
Receiver?.NetworkClose(this);
|
||||
}
|
||||
|
||||
public AsyncReply<bool> Connect(string hostname, ushort port) => new(true);
|
||||
public bool Begin() => true;
|
||||
public AsyncReply<bool> BeginAsync()
|
||||
{
|
||||
Interlocked.Increment(ref BeginCalls);
|
||||
return beginReply ?? new AsyncReply<bool>(true);
|
||||
}
|
||||
public AsyncReply<ISocket> AcceptAsync() => new((ISocket)null!);
|
||||
public ISocket Accept() => null!;
|
||||
public void Hold() { }
|
||||
public void Unhold() { }
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
Close();
|
||||
OnDestroy?.Invoke(this);
|
||||
OnDestroy = null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestListener : ISocket
|
||||
{
|
||||
private readonly Queue<ISocket> accepted;
|
||||
|
||||
public TestListener(params ISocket[] accepted)
|
||||
=> this.accepted = new Queue<ISocket>(accepted);
|
||||
|
||||
public event DestroyedEvent? OnDestroy;
|
||||
public SocketState State { get; private set; } = SocketState.Listening;
|
||||
public INetworkReceiver<ISocket> Receiver { get; set; } = null!;
|
||||
public IPEndPoint RemoteEndPoint => null!;
|
||||
public IPEndPoint LocalEndPoint { get; } = new(IPAddress.Loopback, 10518);
|
||||
|
||||
public ISocket Accept()
|
||||
{
|
||||
lock (accepted)
|
||||
return accepted.Count == 0 ? null! : accepted.Dequeue();
|
||||
}
|
||||
|
||||
public void Close() => State = SocketState.Closed;
|
||||
public void Destroy()
|
||||
{
|
||||
Close();
|
||||
OnDestroy?.Invoke(this);
|
||||
}
|
||||
|
||||
public AsyncReply<bool> SendAsync(byte[] message, int offset, int length) => new(false);
|
||||
public void Send(byte[] message) { }
|
||||
public void Send(byte[] message, int offset, int length) { }
|
||||
public AsyncReply<bool> Connect(string hostname, ushort port) => new(false);
|
||||
public bool Begin() => false;
|
||||
public AsyncReply<bool> BeginAsync() => new(false);
|
||||
public AsyncReply<ISocket> AcceptAsync() => new(Accept());
|
||||
public void Hold() { }
|
||||
public void Unhold() { }
|
||||
}
|
||||
|
||||
private sealed class TestConnection : NetworkConnection
|
||||
{
|
||||
protected override void DataReceived(NetworkBuffer buffer) { }
|
||||
protected override void Connected() { }
|
||||
protected override void Disconnected() { }
|
||||
}
|
||||
|
||||
private sealed class TestServer : NetworkServer<TestConnection>
|
||||
{
|
||||
protected override void ClientDisconnected(TestConnection connection) { }
|
||||
protected override void ClientConnected(TestConnection connection) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using Esiur.Proxy;
|
||||
|
||||
namespace Esiur.Tests.Unit;
|
||||
|
||||
public class TypeDefGeneratorSafetyTests
|
||||
{
|
||||
[Fact]
|
||||
public void GeneratedFileName_RejectsPathsAndAcceptsQualifiedTypeNames()
|
||||
{
|
||||
Assert.Equal(
|
||||
"Example.Contracts.Device.g.cs",
|
||||
TypeDefGenerator.GetGeneratedFileName("Example.Contracts.Device"));
|
||||
|
||||
Assert.Throws<InvalidDataException>(
|
||||
() => TypeDefGenerator.GetGeneratedFileName("../outside"));
|
||||
Assert.Throws<InvalidDataException>(
|
||||
() => TypeDefGenerator.GetGeneratedFileName("folder/type"));
|
||||
Assert.Throws<InvalidDataException>(
|
||||
() => TypeDefGenerator.GetGeneratedFileName(" "));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cleanup_DeletesOnlyManifestedGeneratedFiles()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), $"esiur-generator-{Guid.NewGuid():N}");
|
||||
var output = Path.Combine(root, "output");
|
||||
Directory.CreateDirectory(output);
|
||||
|
||||
try
|
||||
{
|
||||
var generated = Path.Combine(output, "Old.g.cs");
|
||||
var unlistedGenerated = Path.Combine(output, "Keep.g.cs");
|
||||
var userFile = Path.Combine(output, "notes.txt");
|
||||
var outsideFile = Path.Combine(root, "outside.g.cs");
|
||||
File.WriteAllText(generated, "generated");
|
||||
File.WriteAllText(unlistedGenerated, "unlisted");
|
||||
File.WriteAllText(userFile, "user");
|
||||
File.WriteAllText(outsideFile, "outside");
|
||||
File.WriteAllLines(
|
||||
Path.Combine(output, ".esiur-generated-files"),
|
||||
new[] { "Old.g.cs", "../outside.g.cs", "notes.txt" });
|
||||
|
||||
TypeDefGenerator.DeletePreviouslyGeneratedFiles(new DirectoryInfo(output));
|
||||
|
||||
Assert.False(File.Exists(generated));
|
||||
Assert.True(File.Exists(unlistedGenerated));
|
||||
Assert.True(File.Exists(userFile));
|
||||
Assert.True(File.Exists(outsideFile));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using Esiur.Data;
|
||||
using Esiur.Data.Types;
|
||||
using Esiur.Security.Authority;
|
||||
using Esiur.Security.Permissions;
|
||||
|
||||
namespace Esiur.Tests.Unit;
|
||||
|
||||
public class UserPermissionsManagerTests
|
||||
{
|
||||
[Fact]
|
||||
public void ExplicitMemberGrant_AllowsReadAndWriteActions()
|
||||
{
|
||||
var memberPermissions = new Map<string, object>
|
||||
{
|
||||
[ActionType.GetProperty.ToString()] = "yes",
|
||||
[ActionType.SetProperty.ToString()] = "yes"
|
||||
};
|
||||
var manager = CreateManager(new Map<string, object>
|
||||
{
|
||||
["Value"] = memberPermissions
|
||||
});
|
||||
var session = new Session { RemoteIdentity = "alice" };
|
||||
var member = new MemberDef { Name = "Value" };
|
||||
|
||||
Assert.Equal(
|
||||
Ruling.Allowed,
|
||||
manager.Applicable(null!, session, ActionType.GetProperty, member, null!));
|
||||
Assert.Equal(
|
||||
Ruling.Allowed,
|
||||
manager.Applicable(null!, session, ActionType.SetProperty, member, null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MissingMemberOrAction_DeniesInsteadOfFallingThrough()
|
||||
{
|
||||
var manager = CreateManager(new Map<string, object>
|
||||
{
|
||||
["Value"] = new Map<string, object>
|
||||
{
|
||||
[ActionType.GetProperty.ToString()] = "yes"
|
||||
}
|
||||
});
|
||||
var session = new Session { RemoteIdentity = "alice" };
|
||||
|
||||
Assert.Equal(
|
||||
Ruling.Denied,
|
||||
manager.Applicable(
|
||||
null!,
|
||||
session,
|
||||
ActionType.Execute,
|
||||
new MemberDef { Name = "Missing" },
|
||||
null!));
|
||||
Assert.Equal(
|
||||
Ruling.Denied,
|
||||
manager.Applicable(
|
||||
null!,
|
||||
session,
|
||||
ActionType.SetProperty,
|
||||
new MemberDef { Name = "Value" },
|
||||
null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExplicitResourceGrant_AllowsAttributeInquiry()
|
||||
{
|
||||
var manager = CreateManager(new Map<string, object>
|
||||
{
|
||||
["_get_attributes"] = "yes"
|
||||
});
|
||||
|
||||
Assert.Equal(
|
||||
Ruling.Allowed,
|
||||
manager.Applicable(
|
||||
null!,
|
||||
new Session { RemoteIdentity = "alice" },
|
||||
ActionType.InquireAttributes,
|
||||
null!,
|
||||
null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MissingIdentityAndMalformedPermissionMaps_FailClosed()
|
||||
{
|
||||
var manager = new UserPermissionsManager(new Map<string, object>
|
||||
{
|
||||
["alice"] = "not a permission map"
|
||||
});
|
||||
|
||||
Assert.Equal(
|
||||
Ruling.Denied,
|
||||
manager.Applicable(
|
||||
null!,
|
||||
new Session { RemoteIdentity = "bob" },
|
||||
ActionType.Attach,
|
||||
null!,
|
||||
null!));
|
||||
Assert.Equal(
|
||||
Ruling.Denied,
|
||||
manager.Applicable(
|
||||
null!,
|
||||
new Session { RemoteIdentity = "alice" },
|
||||
ActionType.Attach,
|
||||
null!,
|
||||
null!));
|
||||
}
|
||||
|
||||
private static UserPermissionsManager CreateManager(Map<string, object> permissions)
|
||||
=> new(new Map<string, object> { ["alice"] = permissions });
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "10.0.100",
|
||||
"rollForward": "latestFeature",
|
||||
"allowPrerelease": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user