diff --git a/AGENTS.md b/AGENTS.md index 45a0da9..43cfd2e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,7 @@ Choose the correct suite file based on category: | Smuggling | `src/Http11Probe/TestCases/Suites/SmugglingSuite.cs` | | Malformed Input | `src/Http11Probe/TestCases/Suites/MalformedInputSuite.cs` | | Normalization | `src/Http11Probe/TestCases/Suites/NormalizationSuite.cs` | +| Cookies | `src/Http11Probe/TestCases/Suites/CookieSuite.cs` | Append a `yield return new TestCase { ... };` inside the `GetTestCases()` method. Here is the full schema: @@ -57,6 +58,7 @@ yield return new TestCase | `SMUG-` | Smuggling | | `MAL-` | Malformed Input | | `NORM-` | Normalization | +| `COOK-` | Cookies | | `RFC9112-X.X-` or `RFC9110-X.X-` | Compliance (maps directly to an RFC section) | **Validation patterns — choose ONE:** @@ -137,6 +139,7 @@ This step is **only needed** for `COMP-*` and `RFC*` prefixed tests. The followi - `SMUG-XYZ` → `smuggling/xyz` (lowercased) - `MAL-XYZ` → `malformed-input/xyz` (lowercased) - `NORM-XYZ` → `normalization/xyz` (lowercased) +- `COOK-XYZ` → `cookies/xyz` (lowercased) For compliance tests, add an entry to the `ComplianceSlugs` dictionary: ```csharp @@ -166,6 +169,7 @@ Category slug mapping: | Smuggling | `smuggling` | | Malformed Input | `malformed-input` | | Normalization | `normalization` | +| Cookies | `cookies` | Use this exact template: @@ -272,7 +276,8 @@ Your server MUST listen on **port 8080** and implement these endpoints: | `/` | `HEAD` | Return `200 OK` with no body | | `/` | `POST` | Read the full request body and return it in the response body | | `/` | `OPTIONS` | Return `200 OK` | -| `/echo` | `POST` | Return all received request headers in the response body, one per line as `Name: Value` | +| `/echo` | `GET`, `POST` | Return all received request headers in the response body, one per line as `Name: Value` | +| `/cookie` | `GET`, `POST` | Parse the `Cookie` header and return each cookie as `name=value` on its own line | The `/echo` endpoint is critical for normalization tests. It must echo back all headers the server received, preserving the names as the server internally represents them. @@ -283,6 +288,14 @@ Content-Length: 11 Content-Type: text/plain ``` +The `/cookie` endpoint is used by the Cookies test suite. It must split the `Cookie` header on `;`, trim leading whitespace from each pair, find the first `=`, and output `name=value\n` for each cookie. + +Example — given `Cookie: foo=bar; baz=qux`, the response body should be: +``` +foo=bar +baz=qux +``` + ### Step 3 — Add a Dockerfile Create `src/Servers/YourServer/Dockerfile` that builds and runs the server. @@ -352,6 +365,7 @@ Rules: - `curl http://localhost:8080/` returns 200 - `curl -X POST -d "hello" http://localhost:8080/` returns "hello" - `curl -X POST -d "test" http://localhost:8080/echo` returns headers + - `curl -H "Cookie: foo=bar; baz=qux" http://localhost:8080/cookie` returns `foo=bar` and `baz=qux` on separate lines 4. Run the probe: `dotnet run --project src/Http11Probe.Cli -- --host localhost --port 8080` No changes to CI workflows, configs, or other files are needed. The pipeline auto-discovers servers from `src/Servers/*/probe.json`. diff --git a/docs/content/_index.md b/docs/content/_index.md index aee80c9..a56fe66 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -43,6 +43,7 @@ Http11Probe sends a suite of crafted HTTP requests to each server and checks whe {{< card link="malformed-input" title="Robustness" subtitle="Binary garbage, oversized fields, too many headers, control characters, integer overflow, incomplete requests." icon="lightning-bolt" >}} {{< card link="normalization" title="Normalization" subtitle="Header normalization behavior — underscore-to-hyphen, space before colon, tab in name, case folding on Transfer-Encoding." icon="adjustments" >}} {{< card link="caching" title="Caching" subtitle="Conditional request support — ETag, Last-Modified, If-None-Match precedence, weak comparison, edge cases." icon="beaker" >}} + {{< card link="cookies" title="Cookies" subtitle="Cookie header parsing resilience — oversized values, NUL bytes, control characters, malformed pairs, multiple headers." icon="cake" >}} {{< /cards >}}
diff --git a/docs/content/add-a-test.md b/docs/content/add-a-test.md index 22f35fc..2221391 100644 --- a/docs/content/add-a-test.md +++ b/docs/content/add-a-test.md @@ -14,6 +14,7 @@ Pick the suite that matches your test's category and add a `yield return new Tes | Smuggling | `src/Http11Probe/TestCases/Suites/SmugglingSuite.cs` | | Malformed Input | `src/Http11Probe/TestCases/Suites/MalformedInputSuite.cs` | | Normalization | `src/Http11Probe/TestCases/Suites/NormalizationSuite.cs` | +| Cookies | `src/Http11Probe/TestCases/Suites/CookieSuite.cs` | ```csharp yield return new TestCase @@ -46,6 +47,7 @@ yield return new TestCase | `SMUG-` | Smuggling | | `MAL-` | Malformed Input | | `NORM-` | Normalization | +| `COOK-` | Cookies | | `RFC9112-...` or `RFC9110-...` | Compliance (when the test maps directly to a specific RFC section) | ### Validation options @@ -97,7 +99,7 @@ Expected = new ExpectedBehavior **File:** `src/Http11Probe.Cli/Reporting/DocsUrlMap.cs` -Tests prefixed with `SMUG-`, `MAL-`, or `NORM-` are auto-mapped to their doc URL based on the ID. For example, `SMUG-CL-TE-BOTH` maps to `smuggling/cl-te-both`. +Tests prefixed with `SMUG-`, `MAL-`, `NORM-`, or `COOK-` are auto-mapped to their doc URL based on the ID. For example, `SMUG-CL-TE-BOTH` maps to `smuggling/cl-te-both`. For `COMP-*` or `RFC*` prefixed tests, add an entry to the `ComplianceSlugs` dictionary: diff --git a/docs/content/add-with-ai-agent.md b/docs/content/add-with-ai-agent.md index bc3f7ac..91ee354 100644 --- a/docs/content/add-with-ai-agent.md +++ b/docs/content/add-with-ai-agent.md @@ -34,7 +34,7 @@ For a new **test**, the agent will: For a new **framework**, the agent will: 1. Create a server directory under `src/Servers/` -2. Implement the server with all required endpoints (GET, HEAD, POST, OPTIONS on `/` and POST on `/echo`) +2. Implement the server with all required endpoints (GET, HEAD, POST, OPTIONS on `/`, GET/POST on `/echo`, and GET/POST on `/cookie`) 3. Write a Dockerfile that builds and runs the server on port 8080 4. Add a `probe.json` with the display name diff --git a/docs/content/cookies/_index.md b/docs/content/cookies/_index.md new file mode 100644 index 0000000..7fff443 --- /dev/null +++ b/docs/content/cookies/_index.md @@ -0,0 +1,64 @@ +--- +title: Cookies +layout: wide +toc: false +--- + +## Cookie Handling + +These tests check how servers and frameworks handle adversarial `Cookie` headers. Cookie parsing is done at the framework level, not by application code, so malformed cookies can crash parsers or produce mangled values before your code ever runs. All cookie tests are **unscored** since cookies are governed by RFC 6265, not RFC 9110/9112. + + +
+
Server Name
Click to view Dockerfile and source code
+
Table Row
Click to expand all results for that server
+
Result Cell
Click to see the full HTTP request and response
+
+ +
+
+
+
+
+

Loading...

+ + + + diff --git a/docs/content/docs/caching/etag-304.md b/docs/content/docs/caching/etag-304.md index 3608a10..544b243 100644 --- a/docs/content/docs/caching/etag-304.md +++ b/docs/content/docs/caching/etag-304.md @@ -34,11 +34,11 @@ Captures the `ETag` header from the response for use in step 2. ```http GET / HTTP/1.1\r\n Host: localhost:8080\r\n -If-None-Match: "abc123"\r\n +If-None-Match: {ETag from step 1}\r\n \r\n ``` -Sends the captured ETag value in an `If-None-Match` header. If the resource hasn't changed, the server should return `304 Not Modified`. +Replays the `ETag` value captured from step 1 in an `If-None-Match` header. If the resource hasn't changed, the server should return `304 Not Modified`. If the server did not include an `ETag` header in step 1, the test reports Warn immediately. ## What the RFC says diff --git a/docs/content/docs/caching/etag-in-304.md b/docs/content/docs/caching/etag-in-304.md index a4bfea1..6ca3304 100644 --- a/docs/content/docs/caching/etag-in-304.md +++ b/docs/content/docs/caching/etag-in-304.md @@ -34,11 +34,11 @@ Captures the `ETag` header from the response. ```http GET / HTTP/1.1\r\n Host: localhost:8080\r\n -If-None-Match: "abc123"\r\n +If-None-Match: {ETag from step 1}\r\n \r\n ``` -Sends the captured ETag. If the server returns `304`, this test checks whether the `ETag` header is present in that response. +Replays the `ETag` value captured from step 1. If the server returns `304`, this test checks whether the `ETag` header is present in that response. ## What the RFC says diff --git a/docs/content/docs/caching/etag-weak.md b/docs/content/docs/caching/etag-weak.md index 9ce2a66..9538320 100644 --- a/docs/content/docs/caching/etag-weak.md +++ b/docs/content/docs/caching/etag-weak.md @@ -34,7 +34,7 @@ Captures the `ETag` header from the response. If the ETag is strong (e.g., `"abc ```http GET / HTTP/1.1\r\n Host: localhost:8080\r\n -If-None-Match: W/"abc123"\r\n +If-None-Match: {ETag from step 1, weak}\r\n \r\n ``` diff --git a/docs/content/docs/caching/inm-precedence.md b/docs/content/docs/caching/inm-precedence.md index f2fe8ea..1cd7ad3 100644 --- a/docs/content/docs/caching/inm-precedence.md +++ b/docs/content/docs/caching/inm-precedence.md @@ -34,12 +34,12 @@ Captures the `ETag` header from the response. ```http GET / HTTP/1.1\r\n Host: localhost:8080\r\n -If-None-Match: "abc123"\r\n +If-None-Match: {ETag from step 1}\r\n If-Modified-Since: Thu, 01 Jan 1970 00:00:00 GMT\r\n \r\n ``` -The `If-None-Match` header matches the current ETag (should produce `304`), but the `If-Modified-Since` is set to epoch (should produce `200` since the resource was certainly modified after 1970). If the server returns `304`, it correctly evaluated `If-None-Match` first. +Replays the `ETag` value captured from step 1 in `If-None-Match` (should produce `304`), combined with `If-Modified-Since` set to epoch (should produce `200` since the resource was certainly modified after 1970). If the server returns `304`, it correctly evaluated `If-None-Match` first. ## What the RFC says diff --git a/docs/content/docs/caching/inm-unquoted.md b/docs/content/docs/caching/inm-unquoted.md index 2326500..06cc667 100644 --- a/docs/content/docs/caching/inm-unquoted.md +++ b/docs/content/docs/caching/inm-unquoted.md @@ -34,11 +34,11 @@ Captures the `ETag` header from the response for use in step 2. ```http GET / HTTP/1.1\r\n Host: localhost:8080\r\n -If-None-Match: abc123\r\n +If-None-Match: {ETag from step 1, unquoted}\r\n \r\n ``` -Sends the ETag value without the required surrounding double quotes. According to the RFC grammar, `entity-tag = [ weak ] opaque-tag` and `opaque-tag = DQUOTE *etagc DQUOTE` — the quotes are mandatory. +Sends the ETag value captured from step 1, stripped of the required surrounding double quotes. According to the RFC grammar, `entity-tag = [ weak ] opaque-tag` and `opaque-tag = DQUOTE *etagc DQUOTE` — the quotes are mandatory. ## What the RFC says diff --git a/docs/content/docs/caching/last-modified-304.md b/docs/content/docs/caching/last-modified-304.md index d8d327f..aec84da 100644 --- a/docs/content/docs/caching/last-modified-304.md +++ b/docs/content/docs/caching/last-modified-304.md @@ -34,11 +34,11 @@ Captures the `Last-Modified` header from the response for use in step 2. ```http GET / HTTP/1.1\r\n Host: localhost:8080\r\n -If-Modified-Since: Sun, 01 Jan 2025 00:00:00 GMT\r\n +If-Modified-Since: {Last-Modified from step 1}\r\n \r\n ``` -Sends the captured Last-Modified value in an `If-Modified-Since` header. If the resource hasn't changed since that date, the server should return `304 Not Modified`. +Replays the `Last-Modified` value captured from step 1 in an `If-Modified-Since` header. If the resource hasn't changed since that date, the server should return `304 Not Modified`. If the server did not include a `Last-Modified` header in step 1, the test reports Warn immediately. ## What the RFC says diff --git a/docs/content/docs/cookies/_index.md b/docs/content/docs/cookies/_index.md new file mode 100644 index 0000000..a8e90a2 --- /dev/null +++ b/docs/content/docs/cookies/_index.md @@ -0,0 +1,5 @@ +--- +title: Cookie Handling +sidebar: + open: false +--- diff --git a/docs/content/docs/cookies/control-chars.md b/docs/content/docs/cookies/control-chars.md new file mode 100644 index 0000000..eacceed --- /dev/null +++ b/docs/content/docs/cookies/control-chars.md @@ -0,0 +1,37 @@ +--- +title: "CONTROL-CHARS" +description: "COOK-CONTROL-CHARS cookie test documentation" +weight: 5 +--- + +| | | +|---|---| +| **Test ID** | `COOK-CONTROL-CHARS` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `400 (rejected) or 2xx without control chars` | + +## What it sends + +Control characters (0x01-0x03) in cookie value — dangerous if preserved. + +```http +GET /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: foo=\x01\x02\x03\r\n +\r\n +``` + +## Why it matters + +Control characters in cookie values violate RFC 6265's cookie-octet grammar and can enable response splitting or log injection if passed through to output. + +## Verdicts + +- **Pass** — 400 rejected, or 2xx with control chars stripped +- **Fail** — 2xx with control chars preserved (dangerous), or 500 + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/echo.md b/docs/content/docs/cookies/echo.md new file mode 100644 index 0000000..43eaa7a --- /dev/null +++ b/docs/content/docs/cookies/echo.md @@ -0,0 +1,37 @@ +--- +title: "ECHO" +description: "COOK-ECHO cookie test documentation" +weight: 1 +--- + +| | | +|---|---| +| **Test ID** | `COOK-ECHO` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `2xx with Cookie in body` | + +## What it sends + +Basic Cookie header echoed back by /echo endpoint. + +```http +GET /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: foo=bar\r\n +\r\n +``` + +## Why it matters + +Baseline test — verifies the server's echo endpoint reflects Cookie headers, which is required for all other cookie tests to work. + +## Verdicts + +- **Pass** — 2xx and body contains `Cookie:` header +- **Fail** — No response or missing header + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/empty.md b/docs/content/docs/cookies/empty.md new file mode 100644 index 0000000..6565104 --- /dev/null +++ b/docs/content/docs/cookies/empty.md @@ -0,0 +1,37 @@ +--- +title: "EMPTY" +description: "COOK-EMPTY cookie test documentation" +weight: 3 +--- + +| | | +|---|---| +| **Test ID** | `COOK-EMPTY` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `2xx or 400` | + +## What it sends + +Empty Cookie header value — tests parser resilience. + +```http +GET /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: \r\n +\r\n +``` + +## Why it matters + +Empty Cookie headers can cause null-reference exceptions or crashes in parsers that assume at least one key=value pair. + +## Verdicts + +- **Pass** — 2xx or 400 +- **Fail** — 500 (crash) + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/malformed.md b/docs/content/docs/cookies/malformed.md new file mode 100644 index 0000000..c1d5106 --- /dev/null +++ b/docs/content/docs/cookies/malformed.md @@ -0,0 +1,37 @@ +--- +title: "MALFORMED" +description: "COOK-MALFORMED cookie test documentation" +weight: 7 +--- + +| | | +|---|---| +| **Test ID** | `COOK-MALFORMED` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `2xx or 400` | + +## What it sends + +Completely malformed cookie value (===;;;) — tests parser crash resilience. + +```http +GET /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: ===;;;\r\n +\r\n +``` + +## Why it matters + +Garbage cookie values with no valid key=value structure can crash naive parsers that split on `=` without bounds checking. + +## Verdicts + +- **Pass** — 2xx or 400 +- **Fail** — 500 (crash) + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/many-pairs.md b/docs/content/docs/cookies/many-pairs.md new file mode 100644 index 0000000..7e73026 --- /dev/null +++ b/docs/content/docs/cookies/many-pairs.md @@ -0,0 +1,37 @@ +--- +title: "MANY-PAIRS" +description: "COOK-MANY-PAIRS cookie test documentation" +weight: 6 +--- + +| | | +|---|---| +| **Test ID** | `COOK-MANY-PAIRS` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `2xx or 400/431` | + +## What it sends + +1000 cookie key=value pairs — tests parser performance limits. + +```http +GET /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: k0=v0; k1=v1; ... k999=v999\r\n +\r\n +``` + +## Why it matters + +A large number of cookie pairs can cause O(n^2) parsing behavior, hashtable flooding, or memory exhaustion in frameworks that eagerly parse all cookies. + +## Verdicts + +- **Pass** — 2xx or 400/431 +- **Fail** — 500 (crash) + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/multi-header.md b/docs/content/docs/cookies/multi-header.md new file mode 100644 index 0000000..db9f1e1 --- /dev/null +++ b/docs/content/docs/cookies/multi-header.md @@ -0,0 +1,39 @@ +--- +title: "MULTI-HEADER" +description: "COOK-MULTI-HEADER cookie test documentation" +weight: 8 +--- + +| | | +|---|---| +| **Test ID** | `COOK-MULTI-HEADER` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `2xx with both cookies` | + +## What it sends + +Two separate Cookie headers — should be folded per RFC 6265 §5.4. + +```http +GET /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: a=1\r\n +Cookie: b=2\r\n +\r\n +``` + +## Why it matters + +RFC 6265 §5.4 says the user agent SHOULD combine multiple cookie values with `; `, but servers must handle receiving them separately since some clients and proxies split them. + +## Verdicts + +- **Pass** — 2xx with both a=1 and b=2 in body +- **Warn** — Only one cookie echoed, or 400 +- **Fail** — 500 (crash) + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/nul.md b/docs/content/docs/cookies/nul.md new file mode 100644 index 0000000..f592a95 --- /dev/null +++ b/docs/content/docs/cookies/nul.md @@ -0,0 +1,39 @@ +--- +title: "NUL" +description: "COOK-NUL cookie test documentation" +weight: 4 +--- + +| | | +|---|---| +| **Test ID** | `COOK-NUL` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `400 (rejected) or 2xx without NUL` | + +## What it sends + +NUL byte in cookie value — dangerous if preserved by parser. + +```http +GET /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: foo=\0bar\r\n +\r\n +``` + +The cookie value contains a NUL byte (`0x00`). + +## Why it matters + +NUL bytes in cookie values can truncate strings in C-based parsers, cause log injection, or enable header injection if the NUL terminates a string boundary check. + +## Verdicts + +- **Pass** — 400 rejected, or 2xx with NUL stripped +- **Fail** — 2xx with NUL byte preserved in output (dangerous), or 500 + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/oversized.md b/docs/content/docs/cookies/oversized.md new file mode 100644 index 0000000..b3f3bdb --- /dev/null +++ b/docs/content/docs/cookies/oversized.md @@ -0,0 +1,39 @@ +--- +title: "OVERSIZED" +description: "COOK-OVERSIZED cookie test documentation" +weight: 2 +--- + +| | | +|---|---| +| **Test ID** | `COOK-OVERSIZED` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `400/431 (rejected) or 2xx (survived)` | + +## What it sends + +64KB Cookie header — tests header size limits on cookie data. + +```http +GET /echo HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: big=AAAA...AAAA\r\n +\r\n +``` + +The cookie value contains 65,536 bytes of `A`. + +## Why it matters + +Oversized cookies can trigger buffer overflows, OOM crashes, or excessive memory allocation in parsers that don't enforce size limits. + +## Verdicts + +- **Pass** — 400/431 rejected, or 2xx survived, or connection close +- **Fail** — 500 (crash) + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/parsed-basic.md b/docs/content/docs/cookies/parsed-basic.md new file mode 100644 index 0000000..2b91206 --- /dev/null +++ b/docs/content/docs/cookies/parsed-basic.md @@ -0,0 +1,38 @@ +--- +title: "PARSED-BASIC" +description: "COOK-PARSED-BASIC cookie test documentation" +weight: 9 +--- + +| | | +|---|---| +| **Test ID** | `COOK-PARSED-BASIC` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `2xx with foo=bar in body` | + +## What it sends + +Basic cookie parsed correctly by framework. + +```http +GET /cookie HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: foo=bar\r\n +\r\n +``` + +## Why it matters + +Tests that the framework's cookie parser correctly extracts a simple name=value pair — the most basic cookie parsing operation. + +## Verdicts + +- **Pass** — 2xx and body contains `foo=bar` +- **Warn** — 404 (endpoint not available) +- **Fail** — 500 or mangled output + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/parsed-empty-val.md b/docs/content/docs/cookies/parsed-empty-val.md new file mode 100644 index 0000000..b85bf61 --- /dev/null +++ b/docs/content/docs/cookies/parsed-empty-val.md @@ -0,0 +1,38 @@ +--- +title: "PARSED-EMPTY-VAL" +description: "COOK-PARSED-EMPTY-VAL cookie test documentation" +weight: 11 +--- + +| | | +|---|---| +| **Test ID** | `COOK-PARSED-EMPTY-VAL` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `2xx (no crash)` | + +## What it sends + +Cookie with empty value parsed without crash. + +```http +GET /cookie HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: foo=\r\n +\r\n +``` + +## Why it matters + +Cookies with empty values (`foo=`) are valid per RFC 6265 but can crash parsers that assume a non-empty value after the `=` sign. + +## Verdicts + +- **Pass** — 2xx or 400 +- **Warn** — 404 (endpoint not available) +- **Fail** — 500 (crash) + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/parsed-multi.md b/docs/content/docs/cookies/parsed-multi.md new file mode 100644 index 0000000..ec953bd --- /dev/null +++ b/docs/content/docs/cookies/parsed-multi.md @@ -0,0 +1,38 @@ +--- +title: "PARSED-MULTI" +description: "COOK-PARSED-MULTI cookie test documentation" +weight: 10 +--- + +| | | +|---|---| +| **Test ID** | `COOK-PARSED-MULTI` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `2xx with a=1, b=2, c=3 in body` | + +## What it sends + +Multiple cookies parsed correctly by framework. + +```http +GET /cookie HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: a=1; b=2; c=3\r\n +\r\n +``` + +## Why it matters + +Tests the framework's ability to correctly split and parse multiple semicolon-delimited cookie pairs. + +## Verdicts + +- **Pass** — 2xx and body contains all three pairs +- **Warn** — 404 (endpoint not available) +- **Fail** — Missing pairs or 500 + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/cookies/parsed-special.md b/docs/content/docs/cookies/parsed-special.md new file mode 100644 index 0000000..ab73992 --- /dev/null +++ b/docs/content/docs/cookies/parsed-special.md @@ -0,0 +1,38 @@ +--- +title: "PARSED-SPECIAL" +description: "COOK-PARSED-SPECIAL cookie test documentation" +weight: 12 +--- + +| | | +|---|---| +| **Test ID** | `COOK-PARSED-SPECIAL` | +| **Category** | Cookies | +| **Scored** | No | +| **RFC Level** | N/A | +| **Expected** | `2xx (no crash)` | + +## What it sends + +Cookies with spaces and = in values — tests framework parser edge cases. + +```http +GET /cookie HTTP/1.1\r\n +Host: localhost:8080\r\n +Cookie: a=hello world; b=x=y\r\n +\r\n +``` + +## Why it matters + +Spaces in values and `=` signs within values are common in real-world cookies (e.g., Base64-encoded tokens) and can confuse parsers that split on `=` or whitespace too aggressively. + +## Verdicts + +- **Pass** — 2xx or 400 +- **Warn** — 404 (endpoint not available) +- **Fail** — 500 (crash) + +## Sources + +- [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) — Cookie header diff --git a/docs/content/docs/rfc-requirement-dashboard.md b/docs/content/docs/rfc-requirement-dashboard.md index 77385ec..3e7034e 100644 --- a/docs/content/docs/rfc-requirement-dashboard.md +++ b/docs/content/docs/rfc-requirement-dashboard.md @@ -1,6 +1,6 @@ --- title: "RFC Requirement Dashboard" -description: "Complete RFC 2119 requirement-level analysis for all 203 Http11Probe tests" +description: "Complete RFC 2119 requirement-level analysis for all 215 Http11Probe tests" weight: 2 breadcrumbs: false --- @@ -15,10 +15,10 @@ This dashboard classifies every Http11Probe test by its [RFC 2119](https://www.r | **SHOULD** | 29 | Recommended — valid exceptions exist but must be understood | | **MAY** | 10 | Truly optional — either behavior is fully compliant | | **"ought to"** | 1 | Weaker than SHOULD — recommended but not normative | -| **Unscored** | 39 | Informational — no pass/fail judgement | +| **Unscored** | 51 | Informational — no pass/fail judgement | | **N/A** | 11 | Best-practice / no single RFC verb applies | -**Total: 203 tests** +**Total: 215 tests** --- @@ -222,7 +222,7 @@ Weaker than SHOULD — recommends but does not normatively require. --- -## Unscored Tests (39 tests) +## Unscored Tests (51 tests) These tests are informational — they produce warnings but never fail. @@ -267,6 +267,18 @@ These tests are informational — they produce warnings but never fail. | 37 | `CAP-IMS-INVALID` | Capabilities | [RFC 9110 §13.1.3](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.3) | If-Modified-Since with a garbage (non-HTTP-date) value must be ignored — server should return 200. | | 38 | `CAP-INM-UNQUOTED` | Capabilities | [RFC 9110 §8.8.3](https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3) | If-None-Match with an unquoted ETag violates entity-tag syntax — server should return 200, not 304. | | 39 | `CAP-ETAG-WEAK` | Capabilities | [RFC 9110 §13.1.2](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2) | Weak ETag comparison for GET If-None-Match — server must use weak comparison and return 304. | +| 40 | `COOK-ECHO` | Cookies | [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) | Baseline — confirms /echo endpoint reflects Cookie header. | +| 41 | `COOK-OVERSIZED` | Cookies | [RFC 6265 §6.1](https://www.rfc-editor.org/rfc/rfc6265#section-6.1) | 64KB Cookie header — tests header size limits on cookie data. 400/431 or 2xx both acceptable. | +| 42 | `COOK-EMPTY` | Cookies | [RFC 6265 §4.2](https://www.rfc-editor.org/rfc/rfc6265#section-4.2) | Empty Cookie value — tests parser resilience on empty cookie-string. | +| 43 | `COOK-NUL` | Cookies | [RFC 9110 §5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5) | NUL byte in cookie value — dangerous if preserved by parser. | +| 44 | `COOK-CONTROL-CHARS` | Cookies | [RFC 6265 §4.1.1](https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1) | Control characters (0x01-0x03) in cookie value — not valid cookie-octets. | +| 45 | `COOK-MANY-PAIRS` | Cookies | [RFC 6265 §6.1](https://www.rfc-editor.org/rfc/rfc6265#section-6.1) | 1000 cookie pairs — tests parser performance limits. | +| 46 | `COOK-MALFORMED` | Cookies | [RFC 6265 §4.1.1](https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1) | Completely malformed cookie syntax (===;;;) — tests crash resilience. | +| 47 | `COOK-MULTI-HEADER` | Cookies | [RFC 6265 §5.4](https://www.rfc-editor.org/rfc/rfc6265#section-5.4) | Two separate Cookie headers — should be folded per RFC 6265. | +| 48 | `COOK-PARSED-BASIC` | Cookies | [RFC 6265 §4.1.1](https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1) | Single cookie parsed by framework. | +| 49 | `COOK-PARSED-MULTI` | Cookies | [RFC 6265 §4.2.1](https://www.rfc-editor.org/rfc/rfc6265#section-4.2.1) | Three cookies parsed from semicolon-delimited header. | +| 50 | `COOK-PARSED-EMPTY-VAL` | Cookies | [RFC 6265 §4.1.1](https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1) | Cookie with empty value — *cookie-octet allows zero or more. | +| 51 | `COOK-PARSED-SPECIAL` | Cookies | [RFC 6265 §4.1.1](https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1) | Spaces and = in cookie values — edge cases for parser splitting. | --- @@ -336,6 +348,12 @@ These tests don't map to a single RFC 2119 keyword but enforce defensive best pr |-------|-------| | Unscored | 9 | +### Cookies Suite (12 tests) + +| Level | Tests | +|-------|-------| +| Unscored | 12 | + --- ## RFC Section Cross-Reference @@ -376,4 +394,5 @@ These tests don't map to a single RFC 2119 keyword but enforce defensive best pr | RFC 6585 | 3 | 431 status code | | RFC 3629 | 1 | UTF-8 encoding | | RFC 9113 | 1 | HTTP/2 preface | +| RFC 6265 | 12 | Cookie handling | | N/A | 7 | Best practice / defensive | diff --git a/docs/content/servers/actix.md b/docs/content/servers/actix.md index 2a17a2f..788b292 100644 --- a/docs/content/servers/actix.md +++ b/docs/content/servers/actix.md @@ -1,6 +1,6 @@ --- title: "Actix" -toc: false +toc: true breadcrumbs: false --- @@ -24,7 +24,7 @@ COPY --from=build /src/target/release/actix-server /usr/local/bin/ ENTRYPOINT ["actix-server", "8080"] ``` -## Source — `src/main.rs` +## Source ```rust use actix_web::{web, App, HttpServer, HttpRequest, HttpResponse, Responder}; @@ -37,6 +37,19 @@ async fn echo(req: HttpRequest) -> impl Responder { HttpResponse::Ok().content_type("text/plain").body(body) } +async fn cookie(req: HttpRequest) -> impl Responder { + let mut body = String::new(); + if let Some(raw) = req.headers().get("cookie").and_then(|v| v.to_str().ok()) { + for pair in raw.split(';') { + let trimmed = pair.trim_start(); + if let Some(eq) = trimmed.find('=') { + body.push_str(&format!("{}={}\n", &trimmed[..eq], &trimmed[eq+1..])); + } + } + } + HttpResponse::Ok().content_type("text/plain").body(body) +} + async fn handler(req: HttpRequest, body: web::Bytes) -> HttpResponse { if req.method() == actix_web::http::Method::POST { HttpResponse::Ok() @@ -59,6 +72,7 @@ async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .route("/echo", web::to(echo)) + .route("/cookie", web::to(cookie)) .default_service(web::to(handler)) }) .bind(("0.0.0.0", port))? @@ -66,3 +80,39 @@ async fn main() -> std::io::Result<()> { .await } ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/apache.md b/docs/content/servers/apache.md index 6e7a675..505a916 100644 --- a/docs/content/servers/apache.md +++ b/docs/content/servers/apache.md @@ -1,6 +1,6 @@ --- title: "Apache" -toc: false +toc: true breadcrumbs: false --- @@ -14,10 +14,13 @@ FROM httpd:2.4 COPY src/Servers/ApacheServer/httpd-probe.conf /usr/local/apache2/conf/httpd.conf RUN echo "OK" > /usr/local/apache2/htdocs/index.html COPY src/Servers/ApacheServer/echo.cgi /usr/local/apache2/cgi-bin/echo.cgi -RUN chmod +x /usr/local/apache2/cgi-bin/echo.cgi +COPY src/Servers/ApacheServer/cookie.cgi /usr/local/apache2/cgi-bin/cookie.cgi +RUN chmod +x /usr/local/apache2/cgi-bin/echo.cgi /usr/local/apache2/cgi-bin/cookie.cgi ``` -## Source — `httpd-probe.conf` +## Source + +**`httpd-probe.conf`** ```apache ServerRoot "/usr/local/apache2" @@ -40,13 +43,14 @@ DocumentRoot "/usr/local/apache2/htdocs" ScriptAlias /echo /usr/local/apache2/cgi-bin/echo.cgi +ScriptAlias /cookie /usr/local/apache2/cgi-bin/cookie.cgi Require all granted ``` -## Source — `echo.cgi` +**`echo.cgi`** ```bash #!/bin/sh @@ -62,3 +66,52 @@ if [ -n "$CONTENT_LENGTH" ]; then printf 'Content-Length: %s\n' "$CONTENT_LENGTH" fi ``` + +**`cookie.cgi`** + +```bash +#!/bin/sh +printf 'Content-Type: text/plain\r\n\r\n' +if [ -n "$HTTP_COOKIE" ]; then + echo "$HTTP_COOKIE" | tr ';' '\n' | while read -r pair; do + trimmed=$(echo "$pair" | sed 's/^ *//') + printf '%s\n' "$trimmed" + done +fi +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/aspnet-minimal.md b/docs/content/servers/aspnet-minimal.md index 018238a..a4b94ff 100644 --- a/docs/content/servers/aspnet-minimal.md +++ b/docs/content/servers/aspnet-minimal.md @@ -1,6 +1,6 @@ --- title: "ASP.NET Minimal" -toc: false +toc: true breadcrumbs: false --- @@ -22,7 +22,7 @@ COPY --from=build /app . ENTRYPOINT ["dotnet", "AspNetMinimal.dll"] ``` -## Source — `Program.cs` +## Source ```csharp var builder = WebApplication.CreateBuilder(args); @@ -33,6 +33,14 @@ var app = builder.Build(); app.MapGet("/", () => "OK"); +app.MapMethods("/", ["HEAD"], () => Results.Ok()); + +app.MapMethods("/", ["OPTIONS"], (HttpContext ctx) => +{ + ctx.Response.Headers["Allow"] = "GET, HEAD, POST, OPTIONS"; + return Results.Ok(); +}); + app.MapPost("/", async (HttpContext ctx) => { using var reader = new StreamReader(ctx.Request.Body); @@ -49,5 +57,49 @@ app.Map("/echo", (HttpContext ctx) => return Results.Text(sb.ToString()); }); +app.Map("/cookie", (HttpContext ctx) => +{ + var sb = new System.Text.StringBuilder(); + foreach (var cookie in ctx.Request.Cookies) + sb.AppendLine($"{cookie.Key}={cookie.Value}"); + return Results.Text(sb.ToString()); +}); + app.Run(); ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/bun.md b/docs/content/servers/bun.md index 252cfe9..6810e37 100644 --- a/docs/content/servers/bun.md +++ b/docs/content/servers/bun.md @@ -1,6 +1,6 @@ --- title: "Bun" -toc: false +toc: true breadcrumbs: false --- @@ -15,7 +15,7 @@ COPY src/Servers/BunServer/server.ts . ENTRYPOINT ["bun", "run", "server.ts", "8080"] ``` -## Source — `server.ts` +## Source ```typescript const port = parseInt(Bun.argv[2] || "8080", 10); @@ -32,6 +32,16 @@ Bun.serve({ } return new Response(body, { headers: { "Content-Type": "text/plain" } }); } + if (url.pathname === "/cookie") { + let body = ""; + const raw = req.headers.get("cookie") || ""; + for (const pair of raw.split(";")) { + const trimmed = pair.trimStart(); + const eq = trimmed.indexOf("="); + if (eq > 0) body += trimmed.substring(0, eq) + "=" + trimmed.substring(eq + 1) + "\n"; + } + return new Response(body, { headers: { "Content-Type": "text/plain" } }); + } if (req.method === "POST") { const body = await req.text(); return new Response(body); @@ -42,3 +52,39 @@ Bun.serve({ console.log(`Bun listening on 127.0.0.1:${port}`); ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/caddy.md b/docs/content/servers/caddy.md index cc2e985..4f4f609 100644 --- a/docs/content/servers/caddy.md +++ b/docs/content/servers/caddy.md @@ -1,6 +1,6 @@ --- title: "Caddy" -toc: false +toc: true breadcrumbs: false --- @@ -12,9 +12,12 @@ breadcrumbs: false FROM caddy:2 COPY src/Servers/CaddyServer/Caddyfile /etc/caddy/Caddyfile COPY src/Servers/CaddyServer/echo.html /srv/echo.html +COPY src/Servers/CaddyServer/cookie.html /srv/cookie.html ``` -## Source — `Caddyfile` +## Source + +**`Caddyfile`** ```text :8080 { @@ -39,13 +42,65 @@ COPY src/Servers/CaddyServer/echo.html /srv/echo.html file_server } + handle /cookie { + root * /srv + templates { + mime text/plain + } + rewrite * /cookie.html + file_server + } + respond "OK" 200 } ``` -## Source — `echo.html` +**`echo.html`** ```html {{range $key, $vals := .Req.Header}}{{range $vals}}{{$key}}: {{.}} {{end}}{{end}} ``` + +**`cookie.html`** + +```html +{{range .Req.Header.Cookie}}{{range splitList ";" .}}{{trim .}} +{{end}}{{end}} +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/deno.md b/docs/content/servers/deno.md index c4d8283..a27e37c 100644 --- a/docs/content/servers/deno.md +++ b/docs/content/servers/deno.md @@ -1,6 +1,6 @@ --- title: "Deno" -toc: false +toc: true breadcrumbs: false --- @@ -17,7 +17,7 @@ EXPOSE 8080 CMD ["deno", "run", "--allow-net", "server.ts"] ``` -## Source — `server.ts` +## Source ```typescript Deno.serve({ port: 8080, hostname: "0.0.0.0" }, async (req) => { @@ -29,6 +29,16 @@ Deno.serve({ port: 8080, hostname: "0.0.0.0" }, async (req) => { } return new Response(body, { headers: { "content-type": "text/plain" } }); } + if (url.pathname === "/cookie") { + let body = ""; + const raw = req.headers.get("cookie") || ""; + for (const pair of raw.split(";")) { + const trimmed = pair.trimStart(); + const eq = trimmed.indexOf("="); + if (eq > 0) body += trimmed.substring(0, eq) + "=" + trimmed.substring(eq + 1) + "\n"; + } + return new Response(body, { headers: { "content-type": "text/plain" } }); + } if (req.method === "POST") { const body = await req.text(); return new Response(body, { headers: { "content-type": "text/plain" } }); @@ -36,3 +46,39 @@ Deno.serve({ port: 8080, hostname: "0.0.0.0" }, async (req) => { return new Response("OK", { headers: { "content-type": "text/plain" } }); }); ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/embedio.md b/docs/content/servers/embedio.md index 500f04e..296f5e8 100644 --- a/docs/content/servers/embedio.md +++ b/docs/content/servers/embedio.md @@ -1,6 +1,6 @@ --- title: "EmbedIO" -toc: false +toc: true breadcrumbs: false --- @@ -22,7 +22,7 @@ COPY --from=build /app . ENTRYPOINT ["dotnet", "EmbedIOServer.dll", "8080"] ``` -## Source — `Program.cs` +## Source ```csharp using EmbedIO; @@ -34,6 +34,13 @@ var url = $"http://*:{port}/"; using var server = new WebServer(o => o .WithUrlPrefix(url) .WithMode(HttpListenerMode.EmbedIO)) + .WithModule(new ActionModule("/cookie", HttpVerbs.Any, async ctx => + { + var sb = new System.Text.StringBuilder(); + foreach (System.Net.Cookie cookie in ctx.Request.Cookies) + sb.AppendLine($"{cookie.Name}={cookie.Value}"); + await ctx.SendStringAsync(sb.ToString(), "text/plain", System.Text.Encoding.UTF8); + })) .WithModule(new ActionModule("/echo", HttpVerbs.Any, async ctx => { var sb = new System.Text.StringBuilder(); @@ -60,3 +67,39 @@ using var server = new WebServer(o => o Console.WriteLine($"EmbedIO listening on http://localhost:{port}"); await server.RunAsync(); ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/envoy.md b/docs/content/servers/envoy.md index 1659c2f..9a6e997 100644 --- a/docs/content/servers/envoy.md +++ b/docs/content/servers/envoy.md @@ -1,6 +1,6 @@ --- title: "Envoy" -toc: false +toc: true breadcrumbs: false --- @@ -13,7 +13,7 @@ FROM envoyproxy/envoy:v1.32-latest COPY src/Servers/EnvoyServer/envoy.yaml /etc/envoy/envoy.yaml ``` -## Source — `envoy.yaml` +## Source ```yaml static_resources: @@ -46,6 +46,19 @@ static_resources: end end request_handle:respond({[":status"] = "200", ["content-type"] = "text/plain"}, body) + elseif path == "/cookie" then + local body = "" + local raw = request_handle:headers():get("cookie") + if raw then + for pair in raw:gmatch("[^;]+") do + local trimmed = pair:match("^%s*(.*)") + local eq = trimmed:find("=") + if eq and eq > 1 then + body = body .. trimmed:sub(1, eq-1) .. "=" .. trimmed:sub(eq+1) .. "\n" + end + end + end + request_handle:respond({[":status"] = "200", ["content-type"] = "text/plain"}, body) end end - name: envoy.filters.http.router @@ -63,3 +76,39 @@ static_resources: body: inline_string: "OK" ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/express.md b/docs/content/servers/express.md index b319b72..79fce94 100644 --- a/docs/content/servers/express.md +++ b/docs/content/servers/express.md @@ -1,6 +1,6 @@ --- title: "Express" -toc: false +toc: true breadcrumbs: false --- @@ -17,7 +17,7 @@ COPY src/Servers/ExpressServer/server.js . ENTRYPOINT ["node", "server.js", "8080"] ``` -## Source — `server.js` +## Source ```javascript const express = require("express"); @@ -35,6 +35,17 @@ app.post("/", (req, res) => { req.on("end", () => res.send(Buffer.concat(chunks))); }); +app.all('/cookie', (req, res) => { + let body = ''; + const raw = req.headers.cookie || ''; + for (const pair of raw.split(';')) { + const trimmed = pair.trimStart(); + const eq = trimmed.indexOf('='); + if (eq > 0) body += trimmed.substring(0, eq) + '=' + trimmed.substring(eq + 1) + '\n'; + } + res.set('Content-Type', 'text/plain').send(body); +}); + app.all('/echo', (req, res) => { let body = ''; for (const [name, value] of Object.entries(req.headers)) { @@ -48,3 +59,39 @@ app.listen(port, "127.0.0.1", () => { console.log(`Express listening on 127.0.0.1:${port}`); }); ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/fastendpoints.md b/docs/content/servers/fastendpoints.md index c56352b..e226913 100644 --- a/docs/content/servers/fastendpoints.md +++ b/docs/content/servers/fastendpoints.md @@ -1,6 +1,6 @@ --- title: "FastEndpoints" -toc: false +toc: true breadcrumbs: false --- @@ -22,7 +22,9 @@ COPY --from=build /app . ENTRYPOINT ["dotnet", "FastEndpointsServer.dll"] ``` -## Source — `Program.cs` +## Source + +**`Program.cs`** ```csharp using FastEndpoints; @@ -109,6 +111,26 @@ sealed class OptionsRoot : EndpointWithoutRequest } } +// ── GET/POST /cookie ────────────────────────────────────────── + +sealed class CookieEndpoint : EndpointWithoutRequest +{ + public override void Configure() + { + Verbs("GET", "POST"); + Routes("/cookie"); + AllowAnonymous(); + } + + public override async Task HandleAsync(CancellationToken ct) + { + var sb = new System.Text.StringBuilder(); + foreach (var cookie in HttpContext.Request.Cookies) + sb.AppendLine($"{cookie.Key}={cookie.Value}"); + await HttpContext.Response.WriteAsync(sb.ToString(), ct); + } +} + // ── POST /echo ───────────────────────────────────────────────── sealed class PostEcho : EndpointWithoutRequest @@ -130,7 +152,7 @@ sealed class PostEcho : EndpointWithoutRequest } ``` -## Source — `FastEndpointsServer.csproj` +**`FastEndpointsServer.csproj`** ```xml @@ -148,3 +170,39 @@ sealed class PostEcho : EndpointWithoutRequest ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/fasthttp.md b/docs/content/servers/fasthttp.md index 527ae19..c121358 100644 --- a/docs/content/servers/fasthttp.md +++ b/docs/content/servers/fasthttp.md @@ -1,6 +1,6 @@ --- title: "FastHTTP" -toc: false +toc: true breadcrumbs: false --- @@ -20,13 +20,14 @@ COPY --from=build /fasthttp-server /usr/local/bin/ ENTRYPOINT ["fasthttp-server", "8080"] ``` -## Source — `main.go` +## Source ```go package main import ( "os" + "strings" "github.com/valyala/fasthttp" ) @@ -45,6 +46,15 @@ func main() { ctx.Request.Header.VisitAll(func(key, value []byte) { ctx.WriteString(string(key) + ": " + string(value) + "\n") }) + case "/cookie": + ctx.SetContentType("text/plain") + raw := string(ctx.Request.Header.Peek("Cookie")) + for _, pair := range strings.Split(raw, ";") { + pair = strings.TrimLeft(pair, " ") + if eq := strings.Index(pair, "="); eq > 0 { + ctx.WriteString(pair[:eq] + "=" + pair[eq+1:] + "\n") + } + } default: if string(ctx.Method()) == "POST" { ctx.SetBody(ctx.Request.Body()) @@ -57,3 +67,39 @@ func main() { fasthttp.ListenAndServe("0.0.0.0:"+port, handler) } ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/flask.md b/docs/content/servers/flask.md index 63e5154..77b9ba3 100644 --- a/docs/content/servers/flask.md +++ b/docs/content/servers/flask.md @@ -1,6 +1,6 @@ --- title: "Flask" -toc: false +toc: true breadcrumbs: false --- @@ -16,7 +16,7 @@ COPY src/Servers/FlaskServer/app.py . ENTRYPOINT ["python3", "app.py", "8080"] ``` -## Source — `app.py` +## Source ```python import sys @@ -25,6 +25,13 @@ from werkzeug.routing import Rule app = Flask(__name__) +@app.route('/cookie', methods=['GET','POST','PUT','DELETE','PATCH','OPTIONS','HEAD']) +def cookie_endpoint(): + lines = [] + for name, value in request.cookies.items(): + lines.append(f"{name}={value}") + return '\n'.join(lines) + '\n', 200, {'Content-Type': 'text/plain'} + @app.route('/echo', methods=['GET','POST','PUT','DELETE','PATCH','OPTIONS','HEAD']) def echo(): lines = [] @@ -45,3 +52,39 @@ if __name__ == "__main__": port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080 app.run(host="0.0.0.0", port=port) ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/genhttp.md b/docs/content/servers/genhttp.md index 6c87b81..d263fc1 100644 --- a/docs/content/servers/genhttp.md +++ b/docs/content/servers/genhttp.md @@ -1,6 +1,6 @@ --- title: "GenHTTP" -toc: false +toc: true breadcrumbs: false --- @@ -22,51 +22,97 @@ COPY --from=build /app . ENTRYPOINT ["dotnet", "GenHttpServer.dll", "8080"] ``` -## Source — `Program.cs` +## Source ```csharp -using GenHTTP.Api.Content; using GenHTTP.Api.Protocol; + using GenHTTP.Engine.Internal; + using GenHTTP.Modules.Functional; -using GenHTTP.Modules.Layouting; using GenHTTP.Modules.Practices; -var port = args.Length > 0 && int.TryParse(args[0], out var p) ? p : 8080; +var port = (args.Length > 0 && ushort.TryParse(args[0], out var p)) ? p : (ushort)8080; -var echoHandler = Inline.Create() - .Any(async (IRequest request) => - { - await ValueTask.CompletedTask; - var sb = new System.Text.StringBuilder(); - foreach (var h in request.Headers) - sb.AppendLine($"{h.Key}: {h.Value}"); - return sb.ToString(); - }); - -var rootHandler = Inline.Create() - .Get(async (IRequest request) => +var app = Inline.Create() + .Get("/cookie", (IRequest request) => ParseCookies(request)) + .Post("/cookie", (IRequest request) => ParseCookies(request)) + .Get("/echo", (IRequest request) => Echo(request)) + .Post("/echo", (IRequest request) => Echo(request)) + .Post((Stream body) => RequestContent(body)) + .Any(() => StringContent()); + +return await Host.Create() + .Handler(app) + .Defaults() + .Port(port) + .RunAsync(); + +static string Echo(IRequest request) +{ + var headers = new System.Text.StringBuilder(); + + foreach (var h in request.Headers) { - await ValueTask.CompletedTask; - return "OK"; - }) - .Post(async (IRequest request) => + headers.AppendLine($"{h.Key}: {h.Value}"); + } + + return headers.ToString(); +} + +static string ParseCookies(IRequest request) +{ + var sb = new System.Text.StringBuilder(); + if (request.Headers.TryGetValue("Cookie", out var cookieHeader)) { - if (request.Content is not null) + foreach (var pair in cookieHeader.Split(';')) { - using var reader = new StreamReader(request.Content); - return await reader.ReadToEndAsync(); + var trimmed = pair.TrimStart(); + var eqIdx = trimmed.IndexOf('='); + if (eqIdx > 0) + sb.AppendLine($"{trimmed[..eqIdx]}={trimmed[(eqIdx + 1)..]}"); } - return ""; - }); - -var layout = Layout.Create() - .Add("echo", echoHandler) - .Add(rootHandler); - -await Host.Create() - .Handler(layout) - .Defaults() - .Port((ushort)port) - .RunAsync(); + } + return sb.ToString(); +} + +static string StringContent() => "OK"; + +static Stream RequestContent(Stream body) => body; ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/gin.md b/docs/content/servers/gin.md index 67bbdbc..332ae0b 100644 --- a/docs/content/servers/gin.md +++ b/docs/content/servers/gin.md @@ -1,6 +1,6 @@ --- title: "Gin" -toc: false +toc: true breadcrumbs: false --- @@ -20,7 +20,7 @@ COPY --from=build /gin-server /usr/local/bin/ ENTRYPOINT ["gin-server", "8080"] ``` -## Source — `main.go` +## Source ```go package main @@ -41,6 +41,17 @@ func main() { gin.SetMode(gin.ReleaseMode) r := gin.New() + r.Any("/cookie", func(c *gin.Context) { + var sb strings.Builder + raw := c.GetHeader("Cookie") + for _, pair := range strings.Split(raw, ";") { + pair = strings.TrimLeft(pair, " ") + if eq := strings.Index(pair, "="); eq > 0 { + sb.WriteString(pair[:eq] + "=" + pair[eq+1:] + "\n") + } + } + c.Data(200, "text/plain", []byte(sb.String())) + }) r.Any("/echo", func(c *gin.Context) { var sb strings.Builder for name, values := range c.Request.Header { @@ -61,3 +72,39 @@ func main() { r.Run("0.0.0.0:" + port) } ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/glyph.md b/docs/content/servers/glyph.md index 79d2671..9d70ee3 100644 --- a/docs/content/servers/glyph.md +++ b/docs/content/servers/glyph.md @@ -1,6 +1,6 @@ --- title: "Glyph11" -toc: false +toc: true breadcrumbs: false --- @@ -22,7 +22,7 @@ COPY --from=build /app . ENTRYPOINT ["dotnet", "GlyphServer.dll", "8080"] ``` -## Source — `Program.cs` +## Source ```csharp using System.Buffers; @@ -309,6 +309,24 @@ static byte[] BuildResponse(string method, string path, string? echoBody, List 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"; @@ -332,3 +350,39 @@ 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 + +
+ + + + diff --git a/docs/content/servers/gunicorn.md b/docs/content/servers/gunicorn.md index d065b85..a13b181 100644 --- a/docs/content/servers/gunicorn.md +++ b/docs/content/servers/gunicorn.md @@ -1,6 +1,6 @@ --- title: "Gunicorn" -toc: false +toc: true breadcrumbs: false --- @@ -17,12 +17,24 @@ EXPOSE 8080 CMD ["gunicorn", "-b", "0.0.0.0:8080", "app:app"] ``` -## Source — `app.py` +## Source ```python def app(environ, start_response): path = environ.get('PATH_INFO', '/') + if path == '/cookie': + cookie_str = environ.get('HTTP_COOKIE', '') + lines = [] + for pair in cookie_str.split(';'): + pair = pair.strip() + eq = pair.find('=') + if eq > 0: + lines.append(f"{pair[:eq]}={pair[eq+1:]}") + body = ('\n'.join(lines) + '\n').encode('utf-8') if lines else b'' + start_response('200 OK', [('Content-Type', 'text/plain')]) + return [body] + if path == '/echo': lines = [] for key, value in environ.items(): @@ -47,3 +59,39 @@ def app(environ, start_response): return [body] return [b'OK'] ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/h2o.md b/docs/content/servers/h2o.md index b6f4244..52a3958 100644 --- a/docs/content/servers/h2o.md +++ b/docs/content/servers/h2o.md @@ -1,6 +1,6 @@ --- title: "H2O" -toc: false +toc: true breadcrumbs: false --- @@ -24,7 +24,7 @@ RUN mkdir -p /var/www && echo "OK" > /var/www/index.html ENTRYPOINT ["h2o", "-c", "/etc/h2o/h2o.conf"] ``` -## Source — `h2o.conf` +## Source ```yaml listen: 8080 @@ -45,6 +45,18 @@ hosts: body += "Content-Type: #{env['CONTENT_TYPE']}\n" if env['CONTENT_TYPE'] && !env['CONTENT_TYPE'].empty? body += "Content-Length: #{env['CONTENT_LENGTH']}\n" if env['CONTENT_LENGTH'] && !env['CONTENT_LENGTH'].empty? [200, {"content-type" => "text/plain"}, [body]] + elsif env["PATH_INFO"] == "/cookie" + body = "" + if env["HTTP_COOKIE"] + env["HTTP_COOKIE"].split(";").each do |pair| + trimmed = pair.lstrip + eq = trimmed.index("=") + if eq && eq > 0 + body += "#{trimmed[0...eq]}=#{trimmed[(eq+1)..]}\n" + end + end + end + [200, {"content-type" => "text/plain"}, [body]] elsif env["REQUEST_METHOD"] == "POST" body = env["rack.input"] ? env["rack.input"].read : "" [200, {"content-type" => "text/plain"}, [body]] @@ -53,3 +65,39 @@ hosts: end } ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/haproxy.md b/docs/content/servers/haproxy.md index 49ed095..681b96f 100644 --- a/docs/content/servers/haproxy.md +++ b/docs/content/servers/haproxy.md @@ -1,6 +1,6 @@ --- title: "HAProxy" -toc: false +toc: true breadcrumbs: false --- @@ -14,7 +14,9 @@ COPY src/Servers/HAProxyServer/haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg COPY src/Servers/HAProxyServer/echo.lua /usr/local/etc/haproxy/echo.lua ``` -## Source — `haproxy.cfg` +## Source + +**`haproxy.cfg`** ```text global @@ -30,13 +32,21 @@ defaults frontend http_in bind *:8080 use_backend echo_backend if { path /echo } + use_backend cookie_backend if { path /cookie } + use_backend post_echo_backend if { method POST } http-request return status 200 content-type "text/plain" string "OK" backend echo_backend http-request use-service lua.echo + +backend cookie_backend + http-request use-service lua.cookie + +backend post_echo_backend + http-request use-service lua.echo_body ``` -## Source — `echo.lua` +**`echo.lua`** ```lua core.register_service("echo", "http", function(applet) @@ -53,4 +63,71 @@ core.register_service("echo", "http", function(applet) applet:start_response() applet:send(body) end) + +core.register_service("cookie", "http", function(applet) + local body = "" + local hdrs = applet.headers + if hdrs["cookie"] then + for _, raw in ipairs(hdrs["cookie"]) do + for pair in raw:gmatch("[^;]+") do + local trimmed = pair:match("^%s*(.*)") + local eq = trimmed:find("=") + if eq and eq > 1 then + body = body .. trimmed:sub(1, eq-1) .. "=" .. trimmed:sub(eq+1) .. "\n" + end + end + end + end + applet:set_status(200) + applet:add_header("Content-Type", "text/plain") + applet:add_header("Content-Length", tostring(#body)) + applet:start_response() + applet:send(body) +end) + +core.register_service("echo_body", "http", function(applet) + local body = applet:receive() + if body == nil then body = "" end + applet:set_status(200) + applet:add_header("Content-Type", "text/plain") + applet:add_header("Content-Length", tostring(#body)) + applet:start_response() + applet:send(body) +end) ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/hyper.md b/docs/content/servers/hyper.md index a60cac3..0bebe17 100644 --- a/docs/content/servers/hyper.md +++ b/docs/content/servers/hyper.md @@ -1,6 +1,6 @@ --- title: "Hyper" -toc: false +toc: true breadcrumbs: false --- @@ -24,7 +24,7 @@ COPY --from=build /src/target/release/hyper-server /usr/local/bin/ ENTRYPOINT ["hyper-server", "8080"] ``` -## Source — `src/main.rs` +## Source ```rust use std::convert::Infallible; @@ -50,6 +50,22 @@ async fn handle(req: Request) -> Result collected.to_bytes(), @@ -85,3 +101,39 @@ async fn main() { } } ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/jetty.md b/docs/content/servers/jetty.md index fadac07..0fbfff4 100644 --- a/docs/content/servers/jetty.md +++ b/docs/content/servers/jetty.md @@ -1,6 +1,6 @@ --- title: "Jetty" -toc: false +toc: true breadcrumbs: false --- @@ -22,7 +22,7 @@ COPY --from=build /src/target/jetty-server-1.0.0.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar", "8080"] ``` -## Source — `src/main/java/server/Application.java` +## Source ```java package server; @@ -48,7 +48,22 @@ public class Application extends Handler.Abstract { response.setStatus(200); response.getHeaders().put("Content-Type", "text/plain"); - if ("/echo".equals(request.getHttpURI().getPath())) { + if ("/cookie".equals(request.getHttpURI().getPath())) { + StringBuilder sb = new StringBuilder(); + for (HttpField field : request.getHeaders()) { + if ("Cookie".equalsIgnoreCase(field.getName())) { + for (String pair : field.getValue().split(";")) { + String trimmed = pair.stripLeading(); + int eq = trimmed.indexOf('='); + if (eq > 0) { + sb.append(trimmed, 0, eq).append("=").append(trimmed.substring(eq + 1)).append("\n"); + } + } + } + } + byte[] cookieBody = sb.toString().getBytes(StandardCharsets.UTF_8); + response.write(true, ByteBuffer.wrap(cookieBody), callback); + } else if ("/echo".equals(request.getHttpURI().getPath())) { StringBuilder sb = new StringBuilder(); for (HttpField field : request.getHeaders()) { sb.append(field.getName()).append(": ").append(field.getValue()).append("\n"); @@ -78,3 +93,39 @@ public class Application extends Handler.Abstract { } } ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/lighttpd.md b/docs/content/servers/lighttpd.md index f35a812..8b81b61 100644 --- a/docs/content/servers/lighttpd.md +++ b/docs/content/servers/lighttpd.md @@ -1,6 +1,6 @@ --- title: "Lighttpd" -toc: false +toc: true breadcrumbs: false --- @@ -14,12 +14,15 @@ RUN apk add --no-cache lighttpd COPY src/Servers/LighttpdServer/lighttpd.conf /etc/lighttpd/lighttpd.conf COPY src/Servers/LighttpdServer/index.cgi /var/www/index.cgi COPY src/Servers/LighttpdServer/echo.cgi /var/www/echo.cgi -RUN chmod +x /var/www/index.cgi /var/www/echo.cgi +COPY src/Servers/LighttpdServer/cookie.cgi /var/www/cookie.cgi +RUN chmod +x /var/www/index.cgi /var/www/echo.cgi /var/www/cookie.cgi EXPOSE 8080 CMD ["lighttpd", "-D", "-f", "/etc/lighttpd/lighttpd.conf"] ``` -## Source — `lighttpd.conf` +## Source + +**`lighttpd.conf`** ```text server.document-root = "/var/www" @@ -28,10 +31,10 @@ index-file.names = ("index.cgi") server.modules += ("mod_cgi", "mod_alias") cgi.assign = (".cgi" => "") server.error-handler = "/index.cgi" -alias.url = ("/echo" => "/var/www/echo.cgi") +alias.url = ("/echo" => "/var/www/echo.cgi", "/cookie" => "/var/www/cookie.cgi") ``` -## Source — `index.cgi` +**`index.cgi`** ```bash #!/bin/sh @@ -43,7 +46,7 @@ else fi ``` -## Source — `echo.cgi` +**`echo.cgi`** ```bash #!/bin/sh @@ -59,3 +62,52 @@ if [ -n "$CONTENT_LENGTH" ]; then printf 'Content-Length: %s\n' "$CONTENT_LENGTH" fi ``` + +**`cookie.cgi`** + +```bash +#!/bin/sh +printf 'Content-Type: text/plain\r\n\r\n' +if [ -n "$HTTP_COOKIE" ]; then + echo "$HTTP_COOKIE" | tr ';' '\n' | while read -r pair; do + trimmed=$(echo "$pair" | sed 's/^ *//') + printf '%s\n' "$trimmed" + done +fi +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/nancy.md b/docs/content/servers/nancy.md deleted file mode 100644 index 6584c61..0000000 --- a/docs/content/servers/nancy.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -title: "Nancy" -toc: false -breadcrumbs: false ---- - -**Language:** C# · [View source on GitHub](https://github.com/MDA2AV/Http11Probe/tree/main/src/Servers/NancyServer) - -## Dockerfile - -```dockerfile -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build -WORKDIR /src -COPY Directory.Build.props . -COPY src/Servers/NancyServer/ src/Servers/NancyServer/ -RUN dotnet restore src/Servers/NancyServer/NancyServer.csproj -RUN dotnet publish src/Servers/NancyServer/NancyServer.csproj -c Release -o /app --no-restore - -FROM mcr.microsoft.com/dotnet/runtime:8.0 -WORKDIR /app -COPY --from=build /app . -ENTRYPOINT ["dotnet", "NancyServer.dll", "8080"] -``` - -## Source — `Program.cs` - -```csharp -using Nancy; -using Nancy.Hosting.Self; - -var port = args.Length > 0 ? args[0] : "9006"; -var uri = new Uri($"http://0.0.0.0:{port}"); - -var config = new HostConfiguration { RewriteLocalhost = false }; - -using var host = new NancyHost(config, uri); -host.Start(); - -Console.WriteLine($"Nancy listening on {uri}"); - -var waitHandle = new ManualResetEvent(false); -Console.CancelKeyPress += (_, e) => { e.Cancel = true; waitHandle.Set(); }; -waitHandle.WaitOne(); - -public class EchoModule : NancyModule -{ - public EchoModule() : base("/echo") - { - Get("/", _ => EchoHeaders()); - Post("/", _ => EchoHeaders()); - Put("/", _ => EchoHeaders()); - Delete("/", _ => EchoHeaders()); - Patch("/", _ => EchoHeaders()); - } - - private string EchoHeaders() - { - var sb = new System.Text.StringBuilder(); - foreach (var h in Request.Headers) - foreach (var v in h.Value) - sb.AppendLine($"{h.Key}: {v}"); - return sb.ToString(); - } -} - -public class HomeModule : NancyModule -{ - public HomeModule() - { - Get("/{path*}", _ => "OK"); - Get("/", _ => "OK"); - Post("/{path*}", _ => EchoBody()); - Post("/", _ => EchoBody()); - } - - private string EchoBody() - { - using var reader = new System.IO.StreamReader(Request.Body); - return reader.ReadToEnd(); - } -} -``` diff --git a/docs/content/servers/netcoreserver.md b/docs/content/servers/netcoreserver.md index 7803205..bb52d43 100644 --- a/docs/content/servers/netcoreserver.md +++ b/docs/content/servers/netcoreserver.md @@ -1,6 +1,6 @@ --- title: "NetCoreServer" -toc: false +toc: true breadcrumbs: false --- @@ -22,7 +22,7 @@ COPY --from=build /app . ENTRYPOINT ["dotnet", "NetCoreServerFramework.dll", "8080"] ``` -## Source — `Program.cs` +## Source ```csharp using System.Net; @@ -58,6 +58,25 @@ class OkHttpSession : HttpSession } SendResponseAsync(Response.MakeOkResponse(200).SetBody(sb.ToString())); } + else if (request.Url == "/cookie") + { + var sb = new System.Text.StringBuilder(); + for (int i = 0; i < request.Headers; i++) + { + var (name, value) = request.Header(i); + if (string.Equals(name, "Cookie", StringComparison.OrdinalIgnoreCase)) + { + foreach (var pair in value.Split(';')) + { + var trimmed = pair.TrimStart(); + var eqIdx = trimmed.IndexOf('='); + if (eqIdx > 0) + sb.AppendLine($"{trimmed[..eqIdx]}={trimmed[(eqIdx + 1)..]}"); + } + } + } + SendResponseAsync(Response.MakeOkResponse(200).SetBody(sb.ToString())); + } else if (request.Method == "POST" && request.Body.Length > 0) SendResponseAsync(Response.MakeOkResponse(200).SetBody(request.Body)); else @@ -81,3 +100,39 @@ class OkHttpServer : NetCoreServer.HttpServer protected override void OnError(SocketError error) { } } ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/nginx.md b/docs/content/servers/nginx.md index bdd7998..904e4dd 100644 --- a/docs/content/servers/nginx.md +++ b/docs/content/servers/nginx.md @@ -1,6 +1,6 @@ --- title: "Nginx" -toc: false +toc: true breadcrumbs: false --- @@ -14,7 +14,9 @@ COPY src/Servers/NginxServer/nginx.conf /etc/nginx/nginx.conf COPY src/Servers/NginxServer/echo.js /etc/nginx/echo.js ``` -## Source — `nginx.conf` +## Source + +**`nginx.conf`** ```nginx load_module modules/ngx_http_js_module.so; @@ -45,14 +47,18 @@ http { js_content echo.echo; } + location /cookie { + js_content echo.cookie; + } + location / { - return 200 "OK"; + js_content echo.handler; } } } ``` -## Source — `echo.js` +**`echo.js`** ```javascript function echo(r) { @@ -64,5 +70,65 @@ function echo(r) { r.return(200, body); } -export default { echo }; +function cookie(r) { + var body = ''; + var raw = r.headersIn['Cookie']; + if (raw) { + var pairs = raw.split(';'); + for (var i = 0; i < pairs.length; i++) { + var trimmed = pairs[i].replace(/^\s+/, ''); + var eq = trimmed.indexOf('='); + if (eq > 0) { + body += trimmed.substring(0, eq) + '=' + trimmed.substring(eq + 1) + '\n'; + } + } + } + r.return(200, body); +} + +function handler(r) { + if (r.method === 'POST') { + r.return(200, r.requestText || ''); + } else { + r.return(200, 'OK'); + } +} + +export default { echo, cookie, handler }; ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/node.md b/docs/content/servers/node.md index 4a21d32..ef62aa3 100644 --- a/docs/content/servers/node.md +++ b/docs/content/servers/node.md @@ -1,6 +1,6 @@ --- title: "Node.js" -toc: false +toc: true breadcrumbs: false --- @@ -15,7 +15,7 @@ COPY src/Servers/NodeServer/server.js . ENTRYPOINT ["node", "server.js", "8080"] ``` -## Source — `server.js` +## Source ```javascript const http = require('http'); @@ -29,7 +29,17 @@ const server = http.createServer((req, res) => { } catch { pathname = req.url; } - if (pathname === '/echo') { + if (pathname === '/cookie') { + let body = ''; + const raw = req.headers.cookie || ''; + for (const pair of raw.split(';')) { + const trimmed = pair.trimStart(); + const eq = trimmed.indexOf('='); + if (eq > 0) body += trimmed.substring(0, eq) + '=' + trimmed.substring(eq + 1) + '\n'; + } + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end(body); + } else if (pathname === '/echo') { let body = ''; for (const [name, value] of Object.entries(req.headers)) { if (Array.isArray(value)) value.forEach(v => body += name + ': ' + v + '\n'); @@ -52,3 +62,39 @@ const server = http.createServer((req, res) => { server.listen(port, '0.0.0.0'); ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/ntex.md b/docs/content/servers/ntex.md index f2c4d5f..f1bf88d 100644 --- a/docs/content/servers/ntex.md +++ b/docs/content/servers/ntex.md @@ -1,6 +1,6 @@ --- title: "Ntex" -toc: false +toc: true breadcrumbs: false --- @@ -24,7 +24,7 @@ COPY --from=build /src/target/release/ntex-server /usr/local/bin/ ENTRYPOINT ["ntex-server", "8080"] ``` -## Source — `src/main.rs` +## Source ```rust use ntex::web; @@ -38,6 +38,19 @@ async fn echo(req: web::HttpRequest) -> impl web::Responder { web::HttpResponse::Ok().content_type("text/plain").body(body) } +async fn cookie(req: web::HttpRequest) -> impl web::Responder { + let mut body = String::new(); + if let Some(raw) = req.headers().get("cookie").and_then(|v| v.to_str().ok()) { + for pair in raw.split(';') { + let trimmed = pair.trim_start(); + if let Some(eq) = trimmed.find('=') { + body.push_str(&format!("{}={}\n", &trimmed[..eq], &trimmed[eq+1..])); + } + } + } + web::HttpResponse::Ok().content_type("text/plain").body(body) +} + async fn handler(req: web::HttpRequest, body: Bytes) -> web::HttpResponse { if req.method() == ntex::http::Method::POST { web::HttpResponse::Ok() @@ -60,6 +73,7 @@ async fn main() -> std::io::Result<()> { web::server(|| { web::App::new() .route("/echo", web::to(echo)) + .route("/cookie", web::to(cookie)) .default_service(web::to(handler)) }) .bind(("0.0.0.0", port))? @@ -69,3 +83,39 @@ async fn main() -> std::io::Result<()> { Ok(()) } ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/php.md b/docs/content/servers/php.md index cdccebb..1b1e4da 100644 --- a/docs/content/servers/php.md +++ b/docs/content/servers/php.md @@ -1,6 +1,6 @@ --- title: "PHP" -toc: false +toc: true breadcrumbs: false --- @@ -16,7 +16,7 @@ EXPOSE 8080 CMD ["php", "-S", "0.0.0.0:8080", "index.php"] ``` -## Source — `index.php` +## Source ```php $value) { + echo "$name=$value\n"; + } + exit; +} + header('Content-Type: text/plain'); if ($_SERVER['REQUEST_METHOD'] === 'POST') { echo file_get_contents('php://input'); @@ -35,3 +43,39 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { echo 'OK'; } ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/pingora.md b/docs/content/servers/pingora.md index d0a1fee..04632b9 100644 --- a/docs/content/servers/pingora.md +++ b/docs/content/servers/pingora.md @@ -1,6 +1,6 @@ --- title: "Pingora" -toc: false +toc: true breadcrumbs: false --- @@ -26,7 +26,7 @@ COPY --from=build /src/target/release/pingora-server /usr/local/bin/ ENTRYPOINT ["pingora-server", "8080"] ``` -## Source — `src/main.rs` +## Source ```rust use async_trait::async_trait; @@ -48,6 +48,30 @@ impl ProxyHttp for OkProxy { session: &mut Session, _ctx: &mut Self::CTX, ) -> Result { + let is_cookie = session.req_header().uri.path() == "/cookie"; + if is_cookie { + let mut body_str = String::new(); + if let Some(raw) = session.req_header().headers.get("cookie").and_then(|v| v.to_str().ok()) { + for pair in raw.split(';') { + let trimmed = pair.trim_start(); + if let Some(eq) = trimmed.find('=') { + body_str.push_str(&format!("{}={}\n", &trimmed[..eq], &trimmed[eq+1..])); + } + } + } + let body = Bytes::from(body_str); + let mut header = ResponseHeader::build(200, None)?; + header.insert_header("Content-Type", "text/plain")?; + header.insert_header("Content-Length", &body.len().to_string())?; + session + .write_response_header(Box::new(header), false) + .await?; + session + .write_response_body(Some(body), true) + .await?; + return Ok(true); + } + let is_echo = session.req_header().uri.path() == "/echo"; if is_echo { let mut body_str = String::new(); @@ -115,3 +139,39 @@ fn main() { server.run_forever(); } ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/puma.md b/docs/content/servers/puma.md index 8635a50..bc8cfcd 100644 --- a/docs/content/servers/puma.md +++ b/docs/content/servers/puma.md @@ -1,6 +1,6 @@ --- title: "Puma" -toc: false +toc: true breadcrumbs: false --- @@ -20,7 +20,7 @@ EXPOSE 8080 CMD ["puma", "-b", "tcp://0.0.0.0:8080"] ``` -## Source — `config.ru` +## Source ```ruby app = proc { |env| @@ -30,6 +30,18 @@ app = proc { |env| body += "Content-Type: #{env['CONTENT_TYPE']}\n" if env['CONTENT_TYPE'] body += "Content-Length: #{env['CONTENT_LENGTH']}\n" if env['CONTENT_LENGTH'] [200, { 'Content-Type' => 'text/plain' }, [body]] + elsif env['PATH_INFO'] == '/cookie' + body = "" + if env['HTTP_COOKIE'] + env['HTTP_COOKIE'].split(';').each do |pair| + trimmed = pair.lstrip + eq = trimmed.index('=') + if eq && eq > 0 + body += "#{trimmed[0...eq]}=#{trimmed[(eq+1)..]}\n" + end + end + end + [200, { 'Content-Type' => 'text/plain' }, [body]] elsif env['REQUEST_METHOD'] == 'POST' body = env['rack.input'].read [200, { 'content-type' => 'text/plain' }, [body]] @@ -39,3 +51,39 @@ app = proc { |env| } run app ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/quarkus.md b/docs/content/servers/quarkus.md index fe1efcb..0d72c48 100644 --- a/docs/content/servers/quarkus.md +++ b/docs/content/servers/quarkus.md @@ -1,6 +1,6 @@ --- title: "Quarkus" -toc: false +toc: true breadcrumbs: false --- @@ -22,7 +22,7 @@ COPY --from=build /src/target/quarkus-app/ quarkus-app/ ENTRYPOINT ["java", "-Dquarkus.http.port=8080", "-jar", "quarkus-app/quarkus-run.jar"] ``` -## Source — `src/main/java/server/Application.java` +## Source ```java package server; @@ -58,6 +58,20 @@ public class Application { return body.readAllBytes(); } + @GET + @Path("/cookie") + @Produces(MediaType.TEXT_PLAIN) + public Response cookieGet(@Context HttpHeaders headers) { + return parseCookies(headers); + } + + @POST + @Path("/cookie") + @Produces(MediaType.TEXT_PLAIN) + public Response cookiePost(@Context HttpHeaders headers) { + return parseCookies(headers); + } + @GET @Path("/echo") @Produces(MediaType.TEXT_PLAIN) @@ -72,6 +86,23 @@ public class Application { return echoHeaders(headers); } + private Response parseCookies(HttpHeaders headers) { + StringBuilder sb = new StringBuilder(); + List cookieHeaders = headers.getRequestHeader("Cookie"); + if (cookieHeaders != null) { + for (String raw : cookieHeaders) { + for (String pair : raw.split(";")) { + String trimmed = pair.stripLeading(); + int eq = trimmed.indexOf('='); + if (eq > 0) { + sb.append(trimmed, 0, eq).append("=").append(trimmed.substring(eq + 1)).append("\n"); + } + } + } + } + return Response.ok(sb.toString(), MediaType.TEXT_PLAIN).build(); + } + private Response echoHeaders(HttpHeaders headers) { StringBuilder sb = new StringBuilder(); for (Map.Entry> entry : headers.getRequestHeaders().entrySet()) { @@ -83,3 +114,39 @@ public class Application { } } ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/servicestack.md b/docs/content/servers/servicestack.md index 22568f9..0685992 100644 --- a/docs/content/servers/servicestack.md +++ b/docs/content/servers/servicestack.md @@ -1,6 +1,6 @@ --- title: "ServiceStack" -toc: false +toc: true breadcrumbs: false --- @@ -22,7 +22,7 @@ COPY --from=build /app . ENTRYPOINT ["dotnet", "ServiceStackServer.dll"] ``` -## Source — `Program.cs` +## Source ```csharp using ServiceStack; @@ -39,6 +39,13 @@ app.Map("/echo", (HttpContext ctx) => sb.AppendLine($"{h.Key}: {v}"); return Results.Text(sb.ToString()); }); +app.Map("/cookie", (HttpContext ctx) => +{ + var sb = new System.Text.StringBuilder(); + foreach (var cookie in ctx.Request.Cookies) + sb.AppendLine($"{cookie.Key}={cookie.Value}"); + return Results.Text(sb.ToString()); +}); app.MapFallback(async (HttpContext ctx) => { if (ctx.Request.Method == "POST") @@ -57,3 +64,39 @@ class AppHost : AppHostBase public override void Configure() { } } ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/simplew.md b/docs/content/servers/simplew.md index 2807459..985b2dd 100644 --- a/docs/content/servers/simplew.md +++ b/docs/content/servers/simplew.md @@ -1,6 +1,6 @@ --- title: "SimpleW" -toc: false +toc: true breadcrumbs: false --- @@ -22,7 +22,7 @@ COPY --from=build /app . ENTRYPOINT ["dotnet", "SimpleWServer.dll", "8080"] ``` -## Source — `Program.cs` +## Source ```csharp using System.Net; @@ -33,6 +33,8 @@ var port = args.Length > 0 && int.TryParse(args[0], out var p) ? p : 8080; var server = new SimpleWServer(IPAddress.Any, port); +server.MapGet("/cookie", (HttpSession session) => ParseCookies(session)); +server.MapPost("/cookie", (HttpSession session) => ParseCookies(session)); server.MapGet("/echo", (HttpSession session) => { var sb = new System.Text.StringBuilder(); @@ -52,7 +54,62 @@ server.MapGet("/{path}", () => "OK"); server.MapPost("/", (HttpSession session) => session.Request.BodyString); server.MapPost("/{path}", (HttpSession session) => session.Request.BodyString); +static string ParseCookies(HttpSession session) +{ + var sb = new System.Text.StringBuilder(); + foreach (var h in session.Request.Headers.EnumerateAll()) + { + 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 sb.ToString(); +} + Console.WriteLine($"SimpleW listening on http://localhost:{port}"); await server.RunAsync(); ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/sisk.md b/docs/content/servers/sisk.md index 8cceb4f..4b35dbd 100644 --- a/docs/content/servers/sisk.md +++ b/docs/content/servers/sisk.md @@ -1,6 +1,6 @@ --- title: "Sisk" -toc: false +toc: true breadcrumbs: false --- @@ -22,7 +22,7 @@ COPY --from=build /app . ENTRYPOINT ["dotnet", "SiskServer.dll", "8080"] ``` -## Source — `Program.cs` +## Source ```csharp using Sisk.Core.Http; @@ -44,6 +44,27 @@ app.Router.SetRoute(RouteMethod.Any, Route.AnyPath, request => sb.AppendLine($"{h.Key}: {val}"); return new HttpResponse(200).WithContent(sb.ToString()); } + if (request.Path == "/cookie") + { + var sb = new System.Text.StringBuilder(); + foreach (var h in request.Headers) + { + if (string.Equals(h.Key, "Cookie", StringComparison.OrdinalIgnoreCase)) + { + foreach (var rawVal in h.Value) + { + foreach (var pair in rawVal.Split(';')) + { + var trimmed = pair.TrimStart(); + var eqIdx = trimmed.IndexOf('='); + if (eqIdx > 0) + sb.AppendLine($"{trimmed[..eqIdx]}={trimmed[(eqIdx + 1)..]}"); + } + } + } + } + return new HttpResponse(200).WithContent(sb.ToString()); + } if (request.Method == HttpMethod.Post && request.Body is not null) { var body = request.Body; @@ -54,3 +75,39 @@ app.Router.SetRoute(RouteMethod.Any, Route.AnyPath, request => await app.StartAsync(); ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/spring-boot.md b/docs/content/servers/spring-boot.md index 30a7208..016be63 100644 --- a/docs/content/servers/spring-boot.md +++ b/docs/content/servers/spring-boot.md @@ -1,6 +1,6 @@ --- title: "Spring Boot" -toc: false +toc: true breadcrumbs: false --- @@ -22,7 +22,7 @@ COPY --from=build /src/target/*.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar", "--server.port=8080", "--server.address=127.0.0.1"] ``` -## Source — `src/main/java/server/Application.java` +## Source ```java package server; @@ -58,6 +58,18 @@ public class Application { return request.getInputStream().readAllBytes(); } + @RequestMapping("/cookie") + public ResponseEntity cookieEndpoint(HttpServletRequest request) { + StringBuilder sb = new StringBuilder(); + jakarta.servlet.http.Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (jakarta.servlet.http.Cookie c : cookies) { + sb.append(c.getName()).append("=").append(c.getValue()).append("\n"); + } + } + return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(sb.toString()); + } + @RequestMapping("/echo") public ResponseEntity echo(HttpServletRequest request) { StringBuilder sb = new StringBuilder(); @@ -73,3 +85,39 @@ public class Application { } } ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/tomcat.md b/docs/content/servers/tomcat.md index 96a4a98..42d3a76 100644 --- a/docs/content/servers/tomcat.md +++ b/docs/content/servers/tomcat.md @@ -1,6 +1,6 @@ --- title: "Tomcat" -toc: false +toc: true breadcrumbs: false --- @@ -16,7 +16,9 @@ EXPOSE 8080 CMD ["catalina.sh", "run"] ``` -## Source — `webapp/WEB-INF/web.xml` +## Source + +**`webapp/WEB-INF/web.xml`** ```xml @@ -30,6 +32,15 @@ CMD ["catalina.sh", "run"] /echo + + cookie + /cookie.jsp + + + cookie + /cookie + + ok /ok.jsp @@ -41,7 +52,7 @@ CMD ["catalina.sh", "run"] ``` -## Source — `webapp/ok.jsp` +**`webapp/ok.jsp`** ```jsp <%@page contentType="text/plain" import="java.io.*"%><% @@ -55,7 +66,7 @@ if ("POST".equals(request.getMethod())) { %> ``` -## Source — `webapp/echo.jsp` +**`webapp/echo.jsp`** ```jsp <%@page contentType="text/plain" import="java.util.*"%><% @@ -69,3 +80,52 @@ while (names.hasMoreElements()) { } %> ``` + +**`webapp/cookie.jsp`** + +```jsp +<%@page contentType="text/plain"%><% +jakarta.servlet.http.Cookie[] cookies = request.getCookies(); +if (cookies != null) { + for (jakarta.servlet.http.Cookie c : cookies) { + out.print(c.getName() + "=" + c.getValue() + "\n"); + } +} +%> +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/traefik.md b/docs/content/servers/traefik.md index 0e5702b..2232861 100644 --- a/docs/content/servers/traefik.md +++ b/docs/content/servers/traefik.md @@ -1,6 +1,6 @@ --- title: "Traefik" -toc: false +toc: true breadcrumbs: false --- @@ -28,7 +28,9 @@ RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] ``` -## Source — `traefik.yml` +## Source + +**`traefik.yml`** ```yaml entryPoints: @@ -45,7 +47,7 @@ experimental: moduleName: github.com/jdel/staticresponse ``` -## Source — `dynamic.yml` +**`dynamic.yml`** ```yaml http: @@ -56,6 +58,12 @@ http: - web service: echo-svc + cookie: + rule: "Path(`/cookie`)" + entryPoints: + - web + service: echo-svc + catchall: rule: "PathPrefix(`/`)" entryPoints: @@ -78,36 +86,91 @@ http: body: "OK" ``` -## Source — `echo/main.go` +**`echo/main.go`** ```go package main import ( - "fmt" + "io" "net/http" "strings" ) func main() { - http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) { + http.HandleFunc("/cookie", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") - var sb strings.Builder - for name, values := range r.Header { - for _, v := range values { - sb.WriteString(fmt.Sprintf("%s: %s\n", name, v)) + raw := r.Header.Get("Cookie") + for _, pair := range strings.Split(raw, ";") { + pair = strings.TrimLeft(pair, " ") + if eq := strings.Index(pair, "="); eq > 0 { + w.Write([]byte(pair[:eq] + "=" + pair[eq+1:] + "\n")) } } - fmt.Fprint(w, sb.String()) }) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + w.Write(body) + }) + http.ListenAndServe(":9090", nil) } ``` -## Source — `entrypoint.sh` +**`entrypoint.sh`** ```bash #!/bin/sh /usr/local/bin/echo-server & exec traefik "$@" ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/content/servers/uvicorn.md b/docs/content/servers/uvicorn.md index f69cdda..6076a54 100644 --- a/docs/content/servers/uvicorn.md +++ b/docs/content/servers/uvicorn.md @@ -1,6 +1,6 @@ --- title: "Uvicorn" -toc: false +toc: true breadcrumbs: false --- @@ -17,12 +17,36 @@ EXPOSE 8080 CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"] ``` -## Source — `app.py` +## Source ```python async def app(scope, receive, send): path = scope.get('path', '/') + if path == '/cookie': + cookie_val = '' + for name, value in scope.get('headers', []): + if name.lower() == b'cookie': + cookie_val = value.decode('latin-1') + break + lines = [] + for pair in cookie_val.split(';'): + pair = pair.strip() + eq = pair.find('=') + if eq > 0: + lines.append(f"{pair[:eq]}={pair[eq+1:]}") + body = ('\n'.join(lines) + '\n').encode('utf-8') if lines else b'' + await send({ + 'type': 'http.response.start', + 'status': 200, + 'headers': [(b'content-type', b'text/plain')], + }) + await send({ + 'type': 'http.response.body', + 'body': body, + }) + return + if path == '/echo': lines = [] for name, value in scope.get('headers', []): @@ -58,3 +82,39 @@ async def app(scope, receive, send): 'body': body, }) ``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/docs/hugo.yaml b/docs/hugo.yaml index 75a7f79..e2b3f02 100644 --- a/docs/hugo.yaml +++ b/docs/hugo.yaml @@ -47,6 +47,11 @@ menu: parent: test-details pageRef: /caching weight: 5 + - name: Cookies + identifier: test-details-cookies + parent: test-details + pageRef: /cookies + weight: 6 - name: Glossary identifier: glossary pageRef: /docs diff --git a/docs/static/probe/render.js b/docs/static/probe/render.js index d460cd1..b6b0f66 100644 --- a/docs/static/probe/render.js +++ b/docs/static/probe/render.js @@ -476,7 +476,19 @@ window.ProbeRender = (function () { 'CAP-IMS-FUTURE': '/Http11Probe/docs/caching/ims-future/', 'CAP-IMS-INVALID': '/Http11Probe/docs/caching/ims-invalid/', 'CAP-INM-UNQUOTED': '/Http11Probe/docs/caching/inm-unquoted/', - 'CAP-ETAG-WEAK': '/Http11Probe/docs/caching/etag-weak/' + 'CAP-ETAG-WEAK': '/Http11Probe/docs/caching/etag-weak/', + 'COOK-ECHO': '/Http11Probe/docs/cookies/echo/', + 'COOK-OVERSIZED': '/Http11Probe/docs/cookies/oversized/', + 'COOK-EMPTY': '/Http11Probe/docs/cookies/empty/', + 'COOK-NUL': '/Http11Probe/docs/cookies/nul/', + 'COOK-CONTROL-CHARS': '/Http11Probe/docs/cookies/control-chars/', + 'COOK-MANY-PAIRS': '/Http11Probe/docs/cookies/many-pairs/', + 'COOK-MALFORMED': '/Http11Probe/docs/cookies/malformed/', + 'COOK-MULTI-HEADER': '/Http11Probe/docs/cookies/multi-header/', + 'COOK-PARSED-BASIC': '/Http11Probe/docs/cookies/parsed-basic/', + 'COOK-PARSED-MULTI': '/Http11Probe/docs/cookies/parsed-multi/', + 'COOK-PARSED-EMPTY-VAL': '/Http11Probe/docs/cookies/parsed-empty-val/', + 'COOK-PARSED-SPECIAL': '/Http11Probe/docs/cookies/parsed-special/' }; function testUrl(tid) { @@ -735,7 +747,7 @@ window.ProbeRender = (function () { el.innerHTML = html; } - var CAT_LABELS = { Compliance: 'Compliance', Smuggling: 'Smuggling', MalformedInput: 'Malformed Input', Normalization: 'Normalization', Capabilities: 'Caching' }; + var CAT_LABELS = { Compliance: 'Compliance', Smuggling: 'Smuggling', MalformedInput: 'Malformed Input', Normalization: 'Normalization', Capabilities: 'Caching', Cookies: 'Cookies' }; function renderTable(targetId, categoryKey, ctx, testIdFilter, tableLabel) { injectScrollStyle(); @@ -758,7 +770,7 @@ window.ProbeRender = (function () { var orderedTests = scoredTests.concat(unscoredTests); var shortLabels = orderedTests.map(function (tid) { - return tid.replace(/^(RFC\d+-[\d.]+-|COMP-|SMUG-|MAL-|NORM-)/, ''); + return tid.replace(/^(RFC\d+-[\d.]+-|COMP-|SMUG-|MAL-|NORM-|COOK-)/, ''); }); var unscoredStart = scoredTests.length; @@ -1414,6 +1426,116 @@ window.ProbeRender = (function () { }); } + // ── Per-server results page ──────────────────────────────────── + var SERVER_CAT_ORDER = ['Compliance', 'Smuggling', 'MalformedInput', 'Capabilities', 'Cookies']; + + function renderServerCategoryTable(catEl, results) { + var scored = results.filter(function (r) { return r.scored !== false; }); + var unscoredR = results.filter(function (r) { return r.scored === false; }); + var ordered = scored.concat(unscoredR); + + var html = '
'; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + + ordered.forEach(function (r) { + var isUnscored = r.scored === false; + var opacity = isUnscored ? 'opacity:0.6;' : ''; + var url = testUrl(r.id); + var idHtml = url ? '' + r.id + '' : r.id; + var method = methodFromRequest(r.rawRequest); + var level = r.rfcLevel || 'Must'; + + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + }); + + html += '
TestGotExpectedMethodRFC LevelDescription
' + idHtml + '' + pill(verdictBg(r.verdict), r.got || r.verdict, r.rawResponse, r.behavioralNote, r.rawRequest, r.doubleFlush) + '' + expectedPill(EXPECT_BG, (r.expected || '').replace(/ or close/g, '/\u2715').replace(/\//g, '/\u200B')) + '' + methodTag(method) + '' + rfcLevelTag(level) + '' + (r.description || '') + '
'; + catEl.innerHTML = html; + } + + function renderServerPage(serverName) { + injectScrollStyle(); + var summaryEl = document.getElementById('server-summary'); + var data = window.PROBE_DATA; + if (!data || !data.servers) { + if (summaryEl) summaryEl.innerHTML = '

No probe data available yet. Run the Probe workflow on main to generate results.

'; + return; + } + var sv = null; + data.servers.forEach(function (s) { if (s.name === serverName) sv = s; }); + if (!sv) { + if (summaryEl) summaryEl.innerHTML = '

No results found for ' + serverName + '.

'; + return; + } + + // Summary counts + var scoredPass = 0, scoredWarn = 0, scoredFail = 0, unscored = 0, total = sv.results.length; + sv.results.forEach(function (r) { + if (r.scored === false) { unscored++; return; } + if (r.verdict === 'Pass') scoredPass++; + else if (r.verdict === 'Warn') scoredWarn++; + else scoredFail++; + }); + var scored = total - unscored; + + // Summary bar + if (summaryEl) { + var html = '
'; + var trackBg = document.documentElement.classList.contains('dark') ? '#2a2f38' : '#f0f0f0'; + html += '
'; + if (scored > 0) { + html += '
'; + if (scoredWarn > 0) html += '
'; + if (scoredFail > 0) html += '
'; + } + if (unscored > 0) html += '
'; + html += '
'; + html += '
'; + html += '' + scoredPass + ' pass'; + if (scoredWarn > 0) html += '  ' + scoredWarn + ' warn'; + if (scoredFail > 0) html += '  ' + scoredFail + ' fail'; + html += '  ' + unscored + ' unscored · ' + total + ' total'; + html += '
'; + if (data.commit) { + html += '

Commit: ' + data.commit.id.substring(0, 7) + ' — ' + (data.commit.message || '') + '

'; + } + summaryEl.innerHTML = html; + } + + // Group by category + var byCat = {}; + sv.results.forEach(function (r) { + var cat = r.category || 'Other'; + if (!byCat[cat]) byCat[cat] = []; + byCat[cat].push(r); + }); + + // Render each category into its own div + SERVER_CAT_ORDER.forEach(function (cat) { + var catEl = document.getElementById('results-' + cat.toLowerCase()); + if (!catEl) return; + var results = byCat[cat]; + if (!results || results.length === 0) { + catEl.innerHTML = '

No results for this category yet.

'; + return; + } + renderServerCategoryTable(catEl, results); + }); + } + return { pill: pill, verdictBg: verdictBg, @@ -1421,6 +1543,7 @@ window.ProbeRender = (function () { renderSummary: renderSummary, renderTable: renderTable, renderSubTables: renderSubTables, + renderServerPage: renderServerPage, renderLanguageFilter: renderLanguageFilter, filterByCategory: filterByCategory, renderCategoryFilter: renderCategoryFilter, diff --git a/src/Http11Probe.Cli/Program.cs b/src/Http11Probe.Cli/Program.cs index ed1c8b4..ba8c42e 100644 --- a/src/Http11Probe.Cli/Program.cs +++ b/src/Http11Probe.Cli/Program.cs @@ -65,6 +65,7 @@ testCases.AddRange(MalformedInputSuite.GetTestCases()); testCases.AddRange(NormalizationSuite.GetTestCases()); testCases.AddRange(CapabilitiesSuite.GetSequenceTestCases()); + testCases.AddRange(CookieSuite.GetTestCases()); var runner = new TestRunner(options); diff --git a/src/Http11Probe.Cli/Reporting/DocsUrlMap.cs b/src/Http11Probe.Cli/Reporting/DocsUrlMap.cs index 50c368d..146260a 100644 --- a/src/Http11Probe.Cli/Reporting/DocsUrlMap.cs +++ b/src/Http11Probe.Cli/Reporting/DocsUrlMap.cs @@ -135,6 +135,13 @@ internal static class DocsUrlMap return BaseUrl + "normalization/" + suffix; } + // COOK-* → cookies/{suffix} + if (testId.StartsWith("COOK-", StringComparison.OrdinalIgnoreCase)) + { + var suffix = testId[5..].ToLowerInvariant(); + return BaseUrl + "cookies/" + suffix; + } + return null; } } diff --git a/src/Http11Probe/TestCases/Suites/CookieSuite.cs b/src/Http11Probe/TestCases/Suites/CookieSuite.cs new file mode 100644 index 0000000..c38c7a7 --- /dev/null +++ b/src/Http11Probe/TestCases/Suites/CookieSuite.cs @@ -0,0 +1,473 @@ +using System.Text; +using Http11Probe.Client; +using Http11Probe.Response; + +namespace Http11Probe.TestCases.Suites; + +public static class CookieSuite +{ + public static IEnumerable GetTestCases() + { + // ── Echo-based tests (target /echo, all servers) ───────────── + + yield return new TestCase + { + Id = "COOK-ECHO", + Description = "Basic Cookie header echoed back by /echo endpoint", + Category = TestCategory.Cookies, + Scored = false, + RfcLevel = RfcLevel.NotApplicable, + PayloadFactory = ctx => MakeRequest( + $"GET /echo HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nCookie: foo=bar\r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "2xx with Cookie in body", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Warn : TestVerdict.Fail; + if (response.StatusCode is < 200 or >= 300) + return TestVerdict.Fail; + var body = response.Body ?? ""; + return body.Contains("Cookie:", StringComparison.OrdinalIgnoreCase) + ? TestVerdict.Pass + : TestVerdict.Fail; + } + }, + BehavioralAnalyzer = response => + { + if (response is null) return null; + var body = response.Body ?? ""; + if (body.Contains("foo=bar")) return "Cookie echoed: foo=bar"; + if (body.Contains("Cookie:", StringComparison.OrdinalIgnoreCase)) return "Cookie header present but value differs"; + return "Cookie header missing from echo"; + } + }; + + yield return new TestCase + { + Id = "COOK-OVERSIZED", + Description = "64KB Cookie header — tests header size limits on cookie data", + Category = TestCategory.Cookies, + Scored = false, + RfcLevel = RfcLevel.NotApplicable, + PayloadFactory = ctx => + { + var bigValue = new string('A', 65_536); + return MakeRequest( + $"GET /echo HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nCookie: big={bigValue}\r\n\r\n"); + }, + Expected = new ExpectedBehavior + { + Description = "400/431 (rejected) or 2xx (survived)", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Pass : TestVerdict.Fail; + if (response.StatusCode is 400 or 431) + return TestVerdict.Pass; + if (response.StatusCode is >= 200 and < 300) + return TestVerdict.Pass; // survived + return TestVerdict.Fail; // 500 = crash + } + }, + BehavioralAnalyzer = response => + { + if (response is null) return null; + if (response.StatusCode is 400 or 431) return "Rejected oversized cookie"; + if (response.StatusCode is >= 200 and < 300) return "Accepted 64KB cookie"; + return $"Unexpected: {response.StatusCode}"; + } + }; + + yield return new TestCase + { + Id = "COOK-EMPTY", + Description = "Empty Cookie header value — tests parser resilience", + Category = TestCategory.Cookies, + Scored = false, + RfcLevel = RfcLevel.NotApplicable, + PayloadFactory = ctx => MakeRequest( + $"GET /echo HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nCookie: \r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "2xx or 400", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Pass : TestVerdict.Fail; + if (response.StatusCode is (>= 200 and < 300) or 400) + return TestVerdict.Pass; + return TestVerdict.Fail; // 500 = crash + } + }, + BehavioralAnalyzer = response => + { + if (response is null) return null; + if (response.StatusCode is >= 200 and < 300) return "Accepted empty cookie"; + if (response.StatusCode == 400) return "Rejected empty cookie"; + return $"Unexpected: {response.StatusCode}"; + } + }; + + yield return new TestCase + { + Id = "COOK-NUL", + Description = "NUL byte in cookie value — dangerous if preserved by parser", + Category = TestCategory.Cookies, + Scored = false, + RfcLevel = RfcLevel.NotApplicable, + PayloadFactory = ctx => + { + var request = $"GET /echo HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nCookie: foo=\0bar\r\n\r\n"; + return Encoding.ASCII.GetBytes(request); + }, + Expected = new ExpectedBehavior + { + Description = "400 (rejected) or 2xx without NUL", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Pass : TestVerdict.Fail; + if (response.StatusCode == 400) + return TestVerdict.Pass; // rejected + if (response.StatusCode is >= 200 and < 300) + { + var body = response.Body ?? ""; + // NUL preserved in output = dangerous + return body.Contains('\0') ? TestVerdict.Fail : TestVerdict.Pass; + } + return TestVerdict.Fail; // 500 = crash + } + }, + BehavioralAnalyzer = response => + { + if (response is null) return null; + if (response.StatusCode == 400) return "Rejected NUL in cookie"; + if (response.StatusCode is >= 200 and < 300) + { + var body = response.Body ?? ""; + return body.Contains('\0') ? "NUL byte preserved (dangerous)" : "NUL stripped or cookie dropped"; + } + return $"Unexpected: {response.StatusCode}"; + } + }; + + yield return new TestCase + { + Id = "COOK-CONTROL-CHARS", + Description = "Control characters (0x01-0x03) in cookie value — dangerous if preserved", + Category = TestCategory.Cookies, + Scored = false, + RfcLevel = RfcLevel.NotApplicable, + PayloadFactory = ctx => + { + var request = $"GET /echo HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nCookie: foo=\x01\x02\x03\r\n\r\n"; + return Encoding.ASCII.GetBytes(request); + }, + Expected = new ExpectedBehavior + { + Description = "400 (rejected) or 2xx without control chars", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Pass : TestVerdict.Fail; + if (response.StatusCode == 400) + return TestVerdict.Pass; // rejected + if (response.StatusCode is >= 200 and < 300) + { + var body = response.Body ?? ""; + // Control chars preserved = dangerous + return body.Any(c => c is '\x01' or '\x02' or '\x03') + ? TestVerdict.Fail + : TestVerdict.Pass; + } + return TestVerdict.Fail; // 500 = crash + } + }, + BehavioralAnalyzer = response => + { + if (response is null) return null; + if (response.StatusCode == 400) return "Rejected control chars in cookie"; + if (response.StatusCode is >= 200 and < 300) + { + var body = response.Body ?? ""; + return body.Any(c => c is '\x01' or '\x02' or '\x03') + ? "Control chars preserved (dangerous)" + : "Control chars stripped or cookie dropped"; + } + return $"Unexpected: {response.StatusCode}"; + } + }; + + yield return new TestCase + { + Id = "COOK-MANY-PAIRS", + Description = "1000 cookie key=value pairs — tests parser performance limits", + Category = TestCategory.Cookies, + Scored = false, + RfcLevel = RfcLevel.NotApplicable, + PayloadFactory = ctx => + { + var pairs = string.Join("; ", Enumerable.Range(0, 1000).Select(i => $"k{i}=v{i}")); + return MakeRequest( + $"GET /echo HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nCookie: {pairs}\r\n\r\n"); + }, + Expected = new ExpectedBehavior + { + Description = "2xx or 400/431", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Pass : TestVerdict.Fail; + if (response.StatusCode is (>= 200 and < 300) or 400 or 431) + return TestVerdict.Pass; + return TestVerdict.Fail; // 500 = crash + } + }, + BehavioralAnalyzer = response => + { + if (response is null) return null; + if (response.StatusCode is >= 200 and < 300) return "Accepted 1000 cookie pairs"; + if (response.StatusCode is 400 or 431) return "Rejected 1000 cookie pairs"; + return $"Unexpected: {response.StatusCode}"; + } + }; + + yield return new TestCase + { + Id = "COOK-MALFORMED", + Description = "Completely malformed cookie value (===;;;) — tests parser crash resilience", + Category = TestCategory.Cookies, + Scored = false, + RfcLevel = RfcLevel.NotApplicable, + PayloadFactory = ctx => MakeRequest( + $"GET /echo HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nCookie: ===;;;\r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "2xx or 400", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Pass : TestVerdict.Fail; + if (response.StatusCode is (>= 200 and < 300) or 400) + return TestVerdict.Pass; + return TestVerdict.Fail; // 500 = crash + } + }, + BehavioralAnalyzer = response => + { + if (response is null) return null; + if (response.StatusCode is >= 200 and < 300) return "Accepted malformed cookie"; + if (response.StatusCode == 400) return "Rejected malformed cookie"; + return $"Unexpected: {response.StatusCode}"; + } + }; + + yield return new TestCase + { + Id = "COOK-MULTI-HEADER", + Description = "Two separate Cookie headers — should be folded per RFC 6265 §5.4", + Category = TestCategory.Cookies, + Scored = false, + RfcLevel = RfcLevel.NotApplicable, + PayloadFactory = ctx => MakeRequest( + $"GET /echo HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nCookie: a=1\r\nCookie: b=2\r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "2xx with both cookies", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Warn : TestVerdict.Fail; + if (response.StatusCode is >= 200 and < 300) + { + var body = response.Body ?? ""; + var hasA = body.Contains("a=1"); + var hasB = body.Contains("b=2"); + return hasA && hasB ? TestVerdict.Pass : TestVerdict.Warn; + } + if (response.StatusCode == 400) + return TestVerdict.Warn; // Rejected but didn't crash + return TestVerdict.Fail; // 500 = crash + } + }, + BehavioralAnalyzer = response => + { + if (response is null) return null; + if (response.StatusCode is >= 200 and < 300) + { + var body = response.Body ?? ""; + var hasA = body.Contains("a=1"); + var hasB = body.Contains("b=2"); + if (hasA && hasB) return "Both cookies echoed"; + if (hasA || hasB) return "Only one cookie echoed"; + return "Neither cookie echoed"; + } + if (response.StatusCode == 400) return "Rejected multiple Cookie headers"; + return $"Unexpected: {response.StatusCode}"; + } + }; + + // ── Parsed-cookie tests (target /cookie, AspNetMinimal only) ─ + + yield return new TestCase + { + Id = "COOK-PARSED-BASIC", + Description = "Basic cookie parsed correctly by framework", + Category = TestCategory.Cookies, + Scored = false, + RfcLevel = RfcLevel.NotApplicable, + PayloadFactory = ctx => MakeRequest( + $"GET /cookie HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nCookie: foo=bar\r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "2xx with foo=bar in body", + CustomValidator = MakeParsedValidator("foo=bar") + }, + BehavioralAnalyzer = MakeParsedAnalyzer("foo=bar") + }; + + yield return new TestCase + { + Id = "COOK-PARSED-MULTI", + Description = "Multiple cookies parsed correctly by framework", + Category = TestCategory.Cookies, + Scored = false, + RfcLevel = RfcLevel.NotApplicable, + PayloadFactory = ctx => MakeRequest( + $"GET /cookie HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nCookie: a=1; b=2; c=3\r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "2xx with a=1, b=2, c=3 in body", + CustomValidator = MakeParsedValidator("a=1", "b=2", "c=3") + }, + BehavioralAnalyzer = MakeParsedAnalyzer("a=1", "b=2", "c=3") + }; + + yield return new TestCase + { + Id = "COOK-PARSED-EMPTY-VAL", + Description = "Cookie with empty value parsed without crash", + Category = TestCategory.Cookies, + Scored = false, + RfcLevel = RfcLevel.NotApplicable, + PayloadFactory = ctx => MakeRequest( + $"GET /cookie HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nCookie: foo=\r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "2xx (no crash)", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Warn : TestVerdict.Fail; + if (response.StatusCode == 404) + return TestVerdict.Warn; // endpoint not available + if (response.StatusCode is >= 200 and < 300) + return TestVerdict.Pass; + if (response.StatusCode == 400) + return TestVerdict.Pass; // rejected but didn't crash + return TestVerdict.Fail; // 500 = crash + } + }, + BehavioralAnalyzer = response => + { + if (response is null) return null; + if (response.StatusCode == 404) return "Endpoint not available"; + if (response.StatusCode is >= 200 and < 300) + { + var body = response.Body ?? ""; + return body.Contains("foo=") ? "Parsed foo= (empty value)" : "Survived (cookie may have been dropped)"; + } + if (response.StatusCode == 400) return "Rejected empty cookie value"; + return $"Unexpected: {response.StatusCode}"; + } + }; + + yield return new TestCase + { + Id = "COOK-PARSED-SPECIAL", + Description = "Cookies with spaces and = in values — tests framework parser edge cases", + Category = TestCategory.Cookies, + Scored = false, + RfcLevel = RfcLevel.NotApplicable, + PayloadFactory = ctx => MakeRequest( + $"GET /cookie HTTP/1.1\r\nHost: {ctx.HostHeader}\r\nCookie: a=hello world; b=x=y\r\n\r\n"), + Expected = new ExpectedBehavior + { + Description = "2xx (no crash)", + CustomValidator = (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Warn : TestVerdict.Fail; + if (response.StatusCode == 404) + return TestVerdict.Warn; // endpoint not available + if (response.StatusCode is >= 200 and < 300) + return TestVerdict.Pass; + if (response.StatusCode == 400) + return TestVerdict.Pass; // rejected but didn't crash + return TestVerdict.Fail; // 500 = crash + } + }, + BehavioralAnalyzer = response => + { + if (response is null) return null; + if (response.StatusCode == 404) return "Endpoint not available"; + if (response.StatusCode is >= 200 and < 300) + { + var body = response.Body ?? ""; + var hasA = body.Contains("a="); + var hasB = body.Contains("b="); + if (hasA && hasB) return "Both cookies parsed"; + if (hasA || hasB) return "Partially parsed"; + return "Survived but no cookies parsed"; + } + if (response.StatusCode == 400) return "Rejected special characters in cookie"; + return $"Unexpected: {response.StatusCode}"; + } + }; + } + + private static Func MakeParsedValidator( + params string[] expectedPairs) + { + return (response, state) => + { + if (response is null) + return state == ConnectionState.ClosedByServer ? TestVerdict.Warn : TestVerdict.Fail; + if (response.StatusCode == 404) + return TestVerdict.Warn; // endpoint not available on this server + if (response.StatusCode is >= 200 and < 300) + { + var body = response.Body ?? ""; + return expectedPairs.All(p => body.Contains(p)) + ? TestVerdict.Pass + : TestVerdict.Fail; + } + if (response.StatusCode == 400) + return TestVerdict.Warn; + return TestVerdict.Fail; // 500 = crash + }; + } + + private static Func MakeParsedAnalyzer(params string[] expectedPairs) + { + return response => + { + if (response is null) return null; + if (response.StatusCode == 404) return "Endpoint not available"; + if (response.StatusCode is >= 200 and < 300) + { + var body = response.Body ?? ""; + var found = expectedPairs.Count(p => body.Contains(p)); + return found == expectedPairs.Length + ? $"All {found} cookie(s) parsed" + : $"{found}/{expectedPairs.Length} cookie(s) found"; + } + if (response.StatusCode == 400) return "Rejected"; + return $"Unexpected: {response.StatusCode}"; + }; + } + + private static byte[] MakeRequest(string request) => Encoding.ASCII.GetBytes(request); +} diff --git a/src/Http11Probe/TestCases/TestCategory.cs b/src/Http11Probe/TestCases/TestCategory.cs index 87e77d7..8e29199 100644 --- a/src/Http11Probe/TestCases/TestCategory.cs +++ b/src/Http11Probe/TestCases/TestCategory.cs @@ -8,5 +8,6 @@ public enum TestCategory ResourceLimits, Injection, Normalization, - Capabilities + Capabilities, + Cookies } diff --git a/src/Servers/ActixServer/src/main.rs b/src/Servers/ActixServer/src/main.rs index d77610f..dd1e439 100644 --- a/src/Servers/ActixServer/src/main.rs +++ b/src/Servers/ActixServer/src/main.rs @@ -8,6 +8,19 @@ async fn echo(req: HttpRequest) -> impl Responder { HttpResponse::Ok().content_type("text/plain").body(body) } +async fn cookie(req: HttpRequest) -> impl Responder { + let mut body = String::new(); + if let Some(raw) = req.headers().get("cookie").and_then(|v| v.to_str().ok()) { + for pair in raw.split(';') { + let trimmed = pair.trim_start(); + if let Some(eq) = trimmed.find('=') { + body.push_str(&format!("{}={}\n", &trimmed[..eq], &trimmed[eq+1..])); + } + } + } + HttpResponse::Ok().content_type("text/plain").body(body) +} + async fn handler(req: HttpRequest, body: web::Bytes) -> HttpResponse { if req.method() == actix_web::http::Method::POST { HttpResponse::Ok() @@ -30,6 +43,7 @@ async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .route("/echo", web::to(echo)) + .route("/cookie", web::to(cookie)) .default_service(web::to(handler)) }) .bind(("0.0.0.0", port))? diff --git a/src/Servers/ApacheServer/Dockerfile b/src/Servers/ApacheServer/Dockerfile index c9a8394..6699e78 100644 --- a/src/Servers/ApacheServer/Dockerfile +++ b/src/Servers/ApacheServer/Dockerfile @@ -3,4 +3,5 @@ FROM httpd:2.4 COPY src/Servers/ApacheServer/httpd-probe.conf /usr/local/apache2/conf/httpd.conf RUN echo "OK" > /usr/local/apache2/htdocs/index.html COPY src/Servers/ApacheServer/echo.cgi /usr/local/apache2/cgi-bin/echo.cgi -RUN chmod +x /usr/local/apache2/cgi-bin/echo.cgi +COPY src/Servers/ApacheServer/cookie.cgi /usr/local/apache2/cgi-bin/cookie.cgi +RUN chmod +x /usr/local/apache2/cgi-bin/echo.cgi /usr/local/apache2/cgi-bin/cookie.cgi diff --git a/src/Servers/ApacheServer/cookie.cgi b/src/Servers/ApacheServer/cookie.cgi new file mode 100755 index 0000000..827ab97 --- /dev/null +++ b/src/Servers/ApacheServer/cookie.cgi @@ -0,0 +1,8 @@ +#!/bin/sh +printf 'Content-Type: text/plain\r\n\r\n' +if [ -n "$HTTP_COOKIE" ]; then + echo "$HTTP_COOKIE" | tr ';' '\n' | while read -r pair; do + trimmed=$(echo "$pair" | sed 's/^ *//') + printf '%s\n' "$trimmed" + done +fi diff --git a/src/Servers/ApacheServer/httpd-probe.conf b/src/Servers/ApacheServer/httpd-probe.conf index e2c3401..7a9b30f 100644 --- a/src/Servers/ApacheServer/httpd-probe.conf +++ b/src/Servers/ApacheServer/httpd-probe.conf @@ -18,6 +18,7 @@ DocumentRoot "/usr/local/apache2/htdocs" ScriptAlias /echo /usr/local/apache2/cgi-bin/echo.cgi +ScriptAlias /cookie /usr/local/apache2/cgi-bin/cookie.cgi Require all granted diff --git a/src/Servers/AspNetMinimal/Program.cs b/src/Servers/AspNetMinimal/Program.cs index b0c5804..13b8220 100644 --- a/src/Servers/AspNetMinimal/Program.cs +++ b/src/Servers/AspNetMinimal/Program.cs @@ -30,4 +30,12 @@ return Results.Text(sb.ToString()); }); +app.Map("/cookie", (HttpContext ctx) => +{ + var sb = new System.Text.StringBuilder(); + foreach (var cookie in ctx.Request.Cookies) + sb.AppendLine($"{cookie.Key}={cookie.Value}"); + return Results.Text(sb.ToString()); +}); + app.Run(); diff --git a/src/Servers/BunServer/server.ts b/src/Servers/BunServer/server.ts index 5cc9dd4..6973411 100644 --- a/src/Servers/BunServer/server.ts +++ b/src/Servers/BunServer/server.ts @@ -12,6 +12,16 @@ Bun.serve({ } return new Response(body, { headers: { "Content-Type": "text/plain" } }); } + if (url.pathname === "/cookie") { + let body = ""; + const raw = req.headers.get("cookie") || ""; + for (const pair of raw.split(";")) { + const trimmed = pair.trimStart(); + const eq = trimmed.indexOf("="); + if (eq > 0) body += trimmed.substring(0, eq) + "=" + trimmed.substring(eq + 1) + "\n"; + } + return new Response(body, { headers: { "Content-Type": "text/plain" } }); + } if (req.method === "POST") { const body = await req.text(); return new Response(body); diff --git a/src/Servers/CaddyServer/Caddyfile b/src/Servers/CaddyServer/Caddyfile index 94eaab5..b7113b6 100644 --- a/src/Servers/CaddyServer/Caddyfile +++ b/src/Servers/CaddyServer/Caddyfile @@ -20,5 +20,14 @@ file_server } + handle /cookie { + root * /srv + templates { + mime text/plain + } + rewrite * /cookie.html + file_server + } + respond "OK" 200 } diff --git a/src/Servers/CaddyServer/Dockerfile b/src/Servers/CaddyServer/Dockerfile index 729486b..f2fa5ae 100644 --- a/src/Servers/CaddyServer/Dockerfile +++ b/src/Servers/CaddyServer/Dockerfile @@ -1,3 +1,4 @@ FROM caddy:2 COPY src/Servers/CaddyServer/Caddyfile /etc/caddy/Caddyfile COPY src/Servers/CaddyServer/echo.html /srv/echo.html +COPY src/Servers/CaddyServer/cookie.html /srv/cookie.html diff --git a/src/Servers/CaddyServer/cookie.html b/src/Servers/CaddyServer/cookie.html new file mode 100644 index 0000000..12cab4f --- /dev/null +++ b/src/Servers/CaddyServer/cookie.html @@ -0,0 +1,2 @@ +{{range .Req.Header.Cookie}}{{range splitList ";" .}}{{trim .}} +{{end}}{{end}} \ No newline at end of file diff --git a/src/Servers/DenoServer/server.ts b/src/Servers/DenoServer/server.ts index d87e1b1..d90b714 100644 --- a/src/Servers/DenoServer/server.ts +++ b/src/Servers/DenoServer/server.ts @@ -7,6 +7,16 @@ Deno.serve({ port: 8080, hostname: "0.0.0.0" }, async (req) => { } return new Response(body, { headers: { "content-type": "text/plain" } }); } + if (url.pathname === "/cookie") { + let body = ""; + const raw = req.headers.get("cookie") || ""; + for (const pair of raw.split(";")) { + const trimmed = pair.trimStart(); + const eq = trimmed.indexOf("="); + if (eq > 0) body += trimmed.substring(0, eq) + "=" + trimmed.substring(eq + 1) + "\n"; + } + return new Response(body, { headers: { "content-type": "text/plain" } }); + } if (req.method === "POST") { const body = await req.text(); return new Response(body, { headers: { "content-type": "text/plain" } }); diff --git a/src/Servers/EmbedIOServer/Program.cs b/src/Servers/EmbedIOServer/Program.cs index bddeb36..3391458 100644 --- a/src/Servers/EmbedIOServer/Program.cs +++ b/src/Servers/EmbedIOServer/Program.cs @@ -7,6 +7,13 @@ using var server = new WebServer(o => o .WithUrlPrefix(url) .WithMode(HttpListenerMode.EmbedIO)) + .WithModule(new ActionModule("/cookie", HttpVerbs.Any, async ctx => + { + var sb = new System.Text.StringBuilder(); + foreach (System.Net.Cookie cookie in ctx.Request.Cookies) + sb.AppendLine($"{cookie.Name}={cookie.Value}"); + await ctx.SendStringAsync(sb.ToString(), "text/plain", System.Text.Encoding.UTF8); + })) .WithModule(new ActionModule("/echo", HttpVerbs.Any, async ctx => { var sb = new System.Text.StringBuilder(); diff --git a/src/Servers/EnvoyServer/envoy.yaml b/src/Servers/EnvoyServer/envoy.yaml index a1e2f5d..94f2f58 100644 --- a/src/Servers/EnvoyServer/envoy.yaml +++ b/src/Servers/EnvoyServer/envoy.yaml @@ -28,6 +28,19 @@ static_resources: end end request_handle:respond({[":status"] = "200", ["content-type"] = "text/plain"}, body) + elseif path == "/cookie" then + local body = "" + local raw = request_handle:headers():get("cookie") + if raw then + for pair in raw:gmatch("[^;]+") do + local trimmed = pair:match("^%s*(.*)") + local eq = trimmed:find("=") + if eq and eq > 1 then + body = body .. trimmed:sub(1, eq-1) .. "=" .. trimmed:sub(eq+1) .. "\n" + end + end + end + request_handle:respond({[":status"] = "200", ["content-type"] = "text/plain"}, body) end end - name: envoy.filters.http.router diff --git a/src/Servers/ExpressServer/server.js b/src/Servers/ExpressServer/server.js index 6f00d7e..a0b6e5e 100644 --- a/src/Servers/ExpressServer/server.js +++ b/src/Servers/ExpressServer/server.js @@ -13,6 +13,17 @@ app.post("/", (req, res) => { req.on("end", () => res.send(Buffer.concat(chunks))); }); +app.all('/cookie', (req, res) => { + let body = ''; + const raw = req.headers.cookie || ''; + for (const pair of raw.split(';')) { + const trimmed = pair.trimStart(); + const eq = trimmed.indexOf('='); + if (eq > 0) body += trimmed.substring(0, eq) + '=' + trimmed.substring(eq + 1) + '\n'; + } + res.set('Content-Type', 'text/plain').send(body); +}); + app.all('/echo', (req, res) => { let body = ''; for (const [name, value] of Object.entries(req.headers)) { diff --git a/src/Servers/FastEndpointsServer/Program.cs b/src/Servers/FastEndpointsServer/Program.cs index 6a01f3f..9378f66 100644 --- a/src/Servers/FastEndpointsServer/Program.cs +++ b/src/Servers/FastEndpointsServer/Program.cs @@ -82,6 +82,26 @@ public override async Task HandleAsync(CancellationToken ct) } } +// ── GET/POST /cookie ────────────────────────────────────────── + +sealed class CookieEndpoint : EndpointWithoutRequest +{ + public override void Configure() + { + Verbs("GET", "POST"); + Routes("/cookie"); + AllowAnonymous(); + } + + public override async Task HandleAsync(CancellationToken ct) + { + var sb = new System.Text.StringBuilder(); + foreach (var cookie in HttpContext.Request.Cookies) + sb.AppendLine($"{cookie.Key}={cookie.Value}"); + await HttpContext.Response.WriteAsync(sb.ToString(), ct); + } +} + // ── POST /echo ───────────────────────────────────────────────── sealed class PostEcho : EndpointWithoutRequest diff --git a/src/Servers/FastHttpServer/main.go b/src/Servers/FastHttpServer/main.go index 7b9f9e2..b557285 100644 --- a/src/Servers/FastHttpServer/main.go +++ b/src/Servers/FastHttpServer/main.go @@ -2,6 +2,7 @@ package main import ( "os" + "strings" "github.com/valyala/fasthttp" ) @@ -20,6 +21,15 @@ func main() { ctx.Request.Header.VisitAll(func(key, value []byte) { ctx.WriteString(string(key) + ": " + string(value) + "\n") }) + case "/cookie": + ctx.SetContentType("text/plain") + raw := string(ctx.Request.Header.Peek("Cookie")) + for _, pair := range strings.Split(raw, ";") { + pair = strings.TrimLeft(pair, " ") + if eq := strings.Index(pair, "="); eq > 0 { + ctx.WriteString(pair[:eq] + "=" + pair[eq+1:] + "\n") + } + } default: if string(ctx.Method()) == "POST" { ctx.SetBody(ctx.Request.Body()) diff --git a/src/Servers/FlaskServer/app.py b/src/Servers/FlaskServer/app.py index e98913a..b054e86 100644 --- a/src/Servers/FlaskServer/app.py +++ b/src/Servers/FlaskServer/app.py @@ -4,6 +4,13 @@ app = Flask(__name__) +@app.route('/cookie', methods=['GET','POST','PUT','DELETE','PATCH','OPTIONS','HEAD']) +def cookie_endpoint(): + lines = [] + for name, value in request.cookies.items(): + lines.append(f"{name}={value}") + return '\n'.join(lines) + '\n', 200, {'Content-Type': 'text/plain'} + @app.route('/echo', methods=['GET','POST','PUT','DELETE','PATCH','OPTIONS','HEAD']) def echo(): lines = [] diff --git a/src/Servers/GenHttpServer/GenHttpServer.csproj b/src/Servers/GenHttpServer/GenHttpServer.csproj index 57d2c3c..940edc3 100644 --- a/src/Servers/GenHttpServer/GenHttpServer.csproj +++ b/src/Servers/GenHttpServer/GenHttpServer.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/Servers/GenHttpServer/Program.cs b/src/Servers/GenHttpServer/Program.cs index aafd975..da1737d 100644 --- a/src/Servers/GenHttpServer/Program.cs +++ b/src/Servers/GenHttpServer/Program.cs @@ -8,6 +8,9 @@ var port = (args.Length > 0 && ushort.TryParse(args[0], out var p)) ? p : (ushort)8080; var app = Inline.Create() + .Get("/cookie", (IRequest request) => ParseCookies(request)) + .Post("/cookie", (IRequest request) => ParseCookies(request)) + .Get("/echo", (IRequest request) => Echo(request)) .Post("/echo", (IRequest request) => Echo(request)) .Post((Stream body) => RequestContent(body)) .Any(() => StringContent()); @@ -30,6 +33,18 @@ static string Echo(IRequest request) return headers.ToString(); } +static string ParseCookies(IRequest request) +{ + var sb = new System.Text.StringBuilder(); + + foreach (var cookie in request.Cookies.Values) + { + sb.AppendLine($"{cookie.Name}={cookie.Value}"); + } + + return sb.ToString(); +} + static string StringContent() => "OK"; static Stream RequestContent(Stream body) => body; diff --git a/src/Servers/GinServer/main.go b/src/Servers/GinServer/main.go index 142cae9..4071773 100644 --- a/src/Servers/GinServer/main.go +++ b/src/Servers/GinServer/main.go @@ -16,6 +16,17 @@ func main() { gin.SetMode(gin.ReleaseMode) r := gin.New() + r.Any("/cookie", func(c *gin.Context) { + var sb strings.Builder + raw := c.GetHeader("Cookie") + for _, pair := range strings.Split(raw, ";") { + pair = strings.TrimLeft(pair, " ") + if eq := strings.Index(pair, "="); eq > 0 { + sb.WriteString(pair[:eq] + "=" + pair[eq+1:] + "\n") + } + } + c.Data(200, "text/plain", []byte(sb.String())) + }) r.Any("/echo", func(c *gin.Context) { var sb strings.Builder for name, values := range c.Request.Header { diff --git a/src/Servers/GlyphServer/Program.cs b/src/Servers/GlyphServer/Program.cs index e9ee0f4..f7e16b0 100644 --- a/src/Servers/GlyphServer/Program.cs +++ b/src/Servers/GlyphServer/Program.cs @@ -282,6 +282,24 @@ static byte[] BuildResponse(string method, string path, string? echoBody, List 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"; diff --git a/src/Servers/GunicornServer/app.py b/src/Servers/GunicornServer/app.py index 80e6959..b9feb99 100644 --- a/src/Servers/GunicornServer/app.py +++ b/src/Servers/GunicornServer/app.py @@ -1,6 +1,18 @@ def app(environ, start_response): path = environ.get('PATH_INFO', '/') + if path == '/cookie': + cookie_str = environ.get('HTTP_COOKIE', '') + lines = [] + for pair in cookie_str.split(';'): + pair = pair.strip() + eq = pair.find('=') + if eq > 0: + lines.append(f"{pair[:eq]}={pair[eq+1:]}") + body = ('\n'.join(lines) + '\n').encode('utf-8') if lines else b'' + start_response('200 OK', [('Content-Type', 'text/plain')]) + return [body] + if path == '/echo': lines = [] for key, value in environ.items(): diff --git a/src/Servers/H2OServer/h2o.conf b/src/Servers/H2OServer/h2o.conf index ecc13a5..c38ccd6 100644 --- a/src/Servers/H2OServer/h2o.conf +++ b/src/Servers/H2OServer/h2o.conf @@ -16,6 +16,18 @@ hosts: body += "Content-Type: #{env['CONTENT_TYPE']}\n" if env['CONTENT_TYPE'] && !env['CONTENT_TYPE'].empty? body += "Content-Length: #{env['CONTENT_LENGTH']}\n" if env['CONTENT_LENGTH'] && !env['CONTENT_LENGTH'].empty? [200, {"content-type" => "text/plain"}, [body]] + elsif env["PATH_INFO"] == "/cookie" + body = "" + if env["HTTP_COOKIE"] + env["HTTP_COOKIE"].split(";").each do |pair| + trimmed = pair.lstrip + eq = trimmed.index("=") + if eq && eq > 0 + body += "#{trimmed[0...eq]}=#{trimmed[(eq+1)..]}\n" + end + end + end + [200, {"content-type" => "text/plain"}, [body]] elsif env["REQUEST_METHOD"] == "POST" body = env["rack.input"] ? env["rack.input"].read : "" [200, {"content-type" => "text/plain"}, [body]] diff --git a/src/Servers/HAProxyServer/echo.lua b/src/Servers/HAProxyServer/echo.lua index 05b5e79..509bb58 100644 --- a/src/Servers/HAProxyServer/echo.lua +++ b/src/Servers/HAProxyServer/echo.lua @@ -13,6 +13,27 @@ core.register_service("echo", "http", function(applet) applet:send(body) end) +core.register_service("cookie", "http", function(applet) + local body = "" + local hdrs = applet.headers + if hdrs["cookie"] then + for _, raw in ipairs(hdrs["cookie"]) do + for pair in raw:gmatch("[^;]+") do + local trimmed = pair:match("^%s*(.*)") + local eq = trimmed:find("=") + if eq and eq > 1 then + body = body .. trimmed:sub(1, eq-1) .. "=" .. trimmed:sub(eq+1) .. "\n" + end + end + end + end + applet:set_status(200) + applet:add_header("Content-Type", "text/plain") + applet:add_header("Content-Length", tostring(#body)) + applet:start_response() + applet:send(body) +end) + core.register_service("echo_body", "http", function(applet) local body = applet:receive() if body == nil then body = "" end diff --git a/src/Servers/HAProxyServer/haproxy.cfg b/src/Servers/HAProxyServer/haproxy.cfg index fc01334..1fb1756 100644 --- a/src/Servers/HAProxyServer/haproxy.cfg +++ b/src/Servers/HAProxyServer/haproxy.cfg @@ -11,11 +11,15 @@ defaults frontend http_in bind *:8080 use_backend echo_backend if { path /echo } + use_backend cookie_backend if { path /cookie } use_backend post_echo_backend if { method POST } http-request return status 200 content-type "text/plain" string "OK" backend echo_backend http-request use-service lua.echo +backend cookie_backend + http-request use-service lua.cookie + backend post_echo_backend http-request use-service lua.echo_body diff --git a/src/Servers/HyperServer/src/main.rs b/src/Servers/HyperServer/src/main.rs index 2a596bf..295955a 100644 --- a/src/Servers/HyperServer/src/main.rs +++ b/src/Servers/HyperServer/src/main.rs @@ -21,6 +21,22 @@ async fn handle(req: Request) -> Result collected.to_bytes(), diff --git a/src/Servers/JettyServer/src/main/java/server/Application.java b/src/Servers/JettyServer/src/main/java/server/Application.java index 604f34c..88abfa6 100644 --- a/src/Servers/JettyServer/src/main/java/server/Application.java +++ b/src/Servers/JettyServer/src/main/java/server/Application.java @@ -21,7 +21,22 @@ public boolean handle(Request request, Response response, Callback callback) thr response.setStatus(200); response.getHeaders().put("Content-Type", "text/plain"); - if ("/echo".equals(request.getHttpURI().getPath())) { + if ("/cookie".equals(request.getHttpURI().getPath())) { + StringBuilder sb = new StringBuilder(); + for (HttpField field : request.getHeaders()) { + if ("Cookie".equalsIgnoreCase(field.getName())) { + for (String pair : field.getValue().split(";")) { + String trimmed = pair.stripLeading(); + int eq = trimmed.indexOf('='); + if (eq > 0) { + sb.append(trimmed, 0, eq).append("=").append(trimmed.substring(eq + 1)).append("\n"); + } + } + } + } + byte[] cookieBody = sb.toString().getBytes(StandardCharsets.UTF_8); + response.write(true, ByteBuffer.wrap(cookieBody), callback); + } else if ("/echo".equals(request.getHttpURI().getPath())) { StringBuilder sb = new StringBuilder(); for (HttpField field : request.getHeaders()) { sb.append(field.getName()).append(": ").append(field.getValue()).append("\n"); diff --git a/src/Servers/LighttpdServer/Dockerfile b/src/Servers/LighttpdServer/Dockerfile index f2f97d9..499a3b2 100644 --- a/src/Servers/LighttpdServer/Dockerfile +++ b/src/Servers/LighttpdServer/Dockerfile @@ -3,6 +3,7 @@ RUN apk add --no-cache lighttpd COPY src/Servers/LighttpdServer/lighttpd.conf /etc/lighttpd/lighttpd.conf COPY src/Servers/LighttpdServer/index.cgi /var/www/index.cgi COPY src/Servers/LighttpdServer/echo.cgi /var/www/echo.cgi -RUN chmod +x /var/www/index.cgi /var/www/echo.cgi +COPY src/Servers/LighttpdServer/cookie.cgi /var/www/cookie.cgi +RUN chmod +x /var/www/index.cgi /var/www/echo.cgi /var/www/cookie.cgi EXPOSE 8080 CMD ["lighttpd", "-D", "-f", "/etc/lighttpd/lighttpd.conf"] diff --git a/src/Servers/LighttpdServer/cookie.cgi b/src/Servers/LighttpdServer/cookie.cgi new file mode 100755 index 0000000..827ab97 --- /dev/null +++ b/src/Servers/LighttpdServer/cookie.cgi @@ -0,0 +1,8 @@ +#!/bin/sh +printf 'Content-Type: text/plain\r\n\r\n' +if [ -n "$HTTP_COOKIE" ]; then + echo "$HTTP_COOKIE" | tr ';' '\n' | while read -r pair; do + trimmed=$(echo "$pair" | sed 's/^ *//') + printf '%s\n' "$trimmed" + done +fi diff --git a/src/Servers/LighttpdServer/lighttpd.conf b/src/Servers/LighttpdServer/lighttpd.conf index 5c5ed49..3fd52e7 100644 --- a/src/Servers/LighttpdServer/lighttpd.conf +++ b/src/Servers/LighttpdServer/lighttpd.conf @@ -4,4 +4,4 @@ index-file.names = ("index.cgi") server.modules += ("mod_cgi", "mod_alias") cgi.assign = (".cgi" => "") server.error-handler = "/index.cgi" -alias.url = ("/echo" => "/var/www/echo.cgi") +alias.url = ("/echo" => "/var/www/echo.cgi", "/cookie" => "/var/www/cookie.cgi") diff --git a/src/Servers/NancyServer/Dockerfile b/src/Servers/NancyServer/Dockerfile deleted file mode 100644 index 4894350..0000000 --- a/src/Servers/NancyServer/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build -WORKDIR /src -COPY Directory.Build.props . -COPY src/Servers/NancyServer/ src/Servers/NancyServer/ -RUN dotnet restore src/Servers/NancyServer/NancyServer.csproj -RUN dotnet publish src/Servers/NancyServer/NancyServer.csproj -c Release -o /app --no-restore - -FROM mcr.microsoft.com/dotnet/runtime:8.0 -WORKDIR /app -COPY --from=build /app . -ENTRYPOINT ["dotnet", "NancyServer.dll", "8080"] diff --git a/src/Servers/NancyServer/NancyServer.csproj b/src/Servers/NancyServer/NancyServer.csproj deleted file mode 100644 index baef589..0000000 --- a/src/Servers/NancyServer/NancyServer.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - Exe - net8.0 - - - - - - - - diff --git a/src/Servers/NancyServer/Program.cs b/src/Servers/NancyServer/Program.cs deleted file mode 100644 index 831d378..0000000 --- a/src/Servers/NancyServer/Program.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Nancy; -using Nancy.Hosting.Self; - -var port = args.Length > 0 ? args[0] : "9006"; -var uri = new Uri($"http://0.0.0.0:{port}"); - -var config = new HostConfiguration { RewriteLocalhost = false }; - -using var host = new NancyHost(config, uri); -host.Start(); - -Console.WriteLine($"Nancy listening on {uri}"); - -var waitHandle = new ManualResetEvent(false); -Console.CancelKeyPress += (_, e) => { e.Cancel = true; waitHandle.Set(); }; -waitHandle.WaitOne(); - -public class EchoModule : NancyModule -{ - public EchoModule() : base("/echo") - { - Get("/", _ => EchoHeaders()); - Post("/", _ => EchoHeaders()); - Put("/", _ => EchoHeaders()); - Delete("/", _ => EchoHeaders()); - Patch("/", _ => EchoHeaders()); - } - - private string EchoHeaders() - { - var sb = new System.Text.StringBuilder(); - foreach (var h in Request.Headers) - foreach (var v in h.Value) - sb.AppendLine($"{h.Key}: {v}"); - return sb.ToString(); - } -} - -public class HomeModule : NancyModule -{ - public HomeModule() - { - Get("/{path*}", _ => "OK"); - Get("/", _ => "OK"); - Post("/{path*}", _ => EchoBody()); - Post("/", _ => EchoBody()); - } - - private string EchoBody() - { - using var reader = new System.IO.StreamReader(Request.Body); - return reader.ReadToEnd(); - } -} diff --git a/src/Servers/NetCoreServerFramework/Program.cs b/src/Servers/NetCoreServerFramework/Program.cs index fe2c2a2..fc9791a 100644 --- a/src/Servers/NetCoreServerFramework/Program.cs +++ b/src/Servers/NetCoreServerFramework/Program.cs @@ -31,6 +31,25 @@ protected override void OnReceivedRequest(HttpRequest request) } SendResponseAsync(Response.MakeOkResponse(200).SetBody(sb.ToString())); } + else if (request.Url == "/cookie") + { + var sb = new System.Text.StringBuilder(); + for (int i = 0; i < request.Headers; i++) + { + var (name, value) = request.Header(i); + if (string.Equals(name, "Cookie", StringComparison.OrdinalIgnoreCase)) + { + foreach (var pair in value.Split(';')) + { + var trimmed = pair.TrimStart(); + var eqIdx = trimmed.IndexOf('='); + if (eqIdx > 0) + sb.AppendLine($"{trimmed[..eqIdx]}={trimmed[(eqIdx + 1)..]}"); + } + } + } + SendResponseAsync(Response.MakeOkResponse(200).SetBody(sb.ToString())); + } else if (request.Method == "POST" && request.Body.Length > 0) SendResponseAsync(Response.MakeOkResponse(200).SetBody(request.Body)); else diff --git a/src/Servers/NginxServer/echo.js b/src/Servers/NginxServer/echo.js index 8d1606b..58b19f4 100644 --- a/src/Servers/NginxServer/echo.js +++ b/src/Servers/NginxServer/echo.js @@ -7,6 +7,22 @@ function echo(r) { r.return(200, body); } +function cookie(r) { + var body = ''; + var raw = r.headersIn['Cookie']; + if (raw) { + var pairs = raw.split(';'); + for (var i = 0; i < pairs.length; i++) { + var trimmed = pairs[i].replace(/^\s+/, ''); + var eq = trimmed.indexOf('='); + if (eq > 0) { + body += trimmed.substring(0, eq) + '=' + trimmed.substring(eq + 1) + '\n'; + } + } + } + r.return(200, body); +} + function handler(r) { if (r.method === 'POST') { r.return(200, r.requestText || ''); @@ -15,4 +31,4 @@ function handler(r) { } } -export default { echo, handler }; +export default { echo, cookie, handler }; diff --git a/src/Servers/NginxServer/nginx.conf b/src/Servers/NginxServer/nginx.conf index ce836c5..97b9157 100644 --- a/src/Servers/NginxServer/nginx.conf +++ b/src/Servers/NginxServer/nginx.conf @@ -26,6 +26,10 @@ http { js_content echo.echo; } + location /cookie { + js_content echo.cookie; + } + location / { js_content echo.handler; } diff --git a/src/Servers/NodeServer/server.js b/src/Servers/NodeServer/server.js index 1526756..d4720af 100644 --- a/src/Servers/NodeServer/server.js +++ b/src/Servers/NodeServer/server.js @@ -9,7 +9,17 @@ const server = http.createServer((req, res) => { } catch { pathname = req.url; } - if (pathname === '/echo') { + if (pathname === '/cookie') { + let body = ''; + const raw = req.headers.cookie || ''; + for (const pair of raw.split(';')) { + const trimmed = pair.trimStart(); + const eq = trimmed.indexOf('='); + if (eq > 0) body += trimmed.substring(0, eq) + '=' + trimmed.substring(eq + 1) + '\n'; + } + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end(body); + } else if (pathname === '/echo') { let body = ''; for (const [name, value] of Object.entries(req.headers)) { if (Array.isArray(value)) value.forEach(v => body += name + ': ' + v + '\n'); diff --git a/src/Servers/NtexServer/src/main.rs b/src/Servers/NtexServer/src/main.rs index 96c5074..d440732 100644 --- a/src/Servers/NtexServer/src/main.rs +++ b/src/Servers/NtexServer/src/main.rs @@ -9,6 +9,19 @@ async fn echo(req: web::HttpRequest) -> impl web::Responder { web::HttpResponse::Ok().content_type("text/plain").body(body) } +async fn cookie(req: web::HttpRequest) -> impl web::Responder { + let mut body = String::new(); + if let Some(raw) = req.headers().get("cookie").and_then(|v| v.to_str().ok()) { + for pair in raw.split(';') { + let trimmed = pair.trim_start(); + if let Some(eq) = trimmed.find('=') { + body.push_str(&format!("{}={}\n", &trimmed[..eq], &trimmed[eq+1..])); + } + } + } + web::HttpResponse::Ok().content_type("text/plain").body(body) +} + async fn handler(req: web::HttpRequest, body: Bytes) -> web::HttpResponse { if req.method() == ntex::http::Method::POST { web::HttpResponse::Ok() @@ -31,6 +44,7 @@ async fn main() -> std::io::Result<()> { web::server(|| { web::App::new() .route("/echo", web::to(echo)) + .route("/cookie", web::to(cookie)) .default_service(web::to(handler)) }) .bind(("0.0.0.0", port))? diff --git a/src/Servers/PhpServer/index.php b/src/Servers/PhpServer/index.php index 883666a..9d18334 100644 --- a/src/Servers/PhpServer/index.php +++ b/src/Servers/PhpServer/index.php @@ -7,6 +7,14 @@ exit; } +if ($_SERVER['REQUEST_URI'] === '/cookie') { + header('Content-Type: text/plain'); + foreach ($_COOKIE as $name => $value) { + echo "$name=$value\n"; + } + exit; +} + header('Content-Type: text/plain'); if ($_SERVER['REQUEST_METHOD'] === 'POST') { echo file_get_contents('php://input'); diff --git a/src/Servers/PingoraServer/src/main.rs b/src/Servers/PingoraServer/src/main.rs index 54edf0c..62193f0 100644 --- a/src/Servers/PingoraServer/src/main.rs +++ b/src/Servers/PingoraServer/src/main.rs @@ -17,6 +17,30 @@ impl ProxyHttp for OkProxy { session: &mut Session, _ctx: &mut Self::CTX, ) -> Result { + let is_cookie = session.req_header().uri.path() == "/cookie"; + if is_cookie { + let mut body_str = String::new(); + if let Some(raw) = session.req_header().headers.get("cookie").and_then(|v| v.to_str().ok()) { + for pair in raw.split(';') { + let trimmed = pair.trim_start(); + if let Some(eq) = trimmed.find('=') { + body_str.push_str(&format!("{}={}\n", &trimmed[..eq], &trimmed[eq+1..])); + } + } + } + let body = Bytes::from(body_str); + let mut header = ResponseHeader::build(200, None)?; + header.insert_header("Content-Type", "text/plain")?; + header.insert_header("Content-Length", &body.len().to_string())?; + session + .write_response_header(Box::new(header), false) + .await?; + session + .write_response_body(Some(body), true) + .await?; + return Ok(true); + } + let is_echo = session.req_header().uri.path() == "/echo"; if is_echo { let mut body_str = String::new(); diff --git a/src/Servers/PumaServer/config.ru b/src/Servers/PumaServer/config.ru index 08fd7bb..0afc58c 100644 --- a/src/Servers/PumaServer/config.ru +++ b/src/Servers/PumaServer/config.ru @@ -5,6 +5,18 @@ app = proc { |env| body += "Content-Type: #{env['CONTENT_TYPE']}\n" if env['CONTENT_TYPE'] body += "Content-Length: #{env['CONTENT_LENGTH']}\n" if env['CONTENT_LENGTH'] [200, { 'Content-Type' => 'text/plain' }, [body]] + elsif env['PATH_INFO'] == '/cookie' + body = "" + if env['HTTP_COOKIE'] + env['HTTP_COOKIE'].split(';').each do |pair| + trimmed = pair.lstrip + eq = trimmed.index('=') + if eq && eq > 0 + body += "#{trimmed[0...eq]}=#{trimmed[(eq+1)..]}\n" + end + end + end + [200, { 'Content-Type' => 'text/plain' }, [body]] elsif env['REQUEST_METHOD'] == 'POST' body = env['rack.input'].read [200, { 'content-type' => 'text/plain' }, [body]] diff --git a/src/Servers/QuarkusServer/src/main/java/server/Application.java b/src/Servers/QuarkusServer/src/main/java/server/Application.java index f630de9..138747d 100644 --- a/src/Servers/QuarkusServer/src/main/java/server/Application.java +++ b/src/Servers/QuarkusServer/src/main/java/server/Application.java @@ -31,6 +31,20 @@ public byte[] catchAllPost(InputStream body) throws IOException { return body.readAllBytes(); } + @GET + @Path("/cookie") + @Produces(MediaType.TEXT_PLAIN) + public Response cookieGet(@Context HttpHeaders headers) { + return parseCookies(headers); + } + + @POST + @Path("/cookie") + @Produces(MediaType.TEXT_PLAIN) + public Response cookiePost(@Context HttpHeaders headers) { + return parseCookies(headers); + } + @GET @Path("/echo") @Produces(MediaType.TEXT_PLAIN) @@ -45,6 +59,23 @@ public Response echoPost(@Context HttpHeaders headers) { return echoHeaders(headers); } + private Response parseCookies(HttpHeaders headers) { + StringBuilder sb = new StringBuilder(); + List cookieHeaders = headers.getRequestHeader("Cookie"); + if (cookieHeaders != null) { + for (String raw : cookieHeaders) { + for (String pair : raw.split(";")) { + String trimmed = pair.stripLeading(); + int eq = trimmed.indexOf('='); + if (eq > 0) { + sb.append(trimmed, 0, eq).append("=").append(trimmed.substring(eq + 1)).append("\n"); + } + } + } + } + return Response.ok(sb.toString(), MediaType.TEXT_PLAIN).build(); + } + private Response echoHeaders(HttpHeaders headers) { StringBuilder sb = new StringBuilder(); for (Map.Entry> entry : headers.getRequestHeaders().entrySet()) { diff --git a/src/Servers/ServiceStackServer/Program.cs b/src/Servers/ServiceStackServer/Program.cs index 9038620..62d37ee 100644 --- a/src/Servers/ServiceStackServer/Program.cs +++ b/src/Servers/ServiceStackServer/Program.cs @@ -12,6 +12,13 @@ sb.AppendLine($"{h.Key}: {v}"); return Results.Text(sb.ToString()); }); +app.Map("/cookie", (HttpContext ctx) => +{ + var sb = new System.Text.StringBuilder(); + foreach (var cookie in ctx.Request.Cookies) + sb.AppendLine($"{cookie.Key}={cookie.Value}"); + return Results.Text(sb.ToString()); +}); app.MapFallback(async (HttpContext ctx) => { if (ctx.Request.Method == "POST") diff --git a/src/Servers/SimpleWServer/Program.cs b/src/Servers/SimpleWServer/Program.cs index a6d72e4..80e8340 100644 --- a/src/Servers/SimpleWServer/Program.cs +++ b/src/Servers/SimpleWServer/Program.cs @@ -6,6 +6,8 @@ var server = new SimpleWServer(IPAddress.Any, port); +server.MapGet("/cookie", (HttpSession session) => ParseCookies(session)); +server.MapPost("/cookie", (HttpSession session) => ParseCookies(session)); server.MapGet("/echo", (HttpSession session) => { var sb = new System.Text.StringBuilder(); @@ -25,6 +27,25 @@ server.MapPost("/", (HttpSession session) => session.Request.BodyString); server.MapPost("/{path}", (HttpSession session) => session.Request.BodyString); +static string ParseCookies(HttpSession session) +{ + var sb = new System.Text.StringBuilder(); + foreach (var h in session.Request.Headers.EnumerateAll()) + { + 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 sb.ToString(); +} + Console.WriteLine($"SimpleW listening on http://localhost:{port}"); await server.RunAsync(); diff --git a/src/Servers/SiskServer/Program.cs b/src/Servers/SiskServer/Program.cs index 87902c9..bc50dd6 100644 --- a/src/Servers/SiskServer/Program.cs +++ b/src/Servers/SiskServer/Program.cs @@ -17,6 +17,27 @@ sb.AppendLine($"{h.Key}: {val}"); return new HttpResponse(200).WithContent(sb.ToString()); } + if (request.Path == "/cookie") + { + var sb = new System.Text.StringBuilder(); + foreach (var h in request.Headers) + { + if (string.Equals(h.Key, "Cookie", StringComparison.OrdinalIgnoreCase)) + { + foreach (var rawVal in h.Value) + { + foreach (var pair in rawVal.Split(';')) + { + var trimmed = pair.TrimStart(); + var eqIdx = trimmed.IndexOf('='); + if (eqIdx > 0) + sb.AppendLine($"{trimmed[..eqIdx]}={trimmed[(eqIdx + 1)..]}"); + } + } + } + } + return new HttpResponse(200).WithContent(sb.ToString()); + } if (request.Method == HttpMethod.Post && request.Body is not null) { var body = request.Body; diff --git a/src/Servers/SpringBootServer/src/main/java/server/Application.java b/src/Servers/SpringBootServer/src/main/java/server/Application.java index 9903cc0..77cad06 100644 --- a/src/Servers/SpringBootServer/src/main/java/server/Application.java +++ b/src/Servers/SpringBootServer/src/main/java/server/Application.java @@ -31,6 +31,18 @@ public byte[] indexPost(HttpServletRequest request) throws IOException { return request.getInputStream().readAllBytes(); } + @RequestMapping("/cookie") + public ResponseEntity cookieEndpoint(HttpServletRequest request) { + StringBuilder sb = new StringBuilder(); + jakarta.servlet.http.Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (jakarta.servlet.http.Cookie c : cookies) { + sb.append(c.getName()).append("=").append(c.getValue()).append("\n"); + } + } + return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(sb.toString()); + } + @RequestMapping("/echo") public ResponseEntity echo(HttpServletRequest request) { StringBuilder sb = new StringBuilder(); diff --git a/src/Servers/TomcatServer/webapp/WEB-INF/web.xml b/src/Servers/TomcatServer/webapp/WEB-INF/web.xml index 76b6127..fff22c3 100644 --- a/src/Servers/TomcatServer/webapp/WEB-INF/web.xml +++ b/src/Servers/TomcatServer/webapp/WEB-INF/web.xml @@ -9,6 +9,15 @@ /echo + + cookie + /cookie.jsp + + + cookie + /cookie + + ok /ok.jsp diff --git a/src/Servers/TomcatServer/webapp/cookie.jsp b/src/Servers/TomcatServer/webapp/cookie.jsp new file mode 100644 index 0000000..87b1ce2 --- /dev/null +++ b/src/Servers/TomcatServer/webapp/cookie.jsp @@ -0,0 +1,8 @@ +<%@page contentType="text/plain"%><% +jakarta.servlet.http.Cookie[] cookies = request.getCookies(); +if (cookies != null) { + for (jakarta.servlet.http.Cookie c : cookies) { + out.print(c.getName() + "=" + c.getValue() + "\n"); + } +} +%> \ No newline at end of file diff --git a/src/Servers/TraefikServer/dynamic.yml b/src/Servers/TraefikServer/dynamic.yml index 6b25ace..4a9ac40 100644 --- a/src/Servers/TraefikServer/dynamic.yml +++ b/src/Servers/TraefikServer/dynamic.yml @@ -6,6 +6,12 @@ http: - web service: echo-svc + cookie: + rule: "Path(`/cookie`)" + entryPoints: + - web + service: echo-svc + catchall: rule: "PathPrefix(`/`)" entryPoints: diff --git a/src/Servers/TraefikServer/echo/main.go b/src/Servers/TraefikServer/echo/main.go index 9a930e2..2e41f64 100644 --- a/src/Servers/TraefikServer/echo/main.go +++ b/src/Servers/TraefikServer/echo/main.go @@ -3,9 +3,21 @@ package main import ( "io" "net/http" + "strings" ) func main() { + http.HandleFunc("/cookie", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + raw := r.Header.Get("Cookie") + for _, pair := range strings.Split(raw, ";") { + pair = strings.TrimLeft(pair, " ") + if eq := strings.Index(pair, "="); eq > 0 { + w.Write([]byte(pair[:eq] + "=" + pair[eq+1:] + "\n")) + } + } + }) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) diff --git a/src/Servers/UvicornServer/app.py b/src/Servers/UvicornServer/app.py index f964cfc..b1a7b5c 100644 --- a/src/Servers/UvicornServer/app.py +++ b/src/Servers/UvicornServer/app.py @@ -1,6 +1,30 @@ async def app(scope, receive, send): path = scope.get('path', '/') + if path == '/cookie': + cookie_val = '' + for name, value in scope.get('headers', []): + if name.lower() == b'cookie': + cookie_val = value.decode('latin-1') + break + lines = [] + for pair in cookie_val.split(';'): + pair = pair.strip() + eq = pair.find('=') + if eq > 0: + lines.append(f"{pair[:eq]}={pair[eq+1:]}") + body = ('\n'.join(lines) + '\n').encode('utf-8') if lines else b'' + await send({ + 'type': 'http.response.start', + 'status': 200, + 'headers': [(b'content-type', b'text/plain')], + }) + await send({ + 'type': 'http.response.body', + 'body': body, + }) + return + if path == '/echo': lines = [] for name, value in scope.get('headers', []):