using System.Buffers; using System.Collections.Specialized; using System.Net.Sockets; using System.Text; using System.Web; namespace Uwaa.HTTP; class HttpStream : IDisposable { /// /// The underlying TCP stream. /// readonly Stream Stream; /// /// The read/write buffer. /// readonly BufferedStream Buffer; /// /// Text decoder. /// readonly Decoder Decoder; /// /// 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); try { int charBufferIndex = 0; while (true) { if (await Buffer.ReadAsync(dataBuffer.AsMemory(0, 1), cancelSrc.Token) == 0) if (charBufferIndex == 0) throw new SocketException((int)SocketError.ConnectionReset); else break; if (charBufferIndex >= maxChars) throw new HttpException("Header is too large"); charBufferIndex += Decoder.GetChars(dataBuffer, 0, 1, charBuffer, charBufferIndex, false); if (charBufferIndex >= 2 && charBuffer[charBufferIndex - 1] == '\n' && charBuffer[charBufferIndex - 2] == '\r') { charBufferIndex -= 2; break; } } Decoder.Reset(); return new string(charBuffer, 0, charBufferIndex); } catch (IOException e) { if (e.InnerException is SocketException se) throw se; else throw; } finally { //Clearing the array is unnecessary but it is good security just in case. ArrayPool.Shared.Return(charBuffer, true); ArrayPool.Shared.Return(dataBuffer); } } 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..], cancelSrc.Token); if (count == 0) break; index += count; } return index; } catch (IOException e) { if (e.InnerException is SocketException se) throw se; else throw; } } public ValueTask Write(string text) { CancellationTokenSource cancelSrc = new CancellationTokenSource(); cancelSrc.CancelAfter(Timeout); byte[] data = Encoding.ASCII.GetBytes(text); return Buffer.WriteAsync(data, cancelSrc.Token); } public ValueTask WriteLine(string text) { return Write(text + "\r\n"); } public ValueTask WriteLine() { return Write("\r\n"); } public ValueTask Write(ReadOnlyMemory bytes) { CancellationTokenSource cancelSrc = new CancellationTokenSource(); cancelSrc.CancelAfter(Timeout); try { return Buffer.WriteAsync(bytes, cancelSrc.Token); } catch (IOException e) { if (e.InnerException is SocketException se) throw se; else throw; } } public async Task Flush() { CancellationTokenSource cancelSrc = new CancellationTokenSource(); cancelSrc.CancelAfter(Timeout); try { await Buffer.FlushAsync(cancelSrc.Token); await Stream.FlushAsync(cancelSrc.Token); } catch (IOException e) { if (e.InnerException is SocketException se) throw se; else throw; } } public async ValueTask ReadFields() { HttpFields fields = new HttpFields(); while (true) { string? headerStr = await ReadLine(); if (string.IsNullOrWhiteSpace(headerStr)) break; //End of headers if (fields.Misc != null && fields.Misc.Count >= 30) throw new HttpException("Too many headers"); int splitPoint = headerStr.IndexOf(':'); if (splitPoint == -1) throw new HttpException("A header is invalid"); string name = headerStr.Remove(splitPoint).Trim(); string value = headerStr.Substring(splitPoint + 1).Trim(); fields[name] = value; } return fields; } public async ValueTask ReadContent(HttpFields headers) { if (!headers.ContentLength.HasValue) return null; if (!headers.ContentType.HasValue) throw new HttpException("Content length was sent but no content type"); if (headers.ContentLength.Value > 10_000_000) throw new HttpException("Too much content (max: 10 MB)"); byte[] data = new byte[headers.ContentLength.Value]; await Read(data); return new HttpContent(headers.ContentType.Value, data); } public async ValueTask<(HttpMethod Method, string Path)> ReadRequestHeader() { //Read initial header string header = await ReadLine(); string[] parts = header.Split(' ', 3, StringSplitOptions.RemoveEmptyEntries); if (parts.Length < 2) //breaks specification, must require 3, but impl genuinely only needs 2 throw new HttpException("Invalid initial header"); //Method if (!Enum.TryParse(parts[0], true, out HttpMethod method)) throw new HttpException("Unknown HTTP method"); //Path string path = parts[1].Replace("\\", "/"); return (method, path); } public async ValueTask<(int Code, string Message)> ReadResponseHeader() { string responseHeader = await ReadLine(); string[] parts = responseHeader.Split(' ', 3, StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 3) throw new HttpException("Invalid initial header"); if (!parts[0].Equals("HTTP/1.0", StringComparison.OrdinalIgnoreCase) && !parts[0].Equals("HTTP/1.1", StringComparison.OrdinalIgnoreCase)) throw new HttpException("Unsupported HTTP version"); if (!int.TryParse(parts[1], out int statusCode)) throw new HttpException("Invalid status code"); return (statusCode, parts[2]); } public async Task ReadRequest() { try { (HttpMethod method, string path) = await ReadRequestHeader(); HttpFields fields = await ReadFields(); HttpContent? content = await ReadContent(fields); return new HttpRequest(method, path, fields, content); } catch (FormatException e) { throw new HttpException(e.Message, e); } } public async Task ReadResponse() { try { (int statusCode, string statusMessage) = await ReadResponseHeader(); HttpFields fields = await ReadFields(); HttpContent? content = await ReadContent(fields); return new HttpResponse(statusCode, statusMessage, fields, content); } catch (FormatException e) { throw new HttpException(e.Message, e); } } /// /// Disposes the underlying stream. /// public void Dispose() { ((IDisposable)Stream).Dispose(); } }