about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs28
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPolicyServerEventContent.cs3
-rw-r--r--LibMatrix.Federation/FederationTypes/FederationBackfillResponse.cs14
-rw-r--r--LibMatrix.Federation/FederationTypes/FederationEvent.cs30
-rw-r--r--LibMatrix.Federation/FederationTypes/FederationGetMissingEventsRequest.cs34
-rw-r--r--LibMatrix.Federation/FederationTypes/FederationTransaction.cs26
-rw-r--r--LibMatrix.Federation/FederationTypes/RoomInvite.cs14
-rw-r--r--LibMatrix.Federation/XMatrixAuthorizationScheme.cs29
-rw-r--r--LibMatrix/Helpers/RoomBuilder.cs16
-rw-r--r--LibMatrix/Helpers/RoomUpgradeBuilder.cs15
-rw-r--r--LibMatrix/Responses/Federation/SignedObject.cs3
-rw-r--r--LibMatrix/RoomTypes/GenericRoom.cs5
-rw-r--r--LibMatrix/Services/WellKnownResolver/WellKnownResolverService.cs23
-rw-r--r--LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ClientWellKnownResolver.cs9
-rw-r--r--LibMatrix/Services/WellKnownResolver/WellKnownResolvers/PolicyServerWellKnownResolver.cs28
-rw-r--r--LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ServerWellKnownResolver.cs7
-rw-r--r--LibMatrix/Services/WellKnownResolver/WellKnownResolvers/SupportWellKnownResolver.cs4
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/Spec/DirectoryController.cs51
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/Spec/MembershipsController.cs41
-rw-r--r--Utilities/LibMatrix.FederationTest/FedTest.http13
-rw-r--r--Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml19
-rw-r--r--Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml.cs9
-rw-r--r--Utilities/LibMatrix.FederationTest/Program.cs26
-rw-r--r--Utilities/LibMatrix.FederationTest/Services/ServerAuthService.cs58
24 files changed, 471 insertions, 34 deletions
diff --git a/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs b/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs

index d1cf8be..ccb5d42 100644 --- a/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs +++ b/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs
@@ -11,6 +11,9 @@ public class RoomMessageEventContent : TimelineEventContent { Body = body ?? ""; } + // TODO: https://spec.matrix.org/v1.16/client-server-api/#mimage + // TODO: add `file` for e2ee files + [JsonPropertyName("body")] public string Body { get; set; } @@ -53,7 +56,7 @@ public class RoomMessageEventContent : TimelineEventContent { public class MentionsStruct { [JsonPropertyName("user_ids")] public List<string>? Users { get; set; } - + [JsonPropertyName("room")] public bool? Room { get; set; } } @@ -68,10 +71,33 @@ public class RoomMessageEventContent : TimelineEventContent { [JsonPropertyName("thumbnail_url")] public string? ThumbnailUrl { get; set; } + [JsonPropertyName("thumbnail_info")] + public ThumbnailInfoStruct? ThumbnailInfo { get; set; } + [JsonPropertyName("w")] public int? Width { get; set; } [JsonPropertyName("h")] public int? Height { get; set; } + + /// <summary> + /// Duration of the audio/video in milliseconds, if applicable + /// </summary> + [JsonPropertyName("duration")] + public long? Duration { get; set; } + + public class ThumbnailInfoStruct { + [JsonPropertyName("w")] + public int? Width { get; set; } + + [JsonPropertyName("h")] + public int? Height { get; set; } + + [JsonPropertyName("mimetype")] + public string? MimeType { get; set; } + + [JsonPropertyName("size")] + public long? Size { get; set; } + } } } \ No newline at end of file diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPolicyServerEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPolicyServerEventContent.cs
index 80e254f..78fdc8e 100644 --- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPolicyServerEventContent.cs +++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPolicyServerEventContent.cs
@@ -8,4 +8,7 @@ public class RoomPolicyServerEventContent : EventContent { [JsonPropertyName("via")] public string? Via { get; set; } + + [JsonPropertyName("public_key")] + public string? PublicKey { get; set; } } \ No newline at end of file diff --git a/LibMatrix.Federation/FederationTypes/FederationBackfillResponse.cs b/LibMatrix.Federation/FederationTypes/FederationBackfillResponse.cs new file mode 100644
index 0000000..0fe72bd --- /dev/null +++ b/LibMatrix.Federation/FederationTypes/FederationBackfillResponse.cs
@@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Federation.FederationTypes; + +public class FederationBackfillResponse { + [JsonPropertyName("origin")] + public required string Origin { get; set; } + + [JsonPropertyName("origin_server_ts")] + public required long OriginServerTs { get; set; } + + [JsonPropertyName("pdus")] + public required List<SignedFederationEvent> Pdus { get; set; } +} \ No newline at end of file diff --git a/LibMatrix.Federation/FederationTypes/FederationEvent.cs b/LibMatrix.Federation/FederationTypes/FederationEvent.cs new file mode 100644
index 0000000..05bdcc9 --- /dev/null +++ b/LibMatrix.Federation/FederationTypes/FederationEvent.cs
@@ -0,0 +1,30 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Federation.FederationTypes; + +public class FederationEvent : MatrixEventResponse { + [JsonPropertyName("auth_events")] + public required List<string> AuthEvents { get; set; } = []; + + [JsonPropertyName("prev_events")] + public required List<string> PrevEvents { get; set; } = []; + + [JsonPropertyName("depth")] + public required int Depth { get; set; } +} + +public class SignedFederationEvent : FederationEvent { + [JsonPropertyName("signatures")] + public required Dictionary<string, Dictionary<string, string>> Signatures { get; set; } = new(); + + [JsonPropertyName("hashes")] + public required Dictionary<string, string> Hashes { get; set; } = new(); +} + +public class FederationEphemeralEvent { + [JsonPropertyName("edu_type")] + public required string Type { get; set; } + + [JsonPropertyName("content")] + public required Dictionary<string, object> Content { get; set; } = new(); +} \ No newline at end of file diff --git a/LibMatrix.Federation/FederationTypes/FederationGetMissingEventsRequest.cs b/LibMatrix.Federation/FederationTypes/FederationGetMissingEventsRequest.cs new file mode 100644
index 0000000..f43dd49 --- /dev/null +++ b/LibMatrix.Federation/FederationTypes/FederationGetMissingEventsRequest.cs
@@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Federation.FederationTypes; + +public class FederationGetMissingEventsRequest { + /// <summary> + /// Latest event IDs we already have (aka earliest to return) + /// </summary> + [JsonPropertyName("earliest_events")] + public required List<string> EarliestEvents { get; set; } + + /// <summary> + /// Events we want to get events before + /// </summary> + [JsonPropertyName("latest_events")] + public required List<string> LatestEvents { get; set; } + + /// <summary> + /// 10 by default + /// </summary> + [JsonPropertyName("limit")] + public int Limit { get; set; } + + /// <summary> + /// 0 by default + /// </summary> + [JsonPropertyName("min_depth")] + public long MinDepth { get; set; } +} + +public class FederationGetMissingEventsResponse { + [JsonPropertyName("events")] + public required List<SignedFederationEvent> Events { get; set; } +} \ No newline at end of file diff --git a/LibMatrix.Federation/FederationTypes/FederationTransaction.cs b/LibMatrix.Federation/FederationTypes/FederationTransaction.cs new file mode 100644
index 0000000..0581a08 --- /dev/null +++ b/LibMatrix.Federation/FederationTypes/FederationTransaction.cs
@@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Federation.FederationTypes; + +/// <summary> +/// This only covers v12 rooms for now? +/// </summary> +public class FederationTransaction { + /// <summary> + /// Up to 100 EDUs per transaction + /// </summary> + [JsonPropertyName("edus")] + public List<FederationEvent>? EphemeralEvents { get; set; } + + [JsonPropertyName("origin")] + public required string Origin { get; set; } + + [JsonPropertyName("origin_server_ts")] + public required long OriginServerTs { get; set; } + + /// <summary> + /// Up to 50 PDUs per transaction + /// </summary> + [JsonPropertyName("pdus")] + public List<SignedFederationEvent>? PersistentEvents { get; set; } +} \ No newline at end of file diff --git a/LibMatrix.Federation/FederationTypes/RoomInvite.cs b/LibMatrix.Federation/FederationTypes/RoomInvite.cs new file mode 100644
index 0000000..dc550f3 --- /dev/null +++ b/LibMatrix.Federation/FederationTypes/RoomInvite.cs
@@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Federation.FederationTypes; + +public class RoomInvite { + [JsonPropertyName("event")] + public required SignedFederationEvent Event { get; set; } + + [JsonPropertyName("invite_room_state")] + public required List<MatrixEventResponse> InviteRoomState { get; set; } = []; + + [JsonPropertyName("room_version")] + public required string RoomVersion { get; set; } +} \ No newline at end of file diff --git a/LibMatrix.Federation/XMatrixAuthorizationScheme.cs b/LibMatrix.Federation/XMatrixAuthorizationScheme.cs
index 392cd93..c6be906 100644 --- a/LibMatrix.Federation/XMatrixAuthorizationScheme.cs +++ b/LibMatrix.Federation/XMatrixAuthorizationScheme.cs
@@ -3,6 +3,7 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; using ArcaneLibs.Extensions; using LibMatrix.Abstractions; +using LibMatrix.Extensions; using LibMatrix.Responses.Federation; using Microsoft.Extensions.Primitives; @@ -37,17 +38,27 @@ public class XMatrixAuthorizationScheme { ErrorCode = MatrixException.ErrorCodes.M_UNAUTHORIZED }; - var headerValues = new StringValues(header.Parameter); - foreach (var value in headerValues) { - Console.WriteLine(headerValues.ToJson()); + var headerValues = new Dictionary<string, string>(); + var parts = header.Parameter.Split(','); + foreach (var part in parts) { + var kv = part.Split('=', 2); + if (kv.Length != 2) + continue; + var key = kv[0].Trim(); + var value = kv[1].Trim().Trim('"'); + headerValues[key] = value; } - return new() { - Destination = "", - Key = "", - Origin = "", - Signature = "" + Console.WriteLine("X-Matrix parts: " + headerValues.ToJson(unsafeContent: true)); + + var xma = new XMatrixAuthorizationHeader() { + Destination = headerValues["destination"], + Key = headerValues["key"], + Origin = headerValues["origin"], + Signature = headerValues["sig"] }; + Console.WriteLine("Parsed X-Matrix Auth Header: " + xma.ToJson()); + return xma; } public static XMatrixAuthorizationHeader FromSignedObject(SignedObject<XMatrixRequestSignature> signedObj, VersionedHomeserverPrivateKey currentKey) => @@ -74,7 +85,7 @@ public class XMatrixAuthorizationScheme { [JsonPropertyName("destination")] public required string DestinationServerName { get; set; } - [JsonPropertyName("content"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyName("content"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public JsonObject? Content { get; set; } } } \ No newline at end of file diff --git a/LibMatrix/Helpers/RoomBuilder.cs b/LibMatrix/Helpers/RoomBuilder.cs
index a292f33..1e33bb5 100644 --- a/LibMatrix/Helpers/RoomBuilder.cs +++ b/LibMatrix/Helpers/RoomBuilder.cs
@@ -2,7 +2,9 @@ using System.Diagnostics; using System.Runtime.Intrinsics.X86; using System.Text.RegularExpressions; using ArcaneLibs.Extensions; +using LibMatrix.EventTypes.Spec; using LibMatrix.EventTypes.Spec.State.RoomInfo; +using LibMatrix.EventTypes.Spec.State.Space; using LibMatrix.Homeservers; using LibMatrix.Responses; using LibMatrix.RoomTypes; @@ -85,6 +87,11 @@ public class RoomBuilder { { RoomTombstoneEventContent.EventId, 150 }, { RoomPolicyServerEventContent.EventId, 100 }, { RoomPinnedEventContent.EventId, 50 }, + { RoomTopicEventContent.EventId, 50 }, + { SpaceChildEventContent.EventId, 100 }, + { SpaceParentEventContent.EventId, 100 }, + { RoomMessageReactionEventContent.EventId, 0 }, + { RoomRedactionEventContent.EventId, 0 }, // recommended extensions { "im.vector.modular.widgets", 50 }, // { "m.reaction", 0 }, // we probably don't want these to end up as room state @@ -99,6 +106,7 @@ public class RoomBuilder { public List<string> AdditionalCreators { get; set; } = new(); public virtual async Task<GenericRoom> Create(AuthenticatedHomeserverGeneric homeserver) { + Console.WriteLine($"Creating room on {homeserver.ServerName}..."); var crq = new CreateRoomRequest { PowerLevelContentOverride = new() { EventsDefault = 1000000, @@ -154,8 +162,6 @@ public class RoomBuilder { var room = await homeserver.CreateRoom(crq); - Console.WriteLine("Press any key to continue..."); - Console.ReadKey(true); await SetBasicRoomInfoAsync(room); await SetStatesAsync(room, ImportantState); await SetAccessAsync(room); @@ -167,6 +173,7 @@ public class RoomBuilder { private async Task SendInvites(GenericRoom room) { if (Invites.Count == 0) return; + Console.WriteLine($"Sending {Invites.Count} invites for room {room.RoomId}"); if (SynapseAdminAutoAcceptLocalInvites && room.Homeserver is AuthenticatedHomeserverSynapse synapse) { var localJoinTasks = Invites.Where(u => UserId.Parse(u.Key).ServerName == synapse.ServerName).Select(async entry => { @@ -192,13 +199,14 @@ public class RoomBuilder { catch (MatrixException e) { Console.Error.WriteLine("Failed to invite {0} to {1}: {2}", kvp.Key, room.RoomId, e.Message); } - }); + }).ToList(); await Task.WhenAll(inviteTasks); } private async Task SetStatesAsync(GenericRoom room, List<MatrixEvent> state) { if (state.Count == 0) return; + Console.WriteLine($"Setting {state.Count} state events for {room.RoomId}..."); await room.BulkSendEventsAsync(state); // We chunk this up to try to avoid hitting reverse proxy timeouts // foreach (var group in state.Chunk(chunkSize)) { @@ -233,6 +241,7 @@ public class RoomBuilder { } private async Task SetBasicRoomInfoAsync(GenericRoom room) { + Console.WriteLine($"Setting basic room info for {room.RoomId}..."); if (!string.IsNullOrWhiteSpace(Name.Name)) await room.SendStateEventAsync(RoomNameEventContent.EventId, Name); @@ -255,6 +264,7 @@ public class RoomBuilder { } private async Task SetAccessAsync(GenericRoom room) { + Console.WriteLine($"Setting access settings for {room.RoomId}..."); if (!V12PlusRoomVersions.Contains(Version)) PowerLevels.Users![room.Homeserver.WhoAmI.UserId] = OwnPowerLevel; else { diff --git a/LibMatrix/Helpers/RoomUpgradeBuilder.cs b/LibMatrix/Helpers/RoomUpgradeBuilder.cs
index ced0ef3..ae71f8a 100644 --- a/LibMatrix/Helpers/RoomUpgradeBuilder.cs +++ b/LibMatrix/Helpers/RoomUpgradeBuilder.cs
@@ -15,7 +15,7 @@ namespace LibMatrix.Helpers; public class RoomUpgradeBuilder : RoomBuilder { public RoomUpgradeOptions UpgradeOptions { get; set; } = new(); public string OldRoomId { get; set; } = string.Empty; - public bool CanUpgrade { get; private set; } + public bool CanUpgrade { get; set; } public Dictionary<string, object> AdditionalTombstoneContent { get; set; } = new(); private List<Type> basePolicyTypes = []; @@ -27,7 +27,7 @@ public class RoomUpgradeBuilder : RoomBuilder { basePolicyTypes = ClassCollector<PolicyRuleEventContent>.ResolveFromAllAccessibleAssemblies().ToList(); Console.WriteLine($"Found {basePolicyTypes.Count} policy types in {sw.ElapsedMilliseconds}ms"); CanUpgrade = ( - (await OldRoom.GetPowerLevelsAsync())?.UserHasStatePermission(OldRoom.Homeserver.UserId, RoomTombstoneEventContent.EventId) + (await OldRoom.GetPowerLevelsAsync())?.UserHasStatePermission(OldRoom.Homeserver.UserId, RoomTombstoneEventContent.EventId, true) ?? (await OldRoom.GetRoomCreatorsAsync()).Contains(OldRoom.Homeserver.UserId) ) || (OldRoom.IsV12PlusRoomId && (await OldRoom.GetRoomCreatorsAsync()).Contains(OldRoom.Homeserver.UserId)); @@ -186,6 +186,16 @@ public class RoomUpgradeBuilder : RoomBuilder { await oldRoom.SendStateEventAsync(RoomTombstoneEventContent.EventId, tombstoneContent); } + var oldPls = await oldRoom.GetPowerLevelsAsync(); + if (oldPls?.UserHasStatePermission(oldRoom.Homeserver.UserId, RoomJoinRulesEventContent.EventId, true) ?? true) { + var oldJoinRules = await oldRoom.GetJoinRuleAsync(); + var restrictContent = new RoomJoinRulesEventContent { + JoinRule = RoomJoinRulesEventContent.JoinRules.Restricted, + Allow = (oldJoinRules?.Allow ?? []).Append(new() { RoomId = room.RoomId, Type = "m.room_membership" }).ToList() + }; + await oldRoom.SendStateEventAsync(RoomJoinRulesEventContent.EventId, restrictContent); + } + return room; } @@ -198,6 +208,7 @@ public class RoomUpgradeBuilder : RoomBuilder { public bool UpgradeUnstableValues { get; set; } public bool ForceUpgrade { get; set; } public bool NoopUpgrade { get; set; } + public bool RestrictOldRoom { get; set; } public Msc4321PolicyListUpgradeOptions Msc4321PolicyListUpgradeOptions { get; set; } = new(); [JsonIgnore] diff --git a/LibMatrix/Responses/Federation/SignedObject.cs b/LibMatrix/Responses/Federation/SignedObject.cs
index 3f6ffd6..517bb1f 100644 --- a/LibMatrix/Responses/Federation/SignedObject.cs +++ b/LibMatrix/Responses/Federation/SignedObject.cs
@@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -19,7 +20,7 @@ public class SignedObject<T> { } [JsonExtensionData] - public required JsonObject Content { get; set; } + public JsonObject Content { get; set; } = null!; [JsonIgnore] public T TypedContent { diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
index 6d9a499..550eb58 100644 --- a/LibMatrix/RoomTypes/GenericRoom.cs +++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -321,6 +321,9 @@ public class GenericRoom { public Task<RoomCreateEventContent?> GetCreateEventAsync() => GetStateAsync<RoomCreateEventContent>("m.room.create"); + public Task<RoomPolicyServerEventContent?> GetPolicyServerAsync() => + GetStateAsync<RoomPolicyServerEventContent>(RoomPolicyServerEventContent.EventId); + public async Task<string?> GetRoomType() { var res = await GetStateAsync<RoomCreateEventContent>("m.room.create"); return res.Type; @@ -393,7 +396,7 @@ public class GenericRoom { new UserIdAndReason { UserId = userId, Reason = reason }); public async Task InviteUserAsync(string userId, string? reason = null, bool skipExisting = true) { - if (skipExisting && await GetStateOrNullAsync<RoomMemberEventContent>("m.room.member", userId) is not { Membership: "leave" or "ban" or "join" }) + if (skipExisting && await GetStateOrNullAsync<RoomMemberEventContent>("m.room.member", userId) is { Membership: "ban" or "join" }) return; await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/invite", new UserIdAndReason(userId, reason)); } diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolverService.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolverService.cs
index c5e9d9c..8764096 100644 --- a/LibMatrix/Services/WellKnownResolver/WellKnownResolverService.cs +++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolverService.cs
@@ -14,15 +14,17 @@ public class WellKnownResolverService { private readonly ClientWellKnownResolver _clientWellKnownResolver; private readonly SupportWellKnownResolver _supportWellKnownResolver; private readonly ServerWellKnownResolver _serverWellKnownResolver; + private readonly PolicyServerWellKnownResolver _policyServerWellKnownResolver; private readonly WellKnownResolverConfiguration _configuration; public WellKnownResolverService(ILogger<WellKnownResolverService> logger, ClientWellKnownResolver clientWellKnownResolver, SupportWellKnownResolver supportWellKnownResolver, - WellKnownResolverConfiguration configuration, ServerWellKnownResolver serverWellKnownResolver) { + WellKnownResolverConfiguration configuration, ServerWellKnownResolver serverWellKnownResolver, PolicyServerWellKnownResolver policyServerWellKnownResolver) { _logger = logger; _clientWellKnownResolver = clientWellKnownResolver; _supportWellKnownResolver = supportWellKnownResolver; _configuration = configuration; _serverWellKnownResolver = serverWellKnownResolver; + _policyServerWellKnownResolver = policyServerWellKnownResolver; if (logger is NullLogger<WellKnownResolverService>) { var stackFrame = new StackTrace(true).GetFrame(1); Console.WriteLine( @@ -31,16 +33,26 @@ public class WellKnownResolverService { } public async Task<WellKnownRecords> TryResolveWellKnownRecords(string homeserver, bool includeClient = true, bool includeServer = true, bool includeSupport = true, - WellKnownResolverConfiguration? config = null) { + bool includePolicyServer = true, WellKnownResolverConfiguration? config = null) { WellKnownRecords records = new(); _logger.LogDebug($"Resolving well-knowns for {homeserver}"); - var clientTask = _clientWellKnownResolver.TryResolveWellKnown(homeserver, config ?? _configuration); - var serverTask = _serverWellKnownResolver.TryResolveWellKnown(homeserver, config ?? _configuration); - var supportTask = _supportWellKnownResolver.TryResolveWellKnown(homeserver, config ?? _configuration); + var clientTask = includeClient + ? _clientWellKnownResolver.TryResolveWellKnown(homeserver, config ?? _configuration) + : Task.FromResult<WellKnownResolutionResult<ClientWellKnown?>>(null!); + var serverTask = includeServer + ? _serverWellKnownResolver.TryResolveWellKnown(homeserver, config ?? _configuration) + : Task.FromResult<WellKnownResolutionResult<ServerWellKnown?>>(null!); + var supportTask = includeSupport + ? _supportWellKnownResolver.TryResolveWellKnown(homeserver, config ?? _configuration) + : Task.FromResult<WellKnownResolutionResult<SupportWellKnown?>>(null!); + var policyServerTask = includePolicyServer + ? _policyServerWellKnownResolver.TryResolveWellKnown(homeserver, config ?? _configuration) + : Task.FromResult<WellKnownResolutionResult<PolicyServerWellKnown?>>(null!); if (includeClient && await clientTask is { } clientResult) records.ClientWellKnown = clientResult; if (includeServer && await serverTask is { } serverResult) records.ServerWellKnown = serverResult; if (includeSupport && await supportTask is { } supportResult) records.SupportWellKnown = supportResult; + if (includePolicyServer && await policyServerTask is { } policyServerResult) records.PolicyServerWellKnown = policyServerResult; return records; } @@ -49,6 +61,7 @@ public class WellKnownResolverService { public WellKnownResolutionResult<ClientWellKnown?>? ClientWellKnown { get; set; } public WellKnownResolutionResult<ServerWellKnown?>? ServerWellKnown { get; set; } public WellKnownResolutionResult<SupportWellKnown?>? SupportWellKnown { get; set; } + public WellKnownResolutionResult<PolicyServerWellKnown?>? PolicyServerWellKnown { get; set; } } public class WellKnownResolutionResult<T> { diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ClientWellKnownResolver.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ClientWellKnownResolver.cs
index f52b217..678c077 100644 --- a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ClientWellKnownResolver.cs +++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ClientWellKnownResolver.cs
@@ -1,10 +1,10 @@ using System.Text.Json.Serialization; using ArcaneLibs.Collections; -using LibMatrix.Extensions; using Microsoft.Extensions.Logging; using WellKnownType = LibMatrix.Services.WellKnownResolver.WellKnownResolvers.ClientWellKnown; -using ResultType = - LibMatrix.Services.WellKnownResolver.WellKnownResolverService.WellKnownResolutionResult<LibMatrix.Services.WellKnownResolver.WellKnownResolvers.ClientWellKnown?>; +using ResultType = LibMatrix.Services.WellKnownResolver.WellKnownResolverService.WellKnownResolutionResult< + LibMatrix.Services.WellKnownResolver.WellKnownResolvers.ClientWellKnown? +>; namespace LibMatrix.Services.WellKnownResolver.WellKnownResolvers; @@ -14,7 +14,7 @@ public class ClientWellKnownResolver(ILogger<ClientWellKnownResolver> logger, We StoreNulls = false }; - public Task<WellKnownResolverService.WellKnownResolutionResult<ClientWellKnown>> TryResolveWellKnown(string homeserver, WellKnownResolverConfiguration? config = null) { + public Task<ResultType> TryResolveWellKnown(string homeserver, WellKnownResolverConfiguration? config = null) { config ??= configuration; return ClientWellKnownCache.TryGetOrAdd(homeserver, async () => { logger.LogTrace($"Resolving client well-known: {homeserver}"); @@ -23,7 +23,6 @@ public class ClientWellKnownResolver(ILogger<ClientWellKnownResolver> logger, We await TryGetWellKnownFromUrl($"https://{homeserver}/.well-known/matrix/client", WellKnownResolverService.WellKnownSource.Https); if (result.Content != null) return result; - return result; }); } diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/PolicyServerWellKnownResolver.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/PolicyServerWellKnownResolver.cs new file mode 100644
index 0000000..f7ffd62 --- /dev/null +++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/PolicyServerWellKnownResolver.cs
@@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; +using WellKnownType = LibMatrix.Services.WellKnownResolver.WellKnownResolvers.PolicyServerWellKnown; +using ResultType = LibMatrix.Services.WellKnownResolver.WellKnownResolverService.WellKnownResolutionResult< + LibMatrix.Services.WellKnownResolver.WellKnownResolvers.PolicyServerWellKnown? +>; + +namespace LibMatrix.Services.WellKnownResolver.WellKnownResolvers; + +public class PolicyServerWellKnownResolver(ILogger<PolicyServerWellKnownResolver> logger, WellKnownResolverConfiguration configuration) : BaseWellKnownResolver<WellKnownType> { + public Task<ResultType> TryResolveWellKnown(string homeserver, WellKnownResolverConfiguration? config = null) { + config ??= configuration; + return WellKnownCache.TryGetOrAdd(homeserver, async () => { + logger.LogTrace($"Resolving support well-known: {homeserver}"); + + ResultType result = await TryGetWellKnownFromUrl($"https://{homeserver}/.well-known/matrix/policy_server", WellKnownResolverService.WellKnownSource.Https); + if (result.Content != null) + return result; + + return null; + }); + } +} + +public class PolicyServerWellKnown { + [JsonPropertyName("public_key")] + public string PublicKey { get; set; } = null!; +} \ No newline at end of file diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ServerWellKnownResolver.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ServerWellKnownResolver.cs
index a48d846..f4be57d 100644 --- a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ServerWellKnownResolver.cs +++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ServerWellKnownResolver.cs
@@ -2,8 +2,9 @@ using System.Text.Json.Serialization; using ArcaneLibs.Collections; using Microsoft.Extensions.Logging; using WellKnownType = LibMatrix.Services.WellKnownResolver.WellKnownResolvers.ServerWellKnown; -using ResultType = - LibMatrix.Services.WellKnownResolver.WellKnownResolverService.WellKnownResolutionResult<LibMatrix.Services.WellKnownResolver.WellKnownResolvers.ServerWellKnown?>; +using ResultType = LibMatrix.Services.WellKnownResolver.WellKnownResolverService.WellKnownResolutionResult< + LibMatrix.Services.WellKnownResolver.WellKnownResolvers.ServerWellKnown? +>; namespace LibMatrix.Services.WellKnownResolver.WellKnownResolvers; @@ -13,7 +14,7 @@ public class ServerWellKnownResolver(ILogger<ServerWellKnownResolver> logger, We StoreNulls = false }; - public Task<WellKnownResolverService.WellKnownResolutionResult<ServerWellKnown>> TryResolveWellKnown(string homeserver, WellKnownResolverConfiguration? config = null) { + public Task<ResultType> TryResolveWellKnown(string homeserver, WellKnownResolverConfiguration? config = null) { config ??= configuration; return ClientWellKnownCache.TryGetOrAdd(homeserver, async () => { logger.LogTrace($"Resolving client well-known: {homeserver}"); diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/SupportWellKnownResolver.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/SupportWellKnownResolver.cs
index 99313db..4faff62 100644 --- a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/SupportWellKnownResolver.cs +++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/SupportWellKnownResolver.cs
@@ -1,5 +1,3 @@ -using System.Diagnostics; -using System.Net.Http.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using WellKnownType = LibMatrix.Services.WellKnownResolver.WellKnownResolvers.SupportWellKnown; @@ -16,7 +14,7 @@ public class SupportWellKnownResolver(ILogger<SupportWellKnownResolver> logger, logger.LogTrace($"Resolving support well-known: {homeserver}"); ResultType result = await TryGetWellKnownFromUrl($"https://{homeserver}/.well-known/matrix/support", WellKnownResolverService.WellKnownSource.Https); - if (result.Content != null) + if (result.Content != null) return result; return null; diff --git a/Utilities/LibMatrix.FederationTest/Controllers/Spec/DirectoryController.cs b/Utilities/LibMatrix.FederationTest/Controllers/Spec/DirectoryController.cs new file mode 100644
index 0000000..707a149 --- /dev/null +++ b/Utilities/LibMatrix.FederationTest/Controllers/Spec/DirectoryController.cs
@@ -0,0 +1,51 @@ +using System.Net.Http.Headers; +using LibMatrix.Federation; +using LibMatrix.FederationTest.Services; +using LibMatrix.Homeservers; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.FederationTest.Controllers.Spec; + +[ApiController] +[Route("_matrix/federation/")] +public class DirectoryController(ServerAuthService serverAuth) : ControllerBase { + [HttpGet("v1/publicRooms")] + [HttpPost("v1/publicRooms")] + public async Task<IActionResult> GetPublicRooms() { + if (Request.Headers.ContainsKey("Authorization")) { + Console.WriteLine("INFO | Authorization header found."); + await serverAuth.AssertValidAuthentication(); + } + else Console.WriteLine("INFO | Room directory request without auth"); + + var rooms = new List<PublicRoomDirectoryResult.PublicRoomListItem> { + new() { + GuestCanJoin = false, + RoomId = "!tuiLEoMqNOQezxILzt:rory.gay", + NumJoinedMembers = Random.Shared.Next(), + WorldReadable = false, + CanonicalAlias = "#libmatrix:rory.gay", + Name = "Rory&::LibMatrix", + Topic = $"A .NET {Environment.Version.Major} library for interacting with Matrix" + } + }; + return Ok(new PublicRoomDirectoryResult() { + Chunk = rooms, + TotalRoomCountEstimate = rooms.Count + }); + } + + [HttpGet("v1/query/profile")] + public async Task<IActionResult> GetProfile([FromQuery(Name = "user_id")] string userId) { + if (Request.Headers.ContainsKey("Authorization")) { + Console.WriteLine("INFO | Authorization header found."); + await serverAuth.AssertValidAuthentication(); + } + else Console.WriteLine("INFO | Profile request without auth"); + + return Ok(new { + avatar_url = "mxc://rory.gay/ocRVanZoUTCcifcVNwXgbtTg", + displayname = "Rory&::LibMatrix.FederationTest" + }); + } +} \ No newline at end of file diff --git a/Utilities/LibMatrix.FederationTest/Controllers/Spec/MembershipsController.cs b/Utilities/LibMatrix.FederationTest/Controllers/Spec/MembershipsController.cs new file mode 100644
index 0000000..7c561ad --- /dev/null +++ b/Utilities/LibMatrix.FederationTest/Controllers/Spec/MembershipsController.cs
@@ -0,0 +1,41 @@ +using System.Net.Http.Headers; +using LibMatrix.Federation; +using LibMatrix.Federation.FederationTypes; +using LibMatrix.FederationTest.Services; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.FederationTest.Controllers.Spec; + +[ApiController] +[Route("_matrix/federation/")] +public class MembershipsController(ServerAuthService sas) : ControllerBase { + [HttpGet("v1/make_join/{roomId}/{userId}")] + [HttpPut("v1/send_join/{roomId}/{eventId}")] + [HttpPut("v2/send_join/{roomId}/{eventId}")] + [HttpGet("v1/make_knock/{roomId}/{userId}")] + [HttpPut("v1/send_knock/{roomId}/{eventId}")] + [HttpGet("v1/make_leave/{roomId}/{eventId}")] + [HttpPut("v1/send_leave/{roomId}/{eventId}")] + [HttpPut("v2/send_leave/{roomId}/{eventId}")] + public async Task<IActionResult> JoinKnockMemberships() { + await sas.AssertValidAuthentication(); + return NotFound(new MatrixException() { + ErrorCode = MatrixException.ErrorCodes.M_NOT_FOUND, + Error = "Rory&::LibMatrix.FederationTest does not support membership events." + }.GetAsObject()); + } + + // [HttpPut("v1/invite/{roomId}/{eventId}")] + [HttpPut("v2/invite/{roomId}/{eventId}")] + public async Task<IActionResult> InviteHandler([FromBody] RoomInvite invite) { + await sas.AssertValidAuthentication(); + + Console.WriteLine($"Received invite event from {invite.Event.Sender} for room {invite.Event.RoomId} (version {invite.RoomVersion})\n" + + $"{invite.InviteRoomState.Count} invite room state events."); + + return NotFound(new MatrixException() { + ErrorCode = MatrixException.ErrorCodes.M_NOT_FOUND, + Error = "Rory&::LibMatrix.FederationTest does not support membership events." + }.GetAsObject()); + } +} \ No newline at end of file diff --git a/Utilities/LibMatrix.FederationTest/FedTest.http b/Utilities/LibMatrix.FederationTest/FedTest.http new file mode 100644
index 0000000..26b1cd0 --- /dev/null +++ b/Utilities/LibMatrix.FederationTest/FedTest.http
@@ -0,0 +1,13 @@ +POST https://libmatrix-fed-test.rory.gay/ping +Accept: application/json +Content-Type: application/json + +[ + "matrix.org", + "rory.gay", + "element.io", + "4d2.org", + "mozilla.org", + "fedora.im", + "opensuse.org" +] \ No newline at end of file diff --git a/Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml b/Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml new file mode 100644
index 0000000..283c13e --- /dev/null +++ b/Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml
@@ -0,0 +1,19 @@ +@page "/" +@model LibMatrix.FederationTest.Pages.IndexPage + +@{ + Layout = null; +} + +<!DOCTYPE html> + +<html> + <head> + <title>LibMatrix.FederationTest</title> + </head> + <body> + <div> + If you're seeing this, LibMatrix.FederationTest is running! + </div> + </body> +</html> \ No newline at end of file diff --git a/Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml.cs b/Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml.cs new file mode 100644
index 0000000..0d372b0 --- /dev/null +++ b/Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml.cs
@@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace LibMatrix.FederationTest.Pages; + +public class IndexPage : PageModel { + public void OnGet() { + + } +} \ No newline at end of file diff --git a/Utilities/LibMatrix.FederationTest/Program.cs b/Utilities/LibMatrix.FederationTest/Program.cs
index 18d3421..3e9cb80 100644 --- a/Utilities/LibMatrix.FederationTest/Program.cs +++ b/Utilities/LibMatrix.FederationTest/Program.cs
@@ -1,12 +1,33 @@ +using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using ArcaneLibs.Extensions; +using LibMatrix.Extensions; +using LibMatrix.Federation; using LibMatrix.FederationTest.Services; using LibMatrix.Services; +using Microsoft.AspNetCore.Mvc; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers() + .ConfigureApiBehaviorOptions(options => { + options.InvalidModelStateResponseFactory = context => { + var problemDetails = new ValidationProblemDetails(context.ModelState) { + Status = StatusCodes.Status400BadRequest, + Title = "One or more validation errors occurred.", + Detail = "See the errors property for more details.", + Instance = context.HttpContext.Request.Path + }; + + Console.WriteLine("Model validation failed: " + problemDetails.ToJson()); + + return new BadRequestObjectResult(problemDetails) { + ContentTypes = { "application/problem+json", "application/problem+xml" } + }; + }; + }) .AddJsonOptions(options => { options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; options.JsonSerializerOptions.WriteIndented = true; @@ -20,13 +41,15 @@ builder.Services.AddHttpLogging(options => { options.RequestHeaders.Add("X-Forwarded-Host"); options.RequestHeaders.Add("X-Forwarded-Port"); }); +builder.Services.AddRazorPages(); +builder.Services.AddHttpContextAccessor(); builder.Services.AddRoryLibMatrixServices(); builder.Services.AddSingleton<FederationTestConfiguration>(); builder.Services.AddSingleton<FederationKeyStore>(); +builder.Services.AddScoped<ServerAuthService>(); var app = builder.Build(); - // Configure the HTTP request pipeline. if (true || app.Environment.IsDevelopment()) { app.MapOpenApi(); @@ -35,6 +58,7 @@ if (true || app.Environment.IsDevelopment()) { // app.UseAuthorization(); app.MapControllers(); +app.MapRazorPages(); // app.UseHttpLogging(); app.Run(); \ No newline at end of file diff --git a/Utilities/LibMatrix.FederationTest/Services/ServerAuthService.cs b/Utilities/LibMatrix.FederationTest/Services/ServerAuthService.cs new file mode 100644
index 0000000..58274eb --- /dev/null +++ b/Utilities/LibMatrix.FederationTest/Services/ServerAuthService.cs
@@ -0,0 +1,58 @@ +using System.Net.Http.Headers; +using System.Text.Json.Nodes; +using LibMatrix.Extensions; +using LibMatrix.Federation; +using LibMatrix.FederationTest.Utilities; +using LibMatrix.Responses.Federation; +using LibMatrix.Services; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Http.Features; +using Org.BouncyCastle.Math.EC.Rfc8032; + +namespace LibMatrix.FederationTest.Services; + +public class ServerAuthService(HomeserverProviderService hsProvider, IHttpContextAccessor httpContextAccessor) { + private static Dictionary<string, SignedObject<ServerKeysResponse>> _serverKeysCache = new(); + + public async Task AssertValidAuthentication(XMatrixAuthorizationScheme.XMatrixAuthorizationHeader authHeader) { + var httpContext = httpContextAccessor.HttpContext!; + var hs = await hsProvider.GetFederationClient(authHeader.Origin, ""); + var serverKeys = (_serverKeysCache.TryGetValue(authHeader.Origin, out var sk) && sk.TypedContent.ValidUntil > DateTimeOffset.UtcNow) + ? sk + : _serverKeysCache[authHeader.Origin] = await hs.GetServerKeysAsync(); + var publicKeyBase64 = serverKeys.TypedContent.VerifyKeys[authHeader.Key].Key; + var publicKey = Ed25519Utils.LoadPublicKeyFromEncoded(publicKeyBase64); + var requestAuthenticationData = new XMatrixAuthorizationScheme.XMatrixRequestSignature() { + Method = httpContext.Request.Method, + Uri = httpContext.Features.Get<IHttpRequestFeature>()!.RawTarget, + OriginServerName = authHeader.Origin, + DestinationServerName = authHeader.Destination, + Content = httpContext.Request.HasJsonContentType() ? await httpContext.Request.ReadFromJsonAsync<JsonObject?>() : null + }; + var contentBytes = CanonicalJsonSerializer.SerializeToUtf8Bytes(requestAuthenticationData); + var signatureBytes = UnpaddedBase64.Decode(authHeader.Signature); + + Console.WriteLine($"Validating X-Matrix authorized request\n" + + $" - From: {requestAuthenticationData.OriginServerName}, To: {requestAuthenticationData.DestinationServerName}\n" + + $" - Key: {authHeader.Key} ({publicKeyBase64})\n" + + $" - Signature: {authHeader.Signature}\n" + + $" - Request: {requestAuthenticationData.Method} {requestAuthenticationData.Uri}\n" + + $" - Has request body: {requestAuthenticationData.Content is not null}\n" + + // $" - Canonicalized request body (or null if missing): {(requestAuthenticationData.Content is null ? "(null)" : CanonicalJsonSerializer.Serialize(requestAuthenticationData.Content))}\n" + + $" - Canonicalized message to verify: {System.Text.Encoding.UTF8.GetString(contentBytes)}"); + + if (!publicKey.Verify(Ed25519.Algorithm.Ed25519, null, contentBytes, 0, contentBytes.Length, signatureBytes, 0)) { + throw new UnauthorizedAccessException("Invalid signature in X-Matrix authorization header."); + } + + Console.WriteLine("INFO | Valid X-Matrix authorization header."); + } + + public async Task AssertValidAuthentication() { + await AssertValidAuthentication( + XMatrixAuthorizationScheme.XMatrixAuthorizationHeader.FromHeaderValue( + httpContextAccessor.HttpContext!.Request.GetTypedHeaders().Get<AuthenticationHeaderValue>("Authorization")! + ) + ); + } +} \ No newline at end of file