better MIME and Accept support

This commit is contained in:
uwaa 2024-11-01 08:20:10 +00:00
parent 2f0d5af790
commit 4e11430dac
5 changed files with 185 additions and 42 deletions

View file

@ -16,20 +16,20 @@ public struct HttpContent
/// <summary>
/// The MIME type of the HTTP content.
/// </summary>
public string Type;
public MIMEType Type;
/// <summary>
/// The raw HTTP content body, as bytes.
/// </summary>
public byte[] Content;
public HttpContent(string type, string body)
public HttpContent(MIMEType type, string body)
{
Type = type;
Content = Encoding.UTF8.GetBytes(body);
}
public HttpContent(string type, byte[] body)
public HttpContent(MIMEType type, byte[] body)
{
Type = type;
Content = body;

View file

@ -43,6 +43,11 @@ public sealed class HttpRequest
/// </summary>
public NameValueCollection? Query { get; private set; }
/// <summary>
/// Content MIME types which the request wants. If empty, the request does not care.
/// </summary>
public MIMEType[] Accept = [];
/// <summary>
/// HTTP headers included in the request.
/// </summary>
@ -130,6 +135,55 @@ public sealed class HttpRequest
Headers.Add(name, value);
}
ParseAccept();
}
void ParseAccept()
{
if (!Headers.TryGetValue("Accept", out string? accept))
return;
int count = 1;
for (int i = 0; i < accept.Length; i++)
if (accept[i] == ',')
count++;
Accept = new MIMEType[count];
int resultIndex = 0;
int splStart = 0;
int splEnd = 0;
void flush() => Accept[resultIndex++] = new MIMEType(accept.AsSpan(splStart..(splEnd + 1)));
for (int i = 0; i < accept.Length; i++)
{
switch (accept[i])
{
case ';':
flush();
//Another stupid idea by the W3C. Not interested in implementing it - skip!!!
while (i < accept.Length && accept[i] != ',')
i++;
splStart = i + 1;
continue;
case ',':
flush();
splStart = i + 1;
continue;
case ' ':
if (splEnd <= splStart)
splStart = i + 1; //Trim start
continue; //Trim end
default:
splEnd = i;
continue;
}
}
if (splStart < splEnd)
flush(); //Flush remaining
}
async Task<string> ReadLine()
@ -285,6 +339,21 @@ public sealed class HttpRequest
return new Websocket(this, chosenProtocol);
}
/// <summary>
/// Returns true if the client is accepting the provided type, otherwise false.
/// </summary>
public bool CanAccept(MIMEType type)
{
if (Accept.Length == 0)
return true;
for (int i = 0; i < Accept.Length; i++)
if (Accept[i].Match(type))
return true;
return false;
}
}
/// <summary>

View file

@ -29,9 +29,8 @@ public class HttpResponse
{
if (Body.HasValue)
{
int contentLength = Body.Value.Content.Length;
yield return ("Content-Length", contentLength.ToString());
yield return ("Content-Type", Body.Value.Type);
yield return ("Content-Length", Body.Value.Content.Length.ToString());
yield return ("Content-Type", Body.Value.Type.ToString());
}
}
}

75
MiniHTTP/MIMEType.cs Normal file
View file

@ -0,0 +1,75 @@
namespace MiniHTTP;
/// <summary>
/// Represents a Multipurpose Internet Mail Extensions type, which indicates the nature and format of a document/file/chunk of data.
/// </summary>
public readonly record struct MIMEType
{
public static explicit operator string(MIMEType type) => type.ToString();
public static implicit operator MIMEType(string type) => new MIMEType(type);
/// <summary>
/// The first part of the MIME type, representing the general category into which the data type falls.
/// </summary>
public readonly string? Type { get; init; }
/// <summary>
/// The second part of the MIME type, identifying the exact kind of data the MIME type represents.
/// </summary>
public readonly string? Subtype { get; init; }
/// <summary>
/// Parses a MIME type string.
/// </summary>
/// <param name="text">The string to parse.</param>
/// <exception cref="FormatException">Thrown if MIME type is missing a subtype.</exception>
public MIMEType(ReadOnlySpan<char> text)
{
if (text == "*/*")
{
Type = null;
Subtype = null;
return;
}
int spl = text.IndexOf('/');
if (spl == -1)
{
//MIME types need a subtype to be valid.
throw new FormatException("The provided MIME type is missing a subtype.");
}
if (spl == 1 && text[0] == '*')
Type = null;
else
Type = new string(text[..spl]);
if (spl == text.Length - 2 && text[^1] == '*')
Subtype = null;
else
Subtype = new string(text[(spl + 1)..]);
}
/// <summary>
/// Constructs a MIME type from its two components.
/// </summary>
public MIMEType(string? type, string? subType)
{
Type = type;
Subtype = subType;
}
/// <summary>
/// Determines if the given MIME type matches the pattern specified by this MIME type.
/// </summary>
public readonly bool Match(MIMEType type)
{
return (Type == null || Type.Equals(type.Type, StringComparison.OrdinalIgnoreCase)) && (Subtype == null || Subtype.Equals(type.Subtype, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Generates a string for the MIME type.
/// </summary>
public override readonly string ToString() => $"{Type ?? "*"}/{Subtype ?? "*"}";
}

View file

@ -8,49 +8,49 @@ namespace MiniHTTP.Routing;
/// </summary>
public partial class FileEndpoint : RouterBase
{
static string GuessMIMEType(string extension)
static MIMEType GuessMIMEType(string extension)
{
return extension switch
{
".txt" => "text/plain",
".htm" or "html" => "text/html",
".js" => "text/javascript",
".css" => "text/css",
".csv" => "text/csv",
".txt" => new("text", "plain"),
".htm" or "html" => new("text", "html"),
".js" => new("text", "javascript"),
".css" => new("text", "css"),
".csv" => new("text", "csv"),
".bin" => "application/octet-stream",
".zip" => "application/zip",
".7z" => "application/x-7z-compressed",
".gz" => "application/gzip",
".xml" => "application/xml",
".pdf" => "application/pdf",
".json" => "application/json",
".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" => "image/bmp",
".png" => "image/png",
".jpg" or "jpeg" => "image/jpeg",
".gif" => "image/gif",
".webp" => "image/webp",
".svg" => "image/svg+xml",
".ico" => "image/vnd.microsoft.icon",
".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" => "audio/midi",
".mp3" => "audio/mpeg",
".ogg" or ".oga" or ".opus" => "audio/ogg",
".wav" => "audio/wav",
".weba" => "audio/webm",
".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" => "video/webm",
".mp4" => "video/mp4",
".mpeg" => "video/mpeg",
".ogv" => "video/ogg",
".webm" => new("video", "webm"),
".mp4" => new("video", "mp4"),
".mpeg" => new("video", "mpeg"),
".ogv" => new("video", "ogg"),
".otf" => "font/otf",
".ttf" => "font/ttf",
".woff" => "font/woff",
".woff2" => "font/woff2",
".otf" => new("font", "otf"),
".ttf" => new("font", "ttf"),
".woff" => new("font", "woff"),
".woff2" => new("font", "woff2"),
_ => "application/octet-stream", //Unknown
_ => new("application", "octet-stream"), //Unknown
};
}
@ -79,10 +79,10 @@ public partial class FileEndpoint : RouterBase
string assetPath = $"{Directory}/{asset}";
FileInfo fileInfo = new FileInfo(assetPath);
string mime;
MIMEType mime;
if (string.IsNullOrEmpty(fileInfo.Extension))
{
mime = "text/html";
mime = new MIMEType("text", "html");
assetPath += ".htm";
}
else