using System.Text.RegularExpressions; namespace Uwaa.HTTP.Routing; /// /// Special router which serves static files from a directory in the filesystem. /// public partial class FileEndpoint : RouterBase { static MIMEType GuessMIMEType(string extension) { return extension switch { ".txt" => new("text", "plain"), ".htm" or ".html" => new("text", "html"), ".js" => new("text", "javascript"), ".css" => new("text", "css"), ".csv" => new("text", "csv"), ".bin" => new("application", "octet-stream"), ".zip" => new("application", "zip"), ".7z" => new("application", "x-7z-compressed"), ".gz" => new("application", "gzip"), ".xml" => new("application", "xml"), ".pdf" => new("application", "pdf"), ".json" => new("application", "json"), ".bmp" => new("image", "bmp"), ".png" => new("image", "png"), ".jpg" or "jpeg" => new("image", "jpeg"), ".gif" => new("image", "gif"), ".webp" => new("image", "webp"), ".svg" => new("image", "svg+xml"), ".ico" => new("image", "vnd.microsoft.icon"), ".mid" or ".midi" => new("audio", "midi"), ".mp3" => new("audio", "mpeg"), ".ogg" or ".oga" or ".opus" => new("audio", "ogg"), ".wav" => new("audio", "wav"), ".weba" => new("audio", "webm"), ".webm" => new("video", "webm"), ".mp4" => new("video", "mp4"), ".mpeg" => new("video", "mpeg"), ".ogv" => new("video", "ogg"), ".otf" => new("font", "otf"), ".ttf" => new("font", "ttf"), ".woff" => new("font", "woff"), ".woff2" => new("font", "woff2"), _ => new("application", "octet-stream"), //Unknown }; } /// /// The source directory from which assets should be served. /// public string Directory; public override HttpMethod Method => HttpMethod.GET; public override int Arguments => 1; public FileEndpoint(string directory) { Directory = directory; } protected override async Task GetResponseInner(HttpRequest req, HttpClientInfo info, ArraySegment path) { string asset = path[0]; if (FilenameChecker().IsMatch(asset)) return HttpResponse.BadRequest("Illegal chars in asset path"); return await GetFile(asset); } protected async ValueTask GetFile(string asset) { if (asset == "") asset = "index.html"; string assetPath = $"{Directory}/{asset}"; FileInfo fileInfo = new FileInfo(assetPath); MIMEType mime; if (string.IsNullOrEmpty(fileInfo.Extension)) { mime = new MIMEType("text", "html"); assetPath += ".html"; } else { mime = GuessMIMEType(fileInfo.Extension); } if (!File.Exists(assetPath)) return null; return new HttpContent(mime, await File.ReadAllBytesAsync(assetPath)); } /// /// Ensures filenames are legal. /// /// /// Enforcing a set of legal characters in filenames reduces the potential attack surface against the server. /// /// Returns a regular expression which checks for invalid characters. [GeneratedRegex(@"[^a-zA-Z0-9_\-.()\[\] ]")] protected static partial Regex FilenameChecker(); } /// /// Special router which serves static files from a directory and its subdirectories. /// public partial class FileRecursiveEndpoint : FileEndpoint { public FileRecursiveEndpoint(string directory) : base(directory) { } protected override async Task GetResponseInner(HttpRequest req, HttpClientInfo info, ArraySegment path) { foreach (string pathSeg in path) if (FilenameChecker().IsMatch(pathSeg)) return HttpResponse.BadRequest("Illegal chars in asset path"); string asset = Path.Combine(path.ToArray()); return await GetFile(asset); } }