diff options
author | Rory& <root@rory.gay> | 2024-02-25 09:15:06 +0100 |
---|---|---|
committer | Rory& <root@rory.gay> | 2024-02-25 09:15:06 +0100 |
commit | b27978162f3215f49837fa72d81c94078776db0d (patch) | |
tree | 442b63ab0819d62d8f366bdddc1cb42a74addb8c /MatrixMediaGate | |
download | MatrixMediaGate-b27978162f3215f49837fa72d81c94078776db0d.tar.xz |
Initial commit
Diffstat (limited to 'MatrixMediaGate')
-rw-r--r-- | MatrixMediaGate/MatrixMediaGate.csproj | 10 | ||||
-rw-r--r-- | MatrixMediaGate/Program.cs | 83 | ||||
-rw-r--r-- | MatrixMediaGate/Properties/launchSettings.json | 41 | ||||
-rw-r--r-- | MatrixMediaGate/ProxyConfiguration.cs | 14 | ||||
-rw-r--r-- | MatrixMediaGate/Services/AuthValidator.cs | 71 | ||||
-rw-r--r-- | MatrixMediaGate/appsettings.Development.json | 17 | ||||
-rw-r--r-- | MatrixMediaGate/appsettings.json | 9 | ||||
-rw-r--r-- | MatrixMediaGate/deps.nix | 5 |
8 files changed, 250 insertions, 0 deletions
diff --git a/MatrixMediaGate/MatrixMediaGate.csproj b/MatrixMediaGate/MatrixMediaGate.csproj new file mode 100644 index 0000000..f847a82 --- /dev/null +++ b/MatrixMediaGate/MatrixMediaGate.csproj @@ -0,0 +1,10 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <TargetFramework>net8.0</TargetFramework> + <Nullable>enable</Nullable> + <ImplicitUsings>enable</ImplicitUsings> + <InvariantGlobalization>true</InvariantGlobalization> + </PropertyGroup> + +</Project> diff --git a/MatrixMediaGate/Program.cs b/MatrixMediaGate/Program.cs new file mode 100644 index 0000000..a812d78 --- /dev/null +++ b/MatrixMediaGate/Program.cs @@ -0,0 +1,83 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text.Json; +using MatrixMediaGate; +using MatrixMediaGate.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddSingleton<ProxyConfiguration>(); +builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); +builder.Services.AddScoped<AuthValidator>(); + +var app = builder.Build(); + +async Task Proxy(ProxyConfiguration cfg, AuthValidator auth, HttpContext ctx, ILogger<Program> logger) { + if (ctx is null) return; + var path = ctx.Request.Path.Value; + if(path is null) return; + if (path.StartsWith('/')) + path = path[1..]; + path += ctx.Request.QueryString.Value; + +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + auth.UpdateAuth(); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + + using var handler = new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.None }; + + using var hc = new HttpClient(handler) { BaseAddress = new Uri(cfg.Upstream) }; + var method = new HttpMethod(ctx.Request.Method); + using var req = new HttpRequestMessage(method, path); + foreach (var header in ctx.Request.Headers) { + if (header.Key != "Accept-Encoding" && header.Key != "Content-Type" && header.Key != "Content-Length") req.Headers.Add(header.Key, header.Value.ToArray()); + } + + if (ctx.Request.ContentLength > 0) { + req.Content = new StreamContent(ctx.Request.Body); + if (ctx.Request.ContentType != null) req.Content.Headers.ContentType = new MediaTypeHeaderValue(ctx.Request.ContentType); + if (ctx.Request.ContentLength != null) req.Content.Headers.ContentLength = ctx.Request.ContentLength; + } + + logger.LogInformation($"Proxying {method} {path} to {hc.BaseAddress}{path}"); + + using var response = await hc.SendAsync(req, HttpCompletionOption.ResponseHeadersRead); + ctx.Response.Headers.Clear(); + foreach (var header in response.Headers) { + if (header.Key != "Transfer-Encoding") ctx.Response.Headers[header.Key] = header.Value.ToArray(); + } + + ctx.Response.StatusCode = (int)response.StatusCode; + ctx.Response.ContentType = response.Content.Headers.ContentType?.ToString() ?? "application/json"; + await ctx.Response.StartAsync(); + await using var content = await response.Content.ReadAsStreamAsync(); + await content.CopyToAsync(ctx.Response.Body); + await ctx.Response.Body.FlushAsync(); + await ctx.Response.CompleteAsync(); +} + +async Task ProxyMedia(string serverName, ProxyConfiguration cfg, AuthValidator auth, HttpContext ctx, ILogger<Program> logger) { + if (cfg.TrustedServers.Contains(serverName) || auth.ValidateAuth()) { + await Proxy(cfg, auth, ctx, logger); + } + else { + ctx.Response.StatusCode = 403; + ctx.Response.ContentType = "application/json"; + await ctx.Response.StartAsync(); + var json = JsonSerializer.Serialize(new { errcode = "M_FORBIDDEN", error = "Unauthenticated access to remote media has been disabled on this server." }); + await ctx.Response.WriteAsync(json); + await ctx.Response.Body.FlushAsync(); + await ctx.Response.CompleteAsync(); + } +} + +app.Map("{*_}", Proxy); + +foreach (var route in (string[]) [ + "/_matrix/media/{version}/download/{serverName}/{mediaId}", + "/_matrix/media/{version}/download/{serverName}/{mediaId}/{fileName}", + "/_matrix/media/{version}/thumbnail/{serverName}/{mediaId}", + ]) + app.Map(route, ProxyMedia); + +app.Run(); \ No newline at end of file diff --git a/MatrixMediaGate/Properties/launchSettings.json b/MatrixMediaGate/Properties/launchSettings.json new file mode 100644 index 0000000..dc26c8d --- /dev/null +++ b/MatrixMediaGate/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:41407", + "sslPort": 44394 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5011", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7227;http://localhost:5011", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/MatrixMediaGate/ProxyConfiguration.cs b/MatrixMediaGate/ProxyConfiguration.cs new file mode 100644 index 0000000..0a126d4 --- /dev/null +++ b/MatrixMediaGate/ProxyConfiguration.cs @@ -0,0 +1,14 @@ +namespace MatrixMediaGate; + +public class ProxyConfiguration { + //bind to config + public ProxyConfiguration(IConfiguration configuration) + { + configuration.GetRequiredSection("ProxyConfiguration").Bind(this); + } + + public required string Upstream { get; set; } + public required string Host { get; set; } + public required List<string> TrustedServers { get; set; } + public bool ForceHost { get; set; } +} \ No newline at end of file diff --git a/MatrixMediaGate/Services/AuthValidator.cs b/MatrixMediaGate/Services/AuthValidator.cs new file mode 100644 index 0000000..08ccd14 --- /dev/null +++ b/MatrixMediaGate/Services/AuthValidator.cs @@ -0,0 +1,71 @@ +using System.Net; +using System.Text.Json; + +namespace MatrixMediaGate.Services; + +public class AuthValidator(ILogger<AuthValidator> logger, ProxyConfiguration cfg, IHttpContextAccessor ctx) { + private static Dictionary<string, DateTime> _authCache = new(); + + public async Task<bool> UpdateAuth() { + if (ctx.HttpContext is null) return false; + if (ctx.HttpContext.Connection.RemoteIpAddress is null) return false; + var remote = ctx.HttpContext.Connection.RemoteIpAddress.ToString(); + + + if (_authCache.TryGetValue(remote, out var value)) { + if (value > DateTime.Now.AddSeconds(30)) { + return true; + } + + _authCache.Remove(remote); + } + + string? token = getToken(); + if (token is null) return false; + + using var hc = new HttpClient(); + using var req = new HttpRequestMessage(HttpMethod.Get, $"{cfg.Upstream}/_matrix/client/v3/account/whoami?access_token={token}"); + req.Headers.Host = cfg.Host; + var response = await hc.SendAsync(req); + + if (response.Content.Headers.ContentType?.MediaType != "application/json") return false; + var content = await response.Content.ReadAsStringAsync(); + var json = JsonDocument.Parse(content); + if (json.RootElement.TryGetProperty("user_id", out var userId)) { + _authCache[remote] = DateTime.Now.AddMinutes(5); + logger.LogInformation("Authenticated {userId} on {remote}, expiring at {time}", userId, remote, _authCache[remote]); + return true; + } + + return false; + } + + public bool ValidateAuth() { + if (ctx.HttpContext is null) return false; + if (ctx.HttpContext.Connection.RemoteIpAddress is null) return false; + var remote = ctx.HttpContext.Connection.RemoteIpAddress.ToString(); + + if (_authCache.ContainsKey(remote)) { + if (_authCache[remote] > DateTime.Now) { + return true; + } + + _authCache.Remove(remote); + } + + return false; + } + + private string? getToken() { + if (ctx is null) return null; + if (ctx.HttpContext.Request.Headers.ContainsKey("Authorization")) { + return ctx.HttpContext.Request.Headers["Authorization"].ToString().Split(' ', 2)[1]; + } + else if (ctx.HttpContext.Request.Query.ContainsKey("access_token")) { + return ctx.HttpContext.Request.Query["access_token"]!; + } + else { + return null; + } + } +} \ No newline at end of file diff --git a/MatrixMediaGate/appsettings.Development.json b/MatrixMediaGate/appsettings.Development.json new file mode 100644 index 0000000..627ac37 --- /dev/null +++ b/MatrixMediaGate/appsettings.Development.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Information", + "Microsoft.AspNetCore.Routing": "Warning", + "Microsoft.AspNetCore.Mvc": "Warning" + } + }, + "ProxyConfiguration": { + "Upstream": "https://rory.gay", + "Host": "matrix.rory.gay", + "TrustedServers": [ + "rory.gay" + ] + } +} diff --git a/MatrixMediaGate/appsettings.json b/MatrixMediaGate/appsettings.json new file mode 100644 index 0000000..4d56694 --- /dev/null +++ b/MatrixMediaGate/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/MatrixMediaGate/deps.nix b/MatrixMediaGate/deps.nix new file mode 100644 index 0000000..3d0b76b --- /dev/null +++ b/MatrixMediaGate/deps.nix @@ -0,0 +1,5 @@ +{ fetchNuGet }: [ + (fetchNuGet { pname = "Microsoft.DotNet.ILCompiler"; version = "8.0.1"; sha256 = "1pmh2wdw7n0906x7qc2mlgjhyndap78dm60zrz76bbmzhwc4r0mv"; }) + (fetchNuGet { pname = "Microsoft.NET.ILLink.Tasks"; version = "8.0.1"; sha256 = "1drbgqdcvbpisjn8mqfgba1pwb6yri80qc4mfvyczqwrcsj5k2ja"; }) + (fetchNuGet { pname = "runtime.linux-x64.Microsoft.DotNet.ILCompiler"; version = "8.0.1"; sha256 = "0mky32k6kv2iy14c89xbznyrr33ni616s85z40ny43j5w9scxh04"; }) +] |