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