Compare commits
8 Commits
dev-abyss
...
8465ec5b2a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8465ec5b2a | ||
|
|
ec7306ade2 | ||
|
|
9aa987d52a | ||
|
|
e174238d3c | ||
|
|
cad92f8fa5 | ||
|
|
9b6a4a9982 | ||
|
|
197cf525fb | ||
|
|
ae93b75e41 |
15
.idea/.idea.Abyss/.idea/workspace.xml
generated
15
.idea/.idea.Abyss/.idea/workspace.xml
generated
@@ -11,7 +11,7 @@
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="bf317275-3039-49bb-a475-725a800a0cce" name="Changes" comment="">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Abyss/Components/Services/UserService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Services/UserService.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -85,8 +85,8 @@
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"XThreadsFramesViewSplitterKey": "0.30266345",
|
||||
"git-widget-placeholder": "dev-abyss",
|
||||
"last_opened_file_path": "/storage/Images/31/summary.json",
|
||||
"git-widget-placeholder": "main",
|
||||
"last_opened_file_path": "/home/acite/embd/WebProjects/Abyss/README.md",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
@@ -201,6 +201,15 @@
|
||||
<workItem from="1757687641035" duration="2969000" />
|
||||
<workItem from="1757693751836" duration="667000" />
|
||||
<workItem from="1757694833696" duration="11000" />
|
||||
<workItem from="1757695721386" duration="749000" />
|
||||
<workItem from="1757702942841" duration="32000" />
|
||||
<workItem from="1757735249561" duration="5523000" />
|
||||
<workItem from="1757742881713" duration="2285000" />
|
||||
<workItem from="1757745929389" duration="93000" />
|
||||
<workItem from="1757751423586" duration="2687000" />
|
||||
<workItem from="1757782027930" duration="308000" />
|
||||
<workItem from="1757830765557" duration="1218000" />
|
||||
<workItem from="1757862781213" duration="341000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
|
||||
@@ -9,6 +9,8 @@ using Newtonsoft.Json.Linq;
|
||||
namespace Abyss.Components.Controllers.Media;
|
||||
using System.IO;
|
||||
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ImageController(ILogger<ImageController> logger, ResourceService rs, ConfigureService config) : BaseController
|
||||
@@ -46,6 +48,25 @@ public class ImageController(ILogger<ImageController> logger, ResourceService rs
|
||||
return Ok(await System.IO.File.ReadAllTextAsync(d));
|
||||
}
|
||||
|
||||
[HttpPost("bulkquery")]
|
||||
public async Task<IActionResult> QueryBulk([FromQuery] string token, [FromBody] string[] id)
|
||||
{
|
||||
List<string> result = new List<string>();
|
||||
|
||||
var db = id.Select(x => Helpers.SafePathCombine(ImageFolder, [x, "summary.json"])).ToArray();
|
||||
if (db.Any(x => x == null))
|
||||
return BadRequest();
|
||||
|
||||
if(!await rs.GetAll(db!, token, Ip))
|
||||
return StatusCode(403, new { message = "403 Denied" });
|
||||
|
||||
var rc = db.Select(x => System.IO.File.ReadAllTextAsync(x!)).ToArray();
|
||||
string[] rcs = await Task.WhenAll(rc);
|
||||
var rjs = rcs.Select(JsonConvert.DeserializeObject<Comic>).Select(x => x!).ToArray();
|
||||
|
||||
return Ok(JsonConvert.SerializeObject(rjs));
|
||||
}
|
||||
|
||||
[HttpPost("{id}/bookmark")]
|
||||
public async Task<IActionResult> Bookmark(string id, string token, [FromBody] Bookmark bookmark)
|
||||
{
|
||||
|
||||
@@ -8,6 +8,8 @@ using Newtonsoft.Json;
|
||||
|
||||
namespace Abyss.Components.Controllers.Media;
|
||||
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class VideoController(ILogger<VideoController> logger, ResourceService rs, ConfigureService config) : BaseController
|
||||
@@ -73,6 +75,25 @@ public class VideoController(ILogger<VideoController> logger, ResourceService rs
|
||||
return Ok(await System.IO.File.ReadAllTextAsync(d));
|
||||
}
|
||||
|
||||
[HttpPost("{klass}/bulkquery")]
|
||||
public async Task<IActionResult> QueryBulk([FromQuery] string token, [FromBody] string[] id, [FromRoute] string klass)
|
||||
{
|
||||
List<string> result = new List<string>();
|
||||
|
||||
var db = id.Select(x => Helpers.SafePathCombine(VideoFolder, [klass, x, "summary.json"])).ToArray();
|
||||
if(db.Any(x => x == null))
|
||||
return BadRequest();
|
||||
|
||||
if(!await rs.GetAll(db!, token, Ip))
|
||||
return StatusCode(403, new { message = "403 Denied" });
|
||||
|
||||
var rc = db.Select(x => System.IO.File.ReadAllTextAsync(x!)).ToArray();
|
||||
string[] rcs = await Task.WhenAll(rc);
|
||||
var rjs = rcs.Select(JsonConvert.DeserializeObject<Video>).Select(x => x!).ToList();
|
||||
|
||||
return Ok(JsonConvert.SerializeObject(rjs));
|
||||
}
|
||||
|
||||
[HttpGet("{klass}/{id}/cover")]
|
||||
public async Task<IActionResult> Cover(string klass, string id, string token)
|
||||
{
|
||||
@@ -82,8 +103,6 @@ public class VideoController(ILogger<VideoController> logger, ResourceService rs
|
||||
var r = await rs.Get(d, token, Ip);
|
||||
if (!r) return StatusCode(403, new { message = "403 Denied" });
|
||||
|
||||
_logger.LogInformation($"Cover found for {id}");
|
||||
|
||||
return PhysicalFile(d, "image/jpeg", enableRangeProcessing: true);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ using Abyss.Components.Tools;
|
||||
|
||||
namespace Abyss.Components.Services;
|
||||
|
||||
public class AbyssService(ILogger<AbyssService> logger, ConfigureService config) : IHostedService, IDisposable
|
||||
public class AbyssService(ILogger<AbyssService> logger, ConfigureService config, UserService user) : IHostedService, IDisposable
|
||||
{
|
||||
private Task? _executingTask;
|
||||
private CancellationTokenSource? _cts;
|
||||
@@ -41,6 +41,7 @@ public class AbyssService(ILogger<AbyssService> logger, ConfigureService config)
|
||||
int bytesRead = await upstream.ReadAsync(buffer, 0, buffer.Length, token);
|
||||
if (bytesRead == 0)
|
||||
break;
|
||||
|
||||
await client.WriteAsync(buffer, 0, bytesRead, token);
|
||||
}
|
||||
});
|
||||
@@ -51,10 +52,9 @@ public class AbyssService(ILogger<AbyssService> logger, ConfigureService config)
|
||||
|
||||
private async Task ClientHandlerAsync(TcpClient client, CancellationToken cancellationToken)
|
||||
{
|
||||
var stream = await client.GetAbyssStreamAsync(ct: cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
await using var stream = await client.GetAbyssStreamAsync(ct: cancellationToken, us: user);
|
||||
var request = HttpHelper.Parse(await HttpReader.ReadHttpMessageAsync(stream, cancellationToken));
|
||||
var port = 80;
|
||||
var sp = request.RequestUri?.ToString().Split(':') ?? [];
|
||||
@@ -136,7 +136,6 @@ public class AbyssService(ILogger<AbyssService> logger, ConfigureService config)
|
||||
}
|
||||
finally
|
||||
{
|
||||
stream.Close();
|
||||
client.Close();
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public class ResourceService
|
||||
{
|
||||
InsertRaRow(tasksPath, "root", "rw,r-,r-", true).Wait();
|
||||
}
|
||||
|
||||
|
||||
var livePath = Helpers.SafePathCombine(_config.MediaRoot, "Live");
|
||||
if (livePath != null)
|
||||
{
|
||||
@@ -60,6 +60,121 @@ public class ResourceService
|
||||
return Convert.ToBase64String(r ?? []);
|
||||
}
|
||||
|
||||
public async Task<bool> ValidAll(string[] paths, string token, OperationType type, string ip)
|
||||
{
|
||||
if (paths == null || paths.Length == 0)
|
||||
{
|
||||
_logger.LogError("ValidAll called with empty path set");
|
||||
return false;
|
||||
}
|
||||
|
||||
var mediaRootFull = Path.GetFullPath(_config.MediaRoot);
|
||||
|
||||
// 1. basic path checks & normalize to relative
|
||||
var relPaths = new List<string>(paths.Length);
|
||||
foreach (var p in paths)
|
||||
{
|
||||
if (p == null || !p.StartsWith(mediaRootFull, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogError($"Path outside media root or null: {p}");
|
||||
return false;
|
||||
}
|
||||
|
||||
relPaths.Add(Path.GetRelativePath(_config.MediaRoot, Path.GetFullPath(p)));
|
||||
}
|
||||
|
||||
// 2. validate token and user once
|
||||
string? username = _user.Validate(token, ip);
|
||||
if (username == null)
|
||||
{
|
||||
_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;
|
||||
}
|
||||
|
||||
// 3. build uid -> required ops map (avoid duplicate Uid calculations)
|
||||
var uidToOps = new Dictionary<string, HashSet<OperationType>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var rel in relPaths)
|
||||
{
|
||||
var parts = rel
|
||||
.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar },
|
||||
StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.ToArray();
|
||||
|
||||
// parents (each prefix) require Read
|
||||
for (int i = 0; i < parts.Length - 1; i++)
|
||||
{
|
||||
var subPath = Path.Combine(parts.Take(i + 1).ToArray());
|
||||
var uidDir = Uid(subPath);
|
||||
if (!uidToOps.TryGetValue(uidDir, out var ops))
|
||||
{
|
||||
ops = new HashSet<OperationType>();
|
||||
uidToOps[uidDir] = ops;
|
||||
}
|
||||
|
||||
ops.Add(OperationType.Read);
|
||||
}
|
||||
|
||||
// resource itself requires requested 'type'
|
||||
var resourcePath = (parts.Length == 0) ? string.Empty : Path.Combine(parts);
|
||||
var uidRes = Uid(resourcePath);
|
||||
if (!uidToOps.TryGetValue(uidRes, out var resOps))
|
||||
{
|
||||
resOps = new HashSet<OperationType>();
|
||||
uidToOps[uidRes] = resOps;
|
||||
}
|
||||
|
||||
resOps.Add(type);
|
||||
}
|
||||
|
||||
// 4. batch query DB for all UIDs
|
||||
var uidsNeeded = uidToOps.Keys.ToList();
|
||||
var rasList = await _database.Table<ResourceAttribute>()
|
||||
.Where(r => uidsNeeded.Contains(r.Uid))
|
||||
.ToListAsync();
|
||||
|
||||
var raDict = rasList.ToDictionary(r => r.Uid, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// 5. check each uid once per required operation (cache results per uid+op)
|
||||
var permCache = new Dictionary<(string uid, OperationType op), bool>(); // avoid repeated CheckPermission
|
||||
|
||||
foreach (var kv in uidToOps)
|
||||
{
|
||||
var uid = kv.Key;
|
||||
if (!raDict.TryGetValue(uid, out var ra) || ra == null)
|
||||
{
|
||||
// find an example path string for logging would require reverse map; keep uid for clarity
|
||||
_logger.LogError($"Permission check failed (missing resource attribute): User: {username}, Uid: {uid}");
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var op in kv.Value)
|
||||
{
|
||||
var key = (uid, op);
|
||||
if (!permCache.TryGetValue(key, out var ok))
|
||||
{
|
||||
ok = await CheckPermission(user, ra, op);
|
||||
permCache[key] = ok;
|
||||
}
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
_logger.LogError($"Permission check failed: User: {username}, Uid: {uid}, Type: {op}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> Valid(string path, string token, OperationType type, string ip)
|
||||
{
|
||||
// Path is abs path here, due to Helpers.SafePathCombine
|
||||
@@ -175,6 +290,11 @@ public class ResourceService
|
||||
return await Valid(path, token, OperationType.Read, ip);
|
||||
}
|
||||
|
||||
public async Task<bool> GetAll(string[] path, string token, string ip)
|
||||
{
|
||||
return await ValidAll(path, token, OperationType.Read, ip);
|
||||
}
|
||||
|
||||
public async Task<bool> Update(string path, string token, string ip)
|
||||
{
|
||||
return await Valid(path, token, OperationType.Write, ip);
|
||||
@@ -267,13 +387,14 @@ public class ResourceService
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> Exclude(string path, string token, string ip)
|
||||
{
|
||||
var requester = _user.Validate(token, ip);
|
||||
if (requester != "root")
|
||||
{
|
||||
_logger.LogWarning($"Permission denied: Non-root user '{requester ?? "unknown"}' attempted to exclude resource '{path}'.");
|
||||
_logger.LogWarning(
|
||||
$"Permission denied: Non-root user '{requester ?? "unknown"}' attempted to exclude resource '{path}'.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -174,11 +174,38 @@ public class UserService
|
||||
}
|
||||
}
|
||||
|
||||
static bool VerifySignature(PublicKey publicKey, byte[] data, byte[] signature)
|
||||
public static bool VerifySignature(PublicKey publicKey, byte[] data, byte[] signature)
|
||||
{
|
||||
var algorithm = SignatureAlgorithm.Ed25519;
|
||||
return algorithm.Verify(publicKey, data, signature);
|
||||
}
|
||||
|
||||
public async Task<bool> VerifyAny(byte[] data, byte[] signature)
|
||||
{
|
||||
var users = await _database.Table<User>().ToListAsync();
|
||||
foreach (var u in users)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pubKeyBytes = Convert.FromBase64String(u.PublicKey);
|
||||
var pubKey = PublicKey.Import(
|
||||
SignatureAlgorithm.Ed25519,
|
||||
pubKeyBytes,
|
||||
KeyBlobFormat.RawPublicKey);
|
||||
|
||||
if (VerifySignature(pubKey, data, signature))
|
||||
{
|
||||
_logger.LogInformation($"Signature verified using user {u.Name}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, $"Failed to import public key for {u.Name}");
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public string CreateToken(string user, string ip, TimeSpan lifetime)
|
||||
{
|
||||
|
||||
@@ -5,17 +5,21 @@
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using System.Data;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
using System.Text;
|
||||
using Abyss.Components.Services;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using NSec.Cryptography;
|
||||
|
||||
using ChaCha20Poly1305 = System.Security.Cryptography.ChaCha20Poly1305;
|
||||
|
||||
namespace Abyss.Components.Tools
|
||||
{
|
||||
// TODO: (complete) Since C25519 has already been used for user authentication,
|
||||
// TODO: (complete) why not use that public key to verify user identity when establishing a secure channel here?
|
||||
public sealed class AbyssStream : NetworkStream, IDisposable
|
||||
{
|
||||
private const int PublicKeyLength = 32;
|
||||
@@ -61,7 +65,7 @@ namespace Abyss.Components.Tools
|
||||
/// Handshake: X25519 public exchange (raw) -> shared secret -> HKDF -> AEAD key + saltA + saltB
|
||||
/// send/recv salts are assigned deterministically by lexicographic comparison of raw public keys.
|
||||
/// </summary>
|
||||
public static async Task<AbyssStream> CreateAsync(TcpClient client, byte[]? privateKeyRaw = null, CancellationToken cancellationToken = default)
|
||||
public static async Task<AbyssStream> CreateAsync(TcpClient client, UserService us, byte[]? privateKeyRaw = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (client == null) throw new ArgumentNullException(nameof(client));
|
||||
var socket = client.Client ?? throw new ArgumentException("TcpClient has no underlying socket");
|
||||
@@ -102,6 +106,27 @@ namespace Abyss.Components.Tools
|
||||
|
||||
await ReadExactFromSocketAsync(socket, remotePublic, 0, PublicKeyLength, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var ch = Encoding.UTF8.GetBytes(UserService.GenerateRandomAsciiString(32));
|
||||
sent = 0;
|
||||
while (sent < ch.Length)
|
||||
{
|
||||
var toSend = new ReadOnlyMemory<byte>(ch, sent, ch.Length - sent);
|
||||
sent += await socket.SendAsync(toSend, SocketFlags.None, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var rch = new byte[64];
|
||||
await ReadExactFromSocketAsync(socket, rch, 0, 64, cancellationToken).ConfigureAwait(false);
|
||||
bool rau = await us.VerifyAny(ch, rch);
|
||||
if (!rau) throw new AuthenticationFailureException("");
|
||||
|
||||
var ack = Encoding.UTF8.GetBytes(UserService.GenerateRandomAsciiString(16));
|
||||
sent = 0;
|
||||
while (sent < ack.Length)
|
||||
{
|
||||
var toSend = new ReadOnlyMemory<byte>(ack, sent, ack.Length - sent);
|
||||
sent += await socket.SendAsync(toSend, SocketFlags.None, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// 3) Compute shared secret (X25519)
|
||||
PublicKey remotePub;
|
||||
try
|
||||
@@ -394,18 +419,21 @@ namespace Abyss.Components.Tools
|
||||
}
|
||||
|
||||
var payloadLen = unchecked((uint)(ciphertext.Length + tag.Length));
|
||||
var header = new byte[4];
|
||||
BinaryPrimitives.WriteUInt32BigEndian(header, payloadLen);
|
||||
|
||||
await base.WriteAsync(header, 0, header.Length, cancellationToken).ConfigureAwait(false);
|
||||
var packet = new byte[4 + payloadLen];
|
||||
BinaryPrimitives.WriteUInt32BigEndian(packet.AsSpan(0, 4), payloadLen);
|
||||
|
||||
if (ciphertext.Length > 0)
|
||||
await base.WriteAsync(ciphertext, 0, ciphertext.Length, cancellationToken).ConfigureAwait(false);
|
||||
await base.WriteAsync(tag, 0, tag.Length, cancellationToken).ConfigureAwait(false);
|
||||
ciphertext.CopyTo(packet.AsSpan(4));
|
||||
tag.CopyTo(packet.AsSpan(4 + ciphertext.Length));
|
||||
|
||||
await base.WriteAsync(packet, 0, packet.Length, cancellationToken).ConfigureAwait(false);
|
||||
await base.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Array.Clear(nonce, 0, nonce.Length);
|
||||
Array.Clear(tag, 0, tag.Length);
|
||||
Array.Clear(ciphertext, 0, ciphertext.Length);
|
||||
Array.Clear(packet, 0, packet.Length);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
@@ -518,7 +546,7 @@ namespace Abyss.Components.Tools
|
||||
|
||||
public static class TcpClientAbyssExtensions
|
||||
{
|
||||
public static Task<AbyssStream> GetAbyssStreamAsync(this TcpClient client, byte[]? privateKeyRaw = null, CancellationToken ct = default)
|
||||
=> AbyssStream.CreateAsync(client, privateKeyRaw, ct);
|
||||
public static Task<AbyssStream> GetAbyssStreamAsync(this TcpClient client, UserService us, byte[]? privateKeyRaw = null, CancellationToken ct = default)
|
||||
=> AbyssStream.CreateAsync(client, us, privateKeyRaw, ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ public class Comic
|
||||
[JsonProperty("bookmarks")]
|
||||
public List<Bookmark> Bookmarks { get; set; } = new();
|
||||
[JsonProperty("author")]
|
||||
public string Author { get; set; } = "";
|
||||
public string Author { get; set; } = "";
|
||||
[JsonProperty("tags")]
|
||||
public List<string> Tags { get; set; } = new();
|
||||
[JsonProperty("list")]
|
||||
public List<string> List { get; set; } = new();
|
||||
}
|
||||
@@ -9,7 +9,8 @@
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"MEDIA_ROOT" : "/storage",
|
||||
"ALLOWED_PORTS" : "3000"
|
||||
"ALLOWED_PORTS" : "3000",
|
||||
"DEBUG_MODE": "Debug"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
55
README.md
55
README.md
@@ -1,16 +1,59 @@
|
||||
_<div align="center">
|
||||
<div align="center">
|
||||
|
||||
# Abyss (Server for Aether)
|
||||
|
||||
[](https://github.com/rootacite/Abyss)
|
||||
|
||||
_🚀This is the server of the multimedia application Aether, which can also be extended to other purposes🚀_
|
||||
🚀This is the server of the multimedia application Aether, which can also be extended to other purposes🚀
|
||||
|
||||
<img src="abyss_clip.png" width="25%" alt="Logo">
|
||||
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
**Abyss** is a modern, self-hosted media server and secure proxy platform built with **.NET 9**. It is designed to provide a highly secure, extensible, and efficient solution for managing and streaming media content (images, videos, live streams) while enforcing fine-grained access control and cryptographic authentication.
|
||||
|
||||
### 🎯 Key Features
|
||||
|
||||
- **Media Management**: Organize and serve images, videos, and live streams with structured directory support.
|
||||
- **User Authentication**: Challenge-response authentication using **Ed25519** signatures. No private keys are ever transmitted.
|
||||
- **Role-Based Access Control (RBAC)**: Hierarchical user privileges with configurable permissions for resources.
|
||||
- **Secure Proxy**: Built-in HTTP/S proxy with end-to-end encrypted tunneling using **X25519** key exchange and **ChaCha20-Poly1305** encryption.
|
||||
- **Resource-Level Permissions**: Fine-grained control over files and directories using a SQLite-based attribute system.
|
||||
- **Task System**: Support for background tasks such as media uploads and processing with chunk-based integrity validation.
|
||||
- **RESTful API**: Fully documented API endpoints for media access, user management, and task control.
|
||||
|
||||
### 🛠️ Technology Stack
|
||||
|
||||
- **Backend**: ASP.NET Core 9, MVC, Dependency Injection
|
||||
- **Database**: SQLite with async ORM support
|
||||
- **Cryptography**: NSec.Cryptography (Ed25519, X25519), ChaCha20Poly1305
|
||||
- **Media Handling**: Range requests, MIME type detection, chunked uploads
|
||||
- **Security**: Rate limiting, IP binding, token expiration, secure headers
|
||||
|
||||
### 🔐 Security Highlights
|
||||
|
||||
- Zero-trust architecture: All requests require valid tokens bound to IP addresses.
|
||||
- No plaintext private key transmission.
|
||||
- All media and metadata access is validated against a permission database.
|
||||
- Secure tunneling with forward secrecy via ephemeral key exchange.
|
||||
|
||||
### 📦 Use Cases
|
||||
|
||||
- Personal media library with access control
|
||||
- Secure internal video streaming platform
|
||||
- Proxy server with authenticated tunneling
|
||||
- Task-driven media processing pipeline
|
||||
|
||||
### 🌱 Extensibility
|
||||
|
||||
Abyss is designed with modularity in mind. Its service-based architecture allows easy integration of new media types, authentication providers, or storage backends.
|
||||
|
||||
---
|
||||
|
||||
## Development environment
|
||||
|
||||
@@ -326,4 +369,4 @@ These endpoints provide access to static image resources. A valid token is requi
|
||||
- [ ] Add P/D method to all controllers to achieve dynamic modification of media items
|
||||
- [x] Implement identity management module
|
||||
- [ ] Add a description of the media library directory structure in the READMD document
|
||||
- [x] Add API interface instructions in the READMD document_
|
||||
- [x] Add API interface instructions in the READMD document
|
||||
|
||||
BIN
abyss_clip.png
Normal file
BIN
abyss_clip.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 510 KiB |
Reference in New Issue
Block a user