diff --git a/HTTP.Example/Program.cs b/HTTP.Example/Program.cs index 2a9336b..12dc27e 100644 --- a/HTTP.Example/Program.cs +++ b/HTTP.Example/Program.cs @@ -51,8 +51,8 @@ static class Program router.Add(new CORS()); router.Add("custom", new CustomRoute()); router.Add("subpath", subpath); - router.Add(new FileEndpoint("www-static")); - router.Add(new FileEndpoint("www-dynamic")); + router.Add(new FileEndpoint("www-static") { Index = false }); + router.Add(new FileEndpoint("www-dynamic") { Index = false }); router.Add("", Root); router.Add(new StaticEndpoint(HttpResponse.NotFound("File not found"))); return router; @@ -109,7 +109,7 @@ static class Program if (req.IsWebsocket) 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); return HttpResponse.OK(html); } diff --git a/HTTP.Example/www-static/index.htm b/HTTP.Example/www-static/index.html similarity index 92% rename from HTTP.Example/www-static/index.htm rename to HTTP.Example/www-static/index.html index 40637a5..942f611 100644 --- a/HTTP.Example/www-static/index.htm +++ b/HTTP.Example/www-static/index.html @@ -21,7 +21,7 @@
Websocket test
Custom route
- test.htm
+ test.html

Foo
Bar
diff --git a/HTTP.Example/www-static/test.htm b/HTTP.Example/www-static/test.html similarity index 100% rename from HTTP.Example/www-static/test.htm rename to HTTP.Example/www-static/test.html diff --git a/HTTP.Example/www-static/websocket.htm b/HTTP.Example/www-static/websocket.html similarity index 100% rename from HTTP.Example/www-static/websocket.htm rename to HTTP.Example/www-static/websocket.html diff --git a/HTTP/HttpClient.cs b/HTTP/HttpClient.cs index 1578e8b..9f2903a 100644 --- a/HTTP/HttpClient.cs +++ b/HTTP/HttpClient.cs @@ -42,7 +42,7 @@ public sealed class HttpClient innerStream = ssl; } - HttpStream stream = new HttpStream(innerStream); + HttpStream stream = new HttpStream(innerStream, TimeSpan.FromSeconds(60)); //Send request await FixRequest(request, host, ConnectionType.Close).WriteTo(stream); @@ -136,7 +136,7 @@ public sealed class HttpClient innerStream = ssl; } - stream = new HttpStream(innerStream); + stream = new HttpStream(innerStream, Timeout); } try diff --git a/HTTP/HttpServer.cs b/HTTP/HttpServer.cs index 6cfb29c..12d5a82 100644 --- a/HTTP/HttpServer.cs +++ b/HTTP/HttpServer.cs @@ -56,7 +56,7 @@ public sealed class HttpServer public event Action? OnException; /// - /// 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. /// public TimeSpan Timeout = TimeSpan.FromSeconds(20); @@ -139,11 +139,11 @@ public sealed class HttpServer //HTTP request-response loop while (client.Connected) { - HttpStream httpStream = new HttpStream(stream); + HttpStream httpStream = new HttpStream(stream, Timeout); try { HttpClientInfo clientInfo = new HttpClientInfo(client, endpoint); - HttpRequest req = await httpStream.ReadRequest().WaitAsync(Timeout); + HttpRequest req = await httpStream.ReadRequest(); //Parse path ArraySegment 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"); OnResponse?.Invoke(req, clientInfo, response); - await response.WriteTo(httpStream).WaitAsync(Timeout); + await response.WriteTo(httpStream); if (response is SwitchingProtocols swp) { //Create and run websocket WebsocketRemote ws = new WebsocketRemote(req, clientInfo, httpStream, swp.Fields.WebSocketProtocol); CloseStatus closeStatus = await swp.Callback(ws); - await ws.Close(closeStatus).WaitAsync(Timeout); + await ws.Close(closeStatus); break; //Close } diff --git a/HTTP/HttpStream.cs b/HTTP/HttpStream.cs index 0b9005f..d30253d 100644 --- a/HTTP/HttpStream.cs +++ b/HTTP/HttpStream.cs @@ -23,15 +23,25 @@ class HttpStream : IDisposable /// readonly Decoder Decoder; - public HttpStream(Stream stream) : base() + /// + /// The maximum time the socket may be inactive before it is presumed dead and closed. + /// + public TimeSpan Timeout; + + public HttpStream(Stream stream, TimeSpan timeout) : base() { Stream = stream; + Timeout = timeout; Buffer = new BufferedStream(stream); Decoder = Encoding.ASCII.GetDecoder(); + } public async ValueTask ReadLine() { + CancellationTokenSource cancelSrc = new CancellationTokenSource(); + cancelSrc.CancelAfter(Timeout); + const int maxChars = 4096; byte[] dataBuffer = ArrayPool.Shared.Rent(1); char[] charBuffer = ArrayPool.Shared.Rent(maxChars); @@ -40,7 +50,7 @@ class HttpStream : IDisposable int charBufferIndex = 0; 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) throw new SocketException((int)SocketError.ConnectionReset); else @@ -77,12 +87,15 @@ class HttpStream : IDisposable public async ValueTask Read(Memory buffer) { + CancellationTokenSource cancelSrc = new CancellationTokenSource(); + cancelSrc.CancelAfter(Timeout); + try { int index = 0; while (index < buffer.Length) { - int count = await Buffer.ReadAsync(buffer[index..]); + int count = await Buffer.ReadAsync(buffer[index..], cancelSrc.Token); if (count == 0) break; @@ -102,8 +115,11 @@ class HttpStream : IDisposable public ValueTask Write(string text) { + CancellationTokenSource cancelSrc = new CancellationTokenSource(); + cancelSrc.CancelAfter(Timeout); + byte[] data = Encoding.ASCII.GetBytes(text); - return Buffer.WriteAsync(data); + return Buffer.WriteAsync(data, cancelSrc.Token); } public ValueTask WriteLine(string text) @@ -118,9 +134,11 @@ class HttpStream : IDisposable public ValueTask Write(ReadOnlyMemory bytes) { + CancellationTokenSource cancelSrc = new CancellationTokenSource(); + cancelSrc.CancelAfter(Timeout); try { - return Buffer.WriteAsync(bytes); + return Buffer.WriteAsync(bytes, cancelSrc.Token); } catch (IOException e) { @@ -133,10 +151,12 @@ class HttpStream : IDisposable public async Task Flush() { + CancellationTokenSource cancelSrc = new CancellationTokenSource(); + cancelSrc.CancelAfter(Timeout); try { - await Buffer.FlushAsync(); - await Stream.FlushAsync(); + await Buffer.FlushAsync(cancelSrc.Token); + await Stream.FlushAsync(cancelSrc.Token); } catch (IOException e) { diff --git a/HTTP/Routing/FileEndpoint.cs b/HTTP/Routing/FileEndpoint.cs index df484a7..00833c2 100644 --- a/HTTP/Routing/FileEndpoint.cs +++ b/HTTP/Routing/FileEndpoint.cs @@ -58,6 +58,11 @@ public partial class FileEndpoint : RouterBase /// public string Directory; + /// + /// If true, an empty path will be interpreted as "index.html". + /// + public bool Index = true; + public override HttpMethod Method => HttpMethod.GET; public override int Arguments => 1; @@ -73,12 +78,13 @@ public partial class FileEndpoint : RouterBase if (FilenameChecker().IsMatch(asset)) 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 GetFile(string asset) { - if (asset == "") + if (Index && asset == "") asset = "index.html"; string assetPath = $"{Directory}/{asset}";