[add] function implementation

This commit is contained in:
acite
2025-08-24 00:56:08 +08:00
parent 240b0d98fd
commit 052a2da270
35 changed files with 5261 additions and 4 deletions

24
Abyss/Abyss.csproj Normal file
View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="K4os.Hash.xxHash" Version="1.0.8" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.8"/>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NSec.Cryptography" Version="25.4.0" />
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="Standart.Hash.xxHash" Version="4.0.5" />
<PackageReference Include="System.IO.Hashing" Version="10.0.0-preview.7.25380.108" />
</ItemGroup>
<ItemGroup>
<Folder Include="Components\Controllers\Media\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,19 @@
using Abyss.Components.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
namespace Abyss.Components.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AbyssController(ILogger<AbyssController> logger, ConfigureService config) : Controller
{
private ILogger<AbyssController> _logger = logger;
private ConfigureService _config = config;
[HttpGet]
public IActionResult GetCollection()
{
return Ok($"Abyss {_config.Version}. \nMediaRoot: {_config.MediaRoot}");
}
}

View File

@@ -0,0 +1,59 @@
using Abyss.Components.Services;
using Abyss.Components.Static;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
namespace Abyss.Components.Controllers.Media;
using System.IO;
[ApiController]
[Route("api/[controller]")]
public class ImageController(ILogger<ImageController> logger, ResourceService rs, ConfigureService config) : Controller
{
public readonly string ImageFolder = Path.Combine(config.MediaRoot, "Images");
[HttpPost("init")]
public async Task<IActionResult> InitAsync(string token, string owner)
{
var r = await rs.Initialize(ImageFolder, token, owner, Ip);
if(r) return Ok(r);
return StatusCode(403, new { message = "403 Denied" });
}
[HttpGet]
public async Task<IActionResult> QueryCollections(string token)
{
var r = await rs.Query(ImageFolder, token, Ip);
if(r == null)
return StatusCode(401, new { message = "Unauthorized" });
return Ok(r);
}
[HttpGet("{id}")]
public async Task<IActionResult> Query(string id, string token)
{
var d = Helpers.SafePathCombine(ImageFolder, [id, "summary.json"]);
if (d == null) return StatusCode(403, new { message = "403 Denied" });
var r = await rs.Get(d, token, Ip);
if (!r) return StatusCode(403, new { message = "403 Denied" });
return Ok(await System.IO.File.ReadAllTextAsync(d));
}
[HttpGet("{id}/{file}")]
public async Task<IActionResult> Get(string id, string file, string token)
{
var d = Helpers.SafePathCombine(ImageFolder, [id, file]);
if (d == null) return StatusCode(403, new { message = "403 Denied" });
var r = await rs.Get(d, token, Ip);
if (!r) return StatusCode(403, new { message = "403 Denied" });
return PhysicalFile(d, "image/jpeg", enableRangeProcessing: true);
}
private string Ip => HttpContext.Connection.RemoteIpAddress?.ToString() ?? "127.0.0.1";
}

View File

@@ -0,0 +1,94 @@
using System.Diagnostics;
using Abyss.Components.Services;
using Abyss.Components.Static;
using Microsoft.AspNetCore.Mvc;
namespace Abyss.Components.Controllers.Media;
[ApiController]
[Route("api/[controller]")]
public class VideoController(ILogger<VideoController> logger, ResourceService rs, ConfigureService config) : Controller
{
private ILogger<VideoController> _logger = logger;
public readonly string VideoFolder = Path.Combine(config.MediaRoot, "Videos");
[HttpPost("init")]
public async Task<IActionResult> InitAsync(string token, string owner)
{
var r = await rs.Initialize(VideoFolder, token, owner, Ip);
if(r) return Ok(r);
return StatusCode(403, new { message = "403 Denied" });
}
[HttpGet]
public async Task<IActionResult> GetClass(string token)
{
var r = await rs.Query(VideoFolder, token, Ip);
if(r == null)
return StatusCode(401, new { message = "Unauthorized" });
return Ok(r);
}
[HttpGet("{klass}")]
public async Task<IActionResult> QueryClass(string klass, string token)
{
var d = Helpers.SafePathCombine(VideoFolder, klass);
if (d == null) return StatusCode(403, new { message = "403 Denied" });
var r = await rs.Query(d, token, Ip);
if (r == null) return StatusCode(401, new { message = "Unauthorized" });
return Ok(r);
}
[HttpGet("{klass}/{id}")]
public async Task<IActionResult> QueryVideo(string klass, string id, string token)
{
var d = Helpers.SafePathCombine(VideoFolder, [klass, id, "summary.json"]);
if (d == null) return StatusCode(403, new { message = "403 Denied" });
var r = await rs.Get(d, token, Ip);
if (!r) return StatusCode(403, new { message = "403 Denied" });
return Ok(await System.IO.File.ReadAllTextAsync(d));
}
[HttpGet("{klass}/{id}/cover")]
public async Task<IActionResult> Cover(string klass, string id, string token)
{
var d = Helpers.SafePathCombine(VideoFolder, [klass, id, "cover.jpg"]);
if (d == null) return StatusCode(403, new { message = "403 Denied" });
var r = await rs.Get(d, token, Ip);
if (!r) return StatusCode(403, new { message = "403 Denied" });
return PhysicalFile(d, "image/jpeg", enableRangeProcessing: true);
}
[HttpGet("{klass}/{id}/gallery/{pic}")]
public async Task<IActionResult> Gallery(string klass, string id, string pic, string token)
{
var d = Helpers.SafePathCombine(VideoFolder, [klass, id, "gallery", pic]);
if (d == null) return StatusCode(403, new { message = "403 Denied" });
var r = await rs.Get(d, token, Ip);
if (!r) return StatusCode(403, new { message = "403 Denied" });
return PhysicalFile(d, "image/jpeg", enableRangeProcessing: true);
}
[HttpGet("{klass}/{id}/av")]
public async Task<IActionResult> Av(string klass, string id, string token)
{
var d = Helpers.SafePathCombine(VideoFolder, [klass, id, "video.mp4"]);
if (d == null) return StatusCode(403, new { message = "403 Denied" });
var r = await rs.Get(d, token, Ip);
if (!r) return StatusCode(403, new { message = "403 Denied" });
return PhysicalFile(d, "video/mp4", enableRangeProcessing: true);
}
private string Ip => HttpContext.Connection.RemoteIpAddress?.ToString() ?? "127.0.0.1";
}

View File

@@ -0,0 +1,13 @@
明确几个此目录下的API的开发理念
- 永远不传输私钥
root用户的私钥仅通过服务器shell配置
私钥在客户端生成,仅将公钥传输到服务器
token通过挑战-响应机制创建,加密传输
- 用户管理
创建任何新用户都必须通过一个已有用户的token且新用户权限等级不大于该用户
root用户的权限等级为 **114514**

View File

@@ -0,0 +1,107 @@
// UserController.cs
using System.Text.RegularExpressions;
using Abyss.Components.Services;
using Abyss.Model;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
namespace Abyss.Components.Controllers.Security;
[ApiController]
[Route("api/[controller]")]
[EnableRateLimiting("Fixed")]
public class UserController(UserService user, ILogger<UserController> logger) : Controller
{
private readonly ILogger<UserController> _logger = logger;
private readonly UserService _user = user;
[HttpGet("{user}")]
public async Task<IActionResult> Challenge(string user)
{
var c = await _user.Challenge(user);
if(c == null)
return StatusCode(403, new { message = "Access forbidden" });
return Ok(c);
}
[HttpPost("{user}")]
public async Task<IActionResult> Challenge(string user, [FromBody] ChallengeResponse response)
{
var r = await _user.Verify(user, response.Response, Ip);
if(r == null)
return StatusCode(403, new { message = "Access forbidden" });
return Ok(r);
}
[HttpPost("validate")]
public IActionResult Validate(string token)
{
var u = _user.Validate(token, Ip);
if (u == null)
{
return StatusCode(401, new { message = "Invalid" });
}
return Ok(u);
}
[HttpPost("destroy")]
public IActionResult Destroy(string token)
{
var u = _user.Validate(token, Ip);
if (u == null)
{
return StatusCode(401, new { message = "Invalid" });
}
_user.Destroy(token);
return Ok("Success");
}
[HttpPatch("{user}")]
public async Task<IActionResult> Create(string user, [FromBody] UserCreating creating)
{
// Valid token
var r = await _user.Verify(user, creating.Response, Ip);
if(r == null)
return StatusCode(403, new { message = "Denied" });
// User exists ?
var cu = await _user.QueryUser(creating.Name);
if(cu != null)
return StatusCode(403, new { message = "Denied" });
// Valid username string
if(!IsAlphanumeric(creating.Name))
return StatusCode(403, new { message = "Denied" });
// Valid parent && Privilege
var ou = await _user.QueryUser(_user.Validate(r, Ip) ?? "");
if(creating.Parent != (_user.Validate(r, Ip) ?? "") || creating.Privilege > ou?.Privilege)
return StatusCode(403, new { message = "Denied" });
await _user.CreateUser(new User()
{
Name = creating.Name,
Parent = _user.Validate(r, Ip) ?? "",
Privilege = creating.Privilege,
PublicKey = creating.PublicKey,
} );
_user.Destroy(r);
return Ok("Success");
}
public static bool IsAlphanumeric(string input)
{
if (string.IsNullOrEmpty(input))
return false;
return Regex.IsMatch(input, @"^[a-zA-Z0-9]+$");
}
private string Ip => HttpContext.Connection.RemoteIpAddress?.ToString() ?? "127.0.0.1";
}

View File

@@ -0,0 +1,9 @@
namespace Abyss.Components.Services;
public class ConfigureService
{
public string MediaRoot { get; set; } = Environment.GetEnvironmentVariable("MEDIA_ROOT") ?? "/opt";
public string Version { get; } = "Alpha v0.1";
public string UserDatabase { get; set; } = "user.db";
public string RaDatabase { get; set; } = "ra.db";
}

View File

@@ -0,0 +1,338 @@
// ResourceService.cs
using System.Text;
using System.Text.RegularExpressions;
using Abyss.Components.Static;
using Abyss.Model;
using Microsoft.Extensions.Caching.Memory;
using SQLite;
using System.IO.Hashing;
namespace Abyss.Components.Services;
public enum OperationType
{
Read, // Query, Read
Write, // Write, Delete
Security // Chown, Chmod
}
public class ResourceService
{
private readonly ILogger<ResourceService> _logger;
private readonly ConfigureService _config;
private readonly IMemoryCache _cache;
private readonly UserService _user;
private readonly SQLiteAsyncConnection _database;
private static readonly Regex PermissionRegex =
new Regex(@"^([r-][w-]),([r-][w-]),([r-][w-])$", RegexOptions.Compiled);
public ResourceService(ILogger<ResourceService> logger, ConfigureService config, IMemoryCache cache,
UserService user)
{
_logger = logger;
_config = config;
_cache = cache;
_user = user;
_database = new SQLiteAsyncConnection(config.RaDatabase, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
_database.CreateTableAsync<ResourceAttribute>().Wait();
}
// Create UID only for resources, without considering advanced hash security such as adding salt
private string Uid(string path)
{
var b = Encoding.UTF8.GetBytes(path);
var r = XxHash128.Hash(b, 0x11451419);
return Convert.ToBase64String(r ?? []);
}
public async Task<bool> 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))
return false;
path = Path.GetRelativePath(_config.MediaRoot, path);
string? username = _user.Validate(token, ip);
if (username == null)
{
// No permission granted for invalid tokens
_logger.LogError($"Invalid token: {token}");
return false;
}
User? user = await _user.QueryUser(username);
if (user == null || user.Name != username)
{
_logger.LogError($"Verification failed: {token}");
return false; // Two-factor authentication
}
var parts = path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
.Where(p => !string.IsNullOrEmpty(p))
.ToArray();
for (int i = 0; i < parts.Length - 1; i++)
{
var subPath = Path.Combine(parts.Take(i + 1).ToArray());
var uidDir = Uid(subPath);
var raDir = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uidDir)
.FirstOrDefaultAsync();
if (raDir == null)
{
_logger.LogError($"Permission denied: {username} has no read access to parent directory {subPath}");
return false;
}
if (!await CheckPermission(user, raDir, OperationType.Read))
{
_logger.LogError($"Permission denied: {username} has no read access to parent directory {subPath}");
return false;
}
}
var uid = Uid(path);
ResourceAttribute? ra = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uid)
.FirstOrDefaultAsync();
if (ra == null)
{
_logger.LogError($"Permission check failed: User: {username}, Resource: {path}, Type: {type.ToString()} ");
return false;
}
var l = await CheckPermission(user, ra, type);
if (!l)
{
_logger.LogError($"Permission check failed: User: {username}, Resource: {path}, Type: {type.ToString()} ");
}
return l;
}
private async Task<bool> CheckPermission(User? user, ResourceAttribute? ra, OperationType type)
{
if (user == null || ra == null) return false;
if(!PermissionRegex.IsMatch(ra.Permission)) return false;
var perms = ra.Permission.Split(',');
if (perms.Length != 3) return false;
var owner = await _user.QueryUser(ra.Owner);
if (owner == null) return false;
bool isOwner = ra.Owner == user.Name;
bool isPeer = !isOwner && user.Privilege == owner.Privilege;
bool isOther = !isOwner && !isPeer;
string currentPerm;
if (isOwner) currentPerm = perms[0];
else if (isPeer) currentPerm = perms[1];
else if (isOther) currentPerm = perms[2];
else return false;
switch (type)
{
case OperationType.Read:
return currentPerm.Contains('r') || (user.Privilege > owner.Privilege);
case OperationType.Write:
return currentPerm.Contains('w') || (user.Privilege > owner.Privilege);
case OperationType.Security:
return (isOwner && currentPerm.Contains('w')) || user.Name == "root";
default:
return false;
}
}
public async Task<string[]?> Query(string path, string token, string ip)
{
if(!await Valid(path, token, OperationType.Read, ip))
return null;
if (Helpers.GetPathType(path) != PathType.Directory)
return null;
var files = Directory.EnumerateFileSystemEntries(path, "*", SearchOption.TopDirectoryOnly);
return files.Select(x => Path.GetRelativePath(path, x)).ToArray();
}
public async Task<bool> Get(string path, string token, string ip)
{
return await Valid(path, token, OperationType.Read, ip);
}
public async Task<bool> Initialize(string path, string token, string username, string ip)
{
// 1. Authorization: Verify the operation is performed by 'root'
var requester = _user.Validate(token, ip);
if (requester != "root")
{
_logger.LogWarning($"Permission denied: Non-root user '{requester ?? "unknown"}' attempted to initialize resources.");
return false;
}
// 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.");
return false;
}
var ownerUser = await _user.QueryUser(username);
if (ownerUser == null)
{
_logger.LogError($"Initialization failed: Owner user '{username}' does not exist.");
return false;
}
try
{
// 3. Traversal: Get the root directory and all its descendants (files and subdirectories)
var allPaths = Directory.EnumerateFileSystemEntries(path, "*", SearchOption.AllDirectories).Prepend(path);
// 4. Filtering: Identify which paths are not yet in the database
var newResources = new List<ResourceAttribute>();
foreach (var p in allPaths)
{
var currentPath = Path.GetRelativePath(_config.MediaRoot, p);
var uid = Uid(currentPath);
var existing = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uid).FirstOrDefaultAsync();
// If it's not in the database, add it to our list for batch insertion
if (existing == null)
{
newResources.Add(new ResourceAttribute
{
Uid = uid,
Name = currentPath,
Owner = username,
Permission = "rw,--,--"
});
}
}
// 5. Database Insertion: Add all new resources in a single, efficient transaction
if (newResources.Any())
{
await _database.InsertAllAsync(newResources);
_logger.LogInformation($"Successfully initialized {newResources.Count} new resources under '{path}' for user '{username}'.");
}
else
{
_logger.LogInformation($"No new resources to initialize under '{path}'. All items already exist in the database.");
}
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, $"An error occurred during resource initialization for path '{path}'.");
return false;
}
}
public async Task<bool> Put(string path, string token, string ip)
{
throw new NotImplementedException();
}
public async Task<bool> Delete(string path, string token, string ip)
{
throw new NotImplementedException();
}
public async Task<bool> Chmod(string path, string token, string permission, string ip)
{
if(!await Valid(path, token, OperationType.Security, ip))
return false;
// Validate the permission format using the existing regex
if (!PermissionRegex.IsMatch(permission))
{
_logger.LogError($"Invalid permission format: {permission}");
return false;
}
try
{
path = Path.GetRelativePath(_config.MediaRoot, path);
var uid = Uid(path);
var resource = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uid).FirstOrDefaultAsync();
if (resource == null)
{
_logger.LogError($"Resource not found: {path}");
return false;
}
resource.Permission = permission;
var rowsAffected = await _database.UpdateAsync(resource);
if (rowsAffected > 0)
{
_logger.LogInformation($"Successfully changed permissions for '{path}' to '{permission}'");
return true;
}
else
{
_logger.LogError($"Failed to update permissions for: {path}");
return false;
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error changing permissions for: {path}");
return false;
}
}
public async Task<bool> Chown(string path, string token, string owner, string ip)
{
if(!await Valid(path, token, OperationType.Security, ip))
return false;
// Validate that the new owner exists
var newOwner = await _user.QueryUser(owner);
if (newOwner == null)
{
_logger.LogError($"New owner '{owner}' does not exist");
return false;
}
try
{
path = Path.GetRelativePath(_config.MediaRoot, path);
var uid = Uid(path);
var resource = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uid).FirstOrDefaultAsync();
if (resource == null)
{
_logger.LogError($"Resource not found: {path}");
return false;
}
resource.Owner = owner;
var rowsAffected = await _database.UpdateAsync(resource);
if (rowsAffected > 0)
{
_logger.LogInformation($"Successfully changed ownership of '{path}' to '{owner}'");
return true;
}
else
{
_logger.LogError($"Failed to change ownership for: {path}");
return false;
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error changing ownership for: {path}");
return false;
}
}
}

View File

@@ -0,0 +1,177 @@
// UserService.cs
using System.Security.Cryptography;
using System.Text;
using Abyss.Model;
using Microsoft.Extensions.Caching.Memory;
using NSec.Cryptography;
using SQLite;
namespace Abyss.Components.Services;
public class UserService
{
private readonly ILogger<UserService> _logger;
private readonly ConfigureService _config;
private readonly IMemoryCache _cache;
private readonly SQLiteAsyncConnection _database;
public UserService(ILogger<UserService> logger, ConfigureService config, IMemoryCache cache)
{
_logger = logger;
_config = config;
_cache = cache;
_database = new SQLiteAsyncConnection(config.UserDatabase, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
_database.CreateTableAsync<User>().Wait();
var rootUser = _database.Table<User>().Where(x => x.Name == "root").FirstOrDefaultAsync().Result;
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()
{
Name = "root",
Parent = "root",
PublicKey = publicKeyBase64,
Privilege = 1145141919,
}).Wait();
Console.ReadKey();
}
}
public async Task<string?> Challenge(string user)
{
var u = await _database.Table<User>().Where(x => x.Name == user).FirstOrDefaultAsync();
if (u == null) // Error: User not exists
return null;
if (_cache.TryGetValue(u.Name, out var challenge)) // The previous challenge has not yet expired
_cache.Remove(u.Name);
var c = Convert.ToBase64String(Encoding.UTF8.GetBytes(GenerateRandomAsciiString(32)));
_cache.Set(u.Name,c, DateTimeOffset.Now.AddMinutes(1));
return c;
}
// The challenge source and response source are not necessarily required to be the same,
// but the source that obtains the token must be the same as the source that uses the token in the future
public async Task<string?> Verify(string user, string response, string ip)
{
var u = await _database.Table<User>().Where(x => x.Name == user).FirstOrDefaultAsync();
if (u == null) // Error: User not exists
{
return null;
}
if (_cache.TryGetValue(u.Name, out string? challenge))
{
bool isVerified = VerifySignature(
PublicKey.Import(
SignatureAlgorithm.Ed25519,
Convert.FromBase64String(u.PublicKey),
KeyBlobFormat.RawPublicKey),
Convert.FromBase64String(challenge ?? ""),
Convert.FromBase64String(response));
if (!isVerified)
{
// Verification failed, set the challenge string to random to prevent duplicate verification
_cache.Set(u.Name, $"failed : {GenerateRandomAsciiString(32)}", DateTimeOffset.Now.AddMinutes(1));
return null;
}
else
{
// Remove the challenge string and create a session
_cache.Remove(u.Name);
var s = GenerateRandomAsciiString(64);
_cache.Set(s, $"{u.Name}@{ip}", DateTimeOffset.Now.AddDays(1));
_logger.LogInformation($"Verified {u.Name}@{ip}");
return s;
}
}
return null;
}
public string? Validate(string token, string ip)
{
if (_cache.TryGetValue(token, out string? userAndIp))
{
if (ip != userAndIp?.Split('@')[1])
{
_logger.LogError($"Token used from another Host: {token}");
Destroy(token);
return null;
}
_logger.LogInformation($"Validated {userAndIp}");
return userAndIp?.Split('@')[0];
}
_logger.LogWarning($"Validation failed {token}");
return null;
}
public void Destroy(string token)
{
_cache.Remove(token);
}
public async Task<User?> QueryUser(string user)
{
var u = await _database.Table<User>().Where(x => x.Name == user).FirstOrDefaultAsync();
return u;
}
public async Task CreateUser(User user)
{
await _database.InsertAsync(user);
_logger.LogInformation($"Created user: {user.Name}, Parent: {user.Parent}, Privilege: {user.Privilege}");
}
static Key GenerateKeyPair()
{
var algorithm = SignatureAlgorithm.Ed25519;
var creationParameters = new KeyCreationParameters
{
ExportPolicy = KeyExportPolicies.AllowPlaintextExport
};
return Key.Create(algorithm, creationParameters);
}
public static string GenerateRandomAsciiString(int length)
{
const string asciiChars = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
using (var rng = RandomNumberGenerator.Create())
{
byte[] randomBytes = new byte[length];
rng.GetBytes(randomBytes);
char[] result = new char[length];
for (int i = 0; i < length; i++)
{
result[i] = asciiChars[randomBytes[i] % asciiChars.Length];
}
return new string(result);
}
}
static bool VerifySignature(PublicKey publicKey, byte[] data, byte[] signature)
{
var algorithm = SignatureAlgorithm.Ed25519;
return algorithm.Verify(publicKey, data, signature);
}
}

View File

@@ -0,0 +1,60 @@
namespace Abyss.Components.Static;
public static class Helpers
{
public static string? SafePathCombine(string basePath, params string[] pathParts)
{
if (string.IsNullOrWhiteSpace(basePath))
return null;
if (basePath.Contains("..") || pathParts.Any(p => p.Contains("..")))
return null;
string combinedPath = Path.Combine(basePath, Path.Combine(pathParts));
string fullPath = Path.GetFullPath(combinedPath);
if (!fullPath.StartsWith(Path.GetFullPath(basePath), StringComparison.OrdinalIgnoreCase))
return null;
return fullPath;
}
public static PathType GetPathType(string path)
{
try
{
var attributes = File.GetAttributes(path);
if ((attributes & FileAttributes.Directory) == FileAttributes.Directory)
{
return PathType.Directory;
}
else
{
return PathType.File;
}
}
catch (FileNotFoundException)
{
return PathType.NotFound;
}
catch (DirectoryNotFoundException)
{
return PathType.NotFound;
}
catch (UnauthorizedAccessException)
{
return PathType.AccessDenied;
}
return PathType.NotFound;
}
}
public enum PathType
{
File,
Directory,
NotFound,
AccessDenied
}

View File

@@ -0,0 +1,6 @@
namespace Abyss.Model;
public class ChallengeResponse
{
public string Response { get; set; } = "";
}

View File

@@ -0,0 +1,9 @@
namespace Abyss.Model;
public class ResourceAttribute
{
public string Uid { get; set; } = "@";
public string Name { get; set; } = "@";
public string Owner { get; set; } = "@";
public string Permission { get; set; } = "--,--,--";
}

9
Abyss/Model/User.cs Normal file
View File

@@ -0,0 +1,9 @@
namespace Abyss.Model;
public class User
{
public string Name { get; set; } = "";
public string Parent { get; set; } = "";
public string PublicKey { get; set; } = "";
public int Privilege { get; set; }
}

View File

@@ -0,0 +1,10 @@
namespace Abyss.Model;
public class UserCreating
{
public string Response { get; set; } = "";
public string Name { get; set; } = "";
public string Parent { get; set; } = "";
public string PublicKey { get; set; } = "";
public int Privilege { get; set; }
}

57
Abyss/Program.cs Normal file
View File

@@ -0,0 +1,57 @@
using System.Threading.RateLimiting;
using Abyss.Components.Services;
using Microsoft.AspNetCore.RateLimiting;
namespace Abyss;
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddMemoryCache();
builder.Services.AddOpenApi();
builder.Services.AddControllers();
builder.Services.AddSingleton<ConfigureService>();
builder.Services.AddSingleton<UserService>();
builder.Services.AddSingleton<ResourceService>();
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("Fixed", policyOptions =>
{
// 时间窗口长度
policyOptions.Window = TimeSpan.FromSeconds(30);
policyOptions.PermitLimit = 10;
policyOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
policyOptions.QueueLimit = 0;
});
options.OnRejected = async (context, token) =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
await context.HttpContext.Response.WriteAsync("Too many requests. Please try later.", token);
};
});
builder.Services.BuildServiceProvider().GetRequiredService<UserService>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
// app.UseHttpsRedirection();
app.UseAuthorization();
app.MapStaticAssets();
app.MapControllers();
app.UseRateLimiter();
app.Run();
}
}

View File

@@ -0,0 +1,24 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://192.168.1.244:5198",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"MEDIA_ROOT" : "/storage"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7013;http://localhost:5198",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

9
Abyss/appsettings.json Normal file
View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}