http: example fixes and security fixes

This commit is contained in:
uwaa 2024-12-25 22:14:53 +00:00
parent 97e7d5db5d
commit b4052c6460
8 changed files with 46 additions and 20 deletions

View file

@ -51,8 +51,8 @@ static class Program
router.Add(new CORS()); router.Add(new CORS());
router.Add("custom", new CustomRoute()); router.Add("custom", new CustomRoute());
router.Add("subpath", subpath); router.Add("subpath", subpath);
router.Add(new FileEndpoint("www-static")); router.Add(new FileEndpoint("www-static") { Index = false });
router.Add(new FileEndpoint("www-dynamic")); router.Add(new FileEndpoint("www-dynamic") { Index = false });
router.Add("", Root); router.Add("", Root);
router.Add(new StaticEndpoint(HttpResponse.NotFound("File not found"))); router.Add(new StaticEndpoint(HttpResponse.NotFound("File not found")));
return router; return router;
@ -109,7 +109,7 @@ static class Program
if (req.IsWebsocket) if (req.IsWebsocket)
return Websocket(req, info); return Websocket(req, info);
byte[] indexFile = await File.ReadAllBytesAsync("www-static/index.htm"); byte[] indexFile = await File.ReadAllBytesAsync("www-static/index.html");
HttpContent html = new HttpContent(new MIMEType("text", "html"), indexFile); HttpContent html = new HttpContent(new MIMEType("text", "html"), indexFile);
return HttpResponse.OK(html); return HttpResponse.OK(html);
} }

View file

@ -21,7 +21,7 @@
<div style="padding-left:20px;"> <div style="padding-left:20px;">
<a href="./websocket">Websocket test</a><br> <a href="./websocket">Websocket test</a><br>
<a href="./custom/foo/bar">Custom route</a><br> <a href="./custom/foo/bar">Custom route</a><br>
<a href="./test">test.htm</a><br> <a href="./test">test.html</a><br>
<br> <br>
<a href="./subpath/foo">Foo</a><br> <a href="./subpath/foo">Foo</a><br>
<a href="./subpath/bar">Bar</a><br> <a href="./subpath/bar">Bar</a><br>

View file

@ -42,7 +42,7 @@ public sealed class HttpClient
innerStream = ssl; innerStream = ssl;
} }
HttpStream stream = new HttpStream(innerStream); HttpStream stream = new HttpStream(innerStream, TimeSpan.FromSeconds(60));
//Send request //Send request
await FixRequest(request, host, ConnectionType.Close).WriteTo(stream); await FixRequest(request, host, ConnectionType.Close).WriteTo(stream);
@ -136,7 +136,7 @@ public sealed class HttpClient
innerStream = ssl; innerStream = ssl;
} }
stream = new HttpStream(innerStream); stream = new HttpStream(innerStream, Timeout);
} }
try try

View file

@ -56,7 +56,7 @@ public sealed class HttpServer
public event Action<IPEndPoint, Exception>? OnException; public event Action<IPEndPoint, Exception>? OnException;
/// <summary> /// <summary>
/// The maximum time the socket may be inactive before it is presumed dead and closed. /// The maximum time a socket may be inactive before it is presumed dead and closed.
/// </summary> /// </summary>
public TimeSpan Timeout = TimeSpan.FromSeconds(20); public TimeSpan Timeout = TimeSpan.FromSeconds(20);
@ -139,11 +139,11 @@ public sealed class HttpServer
//HTTP request-response loop //HTTP request-response loop
while (client.Connected) while (client.Connected)
{ {
HttpStream httpStream = new HttpStream(stream); HttpStream httpStream = new HttpStream(stream, Timeout);
try try
{ {
HttpClientInfo clientInfo = new HttpClientInfo(client, endpoint); HttpClientInfo clientInfo = new HttpClientInfo(client, endpoint);
HttpRequest req = await httpStream.ReadRequest().WaitAsync(Timeout); HttpRequest req = await httpStream.ReadRequest();
//Parse path //Parse path
ArraySegment<string> pathSpl = req.Path.Split('/', StringSplitOptions.RemoveEmptyEntries); ArraySegment<string> pathSpl = req.Path.Split('/', StringSplitOptions.RemoveEmptyEntries);
@ -161,14 +161,14 @@ public sealed class HttpServer
HttpResponse? response = (await Router.GetResponse(req, clientInfo, pathSpl)) ?? HttpResponse.NotFound("Router produced no response"); HttpResponse? response = (await Router.GetResponse(req, clientInfo, pathSpl)) ?? HttpResponse.NotFound("Router produced no response");
OnResponse?.Invoke(req, clientInfo, response); OnResponse?.Invoke(req, clientInfo, response);
await response.WriteTo(httpStream).WaitAsync(Timeout); await response.WriteTo(httpStream);
if (response is SwitchingProtocols swp) if (response is SwitchingProtocols swp)
{ {
//Create and run websocket //Create and run websocket
WebsocketRemote ws = new WebsocketRemote(req, clientInfo, httpStream, swp.Fields.WebSocketProtocol); WebsocketRemote ws = new WebsocketRemote(req, clientInfo, httpStream, swp.Fields.WebSocketProtocol);
CloseStatus closeStatus = await swp.Callback(ws); CloseStatus closeStatus = await swp.Callback(ws);
await ws.Close(closeStatus).WaitAsync(Timeout); await ws.Close(closeStatus);
break; //Close break; //Close
} }

View file

@ -23,15 +23,25 @@ class HttpStream : IDisposable
/// </summary> /// </summary>
readonly Decoder Decoder; readonly Decoder Decoder;
public HttpStream(Stream stream) : base() /// <summary>
/// The maximum time the socket may be inactive before it is presumed dead and closed.
/// </summary>
public TimeSpan Timeout;
public HttpStream(Stream stream, TimeSpan timeout) : base()
{ {
Stream = stream; Stream = stream;
Timeout = timeout;
Buffer = new BufferedStream(stream); Buffer = new BufferedStream(stream);
Decoder = Encoding.ASCII.GetDecoder(); Decoder = Encoding.ASCII.GetDecoder();
} }
public async ValueTask<string> ReadLine() public async ValueTask<string> ReadLine()
{ {
CancellationTokenSource cancelSrc = new CancellationTokenSource();
cancelSrc.CancelAfter(Timeout);
const int maxChars = 4096; const int maxChars = 4096;
byte[] dataBuffer = ArrayPool<byte>.Shared.Rent(1); byte[] dataBuffer = ArrayPool<byte>.Shared.Rent(1);
char[] charBuffer = ArrayPool<char>.Shared.Rent(maxChars); char[] charBuffer = ArrayPool<char>.Shared.Rent(maxChars);
@ -40,7 +50,7 @@ class HttpStream : IDisposable
int charBufferIndex = 0; int charBufferIndex = 0;
while (true) while (true)
{ {
if (await Buffer.ReadAsync(dataBuffer.AsMemory(0, 1)) == 0) if (await Buffer.ReadAsync(dataBuffer.AsMemory(0, 1), cancelSrc.Token) == 0)
if (charBufferIndex == 0) if (charBufferIndex == 0)
throw new SocketException((int)SocketError.ConnectionReset); throw new SocketException((int)SocketError.ConnectionReset);
else else
@ -77,12 +87,15 @@ class HttpStream : IDisposable
public async ValueTask<int> Read(Memory<byte> buffer) public async ValueTask<int> Read(Memory<byte> buffer)
{ {
CancellationTokenSource cancelSrc = new CancellationTokenSource();
cancelSrc.CancelAfter(Timeout);
try try
{ {
int index = 0; int index = 0;
while (index < buffer.Length) while (index < buffer.Length)
{ {
int count = await Buffer.ReadAsync(buffer[index..]); int count = await Buffer.ReadAsync(buffer[index..], cancelSrc.Token);
if (count == 0) if (count == 0)
break; break;
@ -102,8 +115,11 @@ class HttpStream : IDisposable
public ValueTask Write(string text) public ValueTask Write(string text)
{ {
CancellationTokenSource cancelSrc = new CancellationTokenSource();
cancelSrc.CancelAfter(Timeout);
byte[] data = Encoding.ASCII.GetBytes(text); byte[] data = Encoding.ASCII.GetBytes(text);
return Buffer.WriteAsync(data); return Buffer.WriteAsync(data, cancelSrc.Token);
} }
public ValueTask WriteLine(string text) public ValueTask WriteLine(string text)
@ -118,9 +134,11 @@ class HttpStream : IDisposable
public ValueTask Write(ReadOnlyMemory<byte> bytes) public ValueTask Write(ReadOnlyMemory<byte> bytes)
{ {
CancellationTokenSource cancelSrc = new CancellationTokenSource();
cancelSrc.CancelAfter(Timeout);
try try
{ {
return Buffer.WriteAsync(bytes); return Buffer.WriteAsync(bytes, cancelSrc.Token);
} }
catch (IOException e) catch (IOException e)
{ {
@ -133,10 +151,12 @@ class HttpStream : IDisposable
public async Task Flush() public async Task Flush()
{ {
CancellationTokenSource cancelSrc = new CancellationTokenSource();
cancelSrc.CancelAfter(Timeout);
try try
{ {
await Buffer.FlushAsync(); await Buffer.FlushAsync(cancelSrc.Token);
await Stream.FlushAsync(); await Stream.FlushAsync(cancelSrc.Token);
} }
catch (IOException e) catch (IOException e)
{ {

View file

@ -58,6 +58,11 @@ public partial class FileEndpoint : RouterBase
/// </summary> /// </summary>
public string Directory; 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 HttpMethod Method => HttpMethod.GET;
public override int Arguments => 1; public override int Arguments => 1;
@ -73,12 +78,13 @@ public partial class FileEndpoint : RouterBase
if (FilenameChecker().IsMatch(asset)) if (FilenameChecker().IsMatch(asset))
return HttpResponse.BadRequest("Illegal chars in asset path"); return HttpResponse.BadRequest("Illegal chars in asset path");
return await GetFile(asset); HttpContent? file = await GetFile(asset);
return file == null ? null : HttpResponse.OK(file);
} }
protected async ValueTask<HttpContent?> GetFile(string asset) protected async ValueTask<HttpContent?> GetFile(string asset)
{ {
if (asset == "") if (Index && asset == "")
asset = "index.html"; asset = "index.html";
string assetPath = $"{Directory}/{asset}"; string assetPath = $"{Directory}/{asset}";