about summary refs log tree commit diff
path: root/Utilities/LibMatrix.FederationTest
diff options
context:
space:
mode:
Diffstat (limited to 'Utilities/LibMatrix.FederationTest')
-rw-r--r--Utilities/LibMatrix.FederationTest/.gitignore1
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/RemoteServerPingController.cs79
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/Spec/FederationKeysController.cs42
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/Spec/FederationVersionController.cs19
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/Spec/WellKnownController.cs19
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/TestController.cs52
-rw-r--r--Utilities/LibMatrix.FederationTest/LibMatrix.FederationTest.csproj18
-rw-r--r--Utilities/LibMatrix.FederationTest/Program.cs40
-rw-r--r--Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs54
-rw-r--r--Utilities/LibMatrix.FederationTest/Services/FederationTestConfiguration.cs10
-rw-r--r--Utilities/LibMatrix.FederationTest/Utilities/Ed25519Utils.cs28
-rw-r--r--Utilities/LibMatrix.FederationTest/appsettings.Development.json13
-rw-r--r--Utilities/LibMatrix.FederationTest/appsettings.json9
13 files changed, 384 insertions, 0 deletions
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": "*" +}