From a0273e33345cfa365b95a8e1eae20cf91d96943b Mon Sep 17 00:00:00 2001 From: acite <1498045907@qq.com> Date: Sun, 5 Oct 2025 16:48:54 +0800 Subject: [PATCH] [feat] Abyssctl Basic functions --- .../bf32ff15-97d4-4301-bb5e-c1d57c7be5c0.xml | 48 +++- .../.idea.Abyss/.idea/data_source_mapping.xml | 6 + .idea/.idea.Abyss/.idea/workspace.xml | 32 ++- Abyss.sln.DotSettings.user | 1 + .../{Module.cs => ModuleAttribute.cs} | 4 +- Abyss/Components/Services/Admin/CtlService.cs | 18 +- .../Services/Admin/Modules/InitModule.cs | 58 ++++ .../Services/Admin/Modules/UserAddModule.cs | 50 ++++ .../Services/Media/ResourceDatabaseService.cs | 20 +- .../Services/Media/ResourceService.cs | 249 +++++++++--------- .../Components/Services/Media/VideoService.cs | 3 +- .../Services/Security/UserService.cs | 48 +--- Abyss/Model/Media/ResourceAttribute.cs | 2 +- Abyss/Model/Security/User.cs | 2 +- Abyss/Program.cs | 2 +- Abyss/Properties/launchSettings.json | 2 +- abyssctl/App/App.cs | 23 +- abyssctl/App/Attributes/ModuleAttribute.cs | 28 ++ abyssctl/App/Modules/HelloOptions.cs | 22 +- abyssctl/App/Modules/InitOptions.cs | 20 ++ abyssctl/App/Modules/UserAddOptions.cs | 26 ++ abyssctl/App/Modules/VersionOptions.cs | 2 + 22 files changed, 428 insertions(+), 238 deletions(-) create mode 100644 .idea/.idea.Abyss/.idea/data_source_mapping.xml rename Abyss/Components/Services/Admin/Attributes/{Module.cs => ModuleAttribute.cs} (89%) create mode 100644 Abyss/Components/Services/Admin/Modules/InitModule.cs create mode 100644 Abyss/Components/Services/Admin/Modules/UserAddModule.cs create mode 100644 abyssctl/App/Attributes/ModuleAttribute.cs create mode 100644 abyssctl/App/Modules/InitOptions.cs create mode 100644 abyssctl/App/Modules/UserAddOptions.cs diff --git a/.idea/.idea.Abyss/.idea/dataSources/bf32ff15-97d4-4301-bb5e-c1d57c7be5c0.xml b/.idea/.idea.Abyss/.idea/dataSources/bf32ff15-97d4-4301-bb5e-c1d57c7be5c0.xml index c859b9f..c26919f 100644 --- a/.idea/.idea.Abyss/.idea/dataSources/bf32ff15-97d4-4301-bb5e-c1d57c7be5c0.xml +++ b/.idea/.idea.Abyss/.idea/dataSources/bf32ff15-97d4-4301-bb5e-c1d57c7be5c0.xml @@ -499,7 +499,7 @@ 1 - 2025-08-23.10:03:56 + 2025-10-05.08:15:22 R @@ -1590,45 +1590,67 @@ 1 - +
1
- + + 1 +
+ + 1 + 1 1 - varchar|0s + integer|0s - + + 1 2 varchar|0s - + + 1 3 - varchar|0s + integer|0s - + + 1 4 varchar|0s - + + Uid + 1 + + + Id + 1 + + 1 TEXT|0s - + 2 TEXT|0s - + 3 TEXT|0s - + 4 INT|0s - + 5 TEXT|0s + + 1 + + + 2 + \ No newline at end of file diff --git a/.idea/.idea.Abyss/.idea/data_source_mapping.xml b/.idea/.idea.Abyss/.idea/data_source_mapping.xml new file mode 100644 index 0000000..9464031 --- /dev/null +++ b/.idea/.idea.Abyss/.idea/data_source_mapping.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/.idea.Abyss/.idea/workspace.xml b/.idea/.idea.Abyss/.idea/workspace.xml index 5196781..3f75433 100644 --- a/.idea/.idea.Abyss/.idea/workspace.xml +++ b/.idea/.idea.Abyss/.idea/workspace.xml @@ -11,9 +11,24 @@ - + + + + + + + + + + + + + + + + @@ -31,6 +46,7 @@ + @@ -49,13 +65,14 @@ - - + + + @@ -88,12 +105,14 @@ + + - { "associatedIndex": 3 @@ -128,7 +147,7 @@ "vue.rearranger.settings.migration": "true" } }]]> - + @@ -275,7 +294,8 @@ - + + diff --git a/Abyss.sln.DotSettings.user b/Abyss.sln.DotSettings.user index f5d1b68..bdeabf4 100644 --- a/Abyss.sln.DotSettings.user +++ b/Abyss.sln.DotSettings.user @@ -1,4 +1,5 @@  + ForceIncluded ForceIncluded ForceIncluded ForceIncluded diff --git a/Abyss/Components/Services/Admin/Attributes/Module.cs b/Abyss/Components/Services/Admin/Attributes/ModuleAttribute.cs similarity index 89% rename from Abyss/Components/Services/Admin/Attributes/Module.cs rename to Abyss/Components/Services/Admin/Attributes/ModuleAttribute.cs index c3e1a4a..5b77900 100644 --- a/Abyss/Components/Services/Admin/Attributes/Module.cs +++ b/Abyss/Components/Services/Admin/Attributes/ModuleAttribute.cs @@ -4,7 +4,7 @@ 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 class ModuleAttribute(int head) : Attribute { public int Head { get; } = head; @@ -13,7 +13,7 @@ public class Module(int head) : Attribute get { Assembly assembly = Assembly.GetExecutingAssembly(); - Type attributeType = typeof(Module); + Type attributeType = typeof(ModuleAttribute); const string targetNamespace = "Abyss.Components.Services.Admin.Modules"; var moduleTypes = assembly.GetTypes() diff --git a/Abyss/Components/Services/Admin/CtlService.cs b/Abyss/Components/Services/Admin/CtlService.cs index 0073d0e..1da672e 100644 --- a/Abyss/Components/Services/Admin/CtlService.cs +++ b/Abyss/Components/Services/Admin/CtlService.cs @@ -6,14 +6,14 @@ using Abyss.Model.Admin; using Newtonsoft.Json; using System.Reflection; +using Abyss.Components.Services.Admin.Attributes; 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 static readonly string SocketPath = Path.Combine(Path.GetTempPath(), "abyss-ctl.sock"); private Task? _executingTask; private CancellationTokenSource? _cts; @@ -21,10 +21,10 @@ public class CtlService(ILogger logger, IServiceProvider serviceProv public Task StartAsync(CancellationToken cancellationToken) { - var t = Module.Modules; + var t = ModuleAttribute.Modules; foreach (var module in t) { - var attr = module.GetCustomAttribute(); + var attr = module.GetCustomAttribute(); if (attr != null) { _handlers[attr.Head] = module; @@ -54,12 +54,12 @@ public class CtlService(ILogger logger, IServiceProvider serviceProv private async Task ExecuteAsync(CancellationToken stoppingToken) { - if (File.Exists(_socketPath)) + if (File.Exists(SocketPath)) { - File.Delete(_socketPath); + File.Delete(SocketPath); } - - var endPoint = new UnixDomainSocketEndPoint(_socketPath); + + var endPoint = new UnixDomainSocketEndPoint(SocketPath); using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); socket.Bind(endPoint); @@ -77,6 +77,8 @@ public class CtlService(ILogger logger, IServiceProvider serviceProv break; } } + + File.Delete(SocketPath); } private async Task HandleClientAsync(Socket clientSocket, CancellationToken stoppingToken) diff --git a/Abyss/Components/Services/Admin/Modules/InitModule.cs b/Abyss/Components/Services/Admin/Modules/InitModule.cs new file mode 100644 index 0000000..6b359f7 --- /dev/null +++ b/Abyss/Components/Services/Admin/Modules/InitModule.cs @@ -0,0 +1,58 @@ +using Abyss.Components.Services.Admin.Attributes; +using Abyss.Components.Services.Admin.Interfaces; +using Abyss.Components.Services.Media; +using Abyss.Components.Services.Misc; +using Abyss.Components.Services.Security; +using Abyss.Model.Admin; +using Abyss.Model.Security; +using NSec.Cryptography; + +namespace Abyss.Components.Services.Admin.Modules; + +[Module(103)] +public class InitModule(ILogger logger, UserService userService, ConfigureService configureService, ResourceDatabaseService resourceDatabaseService): IModule +{ + public async Task ExecuteAsync(Ctl request, CancellationToken ct) + { + bool empty = await userService.IsEmptyUser(); + if (!empty) + return new Ctl + { + Head = 403, + Params = ["Access Denied: User list is not empty."] + }; + + var key = UserService.GenerateKeyPair(); + string privateKeyBase64 = Convert.ToBase64String(key.Export(KeyBlobFormat.RawPrivateKey)); + string publicKeyBase64 = Convert.ToBase64String(key.Export(KeyBlobFormat.RawPublicKey)); + + await userService.AddUserAsync(new User + { + Uuid = 1, + Username = "root", + ParentId = 1, + PublicKey = publicKeyBase64, + Privilege = 1145141919, + }); + + var paths = new string[] { "Tasks", "Live", "Videos", "Images" } + .Select(x => Path.Combine(configureService.MediaRoot, x)); + foreach (var path in paths) + { + if(!Directory.Exists(path)) + Directory.CreateDirectory(path); + + var i = await resourceDatabaseService.InsertRaRow(path, 1, "rw,r-,r-", true); + if (!i) + { + logger.LogError("Could not create resource database"); + } + } + + return new Ctl + { + Head = 200, + Params = [privateKeyBase64] + }; + } +} \ No newline at end of file diff --git a/Abyss/Components/Services/Admin/Modules/UserAddModule.cs b/Abyss/Components/Services/Admin/Modules/UserAddModule.cs new file mode 100644 index 0000000..75c4aee --- /dev/null +++ b/Abyss/Components/Services/Admin/Modules/UserAddModule.cs @@ -0,0 +1,50 @@ +using Abyss.Components.Services.Admin.Attributes; +using Abyss.Components.Services.Admin.Interfaces; +using Abyss.Components.Services.Security; +using Abyss.Model.Admin; +using Abyss.Model.Security; +using NSec.Cryptography; + +namespace Abyss.Components.Services.Admin.Modules; + +[Module(104)] +public class UserAddModule(UserService userService): IModule +{ + public async Task ExecuteAsync(Ctl request, CancellationToken ct) + { + // request.Params[0] -> Username + // request.Params[1] -> Privilege + + if (request.Params.Length != 2 || !UserService.IsAlphanumeric(request.Params[0]) || !int.TryParse(request.Params[1], out var privilege)) + return new Ctl + { + Head = 400, + Params = ["Bad Request"] + }; + + if (await userService.QueryUser(request.Params[0]) != null) + return new Ctl + { + Head = 403, + Params = ["User exists"] + }; + + var key = UserService.GenerateKeyPair(); + string privateKeyBase64 = Convert.ToBase64String(key.Export(KeyBlobFormat.RawPrivateKey)); + string publicKeyBase64 = Convert.ToBase64String(key.Export(KeyBlobFormat.RawPublicKey)); + + await userService.AddUserAsync(new User + { + Username = request.Params[0], + ParentId = 1, + PublicKey = publicKeyBase64, + Privilege = privilege, + }); + + return new Ctl + { + Head = 200, + Params = [privateKeyBase64] + }; + } +} \ No newline at end of file diff --git a/Abyss/Components/Services/Media/ResourceDatabaseService.cs b/Abyss/Components/Services/Media/ResourceDatabaseService.cs index 2842d70..b0ae288 100644 --- a/Abyss/Components/Services/Media/ResourceDatabaseService.cs +++ b/Abyss/Components/Services/Media/ResourceDatabaseService.cs @@ -22,22 +22,9 @@ public class ResourceDatabaseService ResourceDatabase = new SQLiteAsyncConnection(config.RaDatabase, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create); ResourceDatabase.CreateTableAsync().Wait(); - - - var tasksPath = Helpers.SafePathCombine(_config.MediaRoot, "Tasks"); - if (tasksPath != null) - { - InsertRaRow(tasksPath, 1, "rw,r-,r-", true).Wait(); - } - - var livePath = Helpers.SafePathCombine(_config.MediaRoot, "Live"); - if (livePath != null) - { - InsertRaRow(livePath, 1, "rw,r-,r-", true).Wait(); - } } - private async Task InsertRaRow(string fullPath, int owner, string permission, bool update = false) + public async Task InsertRaRow(string fullPath, int owner, string permission, bool update = false) { if (!PermissionRegex.IsMatch(permission)) { @@ -46,11 +33,12 @@ public class ResourceDatabaseService } var path = Path.GetRelativePath(_config.MediaRoot, fullPath); + var uid = Uid(path); if (update) return await ResourceDatabase.InsertOrReplaceAsync(new ResourceAttribute() { - Uid = Uid(path), + Uid = uid, Owner = owner, Permission = permission, }) == 1; @@ -58,7 +46,7 @@ public class ResourceDatabaseService { return await ResourceDatabase.InsertAsync(new ResourceAttribute() { - Uid = Uid(path), + Uid = uid, Owner = owner, Permission = permission, }) == 1; diff --git a/Abyss/Components/Services/Media/ResourceService.cs b/Abyss/Components/Services/Media/ResourceService.cs index 5cc7e32..89785c9 100644 --- a/Abyss/Components/Services/Media/ResourceService.cs +++ b/Abyss/Components/Services/Media/ResourceService.cs @@ -16,21 +16,12 @@ public enum OperationType Security // Chown, Chmod } -public class ResourceService +public class ResourceService( + ILogger logger, + ConfigureService config, + UserService user, + ResourceDatabaseService db) { - private readonly ILogger _logger; - private readonly ConfigureService _config; - private readonly UserService _user; - private readonly ResourceDatabaseService _db; - - public ResourceService(ILogger logger, ConfigureService config, UserService user, ResourceDatabaseService db) - { - _logger = logger; - _config = config; - _user = user; - _db = db; - } - // Create UID only for resources, without considering advanced hash security such as adding salt private async Task> ValidAny(string[] paths, string token, OperationType type, string ip) { @@ -40,7 +31,7 @@ public class ResourceService return result; // empty input -> empty result // Normalize media root - var mediaRootFull = Path.GetFullPath(_config.MediaRoot); + var mediaRootFull = Path.GetFullPath(config.MediaRoot); // Prepare normalized full paths and early-check outside-media-root var fullPaths = new List(paths.Length); @@ -52,7 +43,7 @@ public class ResourceService // record normalized path as key if (!full.StartsWith(mediaRootFull, StringComparison.OrdinalIgnoreCase)) { - _logger.LogError($"Path outside media root or null: {p}"); + logger.LogError($"Path outside media root or null: {p}"); result[full] = false; } else @@ -65,7 +56,7 @@ public class ResourceService catch (Exception ex) { // malformed path -> mark false and continue - _logger.LogError(ex, $"Invalid path encountered in ValidAny: {p}"); + logger.LogError(ex, $"Invalid path encountered in ValidAny: {p}"); try { result[Path.GetFullPath(p)] = false; @@ -81,18 +72,18 @@ public class ResourceService return result; // Validate token and user once - int uuid = _user.Validate(token, ip); + int uuid = user.Validate(token, ip); if (uuid == -1) { - _logger.LogError($"Invalid token: {token}"); + logger.LogError($"Invalid token: {token}"); // all previously-initialized keys remain false return result; } - User? user = await _user.QueryUser(uuid); - if (user == null || user.Uuid != uuid) + User? user1 = await user.QueryUser(uuid); + if (user1 == null || user1.Uuid != uuid) { - _logger.LogError($"Verification failed: {token}"); + logger.LogError($"Verification failed: {token}"); return result; } @@ -107,7 +98,7 @@ public class ResourceService try { // rel path relative to media root for Uid calculation - var rel = Path.GetRelativePath(_config.MediaRoot, full); + var rel = Path.GetRelativePath(config.MediaRoot, full); var parts = rel .Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, @@ -152,7 +143,7 @@ public class ResourceService } catch (Exception ex) { - _logger.LogError(ex, $"Error building requirements for path '{full}' in ValidAny."); + logger.LogError(ex, $"Error building requirements for path '{full}' in ValidAny."); // leave result[full] as false } } @@ -162,7 +153,7 @@ public class ResourceService var rasList = new List(); if (uidsNeeded.Count > 0) { - rasList = await _db.GetResourceAttributesByUidsAsync(uidsNeeded); + rasList = await db.GetResourceAttributesByUidsAsync(uidsNeeded); } var raDict = rasList.ToDictionary(r => r.Uid, StringComparer.OrdinalIgnoreCase); @@ -181,7 +172,7 @@ public class ResourceService { permCache[(uid, op)] = false; var examplePath = uidToExampleRelPath.GetValueOrDefault(uid, uid); - _logger.LogDebug($"ValidAny: missing ResourceAttribute for Uid={uid}, example='{examplePath}'"); + logger.LogDebug($"ValidAny: missing ResourceAttribute for Uid={uid}, example='{examplePath}'"); } continue; @@ -192,7 +183,7 @@ public class ResourceService var key = (uid, op); if (!permCache.TryGetValue(key, out var ok)) { - ok = await CheckPermission(user, ra, op); + ok = await CheckPermission(user1, ra, op); permCache[key] = ok; } } @@ -224,11 +215,11 @@ public class ResourceService { if (paths.Length == 0) { - _logger.LogError("ValidAll called with empty path set"); + logger.LogError("ValidAll called with empty path set"); return false; } - var mediaRootFull = Path.GetFullPath(_config.MediaRoot); + var mediaRootFull = Path.GetFullPath(config.MediaRoot); // 1. basic path checks & normalize to relative var relPaths = new List(paths.Length); @@ -236,25 +227,25 @@ public class ResourceService { if (!p.StartsWith(mediaRootFull, StringComparison.OrdinalIgnoreCase)) { - _logger.LogError($"Path outside media root or null: {p}"); + logger.LogError($"Path outside media root or null: {p}"); return false; } - relPaths.Add(Path.GetRelativePath(_config.MediaRoot, Path.GetFullPath(p))); + relPaths.Add(Path.GetRelativePath(config.MediaRoot, Path.GetFullPath(p))); } // 2. validate token and user once - int uuid = _user.Validate(token, ip); + int uuid = user.Validate(token, ip); if (uuid == -1) { - _logger.LogError($"Invalid token: {token}"); + logger.LogError($"Invalid token: {token}"); return false; } - User? user = await _user.QueryUser(uuid); - if (user == null || user.Uuid != uuid) + User? user1 = await user.QueryUser(uuid); + if (user1 == null || user1.Uuid != uuid) { - _logger.LogError($"Verification failed: {token}"); + logger.LogError($"Verification failed: {token}"); return false; } @@ -303,7 +294,7 @@ public class ResourceService var rasList = new List(); if (uidsNeeded.Count > 0) { - rasList = await _db.GetResourceAttributesByUidsAsync(uidsNeeded); + rasList = await db.GetResourceAttributesByUidsAsync(uidsNeeded); } var raDict = rasList.ToDictionary(r => r.Uid, StringComparer.OrdinalIgnoreCase); @@ -317,7 +308,7 @@ public class ResourceService if (!raDict.TryGetValue(uid, out var ra)) { var examplePath = uidToExampleRelPath.GetValueOrDefault(uid, uid); - _logger.LogError( + logger.LogError( $"Permission check failed (missing resource attribute): User: {uuid}, Resource: {examplePath}, Uid: {uid}"); return false; } @@ -327,14 +318,14 @@ public class ResourceService var key = (uid, op); if (!permCache.TryGetValue(key, out var ok)) { - ok = await CheckPermission(user, ra, op); + ok = await CheckPermission(user1, ra, op); permCache[key] = ok; } if (!ok) { var examplePath = uidToExampleRelPath.TryGetValue(uid, out var p) ? p : uid; - _logger.LogError( + logger.LogError( $"Permission check failed: User: {uuid}, Resource: {examplePath}, Uid: {uid}, Type: {op}"); return false; } @@ -347,23 +338,23 @@ public class ResourceService private async Task Valid(string path, string token, OperationType type, string ip) { // Path is abs path here, due to Helpers.SafePathCombine - if (!path.StartsWith(Path.GetFullPath(_config.MediaRoot), StringComparison.OrdinalIgnoreCase)) + if (!path.StartsWith(Path.GetFullPath(config.MediaRoot), StringComparison.OrdinalIgnoreCase)) return false; - path = Path.GetRelativePath(_config.MediaRoot, path); + path = Path.GetRelativePath(config.MediaRoot, path); - int uuid = _user.Validate(token, ip); + int uuid = user.Validate(token, ip); if (uuid == -1) { // No permission granted for invalid tokens - _logger.LogError($"Invalid token: {token}"); + logger.LogError($"Invalid token: {token}"); return false; } - User? user = await _user.QueryUser(uuid); - if (user == null || user.Uuid != uuid) + User? user1 = await user.QueryUser(uuid); + if (user1 == null || user1.Uuid != uuid) { - _logger.LogError($"Verification failed: {token}"); + logger.LogError($"Verification failed: {token}"); return false; // Two-factor authentication } @@ -374,51 +365,51 @@ public class ResourceService { var subPath = Path.Combine(parts.Take(i + 1).ToArray()); var uidDir = ResourceDatabaseService.Uid(subPath); - var raDir = await _db.GetResourceAttributeByUidAsync(uidDir); + var raDir = await db.GetResourceAttributeByUidAsync(uidDir); if (raDir == null) { - _logger.LogError($"Permission denied: {uuid} has no read access to parent directory {subPath}"); + logger.LogError($"Permission denied: {uuid} has no read access to parent directory {subPath}"); return false; } - if (!await CheckPermission(user, raDir, OperationType.Read)) + if (!await CheckPermission(user1, raDir, OperationType.Read)) { - _logger.LogError($"Permission denied: {uuid} has no read access to parent directory {subPath}"); + logger.LogError($"Permission denied: {uuid} has no read access to parent directory {subPath}"); return false; } } var uid = ResourceDatabaseService.Uid(path); - ResourceAttribute? ra = await _db.GetResourceAttributeByUidAsync(uid); + ResourceAttribute? ra = await db.GetResourceAttributeByUidAsync(uid); if (ra == null) { - _logger.LogError($"Permission check failed: User: {uuid}, Resource: {path}, Type: {type.ToString()} "); + logger.LogError($"Permission check failed: User: {uuid}, Resource: {path}, Type: {type.ToString()} "); return false; } - var l = await CheckPermission(user, ra, type); + var l = await CheckPermission(user1, ra, type); if (!l) { - _logger.LogError($"Permission check failed: User: {uuid}, Resource: {path}, Type: {type.ToString()} "); + logger.LogError($"Permission check failed: User: {uuid}, Resource: {path}, Type: {type.ToString()} "); } return l; } - private async Task CheckPermission(User? user, ResourceAttribute? ra, OperationType type) + private async Task CheckPermission(User? user1, ResourceAttribute? ra, OperationType type) { - if (user == null || ra == null) return false; + if (user1 == null || ra == null) return false; if (!ResourceDatabaseService.PermissionRegex.IsMatch(ra.Permission)) return false; var perms = ra.Permission.Split(','); if (perms.Length != 3) return false; - var owner = await _user.QueryUser(ra.Owner); + var owner = await user.QueryUser(ra.Owner); if (owner == null) return false; - bool isOwner = ra.Owner == user.Uuid; - bool isPeer = !isOwner && user.Privilege == owner.Privilege; + bool isOwner = ra.Owner == user1.Uuid; + bool isPeer = !isOwner && user1.Privilege == owner.Privilege; bool isOther = !isOwner && !isPeer; string currentPerm; @@ -430,11 +421,11 @@ public class ResourceService switch (type) { case OperationType.Read: - return currentPerm.Contains('r') || (user.Privilege > owner.Privilege); + return currentPerm.Contains('r') || (user1.Privilege > owner.Privilege); case OperationType.Write: - return currentPerm.Contains('w') || (user.Privilege > owner.Privilege); + return currentPerm.Contains('w') || (user1.Privilege > owner.Privilege); case OperationType.Security: - return (isOwner && currentPerm.Contains('w')) || user.Uuid == 1; + return (isOwner && currentPerm.Contains('w')) || user1.Uuid == 1; default: return false; } @@ -470,13 +461,13 @@ public class ResourceService } else { - _logger.LogDebug( + logger.LogDebug( $"Query: access denied or not managed for '{entry}' (user token: {token}) - item skipped."); } } catch (Exception exEntry) { - _logger.LogError(exEntry, $"Error processing entry '{entry}' in Query."); + logger.LogError(exEntry, $"Error processing entry '{entry}' in Query."); } } @@ -484,7 +475,7 @@ public class ResourceService } catch (Exception ex) { - _logger.LogError(ex, $"Error while listing directory '{path}' in Query."); + logger.LogError(ex, $"Error while listing directory '{path}' in Query."); return null; } } @@ -539,22 +530,22 @@ public class ResourceService public async Task Initialize(string path, string token, string owner, string ip) { - var u = await _user.QueryUser(owner); + var u = await user.QueryUser(owner); if (u == null || u.Uuid == -1) return false; - return await Initialize(path, token, u.Uuid, ip); + return await Initialize(path, token, u.Uuid!.Value, ip); } public async Task Initialize(string path, string token, int owner, string ip) { // TODO: Use a more elegant Debug mode - if (_config.DebugMode == "Debug") + if (config.DebugMode == "Debug") goto debug; // 1. Authorization: Verify the operation is performed by 'root' - var requester = _user.Validate(token, ip); + var requester = user.Validate(token, ip); if (requester != 1) { - _logger.LogWarning( + logger.LogWarning( $"Permission denied: Non-root user '{requester}' attempted to initialize resources."); return false; } @@ -563,14 +554,14 @@ public class ResourceService // 2. Validation: Ensure the target path and owner are valid if (!Directory.Exists(path)) { - _logger.LogError($"Initialization failed: Path '{path}' does not exist or is not a directory."); + logger.LogError($"Initialization failed: Path '{path}' does not exist or is not a directory."); return false; } - var ownerUser = await _user.QueryUser(owner); + var ownerUser = await user.QueryUser(owner); if (ownerUser == null) { - _logger.LogError($"Initialization failed: Owner user '{owner}' does not exist."); + logger.LogError($"Initialization failed: Owner user '{owner}' does not exist."); return false; } @@ -583,9 +574,9 @@ public class ResourceService var newResources = new List(); foreach (var p in allPaths) { - var currentPath = Path.GetRelativePath(_config.MediaRoot, p); + var currentPath = Path.GetRelativePath(config.MediaRoot, p); var uid = ResourceDatabaseService.Uid(currentPath); - var existing = await _db.GetResourceAttributeByUidAsync(uid); + var existing = await db.GetResourceAttributeByUidAsync(uid); // If it's not in the database, add it to our list for batch insertion if (existing == null) @@ -602,13 +593,13 @@ public class ResourceService // 5. Database Insertion: Add all new resources in a single, efficient transaction if (newResources.Any()) { - await _db.InsertResourceAttributesAsync(newResources); - _logger.LogInformation( + await db.InsertResourceAttributesAsync(newResources); + logger.LogInformation( $"Successfully initialized {newResources.Count} new resources under '{path}' for user '{owner}'."); } else { - _logger.LogInformation( + logger.LogInformation( $"No new resources to initialize under '{path}'. All items already exist in the database."); } @@ -616,84 +607,84 @@ public class ResourceService } catch (Exception ex) { - _logger.LogError(ex, $"An error occurred during resource initialization for path '{path}'."); + logger.LogError(ex, $"An error occurred during resource initialization for path '{path}'."); return false; } } public async Task Exclude(string path, string token, string ip) { - var requester = _user.Validate(token, ip); + var requester = user.Validate(token, ip); if (requester != 1) { - _logger.LogWarning( + logger.LogWarning( $"Permission denied: Non-root user '{requester}' attempted to exclude resource '{path}'."); return false; } try { - var relPath = Path.GetRelativePath(_config.MediaRoot, path); + var relPath = Path.GetRelativePath(config.MediaRoot, path); var uid = ResourceDatabaseService.Uid(relPath); - var resource = await _db.GetResourceAttributeByUidAsync(uid); + var resource = await db.GetResourceAttributeByUidAsync(uid); if (resource == null) { - _logger.LogError($"Exclude failed: Resource '{relPath}' not found in database."); + logger.LogError($"Exclude failed: Resource '{relPath}' not found in database."); return false; } - var deleted = await _db.DeleteByUidAsync(uid); + var deleted = await db.DeleteByUidAsync(uid); if (deleted > 0) { - _logger.LogInformation($"Successfully excluded resource '{relPath}' from management."); + logger.LogInformation($"Successfully excluded resource '{relPath}' from management."); return true; } else { - _logger.LogError($"Failed to exclude resource '{relPath}' from database."); + logger.LogError($"Failed to exclude resource '{relPath}' from database."); return false; } } catch (Exception ex) { - _logger.LogError(ex, $"Error excluding resource '{path}'."); + logger.LogError(ex, $"Error excluding resource '{path}'."); return false; } } public async Task Include(string path, string token, string ip, int owner, string permission) { - var requester = _user.Validate(token, ip); + var requester = user.Validate(token, ip); if (requester != 1) { - _logger.LogWarning( + logger.LogWarning( $"Permission denied: Non-root user '{requester}' attempted to include resource '{path}'."); return false; } if (!ResourceDatabaseService.PermissionRegex.IsMatch(permission)) { - _logger.LogError($"Invalid permission format: {permission}"); + logger.LogError($"Invalid permission format: {permission}"); return false; } - var ownerUser = await _user.QueryUser(owner); + var ownerUser = await user.QueryUser(owner); if (ownerUser == null) { - _logger.LogError($"Include failed: Owner user '{owner}' does not exist."); + logger.LogError($"Include failed: Owner user '{owner}' does not exist."); return false; } try { - var relPath = Path.GetRelativePath(_config.MediaRoot, path); + var relPath = Path.GetRelativePath(config.MediaRoot, path); var uid = ResourceDatabaseService.Uid(relPath); - var existing = await _db.GetResourceAttributeByUidAsync(uid); + var existing = await db.GetResourceAttributeByUidAsync(uid); if (existing != null) { - _logger.LogError($"Include failed: Resource '{relPath}' already exists in database."); + logger.LogError($"Include failed: Resource '{relPath}' already exists in database."); return false; } @@ -704,22 +695,22 @@ public class ResourceService Permission = permission }; - var inserted = await _db.InsertResourceAttributeAsync(newResource); + var inserted = await db.InsertResourceAttributeAsync(newResource); if (inserted > 0) { - _logger.LogInformation( + logger.LogInformation( $"Successfully included '{relPath}' into resource management (Owner={owner}, Permission={permission})."); return true; } else { - _logger.LogError($"Failed to include resource '{relPath}' into database."); + logger.LogError($"Failed to include resource '{relPath}' into database."); return false; } } catch (Exception ex) { - _logger.LogError(ex, $"Error including resource '{path}'."); + logger.LogError(ex, $"Error including resource '{path}'."); return false; } } @@ -728,14 +719,14 @@ public class ResourceService { try { - var relPath = Path.GetRelativePath(_config.MediaRoot, path); + var relPath = Path.GetRelativePath(config.MediaRoot, path); var uid = ResourceDatabaseService.Uid(relPath); - return await _db.ExistsUidAsync(uid); + return await db.ExistsUidAsync(uid); } catch (Exception ex) { - _logger.LogError(ex, $"Error checking existence of resource '{path}'."); + logger.LogError(ex, $"Error checking existence of resource '{path}'."); return false; } } @@ -745,7 +736,7 @@ public class ResourceService // Validate permission format first if (!ResourceDatabaseService.PermissionRegex.IsMatch(permission)) { - _logger.LogError($"Invalid permission format: {permission}"); + logger.LogError($"Invalid permission format: {permission}"); return false; } @@ -758,7 +749,7 @@ public class ResourceService { if (recursive && Directory.Exists(path)) { - _logger.LogInformation($"Recursive directory '{path}'."); + logger.LogInformation($"Recursive directory '{path}'."); targets.Add(path); foreach (var entry in Directory.EnumerateFileSystemEntries(path, "*", SearchOption.AllDirectories)) { @@ -767,17 +758,17 @@ public class ResourceService if (!await ValidAll(targets.ToArray(), token, OperationType.Security, ip)) { - _logger.LogWarning($"Permission denied for recursive chmod on '{path}'"); + logger.LogWarning($"Permission denied for recursive chmod on '{path}'"); return false; } - _logger.LogInformation($"Successfully validated chmod on '{path}'."); + logger.LogInformation($"Successfully validated chmod on '{path}'."); } else { if (!await Valid(path, token, OperationType.Security, ip)) { - _logger.LogWarning($"Permission denied for chmod on '{path}'"); + logger.LogWarning($"Permission denied for chmod on '{path}'"); return false; } @@ -786,35 +777,35 @@ public class ResourceService // Build distinct UIDs var relUids = targets - .Select(t => Path.GetRelativePath(_config.MediaRoot, t)) + .Select(t => Path.GetRelativePath(config.MediaRoot, t)) .Select(rel => ResourceDatabaseService.Uid(rel)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); if (relUids.Count == 0) { - _logger.LogWarning($"No targets resolved for chmod on '{path}'"); + logger.LogWarning($"No targets resolved for chmod on '{path}'"); return false; } // Use DatabaseService to perform chunked updates - var updatedCount = await _db.UpdatePermissionsByUidsAsync(relUids, permission); + var updatedCount = await db.UpdatePermissionsByUidsAsync(relUids, permission); if (updatedCount > 0) { - _logger.LogInformation( + logger.LogInformation( $"Chmod: updated permissions for {updatedCount} resource(s) (root='{path}', recursive={recursive})"); return true; } else { - _logger.LogWarning($"Chmod: no resources updated for '{path}' (recursive={recursive})"); + logger.LogWarning($"Chmod: no resources updated for '{path}' (recursive={recursive})"); return false; } } catch (Exception ex) { - _logger.LogError(ex, $"Error changing permissions for: {path}"); + logger.LogError(ex, $"Error changing permissions for: {path}"); return false; } } @@ -822,10 +813,10 @@ public class ResourceService public async Task Chown(string path, string token, int owner, string ip, bool recursive = false) { // Validate new owner exists - var newOwner = await _user.QueryUser(owner); + var newOwner = await user.QueryUser(owner); if (newOwner == null) { - _logger.LogError($"New owner '{owner}' does not exist"); + logger.LogError($"New owner '{owner}' does not exist"); return false; } @@ -846,7 +837,7 @@ public class ResourceService if (!await ValidAll(targets.ToArray(), token, OperationType.Security, ip)) { - _logger.LogWarning($"Permission denied for recursive chown on '{path}'"); + logger.LogWarning($"Permission denied for recursive chown on '{path}'"); return false; } } @@ -854,7 +845,7 @@ public class ResourceService { if (!await Valid(path, token, OperationType.Security, ip)) { - _logger.LogWarning($"Permission denied for chown on '{path}'"); + logger.LogWarning($"Permission denied for chown on '{path}'"); return false; } @@ -863,35 +854,35 @@ public class ResourceService // Build distinct UIDs var relUids = targets - .Select(t => Path.GetRelativePath(_config.MediaRoot, t)) + .Select(t => Path.GetRelativePath(config.MediaRoot, t)) .Select(rel => ResourceDatabaseService.Uid(rel)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); if (relUids.Count == 0) { - _logger.LogWarning($"No targets resolved for chown on '{path}'"); + logger.LogWarning($"No targets resolved for chown on '{path}'"); return false; } // Use DatabaseService to perform chunked owner updates - var updatedCount = await _db.UpdateOwnerByUidsAsync(relUids, owner); + var updatedCount = await db.UpdateOwnerByUidsAsync(relUids, owner); if (updatedCount > 0) { - _logger.LogInformation( + logger.LogInformation( $"Chown: changed owner for {updatedCount} resource(s) (root='{path}', recursive={recursive})"); return true; } else { - _logger.LogWarning($"Chown: no resources updated for '{path}' (recursive={recursive})"); + logger.LogWarning($"Chown: no resources updated for '{path}' (recursive={recursive})"); return false; } } catch (Exception ex) { - _logger.LogError(ex, $"Error changing ownership for: {path}"); + logger.LogError(ex, $"Error changing ownership for: {path}"); return false; } } @@ -904,20 +895,20 @@ public class ResourceService var full = Path.GetFullPath(path); // ensure it's under media root - var mediaRootFull = Path.GetFullPath(_config.MediaRoot); + var mediaRootFull = Path.GetFullPath(config.MediaRoot); if (!full.StartsWith(mediaRootFull, StringComparison.OrdinalIgnoreCase)) return null; - var rel = Path.GetRelativePath(_config.MediaRoot, full); + var rel = Path.GetRelativePath(config.MediaRoot, full); var uid = ResourceDatabaseService.Uid(rel); - var ra = await _db.GetResourceAttributeByUidAsync(uid); + var ra = await db.GetResourceAttributeByUidAsync(uid); return ra; } catch (Exception ex) { - _logger.LogError(ex, $"GetAttribute failed for path '{path}'"); + logger.LogError(ex, $"GetAttribute failed for path '{path}'"); return null; } } diff --git a/Abyss/Components/Services/Media/VideoService.cs b/Abyss/Components/Services/Media/VideoService.cs index 5f3036d..7a89a83 100644 --- a/Abyss/Components/Services/Media/VideoService.cs +++ b/Abyss/Components/Services/Media/VideoService.cs @@ -11,7 +11,8 @@ public class VideoService(ResourceService rs, ConfigureService config) { public readonly string VideoFolder = Path.Combine(config.MediaRoot, "Videos"); - public async Task Init(string token, string owner, string ip) => await rs.Initialize(VideoFolder, token, owner, ip); + public async Task Init(string token, string owner, string ip) + => await rs.Initialize(VideoFolder, token, owner, ip); public async Task GetClasses(string token, string ip) => (await rs.Query(VideoFolder, token, ip))?.SortLikeWindows(); diff --git a/Abyss/Components/Services/Security/UserService.cs b/Abyss/Components/Services/Security/UserService.cs index d956c84..0bc74f7 100644 --- a/Abyss/Components/Services/Security/UserService.cs +++ b/Abyss/Components/Services/Security/UserService.cs @@ -25,39 +25,16 @@ public class UserService _database = new SQLiteAsyncConnection(config.UserDatabase, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create); _database.CreateTableAsync().Wait(); - var rootUser = _database.Table().Where(x => x.Uuid == 1).FirstOrDefaultAsync().Result; if (config.DebugMode == "Debug") _cache.Set("abyss", $"1@127.0.0.1", DateTimeOffset.Now.AddHours(1)); // Test token, can only be used locally. Will be destroyed in one hour. - - if (rootUser == null) - { - var key = GenerateKeyPair(); - string privateKeyBase64 = Convert.ToBase64String(key.Export(KeyBlobFormat.RawPrivateKey)); - string publicKeyBase64 = Convert.ToBase64String(key.Export(KeyBlobFormat.RawPublicKey)); - - var s = GenerateRandomAsciiString(8); - Console.WriteLine($"Enter the following string to create a root user: '{s}'"); - - if (Console.ReadLine() != s) - { - throw (new Exception("Invalid Input")); - } - Console.WriteLine($"Created root user. Please keep the key safe."); - Console.WriteLine("key: '" + privateKeyBase64 + "'"); - _database.InsertAsync(new User() - { - Uuid = 1, - Username = "root", - ParentId = 1, - PublicKey = publicKeyBase64, - Privilege = 1145141919, - }).Wait(); - - Console.ReadKey(); - } + } + + public async Task IsEmptyUser() + { + return await _database.Table().CountAsync() == 0; } public async Task OpenUserAsync(string user, string token, string? bindIp, string ip) @@ -76,7 +53,7 @@ public class UserService var ipToBind = string.IsNullOrWhiteSpace(bindIp) ? ip : bindIp; - var t = CreateToken(target.Uuid, ipToBind, TimeSpan.FromHours(1)); + var t = CreateToken(target.Uuid!.Value, ipToBind, TimeSpan.FromHours(1)); _logger.LogInformation("Root created 1h token for {User}, bound to {BindIp}, request from {ReqIp}", user, ipToBind, ip); @@ -104,10 +81,10 @@ public class UserService if (creating.Privilege > ou?.Privilege || ou == null) return false; - await CreateUser(new User + await AddUserAsync(new User { Username = creating.Name, - ParentId = ou.Uuid, + ParentId = ou.Uuid!.Value, Privilege = creating.Privilege, PublicKey = creating.PublicKey, }); @@ -122,7 +99,7 @@ public class UserService if (u == null) // Error: User not exists return null; - if (_cache.TryGetValue(u.Uuid, out _)) // The previous challenge has not yet expired + if (_cache.TryGetValue(u.Uuid!.Value, out _)) // The previous challenge has not yet expired _cache.Remove(u.Uuid); var c = Convert.ToBase64String(Encoding.UTF8.GetBytes(GenerateRandomAsciiString(32))); @@ -140,7 +117,7 @@ public class UserService return null; } - if (_cache.TryGetValue(u.Uuid, out string? challenge)) + if (_cache.TryGetValue(u.Uuid!.Value, out string? challenge)) { bool isVerified = VerifySignature( PublicKey.Import( @@ -208,13 +185,13 @@ public class UserService return u; } - public async Task CreateUser(User user) + public async Task AddUserAsync(User user) { await _database.InsertAsync(user); _logger.LogInformation($"Created user: {user.Username}, Uid: {user.Uuid}, Parent: {user.ParentId}, Privilege: {user.Privilege}"); } - static Key GenerateKeyPair() + public static Key GenerateKeyPair() { var algorithm = SignatureAlgorithm.Ed25519; var creationParameters = new KeyCreationParameters @@ -283,7 +260,6 @@ public class UserService return token; } - public static bool IsAlphanumeric(string input) { if (string.IsNullOrEmpty(input)) diff --git a/Abyss/Model/Media/ResourceAttribute.cs b/Abyss/Model/Media/ResourceAttribute.cs index 3499d32..33ad26a 100644 --- a/Abyss/Model/Media/ResourceAttribute.cs +++ b/Abyss/Model/Media/ResourceAttribute.cs @@ -6,7 +6,7 @@ namespace Abyss.Model.Media; public class ResourceAttribute { [PrimaryKey, AutoIncrement] - public int Id { get; set; } + public int? Id { get; set; } [Unique, NotNull] public string Uid { get; init; } = "@"; diff --git a/Abyss/Model/Security/User.cs b/Abyss/Model/Security/User.cs index 721e16b..7b682a8 100644 --- a/Abyss/Model/Security/User.cs +++ b/Abyss/Model/Security/User.cs @@ -6,7 +6,7 @@ namespace Abyss.Model.Security; public class User { [PrimaryKey, AutoIncrement] - public int Uuid { get; set; } + public int? Uuid { get; set; } [Unique, NotNull] public string Username { get; set; } = ""; [NotNull] diff --git a/Abyss/Program.cs b/Abyss/Program.cs index 579abb5..c988421 100644 --- a/Abyss/Program.cs +++ b/Abyss/Program.cs @@ -34,7 +34,7 @@ public class Program builder.Services.AddHostedService(); builder.Services.AddHostedService(); - foreach (var t in Module.Modules) + foreach (var t in ModuleAttribute.Modules) { builder.Services.AddTransient(t); } diff --git a/Abyss/Properties/launchSettings.json b/Abyss/Properties/launchSettings.json index e918606..df8a95a 100644 --- a/Abyss/Properties/launchSettings.json +++ b/Abyss/Properties/launchSettings.json @@ -8,7 +8,7 @@ "applicationUrl": "http://localhost:3000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "MEDIA_ROOT" : "/storage", + "MEDIA_ROOT" : "/opt/abyss", "ALLOWED_PORTS" : "3000", "DEBUG_MODE": "Debug" } diff --git a/abyssctl/App/App.cs b/abyssctl/App/App.cs index d7f19da..565cd92 100644 --- a/abyssctl/App/App.cs +++ b/abyssctl/App/App.cs @@ -2,8 +2,8 @@ using System.Net.Sockets; using System.Reflection; using System.Text; +using abyssctl.App.Attributes; using abyssctl.App.Interfaces; -using abyssctl.App.Modules; using abyssctl.Model; using abyssctl.Static; using CommandLine; @@ -13,16 +13,18 @@ namespace abyssctl.App; public class App { - private static readonly string SocketPath = "ctl.sock"; + private static readonly string SocketPath = Path.Combine(Path.GetTempPath(), "abyss-ctl.sock"); - public static async Task CtlWriteRead(Ctl ctl) + public static async Task CtlWriteRead(string[] param) { + var attr = typeof(T).GetCustomAttribute()!; + 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)); + await socket.WriteBase64Async(Ctl.MakeBase64(attr.Head, param)); var s = Encoding.UTF8.GetString( Convert.FromBase64String(await socket.ReadBase64Async())); return JsonConvert.DeserializeObject(s)!; @@ -41,18 +43,7 @@ public class App { return await Task.Run(() => { - Assembly assembly = Assembly.GetExecutingAssembly(); - Type attributeType = typeof(VerbAttribute); - const string targetNamespace = "abyssctl.App.Modules"; - - var moduleTypes = assembly.GetTypes() - .Where(t => t is { IsClass: true, IsAbstract: false, IsInterface: false }) - .Where(t => t.Namespace == targetNamespace) - .Where(t => typeof(IOptions).IsAssignableFrom(t)) - .Where(t => t.IsDefined(attributeType, inherit: true)) - .ToArray(); - - return Parser.Default.ParseArguments(args, moduleTypes) + return Parser.Default.ParseArguments(args, ModuleAttribute.Modules) .MapResult( (object obj) => { diff --git a/abyssctl/App/Attributes/ModuleAttribute.cs b/abyssctl/App/Attributes/ModuleAttribute.cs new file mode 100644 index 0000000..85910b2 --- /dev/null +++ b/abyssctl/App/Attributes/ModuleAttribute.cs @@ -0,0 +1,28 @@ +using System.Reflection; +using abyssctl.App.Interfaces; +using CommandLine; + +namespace abyssctl.App.Attributes; + +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +public class ModuleAttribute(int head) : Attribute +{ + public int Head { get; } = head; + + public static Type[] Modules + { + get + { + Assembly assembly = Assembly.GetExecutingAssembly(); + const string targetNamespace = "abyssctl.App.Modules"; + + return assembly.GetTypes() + .Where(t => t is { IsClass: true, IsAbstract: false, IsInterface: false }) + .Where(t => t.Namespace == targetNamespace) + .Where(t => typeof(IOptions).IsAssignableFrom(t)) + .Where(t => t.IsDefined(typeof(VerbAttribute), inherit: true)) + .Where(t => t.IsDefined(typeof(ModuleAttribute), inherit: false)) + .ToArray(); + } + } +} \ No newline at end of file diff --git a/abyssctl/App/Modules/HelloOptions.cs b/abyssctl/App/Modules/HelloOptions.cs index b765f84..b54b125 100644 --- a/abyssctl/App/Modules/HelloOptions.cs +++ b/abyssctl/App/Modules/HelloOptions.cs @@ -1,22 +1,30 @@ +using abyssctl.App.Attributes; using abyssctl.App.Interfaces; using abyssctl.Model; using CommandLine; namespace abyssctl.App.Modules; +[Module(100)] [Verb("hello", HelpText = "Say hello to abyss server")] public class HelloOptions: IOptions { + [Option('r', "raw", Default = false, HelpText = "Show raw response.")] + public bool Raw { get; set; } + public async Task Run() { - var r = await App.CtlWriteRead(new Ctl + var r = await App.CtlWriteRead([]); + + if (Raw) { - Head = 100, - Params = [] - }); - - Console.WriteLine($"Response Code: {r.Head}"); - Console.WriteLine($"Params: {string.Join(",", r.Params)}"); + Console.WriteLine($"Response Code: {r.Head}"); + Console.WriteLine($"Params: {string.Join(",", r.Params)}"); + } + else + { + Console.WriteLine($"Server: {string.Join(",", r.Params)}"); + } return 0; } } \ No newline at end of file diff --git a/abyssctl/App/Modules/InitOptions.cs b/abyssctl/App/Modules/InitOptions.cs new file mode 100644 index 0000000..9033f67 --- /dev/null +++ b/abyssctl/App/Modules/InitOptions.cs @@ -0,0 +1,20 @@ +using abyssctl.App.Attributes; +using abyssctl.App.Interfaces; +using CommandLine; + +namespace abyssctl.App.Modules; + + +[Module(103)] +[Verb("init", HelpText = "Initialize abyss server")] +public class InitOptions: IOptions +{ + public async Task Run() + { + var r = await App.CtlWriteRead([]); + 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/UserAddOptions.cs b/abyssctl/App/Modules/UserAddOptions.cs new file mode 100644 index 0000000..46eb472 --- /dev/null +++ b/abyssctl/App/Modules/UserAddOptions.cs @@ -0,0 +1,26 @@ +using abyssctl.App.Attributes; +using abyssctl.App.Interfaces; +using CommandLine; + +namespace abyssctl.App.Modules; + + +[Module(104)] +[Verb("useradd", HelpText = "Add user")] +public class UserAddOptions: IOptions +{ + [Option('u', "username", Required = true, HelpText = "Username for new user.")] + public string Username { get; set; } = ""; + + [Option('p', "privilege", Required = true, HelpText = "User privilege.")] + public int Privilege { get; set; } + public async Task Run() + { + var r = await App.CtlWriteRead([Username, Privilege.ToString()]); + + 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 index 3615a87..f4f83a8 100644 --- a/abyssctl/App/Modules/VersionOptions.cs +++ b/abyssctl/App/Modules/VersionOptions.cs @@ -1,8 +1,10 @@ +using abyssctl.App.Attributes; using abyssctl.App.Interfaces; using CommandLine; namespace abyssctl.App.Modules; +[Module(101)] [Verb("ver", HelpText = "Get server version")] public class VersionOptions: IOptions {