better MIME and Accept support
This commit is contained in:
parent
2f0d5af790
commit
4e11430dac
5 changed files with 185 additions and 42 deletions
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
75
MiniHTTP/MIMEType.cs
Normal 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 ?? "*"}";
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue