|
| 1 | +using System.Buffers; |
| 2 | +using System.Net; |
| 3 | +using System.Net.Sockets; |
| 4 | +using System.Text; |
| 5 | +using Glyph11; |
| 6 | +using Glyph11.Parser.Hardened; |
| 7 | +using Glyph11.Protocol; |
| 8 | +using Glyph11.Validation; |
| 9 | + |
| 10 | +var port = args.Length > 0 && int.TryParse(args[0], out var p) ? p : 5098; |
| 11 | + |
| 12 | +var listener = new TcpListener(IPAddress.Loopback, port); |
| 13 | +listener.Start(); |
| 14 | + |
| 15 | +Console.WriteLine($"GlyphServer listening on http://localhost:{port}"); |
| 16 | + |
| 17 | +using var cts = new CancellationTokenSource(); |
| 18 | +Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; |
| 19 | + |
| 20 | +try |
| 21 | +{ |
| 22 | + while (!cts.Token.IsCancellationRequested) |
| 23 | + { |
| 24 | + var client = await listener.AcceptTcpClientAsync(cts.Token); |
| 25 | + _ = HandleClientAsync(client, cts.Token); |
| 26 | + } |
| 27 | +} |
| 28 | +catch (OperationCanceledException) { } |
| 29 | + |
| 30 | +listener.Stop(); |
| 31 | +Console.WriteLine("Server stopped."); |
| 32 | + |
| 33 | +static async Task HandleClientAsync(TcpClient client, CancellationToken ct) |
| 34 | +{ |
| 35 | + using (client) |
| 36 | + await using (var stream = client.GetStream()) |
| 37 | + { |
| 38 | + var buffer = new byte[65536]; |
| 39 | + var filled = 0; |
| 40 | + var limits = ParserLimits.Default; |
| 41 | + using var request = new BinaryRequest(); |
| 42 | + |
| 43 | + try |
| 44 | + { |
| 45 | + while (!ct.IsCancellationRequested) |
| 46 | + { |
| 47 | + var read = await stream.ReadAsync(buffer.AsMemory(filled), ct); |
| 48 | + if (read == 0) break; |
| 49 | + filled += read; |
| 50 | + |
| 51 | + while (filled > 0) |
| 52 | + { |
| 53 | + var sequence = new ReadOnlySequence<byte>(buffer, 0, filled); |
| 54 | + |
| 55 | + try |
| 56 | + { |
| 57 | + if (!HardenedParser.TryExtractFullHeader(ref sequence, request, in limits, out var bytesRead)) |
| 58 | + break; // Need more data |
| 59 | + |
| 60 | + // Post-parse semantic validation |
| 61 | + if (RequestSemantics.HasTransferEncodingWithContentLength(request) || |
| 62 | + RequestSemantics.HasConflictingContentLength(request) || |
| 63 | + RequestSemantics.HasConflictingCommaSeparatedContentLength(request) || |
| 64 | + RequestSemantics.HasInvalidContentLengthFormat(request) || |
| 65 | + RequestSemantics.HasContentLengthWithLeadingZeros(request) || |
| 66 | + RequestSemantics.HasInvalidHostHeaderCount(request) || |
| 67 | + RequestSemantics.HasInvalidTransferEncoding(request) || |
| 68 | + RequestSemantics.HasDotSegments(request) || |
| 69 | + RequestSemantics.HasFragmentInRequestTarget(request) || |
| 70 | + RequestSemantics.HasBackslashInPath(request) || |
| 71 | + RequestSemantics.HasDoubleEncoding(request) || |
| 72 | + RequestSemantics.HasEncodedNullByte(request) || |
| 73 | + RequestSemantics.HasOverlongUtf8(request)) |
| 74 | + { |
| 75 | + await stream.WriteAsync(MakeErrorResponse(400, "Bad Request"), ct); |
| 76 | + return; |
| 77 | + } |
| 78 | + |
| 79 | + var method = Encoding.ASCII.GetString(request.Method.Span); |
| 80 | + var path = Encoding.ASCII.GetString(request.Path.Span); |
| 81 | + var responseBytes = BuildResponse(method, path); |
| 82 | + await stream.WriteAsync(responseBytes, ct); |
| 83 | + |
| 84 | + // Consume parsed bytes and reset for keep-alive |
| 85 | + if (bytesRead > 0 && bytesRead <= filled) |
| 86 | + { |
| 87 | + Buffer.BlockCopy(buffer, bytesRead, buffer, 0, filled - bytesRead); |
| 88 | + filled -= bytesRead; |
| 89 | + } |
| 90 | + else |
| 91 | + { |
| 92 | + filled = 0; |
| 93 | + } |
| 94 | + |
| 95 | + request.Clear(); |
| 96 | + } |
| 97 | + catch (HttpParseException ex) |
| 98 | + { |
| 99 | + var (code, reason) = ex.IsLimitViolation |
| 100 | + ? (431, "Request Header Fields Too Large") |
| 101 | + : (400, "Bad Request"); |
| 102 | + await stream.WriteAsync(MakeErrorResponse(code, reason), ct); |
| 103 | + return; |
| 104 | + } |
| 105 | + } |
| 106 | + } |
| 107 | + } |
| 108 | + catch (OperationCanceledException) { } |
| 109 | + catch (IOException) { } |
| 110 | + } |
| 111 | +} |
| 112 | + |
| 113 | +static byte[] BuildResponse(string method, string path) |
| 114 | +{ |
| 115 | + var body = $"Hello from GlyphServer\r\nMethod: {method}\r\nPath: {path}\r\n"; |
| 116 | + return MakeResponse(200, "OK", body); |
| 117 | +} |
| 118 | + |
| 119 | +static byte[] MakeResponse(int status, string reason, string body) |
| 120 | +{ |
| 121 | + var bodyBytes = Encoding.UTF8.GetBytes(body); |
| 122 | + 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"; |
| 123 | + var headerBytes = Encoding.ASCII.GetBytes(header); |
| 124 | + |
| 125 | + var result = new byte[headerBytes.Length + bodyBytes.Length]; |
| 126 | + Buffer.BlockCopy(headerBytes, 0, result, 0, headerBytes.Length); |
| 127 | + Buffer.BlockCopy(bodyBytes, 0, result, headerBytes.Length, bodyBytes.Length); |
| 128 | + return result; |
| 129 | +} |
| 130 | + |
| 131 | +static byte[] MakeErrorResponse(int status, string reason) |
| 132 | +{ |
| 133 | + return MakeResponse(status, reason, $"{status} {reason}\r\n"); |
| 134 | +} |
0 commit comments