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);
}
}