From 3dfb7b81b0fe19d37a7bf1183e248ca10c56277c Mon Sep 17 00:00:00 2001 From: Rory& Date: Fri, 23 Feb 2024 11:23:27 +0000 Subject: HS emulator --- .../Controllers/AuthController.cs | 23 ++++++ .../Controllers/HEDebug/HEDebugController.cs | 18 +++++ .../Controllers/UserController.cs | 81 +++++++++++++++++++ .../Controllers/VersionsController.cs | 48 ++++++++++++ .../Controllers/WellKnownController.cs | 32 ++++++++ .../LibMatrix.HomeserverEmulator.csproj | 21 +++++ .../LibMatrix.HomeserverEmulator.http | 6 ++ Tests/LibMatrix.HomeserverEmulator/Program.cs | 90 ++++++++++++++++++++++ .../Properties/launchSettings.json | 31 ++++++++ .../Services/RoomStore.cs | 83 ++++++++++++++++++++ .../Services/TokenService.cs | 26 +++++++ .../Services/UserStore.cs | 72 +++++++++++++++++ .../appsettings.Development.json | 10 +++ .../LibMatrix.HomeserverEmulator/appsettings.json | 41 ++++++++++ 14 files changed, 582 insertions(+) create mode 100644 Tests/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs create mode 100644 Tests/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs create mode 100644 Tests/LibMatrix.HomeserverEmulator/Controllers/UserController.cs create mode 100644 Tests/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs create mode 100644 Tests/LibMatrix.HomeserverEmulator/Controllers/WellKnownController.cs create mode 100644 Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj create mode 100644 Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.http create mode 100644 Tests/LibMatrix.HomeserverEmulator/Program.cs create mode 100644 Tests/LibMatrix.HomeserverEmulator/Properties/launchSettings.json create mode 100644 Tests/LibMatrix.HomeserverEmulator/Services/RoomStore.cs create mode 100644 Tests/LibMatrix.HomeserverEmulator/Services/TokenService.cs create mode 100644 Tests/LibMatrix.HomeserverEmulator/Services/UserStore.cs create mode 100644 Tests/LibMatrix.HomeserverEmulator/appsettings.Development.json create mode 100644 Tests/LibMatrix.HomeserverEmulator/appsettings.json (limited to 'Tests') diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs new file mode 100644 index 0000000..d0496bf --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Nodes; +using LibMatrix.HomeserverEmulator.Services; +using LibMatrix.Responses; +using LibMatrix.Services; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.HomeserverEmulator.Controllers; + +[ApiController] +[Route("/_matrix/client/{version}/")] +public class AuthController(ILogger logger, UserStore userStore) : ControllerBase { + [HttpPost("login")] + public async Task Login(LoginRequest request) { + var user = await userStore.CreateUser($"@{Guid.NewGuid().ToString()}:{Request.Host}", Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), new Dictionary()); + var loginResponse = new LoginResponse { + AccessToken = user.AccessToken, + DeviceId = user.DeviceId, + UserId = user.UserId + }; + + return loginResponse; + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs new file mode 100644 index 0000000..0c4d8bd --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs @@ -0,0 +1,18 @@ +using LibMatrix.HomeserverEmulator.Services; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.HomeserverEmulator.Controllers; + +[ApiController] +[Route("/_hsEmulator")] +public class HEDebugController(ILogger logger, UserStore userStore, RoomStore roomStore) : ControllerBase { + [HttpGet("users")] + public async Task> GetUsers() { + return userStore._users; + } + + [HttpGet("rooms")] + public async Task> GetRooms() { + return roomStore._rooms.ToList(); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/UserController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/UserController.cs new file mode 100644 index 0000000..d763b26 --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/UserController.cs @@ -0,0 +1,81 @@ +using System.Text.Json.Nodes; +using ArcaneLibs.Extensions; +using LibMatrix.HomeserverEmulator.Services; +using LibMatrix.Responses; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.HomeserverEmulator.Controllers; + +[ApiController] +[Route("/_matrix/client/{version}/")] +public class UserController(ILogger logger, TokenService tokenService, UserStore userStore) : ControllerBase { + [HttpGet("account/whoami")] + public async Task Login() { + var token = tokenService.GetAccessToken(); + if (token is null) + throw new MatrixException() { + ErrorCode = "M_UNAUTHORIZED", + Error = "No token passed." + }; + + var user = await userStore.GetUserByToken(token, Random.Shared.Next(101) <= 10, tokenService.GenerateServerName()); + if (user is null) + throw new MatrixException() { + ErrorCode = "M_UNKNOWN_TOKEN", + Error = "Invalid token." + }; + var whoAmIResponse = new WhoAmIResponse { + UserId = user.UserId + }; + return whoAmIResponse; + } + + [HttpGet("profile/{userId}")] + public async Task> GetProfile(string userId) { + var user = await userStore.GetUserById(userId, false); + if (user is null) + throw new MatrixException() { + ErrorCode = "M_NOT_FOUND", + Error = "User not found." + }; + return user.Profile; + } + + [HttpGet("profile/{userId}/{key}")] + public async Task GetProfile(string userId, string key) { + var user = await userStore.GetUserById(userId, false); + if (user is null) + throw new MatrixException() { + ErrorCode = "M_NOT_FOUND", + Error = "User not found." + }; + if (!user.Profile.TryGetValue(key, out var value)) + throw new MatrixException() { + ErrorCode = "M_NOT_FOUND", + Error = "Key not found." + }; + return value; + } + + [HttpGet("joined_rooms")] + public async Task GetJoinedRooms() { + var token = tokenService.GetAccessToken(); + if (token is null) + throw new MatrixException() { + ErrorCode = "M_UNAUTHORIZED", + Error = "No token passed." + }; + + var user = await userStore.GetUserByToken(token, false); + if (user is null) + throw new MatrixException() { + ErrorCode = "M_UNAUTHORIZED", + Error = "Invalid token." + }; + // return user.JoinedRooms; + + return new { + joined_rooms = user.JoinedRooms + }; + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs new file mode 100644 index 0000000..1349fac --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs @@ -0,0 +1,48 @@ +using System.Text.Json.Nodes; +using LibMatrix.Homeservers; +using LibMatrix.Responses; +using LibMatrix.Services; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.HomeserverEmulator.Controllers; + +[ApiController] +[Route("/_matrix/")] +public class VersionsController(ILogger logger) : ControllerBase { + [HttpGet("client/versions")] + public async Task GetClientVersions() { + var clientVersions = new ClientVersionsResponse { + Versions = new() { + "r0.0.1", + "r0.1.0", + "r0.2.0", + "r0.3.0", + "r0.4.0", + "r0.5.0", + "r0.6.0", + "r0.6.1", + "v1.1", + "v1.2", + "v1.3", + "v1.4", + "v1.5", + "v1.6", + "v1.7", + "v1.8", + }, + UnstableFeatures = new() + }; + return clientVersions; + } + + [HttpGet("federation/v1/version")] + public async Task GetServerVersions() { + var clientVersions = new ServerVersionResponse() { + Server = new() { + Name = "LibMatrix.HomeserverEmulator", + Version = "0.0.0" + } + }; + return clientVersions; + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/WellKnownController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/WellKnownController.cs new file mode 100644 index 0000000..97e460d --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/WellKnownController.cs @@ -0,0 +1,32 @@ +using System.Text.Json.Nodes; +using LibMatrix.Services; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.HomeserverEmulator.Controllers; + +[ApiController] +[Route("/.well-known/matrix/")] +public class WellKnownController(ILogger logger) : ControllerBase { + [HttpGet("client")] + public JsonObject GetClientWellKnown() { + var obj = new JsonObject() { + ["m.homeserver"] = new JsonObject() { + ["base_url"] = $"{Request.Scheme}://{Request.Host}" + } + }; + + logger.LogInformation("Serving client well-known: {}", obj); + + return obj; + } + [HttpGet("server")] + public JsonObject GetServerWellKnown() { + var obj = new JsonObject() { + ["m.server"] = $"{Request.Scheme}://{Request.Host}" + }; + + logger.LogInformation("Serving server well-known: {}", obj); + + return obj; + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj b/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj new file mode 100644 index 0000000..e6b4572 --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + true + preview + true + + + + + + + + + + + + diff --git a/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.http b/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.http new file mode 100644 index 0000000..01d5fcc --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.http @@ -0,0 +1,6 @@ +@LibMatrix.HomeserverEmulator_HostAddress = http://localhost:5298 + +GET {{LibMatrix.HomeserverEmulator_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Tests/LibMatrix.HomeserverEmulator/Program.cs b/Tests/LibMatrix.HomeserverEmulator/Program.cs new file mode 100644 index 0000000..516d380 --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Program.cs @@ -0,0 +1,90 @@ +using System.Net.Mime; +using LibMatrix; +using LibMatrix.HomeserverEmulator.Services; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http.Timeouts; +using Microsoft.OpenApi.Models; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => { + c.SwaggerDoc("v1", new OpenApiInfo() { + Version = "v1", + Title = "Rory&::LibMatrix.HomeserverEmulator", + Description = "Partial Matrix implementation" + }); + c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "LibMatrix.HomeserverEmulator.xml")); +}); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + + +builder.Services.AddScoped(); + +builder.Services.AddRequestTimeouts(x => { + x.DefaultPolicy = new RequestTimeoutPolicy { + Timeout = TimeSpan.FromMinutes(10), + WriteTimeoutResponse = async context => { + context.Response.StatusCode = 504; + context.Response.ContentType = "application/json"; + await context.Response.StartAsync(); + await context.Response.WriteAsJsonAsync(new MatrixException() { + ErrorCode = "M_TIMEOUT", + Error = "Request timed out" + }.GetAsJson()); + await context.Response.CompleteAsync(); + } + }; +}); +builder.Services.AddCors(options => { + options.AddPolicy( + "Open", + policy => policy.AllowAnyOrigin().AllowAnyHeader()); +}); +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment() || true) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseCors("Open"); +app.UseExceptionHandler(exceptionHandlerApp => { + exceptionHandlerApp.Run(async context => { + + var exceptionHandlerPathFeature = + context.Features.Get(); + + if (exceptionHandlerPathFeature?.Error is MatrixException mxe) { + context.Response.StatusCode = mxe.ErrorCode switch { + "M_NOT_FOUND" => StatusCodes.Status404NotFound, + "M_UNAUTHORIZED" => StatusCodes.Status401Unauthorized, + _ => StatusCodes.Status500InternalServerError + }; + context.Response.ContentType = MediaTypeNames.Application.Json; + await context.Response.WriteAsync(mxe.GetAsJson()!); + } + else { + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + context.Response.ContentType = MediaTypeNames.Application.Json; + await context.Response.WriteAsync(new MatrixException() { + ErrorCode = "M_UNKNOWN", + Error = exceptionHandlerPathFeature?.Error.ToString() + }.GetAsJson()); + } + }); +}); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/Tests/LibMatrix.HomeserverEmulator/Properties/launchSettings.json b/Tests/LibMatrix.HomeserverEmulator/Properties/launchSettings.json new file mode 100644 index 0000000..8ab6b3d --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:6824", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5298", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Tests/LibMatrix.HomeserverEmulator/Services/RoomStore.cs b/Tests/LibMatrix.HomeserverEmulator/Services/RoomStore.cs new file mode 100644 index 0000000..b4624ab --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Services/RoomStore.cs @@ -0,0 +1,83 @@ +using System.Collections.Concurrent; +using System.Security.Cryptography; +using LibMatrix.EventTypes; +using LibMatrix.EventTypes.Spec.State; +using LibMatrix.Responses; + +namespace LibMatrix.HomeserverEmulator.Services; + +public class RoomStore { + public ConcurrentBag _rooms = new(); + private Dictionary _roomsById = new(); + + private void RebuildIndexes() { + _roomsById = _rooms.ToDictionary(u => u.RoomId); + } + + public Room? GetRoomById(string roomId, bool createIfNotExists = false) { + if (_roomsById.TryGetValue(roomId, out var user)) { + return user; + } + + if (!createIfNotExists) + return null; + + return CreateRoom(new() { }); + } + + public Room CreateRoom(CreateRoomRequest request) { + var room = new Room { + RoomId = $"!{Guid.NewGuid().ToString()}" + }; + if (!string.IsNullOrWhiteSpace(request.Name)) + room.SetStateInternal(new StateEvent() { + Type = RoomNameEventContent.EventId, + TypedContent = new RoomNameEventContent() { + Name = request.Name + } + }); + + if (!string.IsNullOrWhiteSpace(request.RoomAliasName)) + room.SetStateInternal(new StateEvent() { + Type = RoomCanonicalAliasEventContent.EventId, + TypedContent = new RoomCanonicalAliasEventContent() { + Alias = $"#{request.RoomAliasName}:localhost" + } + }); + + foreach (var stateEvent in request.InitialState ?? []) { + room.SetStateInternal(stateEvent); + } + + _rooms.Add(room); + RebuildIndexes(); + return room; + } + + public class Room { + public string RoomId { get; set; } + public List State { get; set; } = new(); + public Dictionary Timeline { get; set; } = new(); + + internal StateEventResponse SetStateInternal(StateEvent request) { + var state = new StateEventResponse() { + Type = request.Type, + StateKey = request.StateKey, + RawContent = request.RawContent, + EventId = Guid.NewGuid().ToString() + }; + State.Add(state); + return state; + } + + public StateEventResponse AddUser(string userId) { + return SetStateInternal(new() { + Type = RoomMemberEventContent.EventId, + StateKey = userId, + TypedContent = new RoomMemberEventContent() { + Membership = "join" + } + }); + } + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Services/TokenService.cs b/Tests/LibMatrix.HomeserverEmulator/Services/TokenService.cs new file mode 100644 index 0000000..8115bee --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Services/TokenService.cs @@ -0,0 +1,26 @@ +namespace LibMatrix.HomeserverEmulator.Services; + +public class TokenService(IHttpContextAccessor accessor) { + public string? GetAccessToken() { + var ctx = accessor.HttpContext; + if (ctx is null) return null; + //qry + if (ctx.Request.Query.TryGetValue("access_token", out var token)) { + return token; + } + //header + if (ctx.Request.Headers.TryGetValue("Authorization", out var auth)) { + var parts = auth.ToString().Split(' '); + if (parts.Length == 2 && parts[0] == "Bearer") { + return parts[1]; + } + } + return null; + } + + public string? GenerateServerName() { + var ctx = accessor.HttpContext; + if (ctx is null) return null; + return ctx.Request.Host.ToString(); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Services/UserStore.cs b/Tests/LibMatrix.HomeserverEmulator/Services/UserStore.cs new file mode 100644 index 0000000..ca1c577 --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Services/UserStore.cs @@ -0,0 +1,72 @@ +using LibMatrix.EventTypes.Spec.State; + +namespace LibMatrix.HomeserverEmulator.Services; + +public class UserStore(RoomStore roomStore) { + public List _users = new(); + private Dictionary _usersById = new(); + private Dictionary _usersByToken = new(); + + private void RebuildIndexes() { + _usersById = _users.ToDictionary(u => u.UserId); + _usersByToken = _users.ToDictionary(u => u.AccessToken); + } + + public async Task GetUserById(string userId, bool createIfNotExists = false) { + if (_usersById.TryGetValue(userId, out var user)) { + return user; + } + + if (!createIfNotExists) + return null; + + return await CreateUser(userId, Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), new Dictionary()); + } + + public async Task GetUserByToken(string token, bool createIfNotExists = false, string? serverName = null) { + if (_usersByToken.TryGetValue(token, out var user)) { + return user; + } + + if (!createIfNotExists) + return null; + if (string.IsNullOrWhiteSpace(serverName)) throw new NullReferenceException("Server name was not passed"); + var uid = $"@{Guid.NewGuid().ToString()}:{serverName}"; + return await CreateUser(uid, Guid.NewGuid().ToString(), token, new Dictionary()); + } + + public async Task CreateUser(string userId, string deviceId, string accessToken, Dictionary profile) { + if (!profile.ContainsKey("displayname")) profile.Add("displayname", userId.Split(":")[0]); + if (!profile.ContainsKey("avatar_url")) profile.Add("avatar_url", null); + var user = new User { + UserId = userId, + DeviceId = deviceId, + AccessToken = accessToken, + Profile = profile + }; + _users.Add(user); + RebuildIndexes(); + + if (roomStore._rooms.Count > 0) + foreach (var item in Random.Shared.GetItems(roomStore._rooms.ToArray(), Math.Min(roomStore._rooms.Count, 400))) { + item.AddUser(userId); + } + + int random = Random.Shared.Next(10); + for (int i = 0; i < random; i++) { + var room = roomStore.CreateRoom(new()); + room.AddUser(userId); + } + + return user; + } + + public class User { + public string UserId { get; set; } + public string AccessToken { get; set; } + public string DeviceId { get; set; } + public Dictionary Profile { get; set; } + + public List JoinedRooms { get; set; } = new(); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/appsettings.Development.json b/Tests/LibMatrix.HomeserverEmulator/appsettings.Development.json new file mode 100644 index 0000000..df83ec5 --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Information", + "Microsoft.AspNetCore.Routing": "Warning", + "Microsoft.AspNetCore.Mvc": "Warning" + } + } +} diff --git a/Tests/LibMatrix.HomeserverEmulator/appsettings.json b/Tests/LibMatrix.HomeserverEmulator/appsettings.json new file mode 100644 index 0000000..b16968a --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/appsettings.json @@ -0,0 +1,41 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + // Configuration for the proxy + "MxApiExtensions": { + // WARNING: this exposes user tokens to servers listed here, which could be a security risk + // Only list servers you trust! + // Keep in mind that token conflicts can occur between servers! + "AuthHomeservers": [ + "rory.gay", + "conduit.rory.gay" + ], + // List of administrator MXIDs for the proxy, this allows them to use administrative and debug endpoints + "Admins": [ + "@emma:rory.gay", + "@emma:conduit.rory.gay" + ], + "FastInitialSync": { + "Enabled": true, + "UseRoomInfoCache": true + }, + "Cache": { + "RoomInfo": { + "BaseTtl": "00:01:00", + "ExtraTtlPerState": "00:00:00.1000000" + } + }, + "DefaultUserConfiguration": { + "ProtocolChanges": { + "DisableThreads": false, + "DisableVoip": false, + "AutoFollowTombstones": false + } + } + } +} -- cgit 1.4.1