diff --git a/Utilities/LibMatrix.FederationTest/.gitignore b/Utilities/LibMatrix.FederationTest/.gitignore
new file mode 100644
index 0000000..4a96773
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/.gitignore
@@ -0,0 +1 @@
+.keys/
diff --git a/Utilities/LibMatrix.FederationTest/Controllers/RemoteServerPingController.cs b/Utilities/LibMatrix.FederationTest/Controllers/RemoteServerPingController.cs
new file mode 100644
index 0000000..8d3a5ea
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Controllers/RemoteServerPingController.cs
@@ -0,0 +1,79 @@
+using LibMatrix.Federation;
+using LibMatrix.Federation.Extensions;
+using LibMatrix.FederationTest.Services;
+using LibMatrix.FederationTest.Utilities;
+using LibMatrix.Services;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.FederationTest.Controllers;
+
+[ApiController]
+public class RemoteServerPingController(FederationTestConfiguration config, FederationKeyStore keyStore, HomeserverResolverService hsResolver) : ControllerBase {
+ [HttpGet]
+ [Route("/ping/{serverName}")]
+ public async Task<object> PingRemoteServer(string serverName) {
+ Dictionary<string, object> responseMessage = [];
+ var hsResolveResult = await hsResolver.ResolveHomeserverFromWellKnown(serverName, enableClient: false);
+ responseMessage["resolveResult"] = hsResolveResult;
+
+ if (!string.IsNullOrWhiteSpace(hsResolveResult.Server)) {
+ try {
+ var ownKey = keyStore.GetCurrentSigningKey();
+ var hs = new AuthenticatedFederationClient(hsResolveResult.Server, new() {
+ PrivateKey = ,
+ OriginServerName = null
+ });
+ var keys = await hs.GetServerKeysAsync();
+ responseMessage["version"] = await hs.GetServerVersionAsync();
+ responseMessage["keys"] = keys;
+
+ responseMessage["keysAreValid"] = keys.SignaturesById[serverName].ToDictionary(
+ sig => (string)sig.Key,
+ sig => keys.ValidateSignature(serverName, sig.Key, Ed25519Utils.LoadPublicKeyFromEncoded(keys.TypedContent.VerifyKeysById[sig.Key].Key))
+ );
+ }
+ catch (Exception ex) {
+ responseMessage["error"] = new {
+ error = "Failed to connect to remote server",
+ message = ex.Message,
+ st = ex.StackTrace,
+ };
+ return responseMessage;
+ }
+ }
+
+ return responseMessage;
+ }
+
+ [HttpPost]
+ [Route("/ping/")]
+ public async IAsyncEnumerable<KeyValuePair<string, object>> PingRemoteServers([FromBody] List<string>? serverNames) {
+ Dictionary<string, object> responseMessage = [];
+
+ if (serverNames == null || !serverNames.Any()) {
+ responseMessage["error"] = "No server names provided";
+ yield return responseMessage.First();
+ yield break;
+ }
+
+ var results = serverNames!.Select(s => (s, PingRemoteServer(s))).ToList();
+ foreach (var result in results) {
+ var (serverName, pingResult) = result;
+ try {
+ responseMessage[serverName] = await pingResult;
+ if (results.Where(x => !x.Item2.IsCompleted).Select(x => x.s).ToList() is { } servers and not { Count: 0 })
+ Console.WriteLine($"INFO | Waiting for servers: {string.Join(", ", servers)}");
+ }
+ catch (Exception ex) {
+ responseMessage[serverName] = new {
+ error = "Failed to ping remote server",
+ message = ex.Message,
+ st = ex.StackTrace,
+ };
+ }
+
+ yield return new KeyValuePair<string, object>(serverName, responseMessage[serverName]);
+ // await Response.Body.FlushAsync();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Controllers/Spec/FederationKeysController.cs b/Utilities/LibMatrix.FederationTest/Controllers/Spec/FederationKeysController.cs
new file mode 100644
index 0000000..6516415
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Controllers/Spec/FederationKeysController.cs
@@ -0,0 +1,42 @@
+using LibMatrix.Abstractions;
+using LibMatrix.Federation.Extensions;
+using LibMatrix.FederationTest.Services;
+using LibMatrix.Homeservers;
+using LibMatrix.Responses.Federation;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.FederationTest.Controllers.Spec;
+
+[ApiController]
+[Route("_matrix/key/v2/")]
+public class FederationKeysController(FederationTestConfiguration config, FederationKeyStore keyStore) {
+ static FederationKeysController() {
+ Console.WriteLine("INFO | FederationKeysController initialized.");
+ }
+
+ private static SignedObject<ServerKeysResponse>? _cachedServerKeysResponse;
+ private static SemaphoreSlim _serverKeyCacheLock = new SemaphoreSlim(1, 1);
+
+ [HttpGet("server")]
+ public async Task<SignedObject<ServerKeysResponse>> GetServerKeys() {
+ await _serverKeyCacheLock.WaitAsync();
+ if (_cachedServerKeysResponse == null || _cachedServerKeysResponse.TypedContent.ValidUntil < DateTime.Now + TimeSpan.FromSeconds(30)) {
+ var keys = keyStore.GetCurrentSigningKey();
+ _cachedServerKeysResponse = new ServerKeysResponse() {
+ ValidUntil = DateTime.Now + TimeSpan.FromMinutes(1),
+ ServerName = config.ServerName,
+ OldVerifyKeys = [],
+ VerifyKeysById = new() {
+ {
+ new() { Algorithm = "ed25519", KeyId = "0" }, new ServerKeysResponse.CurrentVerifyKey() {
+ Key = keys.publicKey.ToUnpaddedBase64(),
+ }
+ }
+ }
+ }.Sign(config.ServerName, new VersionedKeyId() { Algorithm = "ed25519", KeyId = "0" }, keys.privateKey);
+ }
+ _serverKeyCacheLock.Release();
+
+ return _cachedServerKeysResponse;
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Controllers/Spec/FederationVersionController.cs b/Utilities/LibMatrix.FederationTest/Controllers/Spec/FederationVersionController.cs
new file mode 100644
index 0000000..d146cfd
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Controllers/Spec/FederationVersionController.cs
@@ -0,0 +1,19 @@
+using LibMatrix.Homeservers;
+using LibMatrix.Responses.Federation;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.FederationTest.Controllers.Spec;
+
+[ApiController]
+[Route("_matrix/federation/v1/")]
+public class FederationVersionController : ControllerBase {
+ [HttpGet("version")]
+ public ServerVersionResponse GetVersion() {
+ return new ServerVersionResponse {
+ Server = new() {
+ Name = "LibMatrix.Federation",
+ Version = "0.0.0",
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Controllers/Spec/WellKnownController.cs b/Utilities/LibMatrix.FederationTest/Controllers/Spec/WellKnownController.cs
new file mode 100644
index 0000000..b91868c
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Controllers/Spec/WellKnownController.cs
@@ -0,0 +1,19 @@
+using LibMatrix.Services.WellKnownResolver.WellKnownResolvers;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.FederationTest.Controllers.Spec;
+
+[ApiController]
+[Route(".well-known/")]
+public class WellKnownController(ILogger<WellKnownController> logger) : ControllerBase {
+ static WellKnownController() {
+ Console.WriteLine("INFO | WellKnownController initialized.");
+ }
+ [HttpGet("matrix/server")]
+ public ServerWellKnown GetMatrixServerWellKnown() {
+ // {Request.Headers["X-Forwarded-Proto"].FirstOrDefault(Request.Scheme)}://
+ return new() {
+ Homeserver = $"{Request.Headers["X-Forwarded-Host"].FirstOrDefault(Request.Host.Host)}:{Request.Headers["X-Forwarded-Port"].FirstOrDefault("443")}",
+ };
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Controllers/TestController.cs b/Utilities/LibMatrix.FederationTest/Controllers/TestController.cs
new file mode 100644
index 0000000..9c0981d
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Controllers/TestController.cs
@@ -0,0 +1,52 @@
+using System.Text.Json.Nodes;
+using LibMatrix.Abstractions;
+using LibMatrix.Extensions;
+using LibMatrix.Federation;
+using LibMatrix.Federation.Extensions;
+using LibMatrix.FederationTest.Services;
+using LibMatrix.Homeservers;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.FederationTest.Controllers;
+
+[ApiController]
+public class TestController(FederationTestConfiguration config, FederationKeyStore keyStore) : ControllerBase {
+ static TestController() {
+ Console.WriteLine("INFO | TestController initialized.");
+ }
+
+ [HttpGet("/test")]
+ public async Task<JsonObject> GetTest() {
+ var hc = new MatrixHttpClient() {
+ BaseAddress = new Uri("https://matrix.rory.gay")
+ };
+
+ var keyId = new VersionedKeyId() {
+ Algorithm = "ed25519",
+ KeyId = "0"
+ };
+
+ var signatureData = new XMatrixAuthorizationScheme.XMatrixRequestSignature() {
+ Method = "GET",
+ Uri = "/_matrix/federation/v1/user/devices/@emma:rory.gay",
+ OriginServerName = config.ServerName,
+ DestinationServerName = "rory.gay"
+ }
+ .Sign(config.ServerName, keyId, keyStore.GetCurrentSigningKey().privateKey);
+
+ var signature = signatureData.Signatures[config.ServerName][keyId];
+ var headerValue = new XMatrixAuthorizationScheme.XMatrixAuthorizationHeader() {
+ Origin = config.ServerName,
+ Destination = "rory.gay",
+ Key = keyId,
+ Signature = signature
+ }.ToHeaderValue();
+
+ var req = new HttpRequestMessage(HttpMethod.Get, "/_matrix/federation/v1/user/devices/@emma:rory.gay");
+ req.Headers.Add("Authorization", headerValue);
+
+ var response = await hc.SendAsync(req);
+ var content = await response.Content.ReadFromJsonAsync<JsonObject>();
+ return content!;
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/LibMatrix.FederationTest.csproj b/Utilities/LibMatrix.FederationTest/LibMatrix.FederationTest.csproj
new file mode 100644
index 0000000..954a437
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/LibMatrix.FederationTest.csproj
@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>net9.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.8" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\LibMatrix.Federation\LibMatrix.Federation.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Utilities/LibMatrix.FederationTest/Program.cs b/Utilities/LibMatrix.FederationTest/Program.cs
new file mode 100644
index 0000000..18d3421
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Program.cs
@@ -0,0 +1,40 @@
+using System.Text.Json.Serialization;
+using LibMatrix.FederationTest.Services;
+using LibMatrix.Services;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+
+builder.Services.AddControllers()
+ .AddJsonOptions(options => {
+ options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
+ options.JsonSerializerOptions.WriteIndented = true;
+ // options.JsonSerializerOptions.DefaultBufferSize = ;
+ }).AddMvcOptions(o => { o.SuppressOutputFormatterBuffering = true; });
+// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
+builder.Services.AddOpenApi();
+builder.Services.AddHttpLogging(options => {
+ options.LoggingFields = Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.All;
+ options.RequestHeaders.Add("X-Forwarded-Proto");
+ options.RequestHeaders.Add("X-Forwarded-Host");
+ options.RequestHeaders.Add("X-Forwarded-Port");
+});
+
+builder.Services.AddRoryLibMatrixServices();
+builder.Services.AddSingleton<FederationTestConfiguration>();
+builder.Services.AddSingleton<FederationKeyStore>();
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (true || app.Environment.IsDevelopment()) {
+ app.MapOpenApi();
+}
+
+// app.UseAuthorization();
+
+app.MapControllers();
+// app.UseHttpLogging();
+
+app.Run();
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs b/Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs
new file mode 100644
index 0000000..e916703
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs
@@ -0,0 +1,54 @@
+using System.Text.Json;
+using LibMatrix.Abstractions;
+using LibMatrix.FederationTest.Utilities;
+using Org.BouncyCastle.Crypto.Parameters;
+
+namespace LibMatrix.FederationTest.Services;
+
+public class FederationKeyStore(FederationTestConfiguration config) {
+ static FederationKeyStore() {
+ Console.WriteLine("INFO | FederationKeyStore initialized.");
+ }
+
+ private static (Ed25519PrivateKeyParameters privateKey, Ed25519PublicKeyParameters publicKey) currentKeyPair = default;
+
+ public class PrivateKeyCollection {
+
+ public required VersionedHomeserverPrivateKey CurrentSigningKey { get; set; }
+ }
+
+ public PrivateKeyCollection GetCurrentSigningKey() {
+ if(!Directory.Exists(config.KeyStorePath)) Directory.CreateDirectory(config.KeyStorePath);
+ var privateKeyPath = Path.Combine(config.KeyStorePath, "private-keys.json");
+
+ if (!File.Exists(privateKeyPath)) {
+ var keyPair = InternalGetSigningKey();
+ var privateKey = new VersionedHomeserverPrivateKey {
+ PrivateKey = keyPair.privateKey.GetEncoded().ToUnpaddedBase64(),
+ };
+ File.WriteAllText(privateKeyPath, privateKey.ToJson());
+ }
+
+ return JsonSerializer.Deserialize<PrivateKeyCollection>()
+ }
+
+ private (Ed25519PrivateKeyParameters privateKey, Ed25519PublicKeyParameters publicKey) InternalGetSigningKey() {
+ if (currentKeyPair != default) {
+ return currentKeyPair;
+ }
+
+ if(!Directory.Exists(config.KeyStorePath)) Directory.CreateDirectory(config.KeyStorePath);
+
+ var privateKeyPath = Path.Combine(config.KeyStorePath, "signing.key");
+ if (!File.Exists(privateKeyPath)) {
+ var keyPair = Ed25519Utils.GenerateKeyPair();
+ File.WriteAllBytes(privateKeyPath, keyPair.privateKey.GetEncoded());
+ return keyPair;
+ }
+
+ var privateKeyBytes = File.ReadAllBytes(privateKeyPath);
+ var privateKey = Ed25519Utils.LoadPrivateKeyFromEncoded(privateKeyBytes);
+ var publicKey = privateKey.GeneratePublicKey();
+ return currentKeyPair = (privateKey, publicKey);
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Services/FederationTestConfiguration.cs b/Utilities/LibMatrix.FederationTest/Services/FederationTestConfiguration.cs
new file mode 100644
index 0000000..353ddf5
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Services/FederationTestConfiguration.cs
@@ -0,0 +1,10 @@
+namespace LibMatrix.FederationTest.Services;
+
+public class FederationTestConfiguration {
+ public FederationTestConfiguration(IConfiguration configurationSection) {
+ configurationSection.GetRequiredSection("FederationTest").Bind(this);
+ }
+
+ public string ServerName { get; set; } = "localhost";
+ public string KeyStorePath { get; set; } = "./.keys";
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Utilities/Ed25519Utils.cs b/Utilities/LibMatrix.FederationTest/Utilities/Ed25519Utils.cs
new file mode 100644
index 0000000..7714fee
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Utilities/Ed25519Utils.cs
@@ -0,0 +1,28 @@
+using Org.BouncyCastle.Crypto.Generators;
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.Security;
+
+namespace LibMatrix.FederationTest.Utilities;
+
+public class Ed25519Utils {
+ public static (Ed25519PrivateKeyParameters privateKey, Ed25519PublicKeyParameters publicKey) GenerateKeyPair() {
+ Console.WriteLine("Generating new Ed25519 key pair!");
+ var keyPairGen = new Ed25519KeyPairGenerator();
+ keyPairGen.Init(new Ed25519KeyGenerationParameters(new SecureRandom()));
+ var keyPair = keyPairGen.GenerateKeyPair();
+
+ var privateKey = (Ed25519PrivateKeyParameters)keyPair.Private;
+ var publicKey = (Ed25519PublicKeyParameters)keyPair.Public;
+
+ return (privateKey, publicKey);
+ }
+
+ public static Ed25519PublicKeyParameters LoadPublicKeyFromEncoded(string key) {
+ var keyBytes = UnpaddedBase64.Decode(key);
+ return new Ed25519PublicKeyParameters(keyBytes, 0);
+ }
+
+ public static Ed25519PrivateKeyParameters LoadPrivateKeyFromEncoded(byte[] key) {
+ return new Ed25519PrivateKeyParameters(key, 0);
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/appsettings.Development.json b/Utilities/LibMatrix.FederationTest/appsettings.Development.json
new file mode 100644
index 0000000..b6c6151
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/appsettings.Development.json
@@ -0,0 +1,13 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Information",
+ "Microsoft.AspNetCore.Routing": "Warning"
+ }
+ },
+ "FederationTest": {
+ "ServerName": "libmatrix-fed-test.rory.gay",
+ "KeyStorePath": "./.keys"
+ }
+}
diff --git a/Utilities/LibMatrix.FederationTest/appsettings.json b/Utilities/LibMatrix.FederationTest/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
|