diff --git a/.gitignore b/.gitignore index b523ad6..6432d04 100644 --- a/.gitignore +++ b/.gitignore @@ -57,4 +57,6 @@ nunit-*.xml *.db appsettings.json -appsettings.Development.json \ No newline at end of file +appsettings.Development.json +build/ +publish/ diff --git a/.idea/.idea.Abyss/.idea/dictionaries/project.xml b/.idea/.idea.Abyss/.idea/dictionaries/project.xml new file mode 100644 index 0000000..258a037 --- /dev/null +++ b/.idea/.idea.Abyss/.idea/dictionaries/project.xml @@ -0,0 +1,7 @@ + + + + abyssctl + + + \ No newline at end of file diff --git a/.idea/.idea.Abyss/.idea/workspace.xml b/.idea/.idea.Abyss/.idea/workspace.xml index fb33dea..21de6d0 100644 --- a/.idea/.idea.Abyss/.idea/workspace.xml +++ b/.idea/.idea.Abyss/.idea/workspace.xml @@ -4,15 +4,36 @@ Abyss/Abyss.csproj Abyss/Abyss.csproj AbyssCli/AbyssCli.csproj + abyssctl/abyssctl.csproj + + + + + + + + + + + + + + + + - + + + + + @@ -85,51 +117,44 @@ - { - "keyToString": { - ".NET Launch Settings Profile.Abyss: http.executor": "Run", - ".NET Launch Settings Profile.Abyss: https.executor": "Debug", - ".NET Project.AbyssCli.executor": "Run", - "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true", - "ModuleVcsDetector.initialDetectionPerformed": "true", - "Publish to folder.Publish Abyss to folder x86.executor": "Run", - "Publish to folder.Publish Abyss to folder.executor": "Run", - "RunOnceActivity.ShowReadmeOnStart": "true", - "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", - "RunOnceActivity.git.unshallow": "true", - "XThreadsFramesViewSplitterKey": "0.55813956", - "git-widget-placeholder": "main", - "last_opened_file_path": "/home/acite/AciteProjects/Abyss/README.md", - "node.js.detected.package.eslint": "true", - "node.js.detected.package.tslint": "true", - "node.js.selected.package.eslint": "(autodetect)", - "node.js.selected.package.tslint": "(autodetect)", - "nodejs_package_manager_path": "npm", - "settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable", - "vue.rearranger.settings.migration": "true" + - - - - - - - - - - - +}]]> + + + - - @@ -254,6 +278,17 @@ + + + + + + + + + + + diff --git a/Abyss.sln b/Abyss.sln index 08c5c17..ce4ab0d 100644 --- a/Abyss.sln +++ b/Abyss.sln @@ -2,6 +2,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abyss", "Abyss\Abyss.csproj", "{3337C1CD-2419-4922-BC92-AF1A825DDF23}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "abyssctl", "abyssctl\abyssctl.csproj", "{F6DEF111-0CAD-4DCC-8957-7EBAFCF3D2C4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +14,9 @@ Global {3337C1CD-2419-4922-BC92-AF1A825DDF23}.Debug|Any CPU.Build.0 = Debug|Any CPU {3337C1CD-2419-4922-BC92-AF1A825DDF23}.Release|Any CPU.ActiveCfg = Release|Any CPU {3337C1CD-2419-4922-BC92-AF1A825DDF23}.Release|Any CPU.Build.0 = Release|Any CPU + {F6DEF111-0CAD-4DCC-8957-7EBAFCF3D2C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6DEF111-0CAD-4DCC-8957-7EBAFCF3D2C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6DEF111-0CAD-4DCC-8957-7EBAFCF3D2C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6DEF111-0CAD-4DCC-8957-7EBAFCF3D2C4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Abyss.sln.DotSettings.user b/Abyss.sln.DotSettings.user index 6bd98a5..726d3e2 100644 --- a/Abyss.sln.DotSettings.user +++ b/Abyss.sln.DotSettings.user @@ -7,6 +7,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded diff --git a/Abyss/Abyss.csproj b/Abyss/Abyss.csproj index ec64339..a5b2311 100644 --- a/Abyss/Abyss.csproj +++ b/Abyss/Abyss.csproj @@ -6,6 +6,10 @@ enable + + ../build/ + + diff --git a/Abyss/Components/Controllers/Security/UserController.cs b/Abyss/Components/Controllers/Security/UserController.cs index c5664e1..ce725bb 100644 --- a/Abyss/Components/Controllers/Security/UserController.cs +++ b/Abyss/Components/Controllers/Security/UserController.cs @@ -14,7 +14,7 @@ namespace Abyss.Components.Controllers.Security; [ApiController] [Route("api/[controller]")] [EnableRateLimiting("Fixed")] -public class UserController(UserService userService, ILogger logger) : BaseController +public class UserController(UserService userService) : BaseController { [HttpGet("{user}")] public async Task Challenge(string user) diff --git a/Abyss/Components/Controllers/Task/TaskController.cs b/Abyss/Components/Controllers/Task/TaskController.cs index 00adfaa..addc1df 100644 --- a/Abyss/Components/Controllers/Task/TaskController.cs +++ b/Abyss/Components/Controllers/Task/TaskController.cs @@ -35,27 +35,26 @@ public class TaskController(ConfigureService config, TaskService taskService) : return Ok(JsonConvert.SerializeObject(r, Formatting.Indented)); } - [HttpGet("{id}")] - public async Task GetTask(string id) - { - throw new NotImplementedException(); - } - - [HttpPatch("{id}")] - public async Task PutChip(string id) - { - throw new NotImplementedException(); - } - - [HttpPost("{id}")] - public async Task VerifyChip(string id) - { - throw new NotImplementedException(); - } - - [HttpDelete("{id}")] - public async Task DeleteTask(string id) - { - throw new NotImplementedException(); - } + // [HttpGet("{id}")] + // public async Task GetTask(string id) + // { + // throw new NotImplementedException(); + // } + // + // [HttpPatch("{id}")] + // public async Task PutChip(string id) + // { + // throw new NotImplementedException(); + // } + // + // [HttpPost("{id}")] + // public async Task VerifyChip(string id) + // { + // throw new NotImplementedException(); + // } + // [HttpDelete("{id}")] + // public async Task DeleteTask(string id) + // { + // throw new NotImplementedException(); + // } } \ No newline at end of file diff --git a/Abyss/Components/Services/Admin/Attributes/Module.cs b/Abyss/Components/Services/Admin/Attributes/Module.cs new file mode 100644 index 0000000..c3e1a4a --- /dev/null +++ b/Abyss/Components/Services/Admin/Attributes/Module.cs @@ -0,0 +1,29 @@ +using System.Reflection; +using Abyss.Components.Services.Admin.Interfaces; + +namespace Abyss.Components.Services.Admin.Attributes; + +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +public class Module(int head) : Attribute +{ + public int Head { get; } = head; + + public static Type[] Modules + { + get + { + Assembly assembly = Assembly.GetExecutingAssembly(); + Type attributeType = typeof(Module); + const string targetNamespace = "Abyss.Components.Services.Admin.Modules"; + + var moduleTypes = assembly.GetTypes() + .Where(t => t is { IsClass: true, IsAbstract: false, IsInterface: false }) + .Where(t => t.Namespace == targetNamespace) + .Where(t => typeof(IModule).IsAssignableFrom(t)) + .Where(t => t.IsDefined(attributeType, inherit: false)) + .ToArray(); + + return moduleTypes; + } + } +} \ No newline at end of file diff --git a/Abyss/Components/Services/Admin/CtlService.cs b/Abyss/Components/Services/Admin/CtlService.cs new file mode 100644 index 0000000..0073d0e --- /dev/null +++ b/Abyss/Components/Services/Admin/CtlService.cs @@ -0,0 +1,110 @@ +using System.Net.Sockets; +using System.Text; + +using Abyss.Components.Static; +using Abyss.Model.Admin; +using Newtonsoft.Json; + +using System.Reflection; +using Abyss.Components.Services.Admin.Interfaces; +using Module = Abyss.Components.Services.Admin.Attributes.Module; + +namespace Abyss.Components.Services.Admin; + +public class CtlService(ILogger logger, IServiceProvider serviceProvider) : IHostedService +{ + private readonly string _socketPath = "ctl.sock"; + + private Task? _executingTask; + private CancellationTokenSource? _cts; + private Dictionary _handlers = new(); + + public Task StartAsync(CancellationToken cancellationToken) + { + var t = Module.Modules; + foreach (var module in t) + { + var attr = module.GetCustomAttribute(); + if (attr != null) + { + _handlers[attr.Head] = module; + } + } + + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _executingTask = ExecuteAsync(_cts.Token); + return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + if (_executingTask == null) + return; + + try + { + _cts?.CancelAsync(); + } + finally + { + await Task.WhenAny(_executingTask, + Task.Delay(Timeout.Infinite, cancellationToken)); + } + } + + private async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (File.Exists(_socketPath)) + { + File.Delete(_socketPath); + } + + var endPoint = new UnixDomainSocketEndPoint(_socketPath); + + using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + socket.Bind(endPoint); + socket.Listen(5); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + var clientSocket = await socket.AcceptAsync(stoppingToken); + _ = HandleClientAsync(clientSocket, stoppingToken); + } + catch (OperationCanceledException) + { + break; + } + } + } + + private async Task HandleClientAsync(Socket clientSocket, CancellationToken stoppingToken) + { + async Task _400() + { + await clientSocket.WriteBase64Async(Ctl.MakeBase64(400, ["Bad Request"]), stoppingToken); + } + + try + { + var s = Encoding.UTF8.GetString( + Convert.FromBase64String(await clientSocket.ReadBase64Async(stoppingToken))); + var json = JsonConvert.DeserializeObject(s); + + if (json == null || !_handlers.TryGetValue(json.Head, out var handler)) + { + await _400(); + return; + } + + var module = (serviceProvider.GetRequiredService(handler) as IModule)!; + var r = await module.ExecuteAsync(json, stoppingToken); + await clientSocket.WriteBase64Async(Ctl.MakeBase64(r.Head, r.Params), stoppingToken); + } + catch (Exception e) + { + logger.LogError(e, "Error while handling client connection"); + } + } +} \ No newline at end of file diff --git a/Abyss/Components/Services/Admin/Interfaces/IModule.cs b/Abyss/Components/Services/Admin/Interfaces/IModule.cs new file mode 100644 index 0000000..a599879 --- /dev/null +++ b/Abyss/Components/Services/Admin/Interfaces/IModule.cs @@ -0,0 +1,8 @@ +using Abyss.Model.Admin; + +namespace Abyss.Components.Services.Admin.Interfaces; + +public interface IModule +{ + public Task ExecuteAsync(Ctl request, CancellationToken ct); +} \ No newline at end of file diff --git a/Abyss/Components/Services/Admin/Modules/HelloModule.cs b/Abyss/Components/Services/Admin/Modules/HelloModule.cs new file mode 100644 index 0000000..599aa15 --- /dev/null +++ b/Abyss/Components/Services/Admin/Modules/HelloModule.cs @@ -0,0 +1,18 @@ +using Abyss.Components.Services.Admin.Attributes; +using Abyss.Components.Services.Admin.Interfaces; +using Abyss.Model.Admin; + +namespace Abyss.Components.Services.Admin.Modules; + +[Module(100)] +public class HelloModule: IModule +{ + public async Task ExecuteAsync(Ctl request, CancellationToken ct) + { + return await Task.FromResult(new Ctl + { + Head = 200, + Params = ["Hi"], + }); + } +} \ No newline at end of file diff --git a/Abyss/Components/Services/Admin/Modules/VersionModule.cs b/Abyss/Components/Services/Admin/Modules/VersionModule.cs new file mode 100644 index 0000000..58646ab --- /dev/null +++ b/Abyss/Components/Services/Admin/Modules/VersionModule.cs @@ -0,0 +1,14 @@ +using Abyss.Components.Services.Admin.Attributes; +using Abyss.Components.Services.Admin.Interfaces; +using Abyss.Model.Admin; + +namespace Abyss.Components.Services.Admin.Modules; + +[Module(101)] +public class VersionModule: IModule +{ + public async Task ExecuteAsync(Ctl request, CancellationToken ct) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Abyss/Components/Services/Media/ComicService.cs b/Abyss/Components/Services/Media/ComicService.cs index 07fe4f6..6972cb8 100644 --- a/Abyss/Components/Services/Media/ComicService.cs +++ b/Abyss/Components/Services/Media/ComicService.cs @@ -3,11 +3,10 @@ using Abyss.Components.Static; using Abyss.Model.Media; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; -using Task = System.Threading.Tasks.Task; namespace Abyss.Components.Services.Media; -public class ComicService(ILogger logger, ResourceService rs, ConfigureService config) +public class ComicService(ResourceService rs, ConfigureService config) { public readonly string ImageFolder = Path.Combine(config.MediaRoot, "Images"); diff --git a/Abyss/Components/Services/Media/TaskService.cs b/Abyss/Components/Services/Media/TaskService.cs index 130835e..1a3e4a2 100644 --- a/Abyss/Components/Services/Media/TaskService.cs +++ b/Abyss/Components/Services/Media/TaskService.cs @@ -10,7 +10,7 @@ namespace Abyss.Components.Services.Media; -public class TaskService(ILogger logger, ConfigureService config, ResourceService rs, UserService user) +public class TaskService(ConfigureService config, ResourceService rs, UserService user) { public readonly string TaskFolder = Path.Combine(config.MediaRoot, "Tasks"); public readonly string VideoFolder = Path.Combine(config.MediaRoot, "Videos"); @@ -26,7 +26,7 @@ public class TaskService(ILogger logger, ConfigureService config, R foreach (var i in r ?? []) { var p = Helpers.SafePathCombine(TaskFolder, [i, "task.json"]); - var c = JsonConvert.DeserializeObject(await System.IO.File.ReadAllTextAsync(p ?? "")); + var c = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(p ?? "")); if(c?.Owner == u) s.Add(i); } @@ -50,7 +50,8 @@ public class TaskService(ILogger logger, ConfigureService config, R switch ((TaskType)creation.Type) { case TaskType.Image: - return await CreateImageTask(token, ip, creation); + throw new NotImplementedException(); + // return await CreateImageTask(token, ip, creation); case TaskType.Video: return await CreateVideoTask(token, ip, creation); default: @@ -105,10 +106,10 @@ public class TaskService(ILogger logger, ConfigureService config, R return r; } - private async Task CreateImageTask(string token, string ip, TaskCreation creation) - { - throw new NotImplementedException(); - } + // private async Task CreateImageTask(string token, string ip, TaskCreation creation) + // { + // throw new NotImplementedException(); + // } public static uint GenerateUniqueId(string parentDirectory) { diff --git a/Abyss/Components/Services/Media/VideoService.cs b/Abyss/Components/Services/Media/VideoService.cs index a34efa6..5f3036d 100644 --- a/Abyss/Components/Services/Media/VideoService.cs +++ b/Abyss/Components/Services/Media/VideoService.cs @@ -7,7 +7,7 @@ using Newtonsoft.Json; namespace Abyss.Components.Services.Media; -public class VideoService(ILogger logger, ResourceService rs, ConfigureService config) +public class VideoService(ResourceService rs, ConfigureService config) { public readonly string VideoFolder = Path.Combine(config.MediaRoot, "Videos"); diff --git a/Abyss/Components/Static/SocketExtensions.cs b/Abyss/Components/Static/SocketExtensions.cs new file mode 100644 index 0000000..1f0989b --- /dev/null +++ b/Abyss/Components/Static/SocketExtensions.cs @@ -0,0 +1,51 @@ + +using System.Net.Sockets; +using System.Text; + +namespace Abyss.Components.Static; + +public static class SocketExtensions +{ + public static async Task ReadBase64Async(this Socket socket, CancellationToken cancellationToken = default) + { + var buffer = new byte[4096]; + var sb = new StringBuilder(); + + while (true) + { + int bytesRead = await socket.ReceiveAsync(buffer, SocketFlags.None, cancellationToken).ConfigureAwait(false); + if (bytesRead == 0) + throw new SocketException((int)SocketError.ConnectionReset); + + string chunk = Encoding.UTF8.GetString(buffer, 0, bytesRead); + sb.Append(chunk); + + int newlineIndex = sb.ToString().IndexOf('\n'); + if (newlineIndex >= 0) + { + string base64 = sb.ToString(0, newlineIndex).Trim(); + sb.Remove(0, newlineIndex + 1); + return base64; + } + } + } + + public static async Task WriteBase64Async(this Socket socket, string base64, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(base64)) + throw new ArgumentException("Base64 string cannot be null or empty.", nameof(base64)); + + string message = base64 + "\n"; + byte[] data = Encoding.UTF8.GetBytes(message); + + int totalSent = 0; + while (totalSent < data.Length) + { + int sent = await socket.SendAsync(data.AsMemory(totalSent), SocketFlags.None, cancellationToken).ConfigureAwait(false); + if (sent == 0) + throw new SocketException((int)SocketError.ConnectionReset); + + totalSent += sent; + } + } +} \ No newline at end of file diff --git a/Abyss/Model/Admin/Ctl.cs b/Abyss/Model/Admin/Ctl.cs new file mode 100644 index 0000000..5897380 --- /dev/null +++ b/Abyss/Model/Admin/Ctl.cs @@ -0,0 +1,19 @@ +using System.Text; +using Newtonsoft.Json; + +namespace Abyss.Model.Admin; + +public class Ctl +{ + [JsonProperty("head")] + public int Head { get; set; } + + [JsonProperty("params")] public string[] Params { get; set; } = []; + + public static string MakeBase64(int head, string[] param) + { + return Convert.ToBase64String( + Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new Ctl + { Head = head, Params = param }))); + } +} \ No newline at end of file diff --git a/Abyss/Program.cs b/Abyss/Program.cs index ffb83ef..579abb5 100644 --- a/Abyss/Program.cs +++ b/Abyss/Program.cs @@ -2,7 +2,9 @@ using System.Threading.RateLimiting; using Abyss.Components.Controllers.Middleware; using Abyss.Components.Controllers.Task; - +using Abyss.Components.Services.Admin; +using Abyss.Components.Services.Admin.Attributes; +using Abyss.Components.Services.Admin.Modules; using Abyss.Components.Services.Media; using Abyss.Components.Services.Misc; using Abyss.Components.Services.Security; @@ -30,6 +32,12 @@ public class Program builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); + builder.Services.AddHostedService(); + + foreach (var t in Module.Modules) + { + builder.Services.AddTransient(t); + } builder.Services.AddRateLimiter(options => { diff --git a/abyssctl/App/App.cs b/abyssctl/App/App.cs new file mode 100644 index 0000000..70b05e0 --- /dev/null +++ b/abyssctl/App/App.cs @@ -0,0 +1,49 @@ + +using System.Net.Sockets; +using System.Text; +using abyssctl.App.Modules; +using abyssctl.Model; +using abyssctl.Static; +using CommandLine; +using Newtonsoft.Json; + +namespace abyssctl.App; + +public class App +{ + private static readonly string SocketPath = "ctl.sock"; + + public static async Task CtlWriteRead(Ctl ctl) + { + var endPoint = new UnixDomainSocketEndPoint(SocketPath); + using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + try + { + await socket.ConnectAsync(endPoint); + await socket.WriteBase64Async(Ctl.MakeBase64(ctl.Head, ctl.Params)); + var s = Encoding.UTF8.GetString( + Convert.FromBase64String(await socket.ReadBase64Async())); + return JsonConvert.DeserializeObject(s)!; + } + catch (Exception e) + { + return new Ctl + { + Head = 500, + Params = [e.Message] + }; + } + } + + public async Task RunAsync(string[] args) + { + return await Task.Run(() => + { + return Parser.Default.ParseArguments(args) + .MapResult( + (HelloOptions opt) => HelloOptions.Run(opt), + (VersionOptions opt) => VersionOptions.Run(opt), + _ => 1); + }); + } +} \ No newline at end of file diff --git a/abyssctl/App/Modules/HelloOptions.cs b/abyssctl/App/Modules/HelloOptions.cs new file mode 100644 index 0000000..610a37a --- /dev/null +++ b/abyssctl/App/Modules/HelloOptions.cs @@ -0,0 +1,20 @@ +using abyssctl.Model; +using CommandLine; + +namespace abyssctl.App.Modules; + +[Verb("hello", HelpText = "Say hello to abyss server")] +public class HelloOptions +{ + public static int Run(HelloOptions opts) + { + var r = App.CtlWriteRead(new Ctl + { + Head = 100, + Params = [] + }).GetAwaiter().GetResult(); + Console.WriteLine($"Response Code: {r.Head}"); + Console.WriteLine($"Params: {string.Join(",", r.Params)}"); + return 0; + } +} \ No newline at end of file diff --git a/abyssctl/App/Modules/VersionOptions.cs b/abyssctl/App/Modules/VersionOptions.cs new file mode 100644 index 0000000..ea2e9a6 --- /dev/null +++ b/abyssctl/App/Modules/VersionOptions.cs @@ -0,0 +1,13 @@ +using CommandLine; + +namespace abyssctl.App.Modules; + +[Verb("ver", HelpText = "Get server version")] +public class VersionOptions +{ + public static int Run(VersionOptions opts) + { + Console.WriteLine("Version"); + return 0; + } +} \ No newline at end of file diff --git a/abyssctl/Model/Ctl.cs b/abyssctl/Model/Ctl.cs new file mode 100644 index 0000000..625906a --- /dev/null +++ b/abyssctl/Model/Ctl.cs @@ -0,0 +1,19 @@ +using System.Text; +using Newtonsoft.Json; + +namespace abyssctl.Model; + +public class Ctl +{ + [JsonProperty("head")] public int Head { get; set; } + + [JsonProperty("params")] public string[] Params { get; set; } = []; + + public static string MakeBase64(int head, string[] param) + { + return Convert.ToBase64String( + Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject(new Ctl + { Head = head, Params = param }))); + } +} \ No newline at end of file diff --git a/abyssctl/Program.cs b/abyssctl/Program.cs new file mode 100644 index 0000000..55e0c10 --- /dev/null +++ b/abyssctl/Program.cs @@ -0,0 +1,13 @@ + + +namespace abyssctl; + + +static class Program +{ + static async Task Main(string[] args) + { + var app = new App.App(); + return await app.RunAsync(args); + } +} \ No newline at end of file diff --git a/abyssctl/Static/SocketExtensions.cs b/abyssctl/Static/SocketExtensions.cs new file mode 100644 index 0000000..0ea202e --- /dev/null +++ b/abyssctl/Static/SocketExtensions.cs @@ -0,0 +1,50 @@ +using System.Net.Sockets; +using System.Text; + +namespace abyssctl.Static; + +public static class SocketExtensions +{ + public static async Task ReadBase64Async(this Socket socket, CancellationToken cancellationToken = default) + { + var buffer = new byte[4096]; + var sb = new StringBuilder(); + + while (true) + { + int bytesRead = await socket.ReceiveAsync(buffer, SocketFlags.None, cancellationToken).ConfigureAwait(false); + if (bytesRead == 0) + throw new SocketException((int)SocketError.ConnectionReset); + + string chunk = Encoding.UTF8.GetString(buffer, 0, bytesRead); + sb.Append(chunk); + + int newlineIndex = sb.ToString().IndexOf('\n'); + if (newlineIndex >= 0) + { + string base64 = sb.ToString(0, newlineIndex).Trim(); + sb.Remove(0, newlineIndex + 1); + return base64; + } + } + } + + public static async Task WriteBase64Async(this Socket socket, string base64, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(base64)) + throw new ArgumentException("Base64 string cannot be null or empty.", nameof(base64)); + + string message = base64 + "\n"; + byte[] data = Encoding.UTF8.GetBytes(message); + + int totalSent = 0; + while (totalSent < data.Length) + { + int sent = await socket.SendAsync(data.AsMemory(totalSent), SocketFlags.None, cancellationToken).ConfigureAwait(false); + if (sent == 0) + throw new SocketException((int)SocketError.ConnectionReset); + + totalSent += sent; + } + } +} \ No newline at end of file diff --git a/abyssctl/abyssctl.csproj b/abyssctl/abyssctl.csproj new file mode 100644 index 0000000..a31e8f8 --- /dev/null +++ b/abyssctl/abyssctl.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0 + 13 + enable + enable + false + true + + + + ../build/ + + + + + + + + + +