diff --git a/extra/admin-api/Spacebar.UApi/Controllers/GuildTemplatesController.cs b/extra/admin-api/Spacebar.UApi/Controllers/GuildTemplatesController.cs index 6206a6c..270b554 100644 --- a/extra/admin-api/Spacebar.UApi/Controllers/GuildTemplatesController.cs +++ b/extra/admin-api/Spacebar.UApi/Controllers/GuildTemplatesController.cs @@ -8,9 +8,9 @@ [ApiController] public class GuildTemplatesController(ILogger logger, SpacebarDbContext db, SpacebarAspNetAuthenticationService authService, TemplateImportService importService) : ControllerBase { - [HttpPost("/api/v10/guilds/templates/{templateId}")] - public async Task UseTemplate(string templateId, UseGuildTemplateRequest request) { - var user = await authService.GetCurrentUserAsync(Request); - await importService.CreateGuildFromTemplateById(templateId, request, user); - } + // [HttpPost("/api/v10/guilds/templates/{templateId}")] + // public async Task UseTemplate(string templateId, UseGuildTemplateRequest request) { + // var user = await authService.GetCurrentUserAsync(Request); + // await importService.CreateGuildFromTemplateById(templateId, request, user); + // } } \ No newline at end of file diff --git a/extra/admin-api/Spacebar.UApi/Program.cs b/extra/admin-api/Spacebar.UApi/Program.cs index d6bb0d9..0a2a1b3 100644 --- a/extra/admin-api/Spacebar.UApi/Program.cs +++ b/extra/admin-api/Spacebar.UApi/Program.cs @@ -1,6 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using ArcaneLibs; +using ArcaneLibs.Extensions; using Microsoft.EntityFrameworkCore; +using Spacebar.ConfigModel; +using Spacebar.ConfigModel.Extensions; using Spacebar.Interop.Authentication; using Spacebar.Interop.Authentication.AspNetCore; using Spacebar.Models.Db.Contexts; @@ -30,6 +34,25 @@ .EnableDetailedErrors(); }); +if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("CONFIG_PATH"))) + builder.Services.AddSingleton(sp => { + var cfgPath = Environment.GetEnvironmentVariable("CONFIG_PATH"); + sp.GetService>().LogInformation("Using config from path: {path}", cfgPath); + return JsonSerializer.Deserialize(File.ReadAllText(cfgPath)); + }); +else + builder.Services.AddSingleton(sp => { + sp.GetService>().LogInformation("Using config from database..."); + var db = sp.GetService(); + var config = db.Configs + .OrderBy(x => x.Key) + .ToDictionary(x => x.Key, x => x.Value); + + var readConfig = config.ToNestedJsonObject(); + return readConfig.Deserialize(); + }); + +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -45,6 +68,7 @@ app.UseAuthorization(); app.MapControllers(); +StreamingHttpClient.LogRequests = false; app.Use((context, next) => { context.Response.Headers["Access-Control-Allow-Origin"] = "*"; context.Response.Headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"; @@ -57,13 +81,38 @@ return next(); }); +// add some special sauce +app.Map("/", async context => { + var client = new StreamingHttpClient(); + var cfg = context.RequestServices.GetService(); + var requestMessage = new HttpRequestMessage(new HttpMethod(context.Request.Method), cfg.FallbackApiEndpoint); + + foreach (var header in context.Request.Headers) + if (header.Key is not ("Accept-Encoding" or "Host")) + requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()); + + var responseMessage = await client.SendUnhandledAsync(requestMessage, CancellationToken.None); + context.Response.StatusCode = (int)responseMessage.StatusCode; + + foreach (var header in responseMessage.Headers) context.Response.Headers[header.Key] = header.Value.ToArray(); + foreach (var header in responseMessage.Content.Headers) context.Response.Headers[header.Key] = header.Value.ToArray(); + + // await responseMessage.Content.CopyToAsync(context.Response.Body); + var txt = await responseMessage.Content.ReadAsStringAsync(); + txt = txt.Replace("your very own Spacebar instance", "your very own Spacebar instance with μAPI"); + var data = txt.AsBytes().ToArray(); + context.Response.Headers.ContentLength = data.Length; + await context.Response.Body.WriteAsync(data); +}); + // fallback to proxy in case we dont have a specific endpoint... // TODO config app.MapFallback("{*_}", async context => { var client = new StreamingHttpClient(); + var cfg = context.RequestServices.GetService(); var requestMessage = new HttpRequestMessage( new HttpMethod(context.Request.Method), - "http://api.old.server.spacebar.chat" + context.Request.Path + context.Request.QueryString + cfg.FallbackApiEndpoint + context.Request.Path + context.Request.QueryString ) { Content = new StreamContent(context.Request.Body) }; diff --git a/extra/admin-api/Spacebar.UApi/Services/UApiConfiguration.cs b/extra/admin-api/Spacebar.UApi/Services/UApiConfiguration.cs new file mode 100644 index 0000000..84b0149 --- /dev/null +++ b/extra/admin-api/Spacebar.UApi/Services/UApiConfiguration.cs @@ -0,0 +1,10 @@ +namespace Spacebar.UApi.Services; + +public class UApiConfiguration { + public UApiConfiguration(IConfiguration config) { + config.GetRequiredSection("Spacebar").GetRequiredSection("UApi").Bind(this); + } + + // ... for unhandled routes + public string? FallbackApiEndpoint { get; set; } +} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.UApi/appsettings.Development.json b/extra/admin-api/Spacebar.UApi/appsettings.Development.json index 0c208ae..524b10d 100644 --- a/extra/admin-api/Spacebar.UApi/appsettings.Development.json +++ b/extra/admin-api/Spacebar.UApi/appsettings.Development.json @@ -4,5 +4,17 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "ConnectionStrings": { + "Spacebar": "Host=127.0.0.1; Username=postgres; Database=spacebar; Port=5433; Include Error Detail=true; Maximum Pool Size=1000; Command Timeout=6000; Timeout=600;" + }, + "Spacebar": { + "Authentication": { + "PublicKeyPath": "../../../jwt.key.pub", + "PrivateKeyPath": "../../../jwt.key" + }, + "UApi":{ + "FallbackApiEndpoint": "http://localhost:3113" + } } } diff --git a/extra/admin-api/Spacebar.UApi/deps.json b/extra/admin-api/Spacebar.UApi/deps.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/extra/admin-api/Spacebar.UApi/deps.json @@ -0,0 +1 @@ +[] diff --git a/extra/admin-api/outputs.nix b/extra/admin-api/outputs.nix index 4343010..fa80b39 100644 --- a/extra/admin-api/outputs.nix +++ b/extra/admin-api/outputs.nix @@ -218,6 +218,22 @@ proj.Spacebar-Models-Generic ]; }; + Spacebar-UApi = makeNupkg { + name = "Spacebar.UApi"; + nugetDeps = Spacebar.UApi/deps.json; + projectFile = "Spacebar.UApid.csproj"; + srcRoot = ./Spacebar.UApi; + packNupkg = false; + projectReferences = [ + proj.Spacebar-DataMappings-Generic + proj.Spacebar-Interop-Authentication + proj.Spacebar-Interop-Authentication-AspNetCore + proj.Spacebar-Interop-Replication-Abstractions + proj.Spacebar-Models-Db + proj.Spacebar-Models-Gateway + proj.Spacebar-Models-Generic + ]; + }; # Spacebar-AdminApi-TestClient = makeNupkg { # name = "Spacebar.AdminApi.TestClient"; # projectFile = "Utilities/Spacebar.AdminApi.TestClient/Spacebar.AdminApi.TestClient.csproj"; @@ -257,6 +273,15 @@ Expose = [ "5000" ]; }; }; + containers.docker.uapi = pkgs.dockerTools.buildLayeredImage { + name = "spacebar-server-ts-cdn-cs"; + tag = builtins.replaceStrings [ "+" ] [ "_" ] self.packages.${system}.Spacebar-AdminApi.version; + contents = [ self.packages.${system}.Spacebar-AdminApi ]; + config = { + Cmd = [ "${lib.getExe self.outputs.packages.${system}.Spacebar-AdminApi}" ]; + Expose = [ "5000" ]; + }; + }; } ) // { diff --git a/nix/modules/default/cs/uapi.nix b/nix/modules/default/cs/uapi.nix new file mode 100644 index 0000000..262ce6b --- /dev/null +++ b/nix/modules/default/cs/uapi.nix @@ -0,0 +1,163 @@ +self: +{ + config, + lib, + pkgs, + spacebar, + ... +}: + +let + secrets = import ../secrets.nix { inherit lib config; }; + cfg = config.services.spacebarchat-server; + jsonFormat = pkgs.formats.json { }; +in +{ + imports = [ ]; + options.services.spacebarchat-server.uApi = lib.mkOption { + default = { }; + description = "Configuration for C# API overlay."; + type = lib.types.submodule { + options = { + enable = lib.mkEnableOption "Enable C# API overlay."; + extraConfiguration = lib.mkOption { + type = jsonFormat.type; + default = import ./default-appsettings-json.nix; + description = "Extra appsettings.json configuration for the C# API overlay."; + }; + }; + }; + }; + + config = lib.mkIf cfg.uApi.enable ( + let + makeServerTsService = ( + conf: + lib.recursiveUpdate + (lib.recursiveUpdate { + documentation = [ "https://docs.spacebar.chat/" ]; + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + environment = secrets.systemdEnvironment; + serviceConfig = { + LoadCredential = secrets.systemdLoadCredentials; + + User = "spacebarchat"; + Group = "spacebarchat"; + DynamicUser = false; + + LockPersonality = true; + NoNewPrivileges = true; + + ProtectClock = true; + ProtectControlGroups = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateUsers = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + "@chown" # Required for copying files with FICLONE, apparently. + ]; + CapabilityBoundingSet = [ + "~CAP_SYS_ADMIN" + "~CAP_AUDIT_*" + "~CAP_NET_(BIND_SERVICE|BROADCAST|RAW)" + "~CAP_NET_ADMIN" # No use for this as we don't currently use iptables for enforcing instance bans + "~CAP_SYS_TIME" + "~CAP_KILL" + "~CAP_(DAC_*|FOWNER|IPC_OWNER)" + "~CAP_LINUX_IMMUTABLE" + "~CAP_IPC_LOCK" + "~CAP_BPF" + "~CAP_SYS_TTY_CONFIG" + "~CAP_SYS_BOOT" + "~CAP_SYS_CHROOT" + "~CAP_BLOCK_SUSPEND" + "~CAP_LEASE" + "~CAP_(CHOWN|FSETID|FSETFCAP)" # Check if we need CAP_CHOWN for `fchown()` (FICLONE)? + "~CAP_SET(UID|GID|PCAP)" + "~CAP_MAC_*" + "~CAP_SYS_PTRACE" + "~CAP_SYS_(NICE|RESOURCE)" + "~CAP_SYS_RAWIO" + "~CAP_SYSLOG" + ]; + RestrictSUIDSGID = true; + + WorkingDirectory = "/var/lib/spacebar"; + StateDirectory = "spacebar"; + StateDirectoryMode = "0750"; + RuntimeDirectory = "spacebar"; + RuntimeDirectoryMode = "0750"; + ReadWritePaths = [ cfg.cdnPath ]; + NoExecPaths = [ cfg.cdnPath ]; + + Restart = "on-failure"; + RestartSec = 10; + StartLimitBurst = 5; + UMask = "077"; + } + // lib.optionalAttrs (cfg.databaseFile != null) { EnvironmentFile = cfg.databaseFile; }; + } conf) + { + } + ); + in + { + assertions = [ + { + assertion = + cfg.adminApi.extraConfiguration ? ConnectionStrings + && cfg.adminApi.extraConfiguration.ConnectionStrings ? Spacebar + && cfg.adminApi.extraConfiguration.ConnectionStrings.Spacebar != null; + message = '' + Admin API: Setting a database connection string in extraConfiguration (`extraConfiguration.ConnectionStrings.Spacebar`) is required when using C# services. + Example: Host=127.0.0.1; Username=Spacebar; Password=SuperSecurePassword12; Database=spacebar; Port=5432; Include Error Detail=true; Maximum Pool Size=1000; Command Timeout=6000; Timeout=600; + ''; + } + ]; + + services.spacebarchat-server.settings.admin = { + endpointPublic = "http${if cfg.adminApiEndpoint.useSsl then "s" else ""}://${cfg.adminApiEndpoint.host}:${toString cfg.adminApiEndpoint.publicPort}"; + endpointPrivate = "http://127.0.0.1:${builtins.toString cfg.adminApiEndpoint.localPort}"; + }; + + systemd.services.spacebar-admin-api = makeServerTsService { + description = "Spacebar Server - Admin API"; + environment = builtins.mapAttrs (_: val: builtins.toString val) ( + { + # things we set by default... + EVENT_TRANSMISSION = "unix"; + EVENT_SOCKET_PATH = "/run/spacebar/"; + } + // cfg.extraEnvironment + // { + # things we force... + # CONFIG_PATH = configFile; + CONFIG_READONLY = 1; + ASPNETCORE_URLS = "http://0.0.0.0:${toString cfg.uApi.localPort}"; + STORAGE_LOCATION = cfg.cdnPath; + APPSETTINGS_PATH = jsonFormat.generate "appsettings.spacebar-uapi.json" (lib.recursiveUpdate (import ./default-appsettings-json.nix) cfg.uApi.extraConfiguration); + } + ); + serviceConfig = { + ExecStart = "${self.packages.${pkgs.stdenv.hostPlatform.system}.Spacebar-UApi}/bin/Spacebar.UApi"; + }; + }; + } + ); +} diff --git a/nix/modules/default/default.nix b/nix/modules/default/default.nix index ac20d3c..8306aba 100644 --- a/nix/modules/default/default.nix +++ b/nix/modules/default/default.nix @@ -47,6 +47,7 @@ ./users.nix (import ./cs/gateway-offload-cs.nix self) (import ./cs/admin-api.nix self) + (import ./cs/uapi.nix self) ]; options.services.spacebarchat-server = let