XFEExtension.NetCore.ServerInteractive
[DLL] Server interaction extension, including user identity verification and querying in conjunction with AutoConfig
关注
0
Fork
0
Star
0
using System.Net;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using XFEExtension.NetCore.CyberComm;
using XFEExtension.NetCore.ServerInteractive.Models;
using Xunit;
using XFEExtension.NetCore.ServerInteractive.Models.UserModels;
using XFEExtension.NetCore.ServerInteractive.Utilities.Extensions;
using XFEExtension.NetCore.ServerInteractive.Utilities.Helpers;
using XFEExtension.NetCore.ServerInteractive.Utilities.Requester;
using XFEExtension.NetCore.ServerInteractive.Utilities.Server;
namespace XFEExtension.NetCore.ServerInteractive.Test;
public sealed class CyberCommIntegrationTests
{
[Fact]
public async Task HttpServer_ProcessesPostAndStopsCleanly()
{
var port = GetFreePort();
await using var server = CreateServer(port);
server.HttpRequestHandler = async (context, _) =>
{
await context.Response.WriteTextAsync(context.RequestBody, HttpStatusCode.OK, "application/json");
context.Response.Complete();
};
await server.StartAsync();
using var client = new HttpClient();
using var response = await client.PostAsync($"http://127.0.0.1:{port}/echo", new StringContent("{\"ok\":true}", Encoding.UTF8, "application/json"));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("{\"ok\":true}", await response.Content.ReadAsStringAsync());
await server.StopAsync();
Assert.Equal(CyberCommServerState.Stopped, server.State);
}
[Fact]
public async Task HttpServer_RejectsConflictingContentLength()
{
var port = GetFreePort();
await using var server = CreateServer(port);
await server.StartAsync();
using var client = new TcpClient();
await client.ConnectAsync(IPAddress.Loopback, port);
await using var stream = client.GetStream();
var request = $"POST / HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nContent-Length: 1\r\nContent-Length: 2\r\nConnection: close\r\n\r\nx";
await stream.WriteAsync(Encoding.ASCII.GetBytes(request));
var buffer = new byte[1024];
var count = await stream.ReadAsync(buffer);
Assert.StartsWith("HTTP/1.1 400", Encoding.ASCII.GetString(buffer, 0, count));
}
[Fact]
public async Task HttpServer_ReadsChunkedRequestBody()
{
var port = GetFreePort();
await using var server = CreateServer(port);
server.HttpRequestHandler = async (context, _) =>
{
await context.Response.WriteTextAsync(context.RequestBody);
context.Response.Complete();
};
await server.StartAsync();
using var client = new TcpClient();
await client.ConnectAsync(IPAddress.Loopback, port);
await using var stream = client.GetStream();
var request = $"POST /chunk HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n5\r\nhello\r\n0\r\n\r\n";
await stream.WriteAsync(Encoding.ASCII.GetBytes(request));
using var output = new MemoryStream();
var buffer = new byte[1024];
int count;
while ((count = await stream.ReadAsync(buffer)) > 0) await output.WriteAsync(buffer.AsMemory(0, count));
Assert.EndsWith("hello", Encoding.UTF8.GetString(output.ToArray()));
}
[Fact]
public async Task WebSocketClientAndServer_EchoAndDisconnectWithoutReconnect()
{
var port = GetFreePort();
await using var server = CreateServer(port);
server.WebSocketMessageHandler = async (args, _) =>
{
if (args.MessageType == BackMessageType.Text) await args.ReplyMessage(args.TextMessage!);
};
await server.StartAsync();
await using var client = new CyberCommClient(new CyberCommClientOptions
{
ServerUri = new Uri($"ws://127.0.0.1:{port}/ws"),
Reconnect = new() { Enabled = false }
});
var received = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
client.MessageHandler = (args, _) =>
{
if (args.MessageType == BackMessageType.Text) received.TrySetResult(args.TextMessage!);
return ValueTask.CompletedTask;
};
await client.ConnectAsync();
Assert.True((await client.SendTextAsync("echo")).IsSuccess);
Assert.Equal("echo", await received.Task.WaitAsync(TimeSpan.FromSeconds(5)));
await Task.WhenAll(Enumerable.Range(0, 8).Select(_ => client.DisconnectAsync()));
Assert.Equal(CyberCommClientState.Disconnected, client.State);
await Task.Delay(200);
Assert.False(client.IsConnected);
}
[Fact]
public async Task WebSocketClient_AbnormalDisconnectCreatesANewSocketForReconnect()
{
var port = GetFreePort();
await using var server = CreateServer(port);
server.WebSocketMessageHandler = (args, _) =>
{
if (args.TextMessage == "drop") args.CurrentWebSocket.Abort();
return ValueTask.CompletedTask;
};
await server.StartAsync();
await using var client = new CyberCommClient(new CyberCommClientOptions
{
ServerUri = new Uri($"ws://127.0.0.1:{port}/ws"),
Reconnect = new()
{
Enabled = true,
MaxAttempts = 10,
InitialDelay = TimeSpan.FromMilliseconds(20),
MaxDelay = TimeSpan.FromMilliseconds(100),
JitterRatio = 0
}
});
var connectionCount = 0;
var reconnected = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
client.ConnectedHandler = _ =>
{
if (Interlocked.Increment(ref connectionCount) == 2) reconnected.TrySetResult();
return ValueTask.CompletedTask;
};
await client.ConnectAsync();
var firstSocket = client.ClientWebSocket;
Assert.True((await client.SendTextAsync("drop")).IsSuccess);
await reconnected.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.NotSame(firstSocket, client.ClientWebSocket);
Assert.Equal(CyberCommClientState.Connected, client.State);
}
[Fact]
public async Task HttpsAndWss_UseTheSameBclTlsTransport()
{
var port = GetFreePort();
using var certificate = CreateCertificate();
await using var server = new CyberCommServer(new CyberCommServerOptions
{
ListenUrls = [$"https://127.0.0.1:{port}/"],
Tls = new() { Certificate = certificate }
});
server.HttpRequestHandler = async (context, _) =>
{
await context.Response.WriteTextAsync("secure");
context.Response.Complete();
};
server.ErrorHandler = Console.WriteLine;
server.WebSocketMessageHandler = async (args, _) => await args.ReplyMessage(args.TextMessage!);
await server.StartAsync();
using var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = static (_, _, _, _) => true };
using var httpClient = new HttpClient(handler);
Assert.Equal("secure", await httpClient.GetStringAsync($"https://127.0.0.1:{port}/health"));
await using var webSocketClient = new CyberCommClient(new CyberCommClientOptions
{
ServerUri = new Uri($"wss://127.0.0.1:{port}/ws"),
Reconnect = new() { Enabled = false },
RemoteCertificateValidationCallback = static (_, _, _, _) => true
});
var received = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
webSocketClient.MessageHandler = (args, _) =>
{
if (args.TextMessage is not null) received.TrySetResult(args.TextMessage);
return ValueTask.CompletedTask;
};
await webSocketClient.ConnectAsync();
Assert.True((await webSocketClient.SendTextAsync("wss")).IsSuccess);
Assert.Equal("wss", await received.Task.WaitAsync(TimeSpan.FromSeconds(5)));
}
[Fact]
public async Task TlsOptions_LoadExternalPfxAndPemFiles()
{
var testDirectory = Path.Combine(Path.GetTempPath(), $"cybercomm-tls-{Guid.NewGuid():N}");
Directory.CreateDirectory(testDirectory);
try
{
using var certificate = CreateCertificate();
const string password = "test-only-password";
var pfxPath = Path.Combine(testDirectory, "server.pfx");
var certificatePath = Path.Combine(testDirectory, "server.crt.pem");
var privateKeyPath = Path.Combine(testDirectory, "server.key.pem");
await File.WriteAllBytesAsync(pfxPath, certificate.Export(X509ContentType.Pkcs12, password));
await File.WriteAllTextAsync(certificatePath, certificate.ExportCertificatePem());
using (var privateKey = certificate.GetRSAPrivateKey())
await File.WriteAllTextAsync(privateKeyPath, privateKey!.ExportPkcs8PrivateKeyPem());
await AssertTlsConfigurationStartsAsync(new CyberCommTlsOptions
{
PfxPath = pfxPath,
PfxPassword = password
});
await AssertTlsConfigurationStartsAsync(new CyberCommTlsOptions
{
CertificatePemPath = certificatePath,
PrivateKeyPemPath = privateKeyPath
});
}
finally
{
Directory.Delete(testDirectory, true);
}
}
[Fact]
public async Task Lifecycle_ConcurrentCallsShareOneTransition()
{
var port = GetFreePort();
await using var server = CreateServer(port);
await Task.WhenAll(Enumerable.Range(0, 8).Select(_ => server.StartAsync()));
Assert.Equal(CyberCommServerState.Running, server.State);
await Task.WhenAll(Enumerable.Range(0, 8).Select(_ => server.StopAsync()));
Assert.Equal(CyberCommServerState.Stopped, server.State);
}
[Fact]
public async Task StopAsync_WaitsForActiveRequestButNotForKeepAliveIdleTimeout()
{
var port = GetFreePort();
await using var server = new CyberCommServer(new CyberCommServerOptions
{
ListenUrls = [$"http://127.0.0.1:{port}/"],
Limits = new()
{
HandlerTimeout = TimeSpan.FromSeconds(5),
IdleTimeout = TimeSpan.FromSeconds(5),
CloseTimeout = TimeSpan.FromSeconds(2)
}
});
var started = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
server.HttpRequestHandler = async (context, cancellationToken) =>
{
started.TrySetResult();
await release.Task.WaitAsync(cancellationToken);
await context.Response.WriteTextAsync("finished");
context.Response.Complete();
};
await server.StartAsync();
using var client = new HttpClient();
var request = client.GetAsync($"http://127.0.0.1:{port}/work");
await started.Task.WaitAsync(TimeSpan.FromSeconds(2));
var stop = server.StopAsync();
await Task.Delay(50);
Assert.False(stop.IsCompleted);
release.TrySetResult();
using var response = await request;
Assert.Equal("finished", await response.Content.ReadAsStringAsync());
await stop.WaitAsync(TimeSpan.FromSeconds(1));
Assert.Equal(CyberCommServerState.Stopped, server.State);
}
[Fact]
public async Task ServerInteractive_SharesOneCyberCommListenerAcrossBasePaths()
{
var port = GetFreePort();
var listenUrl = $"http://0.0.0.0:{port}/";
var firstCore = XFEServerCoreBuilder.CreateBuilder()
.AddConnectService()
.Build(options =>
{
options.ServerCoreName = "first";
options.MainEntryPoint = "first";
options.AcceptGet = true;
options.BindIP(listenUrl);
});
var secondCore = XFEServerCoreBuilder.CreateBuilder()
.AddConnectService()
.Build(options =>
{
options.ServerCoreName = "second";
options.MainEntryPoint = "second";
options.AcceptGet = true;
options.BindIP(listenUrl);
});
await using var server = XFEServerBuilder.CreateBuilder()
.UseXFEServer()
.AddServerCore(firstCore)
.AddServerCore(secondCore)
.Build();
await server.StartAsync();
using var client = new HttpClient();
using var firstResponse = await client.GetAsync($"http://127.0.0.1:{port}/first/check_connect");
using var secondResponse = await client.GetAsync($"http://127.0.0.1:{port}/second/check_connect");
using var missingResponse = await client.GetAsync($"http://127.0.0.1:{port}/missing/check_connect");
Assert.Equal(HttpStatusCode.OK, firstResponse.StatusCode);
Assert.Equal(HttpStatusCode.OK, secondResponse.StatusCode);
Assert.Equal(HttpStatusCode.NotFound, missingResponse.StatusCode);
}
[Fact]
public async Task ServerInteractive_RoutesDirectIpDnsAndEdgeOneRequestsByOriginPort()
{
var originPort = GetFreePort();
var core = XFEServerCoreBuilder.CreateBuilder()
.AddConnectService()
.Build(options =>
{
options.ServerCoreName = "edge-one-origin";
options.MainEntryPoint = "api";
options.AcceptGet = true;
options.BindIP($"http://*:{originPort}/");
});
await using var server = XFEServerBuilder.CreateBuilder()
.AddXFEExceptionProcess()
.AddXFECoreProcessor()
.AddServerCore(core)
.Build();
await server.StartAsync();
var directIpResponse = await SendRawAsync(originPort,
"GET /api/check_connect HTTP/1.1\r\n" +
$"Host: 127.0.0.1:{originPort}\r\n" +
"Connection: close\r\n\r\n");
var directDnsResponse = await SendRawAsync(originPort,
"GET /api/check_connect HTTP/1.1\r\n" +
$"Host: toolbox.api.xfe.studio:{originPort}\r\n" +
"Connection: close\r\n\r\n");
var edgeOneResponse = await SendRawAsync(originPort,
"GET /api/check_connect HTTP/1.1\r\n" +
"Host: toolbox.api.xfe.studio\r\n" +
"X-Forwarded-Proto: http\r\n" +
"Connection: close\r\n\r\n");
Assert.StartsWith("HTTP/1.1 200", directIpResponse);
Assert.StartsWith("HTTP/1.1 200", directDnsResponse);
Assert.StartsWith("HTTP/1.1 200", edgeOneResponse);
}
[Fact]
public async Task TableRequester_AcceptsCreatedAndUsesPageSize()
{
var port = GetFreePort();
await using var server = CreateServer(port);
var getRequestBody = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
server.HttpRequestHandler = async (context, _) =>
{
if (context.RequestUri.AbsolutePath.EndsWith("/add/userFaceInfo", StringComparison.Ordinal))
await context.Response.WriteTextAsync("{\"id\":\"1\",\"version\":1}", HttpStatusCode.Created, "application/json");
else
{
getRequestBody.TrySetResult(context.RequestBody);
await context.Response.WriteTextAsync("{\"totalCount\":0,\"lastPage\":0,\"dataList\":[]}", HttpStatusCode.OK, "application/json");
}
context.Response.Complete();
};
await server.StartAsync();
var requester = new TableRequester { RequestAddress = $"http://127.0.0.1:{port}/api/" };
Assert.True(await requester.Add(new UserFaceInfo { Id = "1", Version = 1 }));
_ = await requester.Get<UserFaceInfo>(2, 1);
var body = await getRequestBody.Task.WaitAsync(TimeSpan.FromSeconds(2));
Assert.Contains("\"pageSize\":2", body, StringComparison.Ordinal);
Assert.DoesNotContain("pageCount", body, StringComparison.Ordinal);
}
[Fact]
public async Task Client_InitialFailureWithoutReconnectBecomesFaulted()
{
var port = GetFreePort();
await using var client = new CyberCommClient(new CyberCommClientOptions
{
ServerUri = new Uri($"ws://127.0.0.1:{port}/"),
Reconnect = new() { Enabled = false }
});
await Assert.ThrowsAsync<XFEExtension.NetCore.Exceptions.XFECyberCommException>(() => client.ConnectAsync());
Assert.Equal(CyberCommClientState.Faulted, client.State);
}
[Fact]
public async Task HttpServer_RejectsUnsupportedMethodWith405()
{
var port = GetFreePort();
await using var server = CreateServer(port);
await server.StartAsync();
var response = await SendRawAsync(port, $"TRACE / HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nConnection: close\r\n\r\n");
Assert.StartsWith("HTTP/1.1 405", response);
Assert.Contains("Allow:", response);
}
[Fact]
public async Task HttpServer_HandlerWithoutResponseGetsDeterministic404()
{
var port = GetFreePort();
await using var server = CreateServer(port);
server.HttpRequestHandler = static (_, _) => ValueTask.CompletedTask;
await server.StartAsync();
using var client = new HttpClient();
using var response = await client.GetAsync($"http://127.0.0.1:{port}/missing");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task HttpServer_RejectsOversizedBodyBeforeReadingIt()
{
var port = GetFreePort();
await using var server = new CyberCommServer(new CyberCommServerOptions
{
ListenUrls = [$"http://127.0.0.1:{port}/"],
Limits = new() { MaxHttpBodyBytes = 10, MaxHttpChunkBytes = 10 }
});
await server.StartAsync();
var response = await SendRawAsync(port,
$"POST / HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nContent-Length: 11\r\nConnection: close\r\n\r\n");
Assert.StartsWith("HTTP/1.1 413", response);
}
[Fact]
public async Task Server_InvalidOptionsBecomeFaultedInsteadOfStayingStarting()
{
await using var server = new CyberCommServer(new CyberCommServerOptions());
await Assert.ThrowsAsync<InvalidOperationException>(() => server.StartAsync());
Assert.Equal(CyberCommServerState.Faulted, server.State);
}
[Fact]
public async Task WebSocket_UnassembledModeDoesNotDropFirstFragment()
{
var port = GetFreePort();
await using var server = new CyberCommServer(new CyberCommServerOptions
{
ListenUrls = [$"http://127.0.0.1:{port}/"],
AssembleWebSocketMessages = false
});
var fragments = new List<string>();
var completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
server.WebSocketMessageHandler = (args, _) =>
{
fragments.Add(args.TextMessage ?? string.Empty);
if (args.EndOfMessage) completed.TrySetResult();
return ValueTask.CompletedTask;
};
await server.StartAsync();
using var client = new ClientWebSocket();
await client.ConnectAsync(new Uri($"ws://127.0.0.1:{port}/ws"), CancellationToken.None);
await client.SendAsync("first-"u8.ToArray(), WebSocketMessageType.Text, false, CancellationToken.None);
await client.SendAsync("second"u8.ToArray(), WebSocketMessageType.Text, true, CancellationToken.None);
await completed.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.Equal(new[] { "first-", "second" }, fragments);
}
[Theory]
[InlineData("v1/*", "v1/test", true)]
[InlineData("v1/*", "v1/test/more", false)]
[InlineData("v1/*", "v1", false)]
[InlineData("v1/**", "v1", true)]
[InlineData("v1/**", "v1/test/more", true)]
public void WildcardRoute_HasBoundedSemantics(string pattern, string route, bool expected)
=> Assert.Equal(expected, RouteMatchHelper.MatchWildcardRoute(pattern, route));
[Fact]
public void PasswordCredential_VerifiesMigratesAndPublicDtoOmitsSecrets()
{
var credential = PasswordHasher.Hash("correct horse battery staple", 100_000);
Assert.True(PasswordHasher.Verify("correct horse battery staple", credential));
Assert.False(PasswordHasher.Verify("wrong", credential));
Assert.False(PasswordHasher.Verify("anything", new PasswordCredential
{
Iterations = 600_000,
Salt = Convert.ToBase64String(new byte[16]),
Hash = string.Empty
}));
var user = new User { UserName = "demo", Password = "legacy" };
Assert.Equal(UserOperateResult.Success, UserHelper.GetUser("demo", "legacy", [user], out _, out var upgraded));
Assert.True(upgraded);
Assert.Empty(user.Password);
Assert.NotNull(user.PasswordCredential);
var json = JsonSerializer.Serialize(UserPublicDto.From(user));
Assert.DoesNotContain("legacy", json, StringComparison.Ordinal);
Assert.DoesNotContain("PasswordCredential", json, StringComparison.Ordinal);
}
private static CyberCommServer CreateServer(int port) => new(new CyberCommServerOptions
{
ListenUrls = [$"http://127.0.0.1:{port}/"],
Limits = new()
{
HandlerTimeout = TimeSpan.FromSeconds(5),
ReceiveTimeout = TimeSpan.FromSeconds(5),
IdleTimeout = TimeSpan.FromSeconds(5)
}
});
private static int GetFreePort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
private static async Task<string> SendRawAsync(int port, string request)
{
using var client = new TcpClient();
await client.ConnectAsync(IPAddress.Loopback, port);
await using var stream = client.GetStream();
await stream.WriteAsync(Encoding.ASCII.GetBytes(request));
using var output = new MemoryStream();
var buffer = new byte[1024];
int count;
while ((count = await stream.ReadAsync(buffer)) > 0)
await output.WriteAsync(buffer.AsMemory(0, count));
return Encoding.ASCII.GetString(output.ToArray());
}
private static async Task AssertTlsConfigurationStartsAsync(CyberCommTlsOptions tlsOptions)
{
var port = GetFreePort();
await using var server = new CyberCommServer(new CyberCommServerOptions
{
ListenUrls = [$"https://127.0.0.1:{port}/"],
Tls = tlsOptions
});
server.HttpRequestHandler = async (context, _) =>
{
await context.Response.WriteTextAsync("secure");
context.Response.Complete();
};
await server.StartAsync();
using var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = static (_, _, _, _) => true };
using var client = new HttpClient(handler);
Assert.Equal("secure", await client.GetStringAsync($"https://127.0.0.1:{port}/"));
}
private static X509Certificate2 CreateCertificate()
{
using var rsa = RSA.Create(2048);
var request = new CertificateRequest("CN=localhost", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, false));
request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new("1.3.6.1.5.5.7.3.1") }, false));
var names = new SubjectAlternativeNameBuilder();
names.AddDnsName("localhost");
names.AddIpAddress(IPAddress.Loopback);
request.CertificateExtensions.Add(names.Build());
using var ephemeral = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-1), DateTimeOffset.UtcNow.AddDays(1));
return X509CertificateLoader.LoadPkcs12(ephemeral.Export(X509ContentType.Pkcs12), null,
X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet);
}
}
using System.Net;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using XFEExtension.NetCore.CyberComm;
using XFEExtension.NetCore.ServerInteractive.Models;
using Xunit;
using XFEExtension.NetCore.ServerInteractive.Models.UserModels;
using XFEExtension.NetCore.ServerInteractive.Utilities.Extensions;
using XFEExtension.NetCore.ServerInteractive.Utilities.Helpers;
using XFEExtension.NetCore.ServerInteractive.Utilities.Requester;
using XFEExtension.NetCore.ServerInteractive.Utilities.Server;
namespace XFEExtension.NetCore.ServerInteractive.Test;
public sealed class CyberCommIntegrationTests
{
[Fact]
public async Task HttpServer_ProcessesPostAndStopsCleanly()
{
var port = GetFreePort();
await using var server = CreateServer(port);
server.HttpRequestHandler = async (context, _) =>
{
await context.Response.WriteTextAsync(context.RequestBody, HttpStatusCode.OK, "application/json");
context.Response.Complete();
};
await server.StartAsync();
using var client = new HttpClient();
using var response = await client.PostAsync($"http://127.0.0.1:{port}/echo", new StringContent("{\"ok\":true}", Encoding.UTF8, "application/json"));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("{\"ok\":true}", await response.Content.ReadAsStringAsync());
await server.StopAsync();
Assert.Equal(CyberCommServerState.Stopped, server.State);
}
[Fact]
public async Task HttpServer_RejectsConflictingContentLength()
{
var port = GetFreePort();
await using var server = CreateServer(port);
await server.StartAsync();
using var client = new TcpClient();
await client.ConnectAsync(IPAddress.Loopback, port);
await using var stream = client.GetStream();
var request = $"POST / HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nContent-Length: 1\r\nContent-Length: 2\r\nConnection: close\r\n\r\nx";
await stream.WriteAsync(Encoding.ASCII.GetBytes(request));
var buffer = new byte[1024];
var count = await stream.ReadAsync(buffer);
Assert.StartsWith("HTTP/1.1 400", Encoding.ASCII.GetString(buffer, 0, count));
}
[Fact]
public async Task HttpServer_ReadsChunkedRequestBody()
{
var port = GetFreePort();
await using var server = CreateServer(port);
server.HttpRequestHandler = async (context, _) =>
{
await context.Response.WriteTextAsync(context.RequestBody);
context.Response.Complete();
};
await server.StartAsync();
using var client = new TcpClient();
await client.ConnectAsync(IPAddress.Loopback, port);
await using var stream = client.GetStream();
var request = $"POST /chunk HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n5\r\nhello\r\n0\r\n\r\n";
await stream.WriteAsync(Encoding.ASCII.GetBytes(request));
using var output = new MemoryStream();
var buffer = new byte[1024];
int count;
while ((count = await stream.ReadAsync(buffer)) > 0) await output.WriteAsync(buffer.AsMemory(0, count));
Assert.EndsWith("hello", Encoding.UTF8.GetString(output.ToArray()));
}
[Fact]
public async Task WebSocketClientAndServer_EchoAndDisconnectWithoutReconnect()
{
var port = GetFreePort();
await using var server = CreateServer(port);
server.WebSocketMessageHandler = async (args, _) =>
{
if (args.MessageType == BackMessageType.Text) await args.ReplyMessage(args.TextMessage!);
};
await server.StartAsync();
await using var client = new CyberCommClient(new CyberCommClientOptions
{
ServerUri = new Uri($"ws://127.0.0.1:{port}/ws"),
Reconnect = new() { Enabled = false }
});
var received = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
client.MessageHandler = (args, _) =>
{
if (args.MessageType == BackMessageType.Text) received.TrySetResult(args.TextMessage!);
return ValueTask.CompletedTask;
};
await client.ConnectAsync();
Assert.True((await client.SendTextAsync("echo")).IsSuccess);
Assert.Equal("echo", await received.Task.WaitAsync(TimeSpan.FromSeconds(5)));
await Task.WhenAll(Enumerable.Range(0, 8).Select(_ => client.DisconnectAsync()));
Assert.Equal(CyberCommClientState.Disconnected, client.State);
await Task.Delay(200);
Assert.False(client.IsConnected);
}
[Fact]
public async Task WebSocketClient_AbnormalDisconnectCreatesANewSocketForReconnect()
{
var port = GetFreePort();
await using var server = CreateServer(port);
server.WebSocketMessageHandler = (args, _) =>
{
if (args.TextMessage == "drop") args.CurrentWebSocket.Abort();
return ValueTask.CompletedTask;
};
await server.StartAsync();
await using var client = new CyberCommClient(new CyberCommClientOptions
{
ServerUri = new Uri($"ws://127.0.0.1:{port}/ws"),
Reconnect = new()
{
Enabled = true,
MaxAttempts = 10,
InitialDelay = TimeSpan.FromMilliseconds(20),
MaxDelay = TimeSpan.FromMilliseconds(100),
JitterRatio = 0
}
});
var connectionCount = 0;
var reconnected = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
client.ConnectedHandler = _ =>
{
if (Interlocked.Increment(ref connectionCount) == 2) reconnected.TrySetResult();
return ValueTask.CompletedTask;
};
await client.ConnectAsync();
var firstSocket = client.ClientWebSocket;
Assert.True((await client.SendTextAsync("drop")).IsSuccess);
await reconnected.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.NotSame(firstSocket, client.ClientWebSocket);
Assert.Equal(CyberCommClientState.Connected, client.State);
}
[Fact]
public async Task HttpsAndWss_UseTheSameBclTlsTransport()
{
var port = GetFreePort();
using var certificate = CreateCertificate();
await using var server = new CyberCommServer(new CyberCommServerOptions
{
ListenUrls = [$"https://127.0.0.1:{port}/"],
Tls = new() { Certificate = certificate }
});
server.HttpRequestHandler = async (context, _) =>
{
await context.Response.WriteTextAsync("secure");
context.Response.Complete();
};
server.ErrorHandler = Console.WriteLine;
server.WebSocketMessageHandler = async (args, _) => await args.ReplyMessage(args.TextMessage!);
await server.StartAsync();
using var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = static (_, _, _, _) => true };
using var httpClient = new HttpClient(handler);
Assert.Equal("secure", await httpClient.GetStringAsync($"https://127.0.0.1:{port}/health"));
await using var webSocketClient = new CyberCommClient(new CyberCommClientOptions
{
ServerUri = new Uri($"wss://127.0.0.1:{port}/ws"),
Reconnect = new() { Enabled = false },
RemoteCertificateValidationCallback = static (_, _, _, _) => true
});
var received = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
webSocketClient.MessageHandler = (args, _) =>
{
if (args.TextMessage is not null) received.TrySetResult(args.TextMessage);
return ValueTask.CompletedTask;
};
await webSocketClient.ConnectAsync();
Assert.True((await webSocketClient.SendTextAsync("wss")).IsSuccess);
Assert.Equal("wss", await received.Task.WaitAsync(TimeSpan.FromSeconds(5)));
}
[Fact]
public async Task TlsOptions_LoadExternalPfxAndPemFiles()
{
var testDirectory = Path.Combine(Path.GetTempPath(), $"cybercomm-tls-{Guid.NewGuid():N}");
Directory.CreateDirectory(testDirectory);
try
{
using var certificate = CreateCertificate();
const string password = "test-only-password";
var pfxPath = Path.Combine(testDirectory, "server.pfx");
var certificatePath = Path.Combine(testDirectory, "server.crt.pem");
var privateKeyPath = Path.Combine(testDirectory, "server.key.pem");
await File.WriteAllBytesAsync(pfxPath, certificate.Export(X509ContentType.Pkcs12, password));
await File.WriteAllTextAsync(certificatePath, certificate.ExportCertificatePem());
using (var privateKey = certificate.GetRSAPrivateKey())
await File.WriteAllTextAsync(privateKeyPath, privateKey!.ExportPkcs8PrivateKeyPem());
await AssertTlsConfigurationStartsAsync(new CyberCommTlsOptions
{
PfxPath = pfxPath,
PfxPassword = password
});
await AssertTlsConfigurationStartsAsync(new CyberCommTlsOptions
{
CertificatePemPath = certificatePath,
PrivateKeyPemPath = privateKeyPath
});
}
finally
{
Directory.Delete(testDirectory, true);
}
}
[Fact]
public async Task Lifecycle_ConcurrentCallsShareOneTransition()
{
var port = GetFreePort();
await using var server = CreateServer(port);
await Task.WhenAll(Enumerable.Range(0, 8).Select(_ => server.StartAsync()));
Assert.Equal(CyberCommServerState.Running, server.State);
await Task.WhenAll(Enumerable.Range(0, 8).Select(_ => server.StopAsync()));
Assert.Equal(CyberCommServerState.Stopped, server.State);
}
[Fact]
public async Task StopAsync_WaitsForActiveRequestButNotForKeepAliveIdleTimeout()
{
var port = GetFreePort();
await using var server = new CyberCommServer(new CyberCommServerOptions
{
ListenUrls = [$"http://127.0.0.1:{port}/"],
Limits = new()
{
HandlerTimeout = TimeSpan.FromSeconds(5),
IdleTimeout = TimeSpan.FromSeconds(5),
CloseTimeout = TimeSpan.FromSeconds(2)
}
});
var started = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
server.HttpRequestHandler = async (context, cancellationToken) =>
{
started.TrySetResult();
await release.Task.WaitAsync(cancellationToken);
await context.Response.WriteTextAsync("finished");
context.Response.Complete();
};
await server.StartAsync();
using var client = new HttpClient();
var request = client.GetAsync($"http://127.0.0.1:{port}/work");
await started.Task.WaitAsync(TimeSpan.FromSeconds(2));
var stop = server.StopAsync();
await Task.Delay(50);
Assert.False(stop.IsCompleted);
release.TrySetResult();
using var response = await request;
Assert.Equal("finished", await response.Content.ReadAsStringAsync());
await stop.WaitAsync(TimeSpan.FromSeconds(1));
Assert.Equal(CyberCommServerState.Stopped, server.State);
}
[Fact]
public async Task ServerInteractive_SharesOneCyberCommListenerAcrossBasePaths()
{
var port = GetFreePort();
var listenUrl = $"http://0.0.0.0:{port}/";
var firstCore = XFEServerCoreBuilder.CreateBuilder()
.AddConnectService()
.Build(options =>
{
options.ServerCoreName = "first";
options.MainEntryPoint = "first";
options.AcceptGet = true;
options.BindIP(listenUrl);
});
var secondCore = XFEServerCoreBuilder.CreateBuilder()
.AddConnectService()
.Build(options =>
{
options.ServerCoreName = "second";
options.MainEntryPoint = "second";
options.AcceptGet = true;
options.BindIP(listenUrl);
});
await using var server = XFEServerBuilder.CreateBuilder()
.UseXFEServer()
.AddServerCore(firstCore)
.AddServerCore(secondCore)
.Build();
await server.StartAsync();
using var client = new HttpClient();
using var firstResponse = await client.GetAsync($"http://127.0.0.1:{port}/first/check_connect");
using var secondResponse = await client.GetAsync($"http://127.0.0.1:{port}/second/check_connect");
using var missingResponse = await client.GetAsync($"http://127.0.0.1:{port}/missing/check_connect");
Assert.Equal(HttpStatusCode.OK, firstResponse.StatusCode);
Assert.Equal(HttpStatusCode.OK, secondResponse.StatusCode);
Assert.Equal(HttpStatusCode.NotFound, missingResponse.StatusCode);
}
[Fact]
public async Task ServerInteractive_RoutesDirectIpDnsAndEdgeOneRequestsByOriginPort()
{
var originPort = GetFreePort();
var core = XFEServerCoreBuilder.CreateBuilder()
.AddConnectService()
.Build(options =>
{
options.ServerCoreName = "edge-one-origin";
options.MainEntryPoint = "api";
options.AcceptGet = true;
options.BindIP($"http://*:{originPort}/");
});
await using var server = XFEServerBuilder.CreateBuilder()
.AddXFEExceptionProcess()
.AddXFECoreProcessor()
.AddServerCore(core)
.Build();
await server.StartAsync();
var directIpResponse = await SendRawAsync(originPort,
"GET /api/check_connect HTTP/1.1\r\n" +
$"Host: 127.0.0.1:{originPort}\r\n" +
"Connection: close\r\n\r\n");
var directDnsResponse = await SendRawAsync(originPort,
"GET /api/check_connect HTTP/1.1\r\n" +
$"Host: toolbox.api.xfe.studio:{originPort}\r\n" +
"Connection: close\r\n\r\n");
var edgeOneResponse = await SendRawAsync(originPort,
"GET /api/check_connect HTTP/1.1\r\n" +
"Host: toolbox.api.xfe.studio\r\n" +
"X-Forwarded-Proto: http\r\n" +
"Connection: close\r\n\r\n");
Assert.StartsWith("HTTP/1.1 200", directIpResponse);
Assert.StartsWith("HTTP/1.1 200", directDnsResponse);
Assert.StartsWith("HTTP/1.1 200", edgeOneResponse);
}
[Fact]
public async Task TableRequester_AcceptsCreatedAndUsesPageSize()
{
var port = GetFreePort();
await using var server = CreateServer(port);
var getRequestBody = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
server.HttpRequestHandler = async (context, _) =>
{
if (context.RequestUri.AbsolutePath.EndsWith("/add/userFaceInfo", StringComparison.Ordinal))
await context.Response.WriteTextAsync("{\"id\":\"1\",\"version\":1}", HttpStatusCode.Created, "application/json");
else
{
getRequestBody.TrySetResult(context.RequestBody);
await context.Response.WriteTextAsync("{\"totalCount\":0,\"lastPage\":0,\"dataList\":[]}", HttpStatusCode.OK, "application/json");
}
context.Response.Complete();
};
await server.StartAsync();
var requester = new TableRequester { RequestAddress = $"http://127.0.0.1:{port}/api/" };
Assert.True(await requester.Add(new UserFaceInfo { Id = "1", Version = 1 }));
_ = await requester.Get<UserFaceInfo>(2, 1);
var body = await getRequestBody.Task.WaitAsync(TimeSpan.FromSeconds(2));
Assert.Contains("\"pageSize\":2", body, StringComparison.Ordinal);
Assert.DoesNotContain("pageCount", body, StringComparison.Ordinal);
}
[Fact]
public async Task Client_InitialFailureWithoutReconnectBecomesFaulted()
{
var port = GetFreePort();
await using var client = new CyberCommClient(new CyberCommClientOptions
{
ServerUri = new Uri($"ws://127.0.0.1:{port}/"),
Reconnect = new() { Enabled = false }
});
await Assert.ThrowsAsync<XFEExtension.NetCore.Exceptions.XFECyberCommException>(() => client.ConnectAsync());
Assert.Equal(CyberCommClientState.Faulted, client.State);
}
[Fact]
public async Task HttpServer_RejectsUnsupportedMethodWith405()
{
var port = GetFreePort();
await using var server = CreateServer(port);
await server.StartAsync();
var response = await SendRawAsync(port, $"TRACE / HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nConnection: close\r\n\r\n");
Assert.StartsWith("HTTP/1.1 405", response);
Assert.Contains("Allow:", response);
}
[Fact]
public async Task HttpServer_HandlerWithoutResponseGetsDeterministic404()
{
var port = GetFreePort();
await using var server = CreateServer(port);
server.HttpRequestHandler = static (_, _) => ValueTask.CompletedTask;
await server.StartAsync();
using var client = new HttpClient();
using var response = await client.GetAsync($"http://127.0.0.1:{port}/missing");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task HttpServer_RejectsOversizedBodyBeforeReadingIt()
{
var port = GetFreePort();
await using var server = new CyberCommServer(new CyberCommServerOptions
{
ListenUrls = [$"http://127.0.0.1:{port}/"],
Limits = new() { MaxHttpBodyBytes = 10, MaxHttpChunkBytes = 10 }
});
await server.StartAsync();
var response = await SendRawAsync(port,
$"POST / HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nContent-Length: 11\r\nConnection: close\r\n\r\n");
Assert.StartsWith("HTTP/1.1 413", response);
}
[Fact]
public async Task Server_InvalidOptionsBecomeFaultedInsteadOfStayingStarting()
{
await using var server = new CyberCommServer(new CyberCommServerOptions());
await Assert.ThrowsAsync<InvalidOperationException>(() => server.StartAsync());
Assert.Equal(CyberCommServerState.Faulted, server.State);
}
[Fact]
public async Task WebSocket_UnassembledModeDoesNotDropFirstFragment()
{
var port = GetFreePort();
await using var server = new CyberCommServer(new CyberCommServerOptions
{
ListenUrls = [$"http://127.0.0.1:{port}/"],
AssembleWebSocketMessages = false
});
var fragments = new List<string>();
var completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
server.WebSocketMessageHandler = (args, _) =>
{
fragments.Add(args.TextMessage ?? string.Empty);
if (args.EndOfMessage) completed.TrySetResult();
return ValueTask.CompletedTask;
};
await server.StartAsync();
using var client = new ClientWebSocket();
await client.ConnectAsync(new Uri($"ws://127.0.0.1:{port}/ws"), CancellationToken.None);
await client.SendAsync("first-"u8.ToArray(), WebSocketMessageType.Text, false, CancellationToken.None);
await client.SendAsync("second"u8.ToArray(), WebSocketMessageType.Text, true, CancellationToken.None);
await completed.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.Equal(new[] { "first-", "second" }, fragments);
}
[Theory]
[InlineData("v1/*", "v1/test", true)]
[InlineData("v1/*", "v1/test/more", false)]
[InlineData("v1/*", "v1", false)]
[InlineData("v1/**", "v1", true)]
[InlineData("v1/**", "v1/test/more", true)]
public void WildcardRoute_HasBoundedSemantics(string pattern, string route, bool expected)
=> Assert.Equal(expected, RouteMatchHelper.MatchWildcardRoute(pattern, route));
[Fact]
public void PasswordCredential_VerifiesMigratesAndPublicDtoOmitsSecrets()
{
var credential = PasswordHasher.Hash("correct horse battery staple", 100_000);
Assert.True(PasswordHasher.Verify("correct horse battery staple", credential));
Assert.False(PasswordHasher.Verify("wrong", credential));
Assert.False(PasswordHasher.Verify("anything", new PasswordCredential
{
Iterations = 600_000,
Salt = Convert.ToBase64String(new byte[16]),
Hash = string.Empty
}));
var user = new User { UserName = "demo", Password = "legacy" };
Assert.Equal(UserOperateResult.Success, UserHelper.GetUser("demo", "legacy", [user], out _, out var upgraded));
Assert.True(upgraded);
Assert.Empty(user.Password);
Assert.NotNull(user.PasswordCredential);
var json = JsonSerializer.Serialize(UserPublicDto.From(user));
Assert.DoesNotContain("legacy", json, StringComparison.Ordinal);
Assert.DoesNotContain("PasswordCredential", json, StringComparison.Ordinal);
}
private static CyberCommServer CreateServer(int port) => new(new CyberCommServerOptions
{
ListenUrls = [$"http://127.0.0.1:{port}/"],
Limits = new()
{
HandlerTimeout = TimeSpan.FromSeconds(5),
ReceiveTimeout = TimeSpan.FromSeconds(5),
IdleTimeout = TimeSpan.FromSeconds(5)
}
});
private static int GetFreePort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
private static async Task<string> SendRawAsync(int port, string request)
{
using var client = new TcpClient();
await client.ConnectAsync(IPAddress.Loopback, port);
await using var stream = client.GetStream();
await stream.WriteAsync(Encoding.ASCII.GetBytes(request));
using var output = new MemoryStream();
var buffer = new byte[1024];
int count;
while ((count = await stream.ReadAsync(buffer)) > 0)
await output.WriteAsync(buffer.AsMemory(0, count));
return Encoding.ASCII.GetString(output.ToArray());
}
private static async Task AssertTlsConfigurationStartsAsync(CyberCommTlsOptions tlsOptions)
{
var port = GetFreePort();
await using var server = new CyberCommServer(new CyberCommServerOptions
{
ListenUrls = [$"https://127.0.0.1:{port}/"],
Tls = tlsOptions
});
server.HttpRequestHandler = async (context, _) =>
{
await context.Response.WriteTextAsync("secure");
context.Response.Complete();
};
await server.StartAsync();
using var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = static (_, _, _, _) => true };
using var client = new HttpClient(handler);
Assert.Equal("secure", await client.GetStringAsync($"https://127.0.0.1:{port}/"));
}
private static X509Certificate2 CreateCertificate()
{
using var rsa = RSA.Create(2048);
var request = new CertificateRequest("CN=localhost", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, false));
request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new("1.3.6.1.5.5.7.3.1") }, false));
var names = new SubjectAlternativeNameBuilder();
names.AddDnsName("localhost");
names.AddIpAddress(IPAddress.Loopback);
request.CertificateExtensions.Add(names.Build());
using var ephemeral = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-1), DateTimeOffset.UtcNow.AddDays(1));
return X509CertificateLoader.LoadPkcs12(ephemeral.Export(X509ContentType.Pkcs12), null,
X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet);
}
}