summary refs log tree commit diff
path: root/ModAS.Server
diff options
context:
space:
mode:
Diffstat (limited to 'ModAS.Server')
-rw-r--r--ModAS.Server/Attributes/UserAuthAttribute.cs27
-rw-r--r--ModAS.Server/Authentication/AuthMiddleware.cs82
-rw-r--r--ModAS.Server/Authentication/AuthUser.cs9
-rw-r--r--ModAS.Server/Controllers/Admin/RoomQueryController.cs (renamed from ModAS.Server/Controllers/RoomQueryController.cs)0
-rw-r--r--ModAS.Server/Controllers/AppService/PingController.cs73
-rw-r--r--ModAS.Server/Controllers/AppService/TransactionsController.cs23
-rw-r--r--ModAS.Server/Controllers/Debug/DebugController.cs (renamed from ModAS.Server/Controllers/DebugController.cs)30
-rw-r--r--ModAS.Server/Controllers/HomeController.cs11
-rw-r--r--ModAS.Server/Extensions/RequestHeaderExtensinos.cs17
-rw-r--r--ModAS.Server/ModAS.Server.csproj1
-rw-r--r--ModAS.Server/Program.cs8
-rw-r--r--ModAS.Server/Services/AuthenticationService.cs31
-rw-r--r--ModAS.Server/Services/ModASConfiguration.cs5
-rw-r--r--ModAS.Server/Services/PingTask.cs9
-rw-r--r--ModAS.Server/Version.cs5
-rw-r--r--ModAS.Server/appsettings.Development.json3
16 files changed, 289 insertions, 45 deletions
diff --git a/ModAS.Server/Attributes/UserAuthAttribute.cs b/ModAS.Server/Attributes/UserAuthAttribute.cs
new file mode 100644
index 0000000..ef8295a
--- /dev/null
+++ b/ModAS.Server/Attributes/UserAuthAttribute.cs
@@ -0,0 +1,27 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace ModAS.Server.Attributes;
+
+public class UserAuthAttribute : Attribute {
+    public AuthType AuthType { get; set; }
+    public AuthRoles AnyRoles { get; set; }
+
+    public string ToJson() => JsonSerializer.Serialize(new {
+        AuthType,
+        AnyRoles
+    });
+}
+
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum AuthType {
+    User,
+    Server
+}
+
+[JsonConverter(typeof(JsonStringEnumConverter))]
+[Flags]
+public enum AuthRoles {
+    Administrator = 1 << 0,
+    Developer = 1 << 1,
+}
\ No newline at end of file
diff --git a/ModAS.Server/Authentication/AuthMiddleware.cs b/ModAS.Server/Authentication/AuthMiddleware.cs
new file mode 100644
index 0000000..8b7266f
--- /dev/null
+++ b/ModAS.Server/Authentication/AuthMiddleware.cs
@@ -0,0 +1,82 @@
+using System.Net.Http.Headers;
+using System.Text.Json;
+using LibMatrix;
+using LibMatrix.Homeservers;
+using LibMatrix.Services;
+using ModAS.Server.Attributes;
+using MxApiExtensions.Services;
+
+namespace ModAS.Server.Authentication;
+
+public class AuthMiddleware(RequestDelegate next, ILogger<AuthMiddleware> logger, ModASConfiguration config, HomeserverProviderService hsProvider, AppServiceRegistration asr) {
+    public async Task InvokeAsync(HttpContext context) {
+        context.Request.Query.TryGetValue("access_token", out var queryAccessToken);
+        var accessToken = queryAccessToken.FirstOrDefault();
+        accessToken ??= context.Request.GetTypedHeaders().Get<AuthenticationHeaderValue>("Authorization")?.Parameter;
+
+        //get UserAuth custom attribute
+        var endpoint = context.GetEndpoint();
+        if (endpoint is null) {
+            Console.WriteLine($"Ignoring authentication, endpoint is null!");
+            await next(context);
+            return;
+        }
+
+        var authAttribute = endpoint?.Metadata.GetMetadata<UserAuthAttribute>();
+        if (authAttribute is not null)
+            logger.LogInformation($"{nameof(Route)} authorization: {authAttribute.ToJson()}");
+        else if (string.IsNullOrWhiteSpace(accessToken)) {
+            // auth is optional if auth attribute isnt set
+            Console.WriteLine($"Allowing unauthenticated request, AuthAttribute is not set!");
+            await next(context);
+            return;
+        }
+
+        if (string.IsNullOrWhiteSpace(accessToken))
+            if (authAttribute is not null) {
+                context.Response.StatusCode = 401;
+                await context.Response.WriteAsJsonAsync(new MatrixException() {
+                    ErrorCode = "M_UNAUTHORIZED",
+                    Error = "Missing access token"
+                }.GetAsObject());
+                return;
+            }
+
+        try {
+            switch (authAttribute.AuthType) {
+                case AuthType.User:
+                    var authUser = await GetAuthUser(accessToken);
+                    context.Items.Add("AuthUser", authUser);
+                    break;
+                case AuthType.Server:
+                    if (asr.HomeserverToken != accessToken)
+                        throw new MatrixException() {
+                            ErrorCode = "M_UNAUTHORIZED",
+                            Error = "Invalid access token"
+                        };
+
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException();
+            }
+        }
+        catch (MatrixException e) {
+            context.Response.StatusCode = 401;
+            await context.Response.WriteAsJsonAsync(e.GetAsObject());
+            return;
+        }
+
+        await next(context);
+    }
+
+    private async Task<AuthUser> GetAuthUser(string accessToken) {
+        AuthenticatedHomeserverGeneric? homeserver;
+        homeserver = await hsProvider.GetAuthenticatedWithToken(config.ServerName, accessToken, config.HomeserverUrl);
+
+        return new AuthUser() {
+            Homeserver = homeserver,
+            AccessToken = accessToken,
+            Roles = config.Roles.Where(r => r.Value.Contains(homeserver.WhoAmI.UserId)).Select(r => r.Key).ToList()
+        };
+    }
+}
\ No newline at end of file
diff --git a/ModAS.Server/Authentication/AuthUser.cs b/ModAS.Server/Authentication/AuthUser.cs
new file mode 100644
index 0000000..f91656f
--- /dev/null
+++ b/ModAS.Server/Authentication/AuthUser.cs
@@ -0,0 +1,9 @@
+using LibMatrix.Homeservers;
+
+namespace ModAS.Server.Authentication;
+
+public class AuthUser {
+    public required string AccessToken { get; set; }
+    public required List<string> Roles { get; set; }
+    public required AuthenticatedHomeserverGeneric Homeserver { get; set; }
+}
\ No newline at end of file
diff --git a/ModAS.Server/Controllers/RoomQueryController.cs b/ModAS.Server/Controllers/Admin/RoomQueryController.cs
index a49e5c0..a49e5c0 100644
--- a/ModAS.Server/Controllers/RoomQueryController.cs
+++ b/ModAS.Server/Controllers/Admin/RoomQueryController.cs
diff --git a/ModAS.Server/Controllers/AppService/PingController.cs b/ModAS.Server/Controllers/AppService/PingController.cs
new file mode 100644
index 0000000..7b073c1
--- /dev/null
+++ b/ModAS.Server/Controllers/AppService/PingController.cs
@@ -0,0 +1,73 @@
+using System.IO.Pipelines;
+using System.Net;
+using System.Net.Http.Headers;
+using System.Text.Json;
+using ArcaneLibs;
+using LibMatrix;
+using LibMatrix.EventTypes.Spec;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration.Json;
+using ModAS.Server;
+using ModAS.Server.Attributes;
+using ModAS.Server.Services;
+using MxApiExtensions.Services;
+
+namespace ModAS.Server.Controllers.AppService;
+
+[ApiController]
+public class PingController(
+    AppServiceRegistration asr,
+    ModASConfiguration config,
+    UserProviderService userProvider,
+    RoomContextService roomContextService,
+    RoomStateCacheService stateCacheService) : ControllerBase {
+    private static List<string> _ignoredInvalidationEvents { get; set; } = [
+        RoomMessageEventContent.EventId,
+        RoomMessageReactionEventContent.EventId
+    ];
+
+    [HttpPut("/_matrix/app/v1/transactions/{txnId}")]
+    [UserAuth(AuthType = AuthType.Server)]
+    public async Task<IActionResult> PutTransactions(string txnId) {
+        var data = await JsonSerializer.DeserializeAsync<EventList>(Request.Body);
+        Console.WriteLine(
+            $"PutTransaction: {txnId}: {data.Events.Count} events, {Util.BytesToString(Request.Headers.ContentLength ?? Request.ContentLength ?? Request.Body.Length)}");
+
+        if (!Directory.Exists("data"))
+            Directory.CreateDirectory("data");
+        Directory.CreateDirectory($"data/{txnId}");
+        // var pipe = PipeReader.Create(Request.Body);
+        // await using var file = System.IO.File.OpenWrite($"data/{txnId}");
+        // await pipe.CopyToAsync(file);
+        // await pipe.CompleteAsync();
+        //
+        // Console.WriteLine($"PutTransaction: {txnId}: {Util.BytesToString(file.Length)}");
+        for (var i = 0; i < data.Events.Count; i++) {
+            var evt = data.Events[i];
+            Console.WriteLine($"PutTransaction: {txnId}/{i}: {evt.Type} {evt.StateKey} {evt.Sender}");
+            await System.IO.File.WriteAllTextAsync($"data/{txnId}/{i}-{evt.Type.Replace("/", "")}-{evt.StateKey.Replace("/", "")}-{evt.Sender?.Replace("/", "")}.json",
+                JsonSerializer.Serialize(evt));
+
+            if (evt.Sender.EndsWith(':' + config.ServerName)) {
+                Console.WriteLine("PutTransaction: sender is local user, updating data...");
+                try {
+                    var user = await userProvider.GetImpersonatedHomeserver(evt.Sender);
+                    var rooms = await user.GetJoinedRooms();
+                    foreach (var room in rooms) {
+                        await roomContextService.GetRoomContext(room);
+                    }
+                }
+                catch (Exception e) {
+                    Console.WriteLine($"PutTransaction: failed to update data: {e}");
+                }
+            }
+            else
+                Console.WriteLine("PutTransaction: sender is remote user");
+
+            if (!string.IsNullOrWhiteSpace(evt.RoomId) && !_ignoredInvalidationEvents.Contains(evt.Type))
+                await stateCacheService.InvalidateRoomState(evt.RoomId);
+        }
+
+        return Ok(new { });
+    }
+}
\ No newline at end of file
diff --git a/ModAS.Server/Controllers/AppService/TransactionsController.cs b/ModAS.Server/Controllers/AppService/TransactionsController.cs
index b74e1e1..53bfaf5 100644
--- a/ModAS.Server/Controllers/AppService/TransactionsController.cs
+++ b/ModAS.Server/Controllers/AppService/TransactionsController.cs
@@ -8,6 +8,7 @@ using LibMatrix.EventTypes.Spec;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Configuration.Json;
 using ModAS.Server;
+using ModAS.Server.Attributes;
 using ModAS.Server.Services;
 using MxApiExtensions.Services;
 
@@ -26,16 +27,17 @@ public class TransactionsController(
     ];
 
     [HttpPut("/_matrix/app/v1/transactions/{txnId}")]
+    [UserAuth(AuthType = AuthType.Server)]
     public async Task<IActionResult> PutTransactions(string txnId) {
-        if (!Request.Headers.ContainsKey("Authorization")) {
-            Console.WriteLine("PutTransaction: missing authorization header");
-            return Unauthorized();
-        }
-
-        if (Request.GetTypedHeaders().Get<AuthenticationHeaderValue>("Authorization")?.Parameter != asr.HomeserverToken) {
-            Console.WriteLine($"PutTransaction: invalid authorization header: {Request.Headers["Authorization"]}");
-            return Unauthorized();
-        }
+        // if (!Request.Headers.ContainsKey("Authorization")) {
+        //     Console.WriteLine("PutTransaction: missing authorization header");
+        //     return Unauthorized();
+        // }
+        //
+        // if (Request.GetTypedHeaders().Get<AuthenticationHeaderValue>("Authorization")?.Parameter != asr.HomeserverToken) {
+        //     Console.WriteLine($"PutTransaction: invalid authorization header: {Request.Headers["Authorization"]}");
+        //     return Unauthorized();
+        // }
 
         var data = await JsonSerializer.DeserializeAsync<EventList>(Request.Body);
         Console.WriteLine(
@@ -53,7 +55,8 @@ public class TransactionsController(
         for (var i = 0; i < data.Events.Count; i++) {
             var evt = data.Events[i];
             Console.WriteLine($"PutTransaction: {txnId}/{i}: {evt.Type} {evt.StateKey} {evt.Sender}");
-            await System.IO.File.WriteAllTextAsync($"data/{txnId}/{i}-{evt.Type}-{evt.StateKey}-{evt.Sender}.json", JsonSerializer.Serialize(evt));
+            await System.IO.File.WriteAllTextAsync($"data/{txnId}/{i}-{evt.Type.Replace("/", "")}-{evt.StateKey.Replace("/", "")}-{evt.Sender?.Replace("/", "")}.json",
+                JsonSerializer.Serialize(evt));
 
             if (evt.Sender.EndsWith(':' + config.ServerName)) {
                 Console.WriteLine("PutTransaction: sender is local user, updating data...");
diff --git a/ModAS.Server/Controllers/DebugController.cs b/ModAS.Server/Controllers/Debug/DebugController.cs
index f0fe91e..7bec3e5 100644
--- a/ModAS.Server/Controllers/DebugController.cs
+++ b/ModAS.Server/Controllers/Debug/DebugController.cs
@@ -2,16 +2,28 @@ using System.Collections.Frozen;
 using ArcaneLibs.Extensions;
 using Elastic.Apm;
 using Elastic.Apm.Api;
-using LibMatrix;
 using LibMatrix.Homeservers;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
+using ModAS.Server.Attributes;
 using ModAS.Server.Services;
 using MxApiExtensions.Services;
 
-namespace ModAS.Server.Controllers;
+namespace ModAS.Server.Controllers.Debug;
 
+/// <summary>
+///   Provides debugging endpoints.
+/// </summary>
+/// <param name="config"><inheritdoc cref="ModASConfiguration"/></param>
+/// <param name="authHsProvider"><inheritdoc cref="UserProviderService"/></param>
+/// <param name="roomContextService"><inheritdoc cref="RoomContextService"/></param>
 [ApiController]
+[UserAuth(AnyRoles = AuthRoles.Developer | AuthRoles.Administrator)]
 public class DebugController(ModASConfiguration config, UserProviderService authHsProvider, RoomContextService roomContextService) : ControllerBase {
+    /// <summary>
+    ///  Returns a JSON object containing the request and response headers.
+    /// </summary>
+    /// <returns>JSON object with request and partial response headers.</returns>
     [HttpGet("/_matrix/_modas/debug")]
     public IActionResult Index() {
         return Ok(new {
@@ -20,6 +32,10 @@ public class DebugController(ModASConfiguration config, UserProviderService auth
         });
     }
 
+    /// <summary>
+    ///  Returns a JSON object containing the configuration.
+    /// </summary>
+    /// <returns></returns>
     [HttpGet("/_matrix/_modas/debug/config")]
     public IActionResult Config() {
         return Ok(config);
@@ -32,17 +48,17 @@ public class DebugController(ModASConfiguration config, UserProviderService auth
 
     [HttpGet("/_matrix/_modas/debug/test_locate_users")]
     public async IAsyncEnumerable<string> TestLocateUsers([FromQuery] string startUser) {
-        List<AuthenticatedHomeserverGeneric> foundUsers = (await authHsProvider.GetValidUsers()).Select(x=>x.Value).ToList();
-        if(!foundUsers.Any(x=>x.WhoAmI.UserId == startUser)) {
+        List<AuthenticatedHomeserverGeneric> foundUsers = (await authHsProvider.GetValidUsers()).Select(x => x.Value).ToList();
+        if (!foundUsers.Any(x => x.WhoAmI.UserId == startUser)) {
             foundUsers.Add(await authHsProvider.GetImpersonatedHomeserver(startUser));
         }
-        
+
         List<string> processedRooms = [], processedUsers = [];
         var foundNew = true;
         while (foundNew) {
             var span1 = currentTransaction.StartSpan("iterateUsers", ApiConstants.TypeApp);
             foundNew = false;
-            var usersToProcess = foundUsers.Where(x => !processedUsers.Any(y=>x.WhoAmI.UserId == y)).ToFrozenSet();
+            var usersToProcess = foundUsers.Where(x => !processedUsers.Any(y => x.WhoAmI.UserId == y)).ToFrozenSet();
             Console.WriteLine($"Got {usersToProcess.Count} users: {string.Join(", ", usersToProcess)}");
 
             var rooms = usersToProcess.Select(async x => await x.GetJoinedRooms());
@@ -54,7 +70,7 @@ public class DebugController(ModASConfiguration config, UserProviderService auth
                     processedRooms.Add(room.RoomId);
                     var roomMembers = await room.GetMembersListAsync(false);
                     foreach (var roomMember in roomMembers) {
-                        if (roomMember.StateKey.EndsWith(':' + config.ServerName) && !foundUsers.Any(x=>x.WhoAmI.UserId == roomMember.StateKey)) {
+                        if (roomMember.StateKey.EndsWith(':' + config.ServerName) && !foundUsers.Any(x => x.WhoAmI.UserId == roomMember.StateKey)) {
                             foundUsers.Add(await authHsProvider.GetImpersonatedHomeserver(roomMember.StateKey));
                             foundNew = true;
                             yield return roomMember.StateKey;
diff --git a/ModAS.Server/Controllers/HomeController.cs b/ModAS.Server/Controllers/HomeController.cs
index 5fd309f..eb17966 100644
--- a/ModAS.Server/Controllers/HomeController.cs
+++ b/ModAS.Server/Controllers/HomeController.cs
@@ -3,17 +3,14 @@ using Microsoft.AspNetCore.Mvc;
 
 namespace ModAS.Server.Controllers;
 
+/// <summary>
+///    Manages the visual homepage.
+/// </summary>
 [ApiController]
 public class HomeController : Controller {
-    private readonly ILogger<HomeController> _logger;
-
-    public HomeController(ILogger<HomeController> logger) {
-        _logger = logger;
-    }
-
+    /// <inheritdoc cref="HomeController"/>
     [HttpGet("/_matrix/_modas")]
     public IActionResult Index() {
-        //return wwwroot/index.html
         return LocalRedirect("/index.html");
     }
 }
\ No newline at end of file
diff --git a/ModAS.Server/Extensions/RequestHeaderExtensinos.cs b/ModAS.Server/Extensions/RequestHeaderExtensinos.cs
new file mode 100644
index 0000000..e40ed8e
--- /dev/null
+++ b/ModAS.Server/Extensions/RequestHeaderExtensinos.cs
@@ -0,0 +1,17 @@
+using Microsoft.AspNetCore.Http.Headers;
+
+namespace MxApiExtensions.Extensions;
+
+public static class RequestHeaderExtensions {
+    public static bool TryGet<T>(this RequestHeaders headers, string name, out T? value) {
+        try {
+            value = headers.Get<T>(name);
+            return true;
+        }
+        catch (Exception) {
+            value = default;
+        }
+
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/ModAS.Server/ModAS.Server.csproj b/ModAS.Server/ModAS.Server.csproj
index 07cc67c..5bda8c0 100644
--- a/ModAS.Server/ModAS.Server.csproj
+++ b/ModAS.Server/ModAS.Server.csproj
@@ -12,6 +12,7 @@
         <StripSymbols>true</StripSymbols>
         <OptimizationPreference>Speed</OptimizationPreference>
         <TieredPGO>true</TieredPGO>
+        <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
     </PropertyGroup>
 
     <ItemGroup>
diff --git a/ModAS.Server/Program.cs b/ModAS.Server/Program.cs
index 0b3d121..248243f 100644
--- a/ModAS.Server/Program.cs
+++ b/ModAS.Server/Program.cs
@@ -9,6 +9,7 @@ using Elastic.Apm.AspNetCore;
 using Elastic.Apm.NetCoreAll;
 using LibMatrix;
 using LibMatrix.Services;
+using ModAS.Server.Authentication;
 using ModAS.Server.Services;
 using MxApiExtensions.Services;
 
@@ -114,13 +115,14 @@ Agent.AddFilter((ISpan span) => {
     return span;
 });
 
-///wwwroot
 app.UseFileServer();
-// app.UseStaticFiles();
-// app.UseDirectoryBrowser();
+
+app.UseRouting();
 
 app.UseCors("Open");
 
 app.MapControllers();
 
+app.UseMiddleware<AuthMiddleware>();
+
 app.Run();
\ No newline at end of file
diff --git a/ModAS.Server/Services/AuthenticationService.cs b/ModAS.Server/Services/AuthenticationService.cs
index 27e12ad..8efc08c 100644
--- a/ModAS.Server/Services/AuthenticationService.cs
+++ b/ModAS.Server/Services/AuthenticationService.cs
@@ -1,20 +1,28 @@
+using System.Net.Http.Headers;
 using LibMatrix;
 using LibMatrix.Services;
+using MxApiExtensions.Extensions;
 using MxApiExtensions.Services;
 
 namespace ModAS.Server.Services;
 
-public class AuthenticationService(ILogger<AuthenticationService> logger, ModASConfiguration config, IHttpContextAccessor request, HomeserverProviderService homeserverProviderService) {
+public class AuthenticationService(
+    ILogger<AuthenticationService> logger,
+    ModASConfiguration config,
+    IHttpContextAccessor request,
+    HomeserverProviderService homeserverProviderService) {
     private readonly HttpRequest _request = request.HttpContext!.Request;
 
     private static Dictionary<string, string> _tokenMap = new();
 
     internal string? GetToken(bool fail = true) {
-        string? token;
-        if (_request.Headers.TryGetValue("Authorization", out var tokens)) {
-            token = tokens.FirstOrDefault()?[7..];
+        //_request.GetTypedHeaders().Get<AuthenticationHeaderValue>("Authorization")?.Parameter != asr.HomeserverToken
+
+        string? token = null;
+        if (_request.GetTypedHeaders().TryGet<AuthenticationHeaderValue>("Authorization", out var authHeader) && !string.IsNullOrWhiteSpace(authHeader?.Parameter)) {
+            token = authHeader.Parameter;
         }
-        else {
+        else if (_request.Query.ContainsKey("access_token")) {
             token = _request.Query["access_token"];
         }
 
@@ -47,18 +55,13 @@ public class AuthenticationService(ILogger<AuthenticationService> logger, ModASC
                 .ToDictionary(l => l[0], l => l[1]);
         }
 
-
         if (_tokenMap.TryGetValue(token, out var mxid)) return mxid;
 
-        var lookupTasks = new Dictionary<string, Task<string?>>();
-        
-        
         logger.LogInformation("Looking up mxid for token {}", token);
         var hs = await homeserverProviderService.GetAuthenticatedWithToken(config.ServerName, token, config.HomeserverUrl);
         try {
             var res = hs.WhoAmI.UserId;
             logger.LogInformation("Got mxid {} for token {}", res, token);
-            await SaveMxidForToken(token, mxid);
 
             return res;
         }
@@ -70,10 +73,4 @@ public class AuthenticationService(ILogger<AuthenticationService> logger, ModASC
             throw;
         }
     }
-
-
-    public async Task SaveMxidForToken(string token, string mxid) {
-        _tokenMap.Add(token, mxid);
-        await File.AppendAllLinesAsync("token_map", new[] { $"{token}\t{mxid}" });
-    }
-}
+}
\ No newline at end of file
diff --git a/ModAS.Server/Services/ModASConfiguration.cs b/ModAS.Server/Services/ModASConfiguration.cs
index 063e838..90f8e9e 100644
--- a/ModAS.Server/Services/ModASConfiguration.cs
+++ b/ModAS.Server/Services/ModASConfiguration.cs
@@ -1,5 +1,8 @@
 namespace MxApiExtensions.Services;
 
+/// <summary>
+///    Configuration for ModAS.
+/// </summary>
 public class ModASConfiguration {
     public ModASConfiguration(IConfiguration configuration) {
         configuration.GetRequiredSection("ModAS").Bind(this);
@@ -7,4 +10,6 @@ public class ModASConfiguration {
 
     public string ServerName { get; set; }
     public string HomeserverUrl { get; set; }
+
+    public Dictionary<string, List<string>> Roles { get; set; }
 }
\ No newline at end of file
diff --git a/ModAS.Server/Services/PingTask.cs b/ModAS.Server/Services/PingTask.cs
new file mode 100644
index 0000000..99a8f40
--- /dev/null
+++ b/ModAS.Server/Services/PingTask.cs
@@ -0,0 +1,9 @@
+namespace ModAS.Server.Services;
+
+public class PingTask : IHostedService, IDisposable {
+    public Task StartAsync(CancellationToken cancellationToken) => throw new NotImplementedException();
+
+    public Task StopAsync(CancellationToken cancellationToken) => throw new NotImplementedException();
+
+    public void Dispose() => throw new NotImplementedException();
+}
\ No newline at end of file
diff --git a/ModAS.Server/Version.cs b/ModAS.Server/Version.cs
new file mode 100644
index 0000000..19aa81c
--- /dev/null
+++ b/ModAS.Server/Version.cs
@@ -0,0 +1,5 @@
+namespace Modas.Server;
+public static class Version
+{
+    public const string Text = "master@v0+";
+}
diff --git a/ModAS.Server/appsettings.Development.json b/ModAS.Server/appsettings.Development.json
index ef51c09..f9b6b0f 100644
--- a/ModAS.Server/appsettings.Development.json
+++ b/ModAS.Server/appsettings.Development.json
@@ -6,6 +6,7 @@
     }
   },
   "ModAS": {
-    
+    "ServerName": "rory.gay",
+    "HomeserverUrl": "https://matrix.rory.gay"
   }
 }