diff options
author | vnugent <public@vaughnnugent.com> | 2024-03-30 22:20:29 -0400 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2024-03-30 22:20:29 -0400 |
commit | 1082bd146549a1aff47877bcd28e6be1ce0ef5e9 (patch) | |
tree | a45babd89ccc8fa344d1d01734778bfe44989a8e /plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Endpoints/WebEndpoint.cs | |
parent | ec9b42f4cacbeae8a0b4d96e48bd9e522b3a9145 (diff) |
feat(app): Add AppData client plugin and browser library updated
Diffstat (limited to 'plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Endpoints/WebEndpoint.cs')
-rw-r--r-- | plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Endpoints/WebEndpoint.cs | 203 |
1 files changed, 203 insertions, 0 deletions
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Endpoints/WebEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Endpoints/WebEndpoint.cs new file mode 100644 index 0000000..9c8f501 --- /dev/null +++ b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Endpoints/WebEndpoint.cs @@ -0,0 +1,203 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Accounts.AppData +* File: WebEndpoint.cs +* +* WebEndpoint.cs is part of VNLib.Plugins.Essentials.Accounts.AppData which +* is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.Accounts is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials.Accounts is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Net; +using System.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; + +using VNLib.Net.Http; +using VNLib.Utils.Logging; +using VNLib.Hashing.Checksums; +using VNLib.Plugins.Essentials.Endpoints; +using VNLib.Plugins.Essentials.Extensions; +using VNLib.Plugins.Extensions.VNCache; +using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Extensions.Validation; + +using VNLib.Plugins.Essentials.Accounts.AppData.Model; +using VNLib.Plugins.Essentials.Accounts.AppData.Stores; + +namespace VNLib.Plugins.Essentials.Accounts.AppData.Endpoints +{ + [ConfigurationName("web_endpoint")] + internal sealed class WebEndpoint : ProtectedWebEndpoint + { + const int DefaultMaxDataSize = 8 * 1024; + + private readonly IAppDataStore _store; + private readonly int MaxDataSize; + private readonly string[] AllowedScopes; + + public WebEndpoint(PluginBase plugin, IConfigScope config) + { + string path = config.GetRequiredProperty("path", p => p.GetString())!; + InitPathAndLog(path, plugin.Log.CreateScope("Endpoint")); + + MaxDataSize = config.GetValueOrDefault("max_data_size", p => p.GetInt32(), DefaultMaxDataSize); + AllowedScopes = config.GetRequiredProperty("allowed_scopes", p => p.EnumerateArray().Select(p => p.GetString()!)).ToArray(); + + bool useCache = false; + + //Cache loading is optional + if (plugin.HasConfigForType<CacheStore>()) + { + //See if caching is enabled + IConfigScope cacheConfig = plugin.GetConfigForType<CacheStore>(); + useCache = cacheConfig.GetValueOrDefault("enabled", e => e.GetBoolean(), false); + + if (useCache && plugin.GetDefaultGlobalCache() is null) + { + plugin.Log.Error("Cache was enabled but no caching library was loaded. Continuing without cache"); + useCache = false; + } + } + + _store = LoadStore(plugin, useCache); + } + + private static IAppDataStore LoadStore(PluginBase plugin, bool withCache) + { + return withCache + ? plugin.GetOrCreateSingleton<CacheStore>() + : plugin.GetOrCreateSingleton<PersistentStorageManager>(); + } + + protected async override ValueTask<VfReturnType> GetAsync(HttpEntity entity) + { + WebMessage webm = new(); + + string? scopeId = entity.QueryArgs.GetValueOrDefault("scope"); + bool noCache = entity.QueryArgs.ContainsKey("no_cache"); + + if (webm.Assert(scopeId != null, "Missing scope")) + { + return VirtualClose(entity, webm, HttpStatusCode.BadRequest); + } + + if (webm.Assert(AllowedScopes.Contains(scopeId), "Invalid scope")) + { + return VirtualClose(entity, webm, HttpStatusCode.BadRequest); + } + + //If the connection has the no-cache header set, also bypass the cache + noCache |= entity.Server.NoCache(); + + //optionally bypass cache if the user requests it + RecordOpFlags flags = noCache ? RecordOpFlags.NoCache : RecordOpFlags.None; + + UserRecordData? record = await _store.GetRecordAsync(entity.Session.UserID, scopeId, flags, entity.EventCancellation); + + if (record is null) + { + return VirtualClose(entity, webm, HttpStatusCode.NotFound); + } + + //return the raw data with the checksum header + entity.SetRecordResponse(record, HttpStatusCode.OK); + return VfReturnType.VirtualSkip; + } + + protected override async ValueTask<VfReturnType> PutAsync(HttpEntity entity) + { + WebMessage webm = new(); + string? scopeId = entity.QueryArgs.GetValueOrDefault("scope"); + bool flush = entity.QueryArgs.ContainsKey("flush"); + + if (webm.Assert(entity.Files.Count == 1, "Invalid file count")) + { + return VirtualClose(entity, webm, HttpStatusCode.BadRequest); + } + + if (webm.Assert(scopeId != null, "Missing scope")) + { + return VirtualClose(entity, webm, HttpStatusCode.BadRequest); + } + + if (webm.Assert(AllowedScopes.Contains(scopeId), "Invalid scope")) + { + return VirtualClose(entity, webm, HttpStatusCode.BadRequest); + } + + FileUpload data = entity.Files[0]; + + if (webm.Assert(data.Length <= MaxDataSize, "Data too large")) + { + return VirtualClose(entity, webm, HttpStatusCode.RequestEntityTooLarge); + } + + byte[] recordData = new byte[data.Length]; + int read = await data.FileData.ReadAsync(recordData, entity.EventCancellation); + + if (webm.Assert(read == recordData.Length, "Failed to read data")) + { + return VirtualClose(entity, webm, HttpStatusCode.InternalServerError); + } + + //Compute checksum on sent data and compare to the header if it exists + ulong checksum = FNV1a.Compute64(recordData); + ulong? userChecksum = entity.Server.GetUserDataChecksum(); + + if (userChecksum.HasValue) + { + //compare the checksums + if (webm.Assert(checksum == userChecksum.Value, "Checksum mismatch")) + { + return VirtualClose(entity, webm, HttpStatusCode.UnprocessableEntity); + } + } + + /* + * If the user specifies the flush flag, the call will wait until the entire record + * is published to the persistent store before returning. Typically if a caching layer is + * used, the record will be written to the cache and the call will return immediately. + */ + RecordOpFlags flags = flush ? RecordOpFlags.WriteThrough : RecordOpFlags.None; + + //Write the record to the store + await _store.SetRecordAsync(entity.Session.UserID, scopeId, recordData, checksum, flags, entity.EventCancellation); + return VirtualClose(entity, HttpStatusCode.Accepted); + } + + protected override async ValueTask<VfReturnType> DeleteAsync(HttpEntity entity) + { + WebMessage webm = new(); + string? scopeId = entity.QueryArgs.GetValueOrDefault("scope"); + + if (webm.Assert(scopeId != null, "Missing scope")) + { + return VirtualClose(entity, webm, HttpStatusCode.BadRequest); + } + + if (webm.Assert(AllowedScopes.Contains(scopeId), "Invalid scope")) + { + return VirtualClose(entity, webm, HttpStatusCode.BadRequest); + } + + //Write the record to the store + await _store.DeleteRecordAsync(entity.Session.UserID, scopeId, entity.EventCancellation); + return VirtualClose(entity, HttpStatusCode.Accepted); + } + } +} |