diff --git a/LarpixServer/Account/Requests.cs b/LarpixServer/Account/Requests.cs index 78062d2..118cb60 100644 --- a/LarpixServer/Account/Requests.cs +++ b/LarpixServer/Account/Requests.cs @@ -863,6 +863,87 @@ public static async Task Auth(HttpContext context, Func next, IQueryCollec userLock.Release(); } } + public static async Task UploadDmAttachmentStaging(HttpContext context, Func next, IQueryCollection query) + { + if (!query.TryGetValue("id", out var idQuery)) return; + if (!context.Request.Headers.TryGetValue("secret", out var secret)) return; + + string id = Utils.GetValidIdOrZero(idQuery.ToString()); + string password = await Utils.GetPassword(id); + string secretStr = await Utils.NonceDecryptBody(id, password, secret, false); + string auth = await Utils.Auth(id, secretStr, password); + + if (auth != Utils.LOGIN_SUCCESS) + { + await context.Response.WriteAsync(Encryption.Encryption.EncryptString(auth, secretStr)); + return; + } + + if (IsActionRateLimited(id)) + { + await context.Response.WriteAsync(Encryption.Encryption.EncryptString("error:rate.limit", password)); + return; + } + + byte[] fileBytes = await LarpixServer.Utils.Utils.LoadBodyBytes(context.Request.Body, 50 * 1024 * 1024); // 50MB limit for attachments + if (fileBytes == null || fileBytes.Length == 0) + { + await context.Response.WriteAsync(Encryption.Encryption.EncryptString("error:empty.body", password)); + return; + } + + string stagingId = Guid.NewGuid().ToString(); + string stagingDir = $"{LarpixServer.Utils.Utils.ROOMS_DIR}/dms_staging"; + string filePath = $"{stagingDir}/{stagingId}"; + + await Fs.WriteFile(filePath, fileBytes); + + // Fire and forget 10 min timer to delete + _ = Task.Run(async () => + { + await Task.Delay(TimeSpan.FromMinutes(10)); + if (Fs.Exists(filePath)) + { + Fs.DeleteFile(filePath); + } + }); + + await context.Response.WriteAsync(Encryption.Encryption.EncryptString($"success:{stagingId}", password)); + } + + public static async Task GetDmAttachment(HttpContext context, Func next, IQueryCollection query) + { + if (!query.TryGetValue("id", out var idQuery)) return; + if (!query.TryGetValue("dm", out var dmQuery)) return; + if (!query.TryGetValue("msg", out var msgQuery)) return; + if (!query.TryGetValue("att", out var attQuery)) return; + if (!context.Request.Headers.TryGetValue("secret", out var secret)) return; + + string id = Utils.GetValidIdOrZero(idQuery.ToString()); + string password = await Utils.GetPassword(id); + string secretStr = await Utils.NonceDecryptBody(id, password, secret, false); + string auth = await Utils.Auth(id, secretStr, password); + + if (auth != Utils.LOGIN_SUCCESS) return; // Don't leak info + + string dmId = dmQuery.ToString(); + string msgId = msgQuery.ToString(); + string attId = attQuery.ToString(); + + // Check if user is in DM + if (!await Room.Requests.IsMemberOfDm(id, dmId)) return; + + string filePath = $"{LarpixServer.Utils.Utils.ROOMS_DIR}/dms/{LarpixServer.Utils.Utils.DOMAIN}/{dmId}/attachments/{msgId}/{attId}"; + if (!Fs.Exists(filePath)) + { + context.Response.StatusCode = 404; + return; + } + + byte[] fileBytes = await Fs.ReadFile(filePath); + context.Response.ContentType = "application/octet-stream"; + await context.Response.Body.WriteAsync(fileBytes, 0, fileBytes.Length); + } } public class CreateHolder diff --git a/LarpixServer/Program.cs b/LarpixServer/Program.cs index 50befbd..ef84300 100644 --- a/LarpixServer/Program.cs +++ b/LarpixServer/Program.cs @@ -24,6 +24,30 @@ public class Program }); var app = builder.Build(); + + string stagingDir = $"{LarpixServer.Utils.Utils.ROOMS_DIR}/dms_staging"; + if (Directory.Exists(stagingDir)) + { + foreach (var file in Directory.GetFiles(stagingDir)) + { + var fileInfo = new FileInfo(file); + var age = DateTime.UtcNow - fileInfo.LastWriteTimeUtc; + var remaining = TimeSpan.FromMinutes(10) - age; + if (remaining <= TimeSpan.Zero) + { + Fs.DeleteFile(file); + } + else + { + _ = Task.Run(async () => + { + await Task.Delay(remaining); + if (Fs.Exists(file)) Fs.DeleteFile(file); + }); + } + } + } + app.UseWebSockets(); app.Use(async (HttpContext context, Func next) => @@ -191,6 +215,12 @@ public class Program case "/_larpix/upload": await Account.Requests.Upload(context, next, query); return; + case "/_larpix/dm/attachment/staging": + await Account.Requests.UploadDmAttachmentStaging(context, next, query); + return; + case "/_larpix/dm/attachment/get": + await Account.Requests.GetDmAttachment(context, next, query); + return; case "/_larpix/federation": await Federation.Receiver.FederationRequest(context, next, query, reader); return; diff --git a/LarpixServer/Room/Requests.cs b/LarpixServer/Room/Requests.cs index c5023c4..d2b34c0 100644 --- a/LarpixServer/Room/Requests.cs +++ b/LarpixServer/Room/Requests.cs @@ -33,7 +33,7 @@ public class Requests return true; } - private static async Task IsMemberOfDm(string id, string dmId) + public static async Task IsMemberOfDm(string id, string dmId) { if (!IsSafeDmId(dmId)) return false; string memberPath = $"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/members/{id};{DOMAIN}"; @@ -401,6 +401,41 @@ public class Requests msg.responded = responded ?? ""; msg.reactions = ""; + if (!string.IsNullOrEmpty(msg.attachment) && msg.attachment != "[]") + { + try + { + var attachmentsNode = System.Text.Json.Nodes.JsonNode.Parse(msg.attachment); + if (attachmentsNode is System.Text.Json.Nodes.JsonArray arr) + { + string attDir = $"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/attachments/{nextId}"; + if (!Directory.Exists(attDir)) Directory.CreateDirectory(attDir); + + for (int i = 0; i < arr.Count; i++) + { + var attObj = arr[i] as System.Text.Json.Nodes.JsonObject; + if (attObj != null && attObj.ContainsKey("staging_id")) + { + string stagingId = attObj["staging_id"].ToString(); + string stagingPath = $"{ROOMS_DIR}/dms_staging/{stagingId}"; + if (Fs.Exists(stagingPath)) + { + string attId = attObj["id"].ToString(); + string destPath = $"{attDir}/{attId}"; + System.IO.File.Move(stagingPath, destPath); + } + attObj.Remove("staging_id"); // Clean it up before saving + } + } + msg.attachment = attachmentsNode.ToJsonString(); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error processing attachments: {ex}"); + } + } + await Fs.WriteFile($"{msgDir}/{nextId}", Encoding.UTF8.GetBytes(JsonSerializer.Serialize(msg, AppJsonSerializerContext.Default.Message))); await Fs.WriteFile($"{msgDir}/last", Encoding.UTF8.GetBytes(nextId.ToString())); diff --git a/LarpixServer/Utils/Utils.cs b/LarpixServer/Utils/Utils.cs index 6396a2a..b8c03d3 100644 --- a/LarpixServer/Utils/Utils.cs +++ b/LarpixServer/Utils/Utils.cs @@ -315,9 +315,9 @@ public class Utils Directory.CreateDirectory(ROOMS_DIR); } if (!Directory.Exists(ROOMS_DIR + "/dms")) - { Directory.CreateDirectory(ROOMS_DIR + "/dms"); - } + if (!Directory.Exists(ROOMS_DIR + "/dms_staging")) + Directory.CreateDirectory(ROOMS_DIR + "/dms_staging"); if (!Directory.Exists(ROOMS_DIR + "/groups")) { Directory.CreateDirectory(ROOMS_DIR + "/groups");