=encoding utf8

=head1 NAME

PAGI::Server::Compliance - HTTP/1.1, HTTP/2, WebSocket, and Security Compliance Documentation

=head1 DESCRIPTION

This document details the compliance testing results for PAGI::Server against
HTTP/1.1 (RFC 7230/7231), HTTP/2 (RFC 9113), WebSocket (RFC 6455), and common
security attack vectors.

PAGI::Server demonstrates full HTTP/1.1 compliance, 93.8% HTTP/2 conformance
(matching the nghttp2 library ceiling), strong security posture, stable resource
management, and 71% WebSocket RFC 6455 compliance.

=head1 TEST ENVIRONMENT

=over 4

=item * B<Server>: PAGI::Server with IO::Async and EV backend

=item * B<Platform>: macOS Darwin / Linux

=item * B<Event Loop>: EV with kqueue (macOS) or epoll (Linux)

=item * B<Test Date>: February 2026

=back

=head1 RESULTS SUMMARY

    +---------------------------+-------+--------+--------+
    | Category                  | Tests | Passed | Failed |
    +---------------------------+-------+--------+--------+
    | HTTP/1.1 Compliance       |    10 |     10 |      0 |
    | HTTP/2 (h2spec)           |   146 |    137 |      9 |
    | Slow HTTP Attacks         |     4 |      4 |      0 |
    | Concurrent Attack+Traffic |     1 |      1 |      0 |
    | Request Smuggling         |     6 |      6 |      0 |
    | nikto Scanner             |     4 |      4 |      0 |
    | Protocol Fuzzing          |    49 |     49 |      0 |
    | Memory/Resource Leaks     |     4 |      4 |      0 |
    | WebSocket (Autobahn)      |   301 |    215 |     86 |
    +---------------------------+-------+--------+--------+

=head1 PAGI SPECIFICATION SUPPORT

Beyond protocol conformance, PAGI::Server implements the optional capabilities
defined in L<PAGI::Spec::Www>, across both HTTP/1.1 and HTTP/2:

=over 4

=item * B<Transport flow control> (C<pagi.transport>) -- C<buffered_amount>,
high/low watermarks, and C<on_high_water>/C<on_drain> backpressure callbacks. On
HTTP/1.1 for http/websocket/sse; on HTTP/2 for http streaming and sse. B<Not yet>
on WebSocket-over-HTTP/2 (see L</"Transport Flow Control (pagi.transport)">).

=item * B<Connection state> (C<pagi.connection>) for HTTP scopes --
C<is_connected>, C<disconnect_reason>, C<on_disconnect> (abnormal only),
C<on_complete> (success only), and C<disconnect_future>.

=item * B<WebSocket Denial Response> (the C<websocket.http.response> extension)
-- reject a handshake with a custom HTTP response instead of a bare C<403>, on
both HTTP/1.1 and HTTP/2.

=item * B<WebSocket over HTTP/2> (RFC 8441 Extended CONNECT).

=item * B<TLS introspection> (the C<tls> extension) -- certificates, negotiated
version, and cipher suite. The server negotiates TLS 1.3 by default
(C<min_version> is a floor, not a pin).

=item * B<Server-Sent Events> over all HTTP methods, with keepalive.

=back

In development mode the server validates outbound events against the spec and
rejects malformed ones.

=head1 HTTP/1.1 COMPLIANCE

PAGI::Server is fully compliant with RFC 7230 (HTTP/1.1 Message Syntax and Routing)
and RFC 7231 (HTTP/1.1 Semantics and Content).

=head2 Compliance Tests

    +------------------------------+----------+--------+--------+
    | Test                         | Expected | Actual | Status |
    +------------------------------+----------+--------+--------+
    | Normal request               |      200 |    200 | PASS   |
    | HTTP/1.1 missing Host        |      400 |    400 | PASS   |
    | HTTP/1.0 no Host             |      200 |    200 | PASS   |
    | Content-Length: abc          |      400 |    400 | PASS   |
    | Content-Length: -1           |      400 |    400 | PASS   |
    | Content-Length: overflow     |      413 |    413 | PASS   |
    | Content-Length: with spaces  |      400 |    400 | PASS   |
    | URI > 8KB                    |      414 |    414 | PASS   |
    | Header > 8KB                 |      431 |    431 | PASS   |
    | CL+TE conflict               |      400 |    400 | PASS   |
    +------------------------------+----------+--------+--------+

=head2 RFC 7230 Section 5.4 - Host Header

HTTP/1.1 requests without a Host header correctly return 400 Bad Request.
HTTP/1.0 requests without Host are allowed per specification.

=head2 RFC 9112 Section 6.3.3 - Message Body Length

A request carrying both Transfer-Encoding and Content-Length is rejected with
400 Bad Request rather than resolved by precedence — the two are treated as
mutually exclusive. This is the stricter posture permitted by RFC 9112
Section 6.3.3 and closes the CL/TE desync that enables HTTP request smuggling.

=head1 HTTP/2 COMPLIANCE (EXPERIMENTAL)

B<HTTP/2 support in PAGI::Server is experimental.> The API and behavior may
change in future releases.

PAGI::Server provides HTTP/2 support via the nghttp2 C library
(L<Net::HTTP2::nghttp2>). HTTP/2 is enabled with the C<--http2> flag and
supports both TLS (h2 via ALPN) and cleartext (h2c) modes.

Tested against h2spec, the standard HTTP/2 conformance testing tool.

=head2 h2spec Results

    137/146 tests passed (93.8%)

    +-----------------------------+-------+--------+--------+
    | Category                    | Tests | Passed | Failed |
    +-----------------------------+-------+--------+--------+
    | Starting HTTP/2             |     1 |      1 |      0 |
    | Streams and Multiplexing    |     5 |      5 |      0 |
    | DATA frames                 |     3 |      3 |      0 |
    | HEADERS frames              |     3 |      3 |      0 |
    | PRIORITY frames             |     5 |      5 |      0 |
    | RST_STREAM frames           |     1 |      1 |      0 |
    | SETTINGS frames             |     1 |      1 |      0 |
    | PING frames                 |     1 |      1 |      0 |
    | GOAWAY frames               |     1 |      1 |      0 |
    | WINDOW_UPDATE frames        |     2 |      2 |      0 |
    | CONTINUATION frames         |     2 |      2 |      0 |
    | HTTP Message Exchanges      |     4 |      4 |      0 |
    | HPACK                       |    13 |     13 |      0 |
    | Detailed RFC 9113 tests     |    98 |     89 |      9 |
    | HPACK RFC 7541 tests        |     6 |      6 |      0 |
    +-----------------------------+-------+--------+--------+

=head2 Remaining Failures (nghttp2 Library Limitations)

All 9 failures are shared with the bare nghttp2 test server and cannot be
fixed at the PAGI level. These are intentional leniencies in the nghttp2 C
library for interoperability:

    +------------+----------------------------------------------------+
    | Test       | Description                                        |
    +------------+----------------------------------------------------+
    | 5.1 #8     | DATA after RST_STREAM (closed stream)              |
    | 5.1 #9     | HEADERS after RST_STREAM (closed stream)           |
    | 5.1 #11    | DATA on fully closed stream                        |
    | 5.1 #12    | HEADERS on fully closed stream                     |
    | 5.1.1 #2   | Stream ID ordering enforcement                    |
    | 5.3.1 #2   | PRIORITY self-dependency detection                 |
    | 6.3 #1     | PRIORITY frame with stream ID 0                   |
    | 6.9.1 #3   | Window overflow (RST_STREAM vs connection close)   |
    | 6.9.2 #2   | Negative flow control window tracking              |
    +------------+----------------------------------------------------+

=head2 Load Testing (h2load)

    Tool: h2load (nghttp2)
    Protocol: h2 over TLS (ALPN negotiated)

    +--------------+------+----------+-----------+---------+---------+
    | Requests     | Conn | Streams  | Req/sec   | Errors  | Timeout |
    +--------------+------+----------+-----------+---------+---------+
    | 10,000       |   10 |      100 |     8,294 |       0 |       0 |
    | 50,000       |   50 |      100 |     6,823 |       0 |       0 |
    +--------------+------+----------+-----------+---------+---------+

Zero failures across 60,000 total requests under concurrent load.

=head2 nghttp Client Validation

    Tool: nghttp (nghttp2 CLI client)

=over 4

=item * B<GET request>: Clean h2 exchange with ALPN, SETTINGS, HPACK compression

=item * B<POST with body>: DATA frame + END_STREAM handled correctly

=item * B<16 concurrent streams>: All responded 200 on a single connection in 3ms

=back

=head2 HTTP/2 Features

=over 4

=item * ALPN negotiation (h2, http/1.1)

=item * Stream multiplexing (100 concurrent streams default)

=item * HPACK header compression (93.7% space savings observed)

=item * Flow control (per-stream and connection-level)

=item * GOAWAY handling (graceful session shutdown)

=item * Stream state validation (END_STREAM tracking, RST_STREAM on violations)

=item * Extended CONNECT for WebSocket over HTTP/2 (RFC 8441)

=item * Server SETTINGS: configurable max_concurrent_streams, initial_window_size,
max_frame_size, max_header_list_size

=back

=head2 Transport Flow Control (pagi.transport)

PAGI exposes outbound flow-control introspection to applications through the
C<pagi.transport> scope key (see L<PAGI::Spec::Www/"Transport Flow Control">):
C<buffered_amount>, the high/low watermarks, and the
C<on_high_water>/C<on_drain> backpressure callbacks. Over HTTP/2 the handle
measures the B<per-stream> send backlog, so each multiplexed stream is bounded
independently.

PAGI::Server provides C<pagi.transport> for HTTP/2 C<http> (streaming responses)
and C<sse> scopes, matching its HTTP/1.1 coverage of C<http>, C<sse>, and
C<websocket>.

B<Known gap:> WebSocket over HTTP/2 (RFC 8441) does B<not> yet provide
C<pagi.transport>. The HTTP/2 WebSocket send path hands each frame to nghttp2
through its push-style C<submit_data> entry point, which keeps no per-stream
send queue for the handle to measure, and the binding exposes no per-stream
buffered-byte API. Closing the gap means converting the HTTP/2 WebSocket send
path to the same pull-based data-provider model that HTTP/2 streaming and SSE
use. Until then a WebSocket application running over HTTP/2 sees
C<pagi.transport> as absent: the L<PAGI::WebSocket> helpers degrade quietly
(C<buffered_amount> reports C<0>, C<is_writable> stays true, and the callbacks
are no-ops). WebSocket over HTTP/1.1 is unaffected and provides the handle
normally.

The long-term goal is parity: C<pagi.transport> for every streaming scope type
over every transport, so an application need not know whether it runs over
HTTP/1.1 or HTTP/2.

=head2 Enabling HTTP/2

    # TLS mode (h2 via ALPN)
    pagi-server --http2 --ssl-cert cert.pem --ssl-key key.pem --app myapp.pl

    # Cleartext mode (h2c)
    pagi-server --http2 --app myapp.pl

Requires L<Net::HTTP2::nghttp2> (XS bindings for the nghttp2 C library).

=head1 SECURITY TESTING

=head2 Slow HTTP Attack Resistance

PAGI::Server's async architecture provides natural resistance to slow HTTP attacks.
Unlike pre-fork servers (Apache, Starman), slow connections don't consume worker
processes.

=head3 Slowloris Attack (Slow Headers)

    Test: 500 connections sending headers very slowly (10 second intervals)
    Duration: 30 seconds
    Result: Service remained fully available
    Status: PASS

=head3 Slow POST Attack (Slow Body)

    Test: 500 connections sending POST body very slowly
    Duration: 30 seconds
    Result: Server actively closed slow connections
    Status: PASS

=head3 Slow Read Attack

    Test: 500 connections reading responses very slowly (32 bytes per 5 seconds)
    Duration: 30 seconds
    Result: Service remained fully available
    Status: PASS

=head3 Concurrent Attack + Normal Traffic

    Test: Slowloris attack while serving normal traffic
    Attack: 500 slow connections
    Normal traffic results:
      - Requests/sec: 3,843
      - Total requests: 76,963
      - Errors: 0
      - p99 latency: 33ms
    Status: PASS

=head2 HTTP Request Smuggling

PAGI::Server is B<NOT VULNERABLE> to HTTP Request Smuggling attacks.

=head3 CL.TE and TE.CL Attack Vectors

When a request presents both Transfer-Encoding and Content-Length, the two are
treated as mutually exclusive per RFC 9112 Section 6.3.3 and the request is
rejected with 400 Bad Request, so neither header can be used to smuggle data
past the other.

=head3 Transfer-Encoding Obfuscation

All obfuscation techniques are handled safely:

    +-------------------------+-------------------+
    | Variant                 | Result            |
    +-------------------------+-------------------+
    | Normal chunked          | TE recognized     |
    | CHUNKED (uppercase)     | TE recognized     |
    | Space before colon      | TE recognized     |
    | Tab after colon         | TE recognized     |
    | Lowercase header        | TE recognized     |
    | Line folding (obs-fold) | TE recognized     |
    | Null byte in value      | 400 Rejected      |
    | Vertical tab            | 400 Rejected      |
    | Form feed               | 400 Rejected      |
    +-------------------------+-------------------+

=head3 Duplicate Content-Length Headers

    +----------------------------------+---------------+
    | Test                             | Result        |
    +----------------------------------+---------------+
    | Duplicate CL (same values)       | 400 Rejected  |
    | Duplicate CL (different values)  | 400 Rejected  |
    | CL with leading zeros            | 200 Accepted  |
    | CL with + sign                   | 400 Rejected  |
    +----------------------------------+---------------+

=head2 HTTP/2 Rapid Reset (CVE-2023-44487)

PAGI-Server defends the HTTP/2 Rapid Reset attack — a peer creating and
immediately resetting streams to bypass C<SETTINGS_MAX_CONCURRENT_STREAMS> and
exhaust resources — via a token-bucket rate limit on incoming C<RST_STREAM>
frames (nghttp2's mitigation, surfaced as C<h2_rst_rate_limit>). Default budget
is burst 1000 / rate 33 per second; on exhaustion the server sends GOAWAY and
drops the connection. This is on by default and requires nghttp2 >= 1.57.
C<h2_max_concurrent_streams> alone does I<not> stop Rapid Reset (reset streams
do not count as concurrent).

=head2 Protocol Fuzzing

49/49 fuzzing tests passed. Server never crashed or became unresponsive.

=head3 Malformed Request Lines

Empty requests, missing path/version, invalid HTTP version, null bytes in
method/path all correctly return 400 Bad Request.

=head3 Malformed Headers

Headers without colon, empty header names, null bytes in headers all correctly
return 400 Bad Request. Excessive headers (>100) return 431.

=head3 Extreme Sizes

    +----------------------+-------------------------+
    | Test                 | Result                  |
    +----------------------+-------------------------+
    | 1MB URI              | Timeout (protected)     |
    | 1MB header value     | Timeout (protected)     |
    | 100KB single header  | 431 Header Too Large    |
    +----------------------+-------------------------+

=head3 Binary/Random Data

Random bytes, all nulls, and mixed binary + HTTP data are handled safely
without crashes.

=head2 nikto Security Scanner

No critical or high-severity vulnerabilities found. Security headers
(X-Frame-Options, X-Content-Type-Options) are application-level responsibilities
and can be added using the L<PAGI::Middleware::SecurityHeaders> middleware.

=head1 RESOURCE MANAGEMENT

=head2 Memory Usage Under Sustained Load

    +-----------+------------+--------+-------------+
    | Requests  | RSS (KB)   | Growth | Growth/100K |
    +-----------+------------+--------+-------------+
    | Initial   |     28,392 |      - |           - |
    | 100K      |     30,048 | +1,656 |      +1,656 |
    | 200K      |     31,048 | +2,656 |      +1,000 |
    | 500K      |     32,736 | +4,344 |        +563 |
    | 1,000K    |     34,944 | +6,552 |        +440 |
    +-----------+------------+--------+-------------+

Memory growth of ~6.5 MB over 1 million requests (~6.5 bytes/request) is normal
Perl memory allocator behavior, not a leak. Growth rate decreases over time.

=head2 File Descriptor Management

    +--------------------------------+--------------------+
    | Phase                          | File Descriptors   |
    +--------------------------------+--------------------+
    | Initial (idle)                 |                 35 |
    | During load (100 concurrent)   |                135 |
    | After load (idle)              |                 35 |
    | After 1M requests              |                 35 |
    +--------------------------------+--------------------+

File descriptors return to baseline after connections close.

=head2 Connection Object Cleanup

WebSocket frame parsers, connection objects after exceptions, and connections
after partial responses are all properly cleaned up.

=head1 WEBSOCKET COMPLIANCE (RFC 6455)

Tested against the Autobahn Testsuite v25.10.1, the industry-standard WebSocket
conformance test suite.

=head2 Results Summary

    +---------------+-------+--------------------------------+
    | Behavior      | Count | Description                    |
    +---------------+-------+--------------------------------+
    | OK            |   202 | Passed                         |
    | NON-STRICT    |    10 | Minor deviations, acceptable   |
    | INFORMATIONAL |     3 | Informational only             |
    | FAILED        |    86 | Failed tests                   |
    | UNIMPLEMENTED |   216 | Compression (intentional)      |
    +---------------+-------+--------------------------------+

Pass rate (excluding compression): B<215/301 (71%)>

=head2 Results by Category

    +----------+----------------------+----+--------+---------------------------+
    | Category | Description          | OK | Failed | Notes                     |
    +----------+----------------------+----+--------+---------------------------+
    |        1 | Text/Binary Messages | 16 |      0 | All pass                  |
    |        2 | Ping/Pong            | 11 |      0 | Control frame size OK     |
    |        3 | Reserved Bits        |  7 |      0 | RSV bits validated        |
    |        4 | Opcodes              | 10 |      0 | Reserved opcodes rejected |
    |        5 | Fragmentation        | 14 |      6 | Edge cases                |
    |        6 | UTF-8 Handling       | 99 |     42 | Streaming validation      |
    |        7 | Close Handling       | 30 |      1 | Close codes validated     |
    |        9 | Limits/Performance   | 14 |     37 | Large messages (>64KB)    |
    |       10 | Misc                 |  1 |      0 | All pass                  |
    |    12-13 | Compression          |  0 |      0 | 216 unimplemented         |
    +----------+----------------------+----+--------+---------------------------+

=head2 Validations Implemented in PAGI::Server

=over 4

=item * B<RSV Bits (Category 3)>

Validates RSV1-3 must be 0 when no extensions are negotiated.
Closes connection with 1002 Protocol Error if non-zero.

=item * B<Reserved Opcodes (Category 4)>

Rejects opcodes 3-7 and 11-15 with 1002 Protocol Error.

=item * B<Control Frame Size (Category 2)>

Rejects ping/pong/close frames with payload >125 bytes per RFC 6455 Section 5.5.

=item * B<Close Code Validation (Category 7)>

Validates close codes per RFC 6455 Section 7.4.1:

    Valid: 1000-1003, 1007-1011, 3000-4999
    Invalid: 0-999, 1004-1006, 1012-2999, 5000+

=item * B<Close Frame Format>

Rejects close frames with 1-byte payload (must be 0 or >=2 bytes).

=item * B<Close Reason UTF-8>

Validates close reason is valid UTF-8.

=back

=head2 Known Limitations

These limitations are in the Protocol::WebSocket library, not PAGI::Server:

=over 4

=item * B<UTF-8 Streaming (Category 6)>

Invalid UTF-8 in fragmented messages is validated at message end, not per-fragment.

=item * B<Fragmentation Edge Cases (Category 5)>

Complex interleaved control/data frame scenarios.

=item * B<Large Messages (Category 9)>

Default 64KB limit. Configurable via C<max_ws_frame_size>:

    my $server = PAGI::Server->new(
        max_ws_frame_size => 16 * 1024 * 1024,  # 16MB
    );

=back

=head2 Compression (Categories 12-13)

PAGI does not implement WebSocket compression (permessage-deflate per RFC 7692).
This is optional and intentionally not supported. 216 tests are marked
UNIMPLEMENTED, which is the correct behavior.

=head1 IMPLEMENTED PROTECTIONS

=head2 Request Parsing

=over 4

=item * B<RFC 7230 Section 5.4>: HTTP/1.1 Host header requirement - returns 400

=item * B<RFC 9112 Section 6.3.3>: TE and CL are mutually exclusive - returns 400 (smuggling protection)

=item * Request line size limit (8KB default) - returns 414

=item * Header size limit (8KB default) - returns 431

=item * Header count limit (100 default) - returns 431

=item * Content-Length validation - returns 400/413

=item * Duplicate Content-Length rejection - returns 400

=item * Control character rejection in headers - returns 400

=back

=head2 WebSocket

=over 4

=item * Frame size limit (C<--max-ws-frame-size>, default 64KB)

=item * Receive queue limit (C<--max-receive-queue>, default 1000 messages)

=item * RSV bits validation (closes with 1002)

=item * Reserved opcode rejection (closes with 1002)

=item * Control frame size validation (closes with 1002)

=item * Close code validation (closes with 1002)

=back

=head2 Resource Protection

=over 4

=item * Connection idle timeout (60 seconds default)

=item * Graceful shutdown with timeout (30 seconds default)

=item * Listener backlog limit (2048 default)

=back

=head1 ARCHITECTURE ADVANTAGES

PAGI::Server's async architecture provides inherent protection against certain
attack classes:

    +------------------+--------------+-------------------------+
    | Server           | Architecture | Slowloris Vulnerability |
    +------------------+--------------+-------------------------+
    | Apache           | Pre-fork     | HIGH - Workers exhaust  |
    | Starman          | Pre-fork     | HIGH - Workers exhaust  |
    | Gazelle          | Pre-fork     | HIGH - Workers exhaust  |
    | PAGI::Server     | Async        | LOW - Event loop OK     |
    | Uvicorn (Python) | Async        | LOW - Similar to PAGI   |
    | Nginx            | Event-driven | LOW - Designed for this |
    +------------------+--------------+-------------------------+

=head1 RECOMMENDATIONS

=head2 Production Deployment

For production deployments, consider:

=over 4

=item 1.

Use a reverse proxy (nginx, HAProxy) for TLS termination, rate limiting,
and per-IP connection limits.

=item 2.

Enable the L<PAGI::Middleware::SecurityHeaders> middleware for security headers.

=item 3.

Set appropriate limits for your use case:

    my $server = PAGI::Server->new(
        timeout           => 60,      # Connection idle timeout
        max_header_size   => 8192,    # 8KB header limit
        max_header_count  => 100,     # Max headers per request
        max_body_size     => 10_000_000,  # 10MB body limit
        max_ws_frame_size => 1_000_000,   # 1MB WebSocket frames
        shutdown_timeout  => 30,      # Graceful shutdown timeout
    );

=back

=head2 Performance Optimization

For optimal performance on macOS, set the EV backend to kqueue:

    LIBEV_FLAGS=8 pagi-server --loop EV --app myapp.pl

=head1 SEE ALSO

L<PAGI::Server> - The main server module

L<PAGI::Middleware::SecurityHeaders> - Security headers middleware

L<RFC 7230|https://tools.ietf.org/html/rfc7230> - HTTP/1.1 Message Syntax and Routing

L<RFC 7231|https://tools.ietf.org/html/rfc7231> - HTTP/1.1 Semantics and Content

L<RFC 9113|https://tools.ietf.org/html/rfc9113> - HTTP/2

L<RFC 6455|https://tools.ietf.org/html/rfc6455> - The WebSocket Protocol

L<h2spec|https://github.com/summerwind/h2spec> - HTTP/2 conformance test suite

L<Autobahn Testsuite|https://github.com/crossbario/autobahn-testsuite> - WebSocket test suite

=head1 AUTHOR

John Napiorkowski E<lt>jjnapiork@cpan.orgE<gt>

=head1 LICENSE

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut
