Uwaa/HTTP/Routing/FileEndpoint.cs

138 lines
4.5 KiB
C#

using System.Text.RegularExpressions;
namespace Uwaa.HTTP.Routing;
/// <summary>
/// Special router which serves static files from a directory in the filesystem.
/// </summary>
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
};
}
/// <summary>
/// The source directory from which assets should be served.
/// </summary>
public string Directory;
/// <summary>
/// If true, an empty path will be interpreted as "index.html".
/// </summary>
public bool Index = true;
public override HttpMethod Method => HttpMethod.GET;
public override int Arguments => 1;
public FileEndpoint(string directory)
{
Directory = directory;
}
protected override async Task<HttpResponse?> GetResponseInner(HttpRequest req, HttpClientInfo info, ArraySegment<string> path)
{
string asset = path[0];
if (FilenameChecker().IsMatch(asset))
return HttpResponse.BadRequest("Illegal chars in asset path");
HttpContent? file = await GetFile(asset);
return file == null ? null : HttpResponse.OK(file);
}
protected async ValueTask<HttpContent?> GetFile(string asset)
{
if (Index && 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));
}
/// <summary>
/// Ensures filenames are legal.
/// </summary>
/// <remarks>
/// Enforcing a set of legal characters in filenames reduces the potential attack surface against the server.
/// </remarks>
/// <returns>Returns a regular expression which checks for invalid characters.</returns>
[GeneratedRegex(@"[^a-zA-Z0-9_\-.()\[\] ]")]
protected static partial Regex FilenameChecker();
}
/// <summary>
/// Special router which serves static files from a directory and its subdirectories.
/// </summary>
public partial class FileRecursiveEndpoint : FileEndpoint
{
public FileRecursiveEndpoint(string directory) : base(directory)
{
}
protected override async Task<HttpResponse?> GetResponseInner(HttpRequest req, HttpClientInfo info, ArraySegment<string> 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);
}
}