Skip to content

Commit ab5a968

Browse files
MDA2AVclaude
andcommitted
Replace manual buffer with PipeReader in GlyphServer
Eliminates fixed/growing byte[] buffer and manual BlockCopy compaction. PipeReader handles pooled, auto-growing buffers natively and feeds ReadOnlySequence<byte> directly to the parser. Server-side limit check rejects headers exceeding MaxTotalHeaderBytes when no terminator arrives. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8258fcf commit ab5a968

1 file changed

Lines changed: 63 additions & 55 deletions

File tree

src/Servers/GlyphServer/Program.cs

Lines changed: 63 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Buffers;
2+
using System.IO.Pipelines;
23
using System.Net;
34
using System.Net.Sockets;
45
using System.Text;
@@ -35,85 +36,92 @@ static async Task HandleClientAsync(TcpClient client, CancellationToken ct)
3536
using (client)
3637
await using (var stream = client.GetStream())
3738
{
38-
var buffer = new byte[65536];
39-
var filled = 0;
4039
var limits = ParserLimits.Default;
40+
var reader = PipeReader.Create(stream);
4141
using var request = new BinaryRequest();
4242

4343
try
4444
{
4545
while (!ct.IsCancellationRequested)
4646
{
47-
if (filled >= buffer.Length)
48-
{
49-
// Buffer full but headers still incomplete — request is too large
50-
await stream.WriteAsync(MakeErrorResponse(431, "Request Header Fields Too Large"), ct);
51-
return;
52-
}
47+
var result = await reader.ReadAsync(ct);
48+
var buffer = result.Buffer;
5349

54-
var read = await stream.ReadAsync(buffer.AsMemory(filled), ct);
55-
if (read == 0) break;
56-
filled += read;
50+
if (result.IsCompleted && buffer.IsEmpty)
51+
break;
5752

58-
while (filled > 0)
59-
{
60-
var sequence = new ReadOnlySequence<byte>(buffer, 0, filled);
53+
var sequence = buffer;
6154

62-
try
55+
try
56+
{
57+
if (!HardenedParser.TryExtractFullHeader(ref sequence, request, in limits, out var bytesRead))
6358
{
64-
if (!HardenedParser.TryExtractFullHeader(ref sequence, request, in limits, out var bytesRead))
65-
break; // Need more data
66-
67-
// Post-parse semantic validation
68-
if (RequestSemantics.HasTransferEncodingWithContentLength(request) ||
69-
RequestSemantics.HasConflictingContentLength(request) ||
70-
RequestSemantics.HasConflictingCommaSeparatedContentLength(request) ||
71-
RequestSemantics.HasInvalidContentLengthFormat(request) ||
72-
RequestSemantics.HasContentLengthWithLeadingZeros(request) ||
73-
RequestSemantics.HasInvalidHostHeaderCount(request) ||
74-
RequestSemantics.HasInvalidTransferEncoding(request) ||
75-
RequestSemantics.HasDotSegments(request) ||
76-
RequestSemantics.HasFragmentInRequestTarget(request) ||
77-
RequestSemantics.HasBackslashInPath(request) ||
78-
RequestSemantics.HasDoubleEncoding(request) ||
79-
RequestSemantics.HasEncodedNullByte(request) ||
80-
RequestSemantics.HasOverlongUtf8(request))
59+
if (buffer.Length > limits.MaxTotalHeaderBytes)
8160
{
82-
await stream.WriteAsync(MakeErrorResponse(400, "Bad Request"), ct);
83-
return;
61+
reader.AdvanceTo(buffer.End);
62+
await stream.WriteAsync(MakeErrorResponse(431, "Request Header Fields Too Large"), ct);
63+
break;
8464
}
8565

86-
var method = Encoding.ASCII.GetString(request.Method.Span);
87-
var path = Encoding.ASCII.GetString(request.Path.Span);
88-
var responseBytes = BuildResponse(method, path);
89-
await stream.WriteAsync(responseBytes, ct);
66+
// Tell the pipe: consumed nothing, examined everything
67+
reader.AdvanceTo(buffer.Start, buffer.End);
9068

91-
// Consume parsed bytes and reset for keep-alive
92-
if (bytesRead > 0 && bytesRead <= filled)
93-
{
94-
Buffer.BlockCopy(buffer, bytesRead, buffer, 0, filled - bytesRead);
95-
filled -= bytesRead;
96-
}
97-
else
98-
{
99-
filled = 0;
100-
}
69+
if (result.IsCompleted)
70+
break;
10171

102-
request.Clear();
72+
continue;
10373
}
104-
catch (HttpParseException ex)
74+
75+
// Post-parse semantic validation (must happen before AdvanceTo — request
76+
// holds ReadOnlyMemory slices into the pipe's buffer)
77+
if (RequestSemantics.HasTransferEncodingWithContentLength(request) ||
78+
RequestSemantics.HasConflictingContentLength(request) ||
79+
RequestSemantics.HasConflictingCommaSeparatedContentLength(request) ||
80+
RequestSemantics.HasInvalidContentLengthFormat(request) ||
81+
RequestSemantics.HasContentLengthWithLeadingZeros(request) ||
82+
RequestSemantics.HasInvalidHostHeaderCount(request) ||
83+
RequestSemantics.HasInvalidTransferEncoding(request) ||
84+
RequestSemantics.HasDotSegments(request) ||
85+
RequestSemantics.HasFragmentInRequestTarget(request) ||
86+
RequestSemantics.HasBackslashInPath(request) ||
87+
RequestSemantics.HasDoubleEncoding(request) ||
88+
RequestSemantics.HasEncodedNullByte(request) ||
89+
RequestSemantics.HasOverlongUtf8(request))
10590
{
106-
var (code, reason) = ex.IsLimitViolation
107-
? (431, "Request Header Fields Too Large")
108-
: (400, "Bad Request");
109-
await stream.WriteAsync(MakeErrorResponse(code, reason), ct);
110-
return;
91+
reader.AdvanceTo(buffer.End);
92+
await stream.WriteAsync(MakeErrorResponse(400, "Bad Request"), ct);
93+
break;
11194
}
95+
96+
// Extract strings while buffer is still valid
97+
var method = Encoding.ASCII.GetString(request.Method.Span);
98+
var path = Encoding.ASCII.GetString(request.Path.Span);
99+
100+
// Advance past consumed bytes, then respond
101+
reader.AdvanceTo(buffer.GetPosition(bytesRead));
102+
103+
var responseBytes = BuildResponse(method, path);
104+
await stream.WriteAsync(responseBytes, ct);
105+
106+
request.Clear();
107+
}
108+
catch (HttpParseException ex)
109+
{
110+
var (code, reason) = ex.IsLimitViolation
111+
? (431, "Request Header Fields Too Large")
112+
: (400, "Bad Request");
113+
reader.AdvanceTo(buffer.End);
114+
await stream.WriteAsync(MakeErrorResponse(code, reason), ct);
115+
break;
112116
}
113117
}
114118
}
115119
catch (OperationCanceledException) { }
116120
catch (IOException) { }
121+
finally
122+
{
123+
await reader.CompleteAsync();
124+
}
117125
}
118126
}
119127

0 commit comments

Comments
 (0)