Glyph11

Language: C# · View source on GitHub

Dockerfile

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY Directory.Build.props .
COPY src/Servers/GlyphServer/ src/Servers/GlyphServer/
RUN dotnet restore src/Servers/GlyphServer/GlyphServer.csproj
RUN dotnet publish src/Servers/GlyphServer/GlyphServer.csproj -c Release -o /app --no-restore

FROM mcr.microsoft.com/dotnet/runtime:10.0
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "GlyphServer.dll", "8080"]

Source

using System.Buffers;
using System.IO.Pipelines;
using System.Net;
using System.Net.Sockets;
using System.Text;
using Glyph11;
using Glyph11.Parser.Hardened;
using Glyph11.Protocol;
using Glyph11.Validation;

var port = args.Length > 0 && int.TryParse(args[0], out var p) ? p : 8080;

var listener = new TcpListener(IPAddress.Any, port);
listener.Start();

Console.WriteLine($"GlyphServer listening on http://localhost:{port}");

using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };

try
{
    while (!cts.Token.IsCancellationRequested)
    {
        var client = await listener.AcceptTcpClientAsync(cts.Token);
        _ = HandleClientAsync(client, cts.Token);
    }
}
catch (OperationCanceledException) { }

listener.Stop();
Console.WriteLine("Server stopped.");

static async Task HandleClientAsync(TcpClient client, CancellationToken ct)
{
    using (client)
    await using (var stream = client.GetStream())
    {
        var limits = ParserLimits.Default;
        var reader = PipeReader.Create(stream);
        using var request = new BinaryRequest();

        try
        {
            while (!ct.IsCancellationRequested)
            {
                // ── Phase 1: parse header ──────────────────────────
                // Loop until we have a complete header. Do NOT advance
                // the pipe yet — request holds ReadOnlyMemory slices
                // into the pipe buffer.
                ReadOnlySequence<byte> headerBuffer;
                int headerByteCount;
                while (true)
                {
                    request.Clear();
                    var result = await reader.ReadAsync(ct);
                    var buffer = result.Buffer;

                    if (result.IsCompleted && buffer.IsEmpty)
                    {
                        await reader.CompleteAsync();
                        return;
                    }

                    var sequence = buffer;
                    try
                    {
                        // TODO FOR SINGLE SEQUENCE THERE ARE NO ALLOCATIONS, FOR MULTI SEGMENT THERE ARE, THAT INTERFERES THE BEHAVIOR
                        // TODO MEANING WE CANT ADVANCE FOR SINGLE SEGMENT CASE

                        if (HardenedParser.TryExtractFullHeader(ref sequence, request, in limits, out var bytesRead))
                        {
                            headerByteCount = bytesRead + 1;
                            headerBuffer = buffer;
                            break;
                        }

                        if (buffer.Length > limits.MaxTotalHeaderBytes)
                        {
                            reader.AdvanceTo(buffer.End);
                            await stream.WriteAsync(MakeErrorResponse(431, "Request Header Fields Too Large"), ct);
                            await reader.CompleteAsync();
                            return;
                        }

                        reader.AdvanceTo(buffer.Start, buffer.End);

                        if (result.IsCompleted)
                        {
                            await reader.CompleteAsync();
                            return;
                        }
                    }
                    catch (HttpParseException ex)
                    {
                        var code = ex.StatusCode;
                        var reason = code switch
                        {
                            431 => "Request Header Fields Too Large",
                            _ => "Bad Request"
                        };
                        reader.AdvanceTo(buffer.End);
                        await stream.WriteAsync(MakeErrorResponse(code, reason), ct);
                        await reader.CompleteAsync();
                        return;
                    }
                }

                // ── Phase 2: semantic validation ───────────────────
                // request slices still point into the live pipe buffer.
                if (RequestSemantics.HasTransferEncodingWithContentLength(request) ||
                    RequestSemantics.HasConflictingContentLength(request) ||
                    RequestSemantics.HasConflictingCommaSeparatedContentLength(request) ||
                    RequestSemantics.HasInvalidContentLengthFormat(request) ||
                    RequestSemantics.HasContentLengthWithLeadingZeros(request) ||
                    RequestSemantics.HasInvalidHostHeaderCount(request) ||
                    RequestSemantics.HasInvalidHostFormat(request) ||
                    RequestSemantics.HasInvalidTransferEncoding(request) ||
                    RequestSemantics.HasAsteriskFormWithoutOptions(request) ||
                    RequestSemantics.HasInvalidConnectRequest(request) ||
                    RequestSemantics.HasDotSegments(request) ||
                    RequestSemantics.HasFragmentInRequestTarget(request) ||
                    RequestSemantics.HasBackslashInPath(request) ||
                    RequestSemantics.HasDoubleEncoding(request) ||
                    RequestSemantics.HasEncodedNullByte(request) ||
                    RequestSemantics.HasOverlongUtf8(request))
                {
                    reader.AdvanceTo(headerBuffer.End);
                    await stream.WriteAsync(MakeErrorResponse(400, "Bad Request"), ct);
                    await reader.CompleteAsync();
                    return;
                }

                // ── Phase 3: extract values & detect framing ───────
                // Copy what we need out of the pipe buffer, then release it.
                var method = Encoding.ASCII.GetString(request.Method.Span);
                var path = Encoding.ASCII.GetString(request.Path.Span);
                var framing = HardenedParser.DetectBodyFraming(request);

                // Extract headers while slices are still valid (before advancing).
                var headerPairs = new List<KeyValuePair<string, string>>();
                for (int i = 0; i < request.Headers.Count; i++)
                {
                    var kv = request.Headers[i];
                    headerPairs.Add(new KeyValuePair<string, string>(
                        Encoding.ASCII.GetString(kv.Key.Span),
                        Encoding.ASCII.GetString(kv.Value.Span)));
                }

                // Now safe to advance past the header bytes.
                reader.AdvanceTo(headerBuffer.GetPosition(headerByteCount));

                // ── Phase 4: consume body ──────────────────────────
                var bodyBytes = new MemoryStream();
                const int maxCapture = 4096;

                switch (framing.Framing)
                {
                    case BodyFraming.ContentLength:
                    {
                        long remaining = framing.ContentLength;
                        while (remaining > 0)
                        {
                            var result = await reader.ReadAsync(ct);
                            var buffer = result.Buffer;
                            long available = Math.Min(buffer.Length, remaining);

                            if (bodyBytes.Length < maxCapture)
                            {
                                var toCapture = (int)Math.Min(available, maxCapture - bodyBytes.Length);
                                foreach (var seg in buffer.Slice(0, toCapture))
                                    bodyBytes.Write(seg.Span);
                            }

                            remaining -= available;
                            reader.AdvanceTo(buffer.GetPosition(available));

                            if (result.IsCompleted && remaining > 0)
                            {
                                await reader.CompleteAsync();
                                return;
                            }
                        }
                        break;
                    }

                    case BodyFraming.Chunked:
                    {
                        var chunked = new ChunkedBodyStream();
                        while (true)
                        {
                            var result = await reader.ReadAsync(ct);
                            var buffer = result.Buffer;

                            ReadOnlySpan<byte> span;
                            byte[]? linearized = null;
                            if (buffer.IsSingleSegment)
                            {
                                span = buffer.FirstSpan;
                            }
                            else
                            {
                                linearized = new byte[buffer.Length];
                                buffer.CopyTo(linearized);
                                span = linearized;
                            }

                            bool done = false;
                            int totalConsumed = 0;
                            while (true)
                            {
                                var localSpan = span[totalConsumed..];
                                var cr = chunked.TryReadChunk(localSpan, out var consumed, out var dataOffset, out var dataLength);
                                totalConsumed += consumed;

                                if (cr == ChunkResult.Chunk && dataLength > 0 && bodyBytes.Length < maxCapture)
                                {
                                    var toCapture = Math.Min(dataLength, maxCapture - (int)bodyBytes.Length);
                                    bodyBytes.Write(localSpan.Slice(dataOffset, toCapture));
                                }

                                if (cr == ChunkResult.Completed)
                                {
                                    done = true;
                                    break;
                                }
                                if (cr == ChunkResult.NeedMoreData)
                                    break;
                                // ChunkResult.Chunk — loop to consume next chunk
                            }

                            reader.AdvanceTo(buffer.GetPosition(totalConsumed));

                            if (done)
                                break;

                            if (result.IsCompleted)
                            {
                                await reader.CompleteAsync();
                                return;
                            }
                        }
                        break;
                    }

                    case BodyFraming.None:
                    default:
                        break;
                }

                // ── Phase 5: send response ─────────────────────────
                var capturedBody = bodyBytes.Length > 0 ? Encoding.ASCII.GetString(bodyBytes.ToArray()) : null;
                var responseBytes = BuildResponse(method, path, capturedBody, headerPairs);
                await stream.WriteAsync(responseBytes, ct);
            }
        }
        catch (OperationCanceledException) { }
        catch (IOException) { }
        catch (HttpParseException ex)
        {
            var code = ex.StatusCode;
            var reason = code switch
            {
                431 => "Request Header Fields Too Large",
                _ => "Bad Request"
            };
            try { await stream.WriteAsync(MakeErrorResponse(code, reason), ct); } catch { }
        }
        finally
        {
            await reader.CompleteAsync();
        }
    }
}

static byte[] BuildResponse(string method, string path, string? echoBody, List<KeyValuePair<string, string>> headers)
{
    if (path == "/echo")
    {
        var sb = new StringBuilder();
        foreach (var h in headers)
            sb.AppendLine($"{h.Key}: {h.Value}");
        return MakeResponse(200, "OK", sb.ToString());
    }
    if (path == "/cookie")
    {
        var sb = new StringBuilder();
        foreach (var h in headers)
        {
            if (string.Equals(h.Key, "Cookie", StringComparison.OrdinalIgnoreCase))
            {
                foreach (var pair in h.Value.Split(';'))
                {
                    var trimmed = pair.TrimStart();
                    var eqIdx = trimmed.IndexOf('=');
                    if (eqIdx > 0)
                        sb.AppendLine($"{trimmed[..eqIdx]}={trimmed[(eqIdx + 1)..]}");
                }
            }
        }
        return MakeResponse(200, "OK", sb.ToString());
    }
    var body = method == "POST" && echoBody is not null
        ? echoBody
        : $"Hello from GlyphServer\r\nMethod: {method}\r\nPath: {path}\r\n";
    return MakeResponse(200, "OK", body);
}

static byte[] MakeResponse(int status, string reason, string body)
{
    var bodyBytes = Encoding.UTF8.GetBytes(body);
    var header = $"HTTP/1.1 {status} {reason}\r\nContent-Type: text/plain\r\nContent-Length: {bodyBytes.Length}\r\nConnection: keep-alive\r\n\r\n";
    var headerBytes = Encoding.ASCII.GetBytes(header);

    var result = new byte[headerBytes.Length + bodyBytes.Length];
    Buffer.BlockCopy(headerBytes, 0, result, 0, headerBytes.Length);
    Buffer.BlockCopy(bodyBytes, 0, result, headerBytes.Length, bodyBytes.Length);
    return result;
}

static byte[] MakeErrorResponse(int status, string reason)
{
    return MakeResponse(status, reason, $"{status} {reason}\r\n");
}

Test Results

Loading results...

Compliance

Smuggling

Malformed Input

Caching

Cookies