Skip to content

Add multi-protocol server description to README#1

Closed
EdmondDantes wants to merge 1 commit intomainfrom
docs/multi-protocol-description
Closed

Add multi-protocol server description to README#1
EdmondDantes wants to merge 1 commit intomainfrom
docs/multi-protocol-description

Conversation

@EdmondDantes
Copy link
Copy Markdown
Contributor

Summary

  • Adds an introductory paragraph explaining TrueAsync Server as a native PHP extension (no external process/proxy)
  • Describes the multi-protocol-in-one-server concept: HTTP/1.1, HTTP/2, HTTP/3, WebSocket, SSE, gRPC on the same port and event loop
  • Explains protocol selection via ALPN negotiation (TLS) and HTTP Upgrade
  • Gives a practical example of running REST, SSE, WebSocket, and gRPC from a single $server->start() call

Test plan

  • Review rendered markdown on GitHub

Explains that TrueAsync Server combines multiple protocols (HTTP/1.1,
HTTP/2, HTTP/3, WebSocket, SSE, gRPC) in a single server instance on
one port, with ALPN negotiation and HTTP Upgrade for protocol selection.
@EdmondDantes EdmondDantes deleted the docs/multi-protocol-description branch April 26, 2026 11:05
EdmondDantes added a commit that referenced this pull request May 6, 2026
FUTURES #1. Each listener now carries its own HTTP_PROTO_MASK_*; the
effective acceptance is listener_mask ∩ server_view_mask. New PHP API:
addHttp1Listener / addHttp2Listener on HttpServerConfig for
protocol-restricted ports (h2c-only, h1-only). Default addListener()
unchanged.

Also adds docs/USAGE.md and fixes the README quick-start to use the
actual public API.
EdmondDantes added a commit that referenced this pull request May 7, 2026
PR #1 of the StaticHandler plan is now functional end-to-end. Files
under a configured StaticHandler mount are served directly in C —
no PHP coroutine, no zend_call_function, no fcall_info_cache lookup
on the static path.

Pipeline:

  http_connection_dispatch_request (src/core/http_connection.c)
    └─ if any mount registered → http_static_try_serve(...)
         ├─ method check (GET/HEAD only; others passthrough)
         ├─ http_static_path_resolve → percent-decode + traversal guard
         │                              + dotfile policy
         ├─ http_static_path_is_hidden (hide-glob match via fnmatch)
         ├─ open(O_RDONLY [|O_NOFOLLOW]) + fstat (synchronous; PR #5
         │                              upgrades to libuv thread pool)
         ├─ ETag/conditional → 304 with empty body
         ├─ slurp into zend_string + http_response_static_set_*
         └─ ctx->skip_php_handler = true

  http_handler_coroutine_entry sees skip_php_handler set and returns
  immediately after the request-counter bump and CoDel sample —
  no PHP VM entry. Existing dispose path flushes the response on the
  wire, so all keep-alive / drain / Alt-Svc / counter convergence
  reuses the existing infrastructure (no http_request_finalize
  extraction needed; the flag-based fold matches the plan's
  acceptance criterion of zero new flush-path duplication).

The accept path now allows static-only deployments — when the
server has zero PHP handlers but at least one static mount,
http_connection_spawn proceeds with handler=NULL and the dispatcher
synthesises a 404 for any request the static path didn't claim.

PHPT matrix in tests/phpt/server/static/ covers:
  001 — 200, MIME (CSS / HTML index), nested file, 404, traversal
        (relative + percent-encoded), HEAD body suppression with
        Content-Length, weak ETag conditional GET → 304, non-GET
        method passthrough.
  002 — dotfile-deny default still 404s under on_missing: Next
        (security guarantee), genuinely-missing files fall through
        to PHP, URLs outside the prefix fall through to PHP.

Pre-existing 153 PHPT tests still pass (1 skip, 0 fail).

Two small public response API helpers added in php_http_server.h:
http_response_static_set_status / set_header / set_body_str —
unguarded direct-field setters for the single-writer C path. The
PHP-facing setters keep their close/streaming guards.

Closes the PR-#1 scope from docs/PLAN_STATIC_HANDLER.md (§5).
PRs #2-#6 (H2/H3, range, precompressed, sendfile, browse) land
incrementally on top of the same dispatch hook.
EdmondDantes added a commit that referenced this pull request May 7, 2026
Code-review pass over PR #1 against docs/CODING_STANDARDS.md:

  Comments — drop decorative WHAT-restatements (function-name says
    it), keep WHY / invariant / RFC-citation comments. Net −72 lines
    of prose for the same information density.

  const — http_static_etag_format takes const struct stat *; fixed-
    size buffer params and read-only locals across the static TUs
    are now const where true. Internal helpers carry const-pointer
    params for inputs they only read.

  Hot-path branch hints — UNEXPECTED on every error/edge branch in
    the dispatch FSM (BAD_REQUEST, FORBIDDEN, HIDE, !opened,
    oversized, slurp failure, hide-glob match), EXPECTED on the
    read-loop success branch. The success path stays straight-
    line.

  MIME table — add precomputed content_type_len per entry so the
    hot path no longer does strlen() on every served file. Single
    MIME() macro keeps the table readable.

  ETag length — replace per-call strlen(etag_buf) with the new
    HTTP_STATIC_ETAG_LEN constant; same for HTTP_STATIC_DATE_LEN.
    The new ZEND_ASSERT in http_static_etag_format caught an
    off-by-one in the original buffer size (was 22, correct is 21
    — the format produces 20 chars + NUL).

  Debug-build invariant — assert_builtin_table_sorted() runs once
    per process under !NDEBUG, replacing a stale "static_assert
    verifies ordering" comment that was never actually wired up.

  Misleading comment — addStaticHandler claimed protocol_mask was
    needed for the start() preflight; in fact static_handler_count
    covers that. Updated to reflect actual intent (symmetry with
    addHttpHandler convention).

All 153 pre-existing PHPT tests still pass; the two static tests
in tests/phpt/server/static/ continue to pass on this refactor.
EdmondDantes added a commit that referenced this pull request May 7, 2026
Security audit pass over PR #1 — five findings, all addressed.

  HIGH — symlink traversal via intermediate components.
    O_NOFOLLOW only protects the final path component; an attacker-
    or operator-created symlink at any earlier level inside the
    mount root could redirect open(2) outside. Added a realpath()-
    based prefix check in resolved_under_root() that runs after
    every successful try_open_candidate; mismatch → 404. The
    realpath/open TOCTOU is acceptable — exploiting it requires
    filesystem write access on the host.

  HIGH — backslash bypass on Windows. The segment validator splits
    on '/' only, so "..\..\etc\passwd" arrived as one segment that
    didn't match ".." and would have escaped the mount on a host
    where the kernel treats '\' as a separator. Backslash is now
    rejected anywhere in the percent-decoded path (literal or %5C);
    URLs never legitimately contain '\' under RFC 3986 anyway.

  MEDIUM — 403 on EACCES disclosed the existence of restricted
    files. Collapsed every "open failed" branch (ENOENT, ELOOP,
    ENOTDIR, EACCES, EPERM) onto the same 404 / on_missing
    fallback used elsewhere — symmetric with dotfile-deny.

  MEDIUM — setCacheControl accepted CRLF/NUL, allowing response-
    splitting if an operator concatenated user input. Added the
    same control-character rejection used by setHeader.

  MEDIUM — SYMLINKS_OWNER documented as "follow if owner-of-link
    matches" but the post-open uid comparison wasn't implemented;
    the mode behaved like FOLLOW. Aliased to REJECT (O_NOFOLLOW)
    until the real owner-match check lands — the policy is no
    weaker than its advertised security promise.

  LOW — http_static_format_http_date used %04d on tm_year + 1900
    without bound-checking; a far-future mtime (filesystem
    tampering) would overflow the fixed buffer logically and
    produce a malformed Last-Modified. Year is now clamped to
    [0, 9999] with a debug-build ZEND_ASSERT on the snprintf
    return.

New PHPT 003-static-security.phpt covers each finding: symlink
escape via /static/sneak/secret.txt → 404, literal and %5C-encoded
backslash → 400, %00 NUL injection → 400, chmod-000 file → not 403,
setCacheControl("\r\n…") → InvalidArgumentException.

All 154 PHPT tests still pass (1 pre-existing skip, 0 fail).
EdmondDantes added a commit that referenced this pull request May 7, 2026
Add §5a "Status" tracking what's actually shipped in this branch
versus what remains. Highlights:

  Closed: full hard-zero coroutine path on plain TCP, security
  fixes (backslash, symlink-traversal via realpath prefix-check,
  EACCES→404, header-injection in setCacheControl), zend_async API
  v0.11 (fs_open + sendfile, in php-src + ext/async submodules).

  Remaining for PR #1's acceptance: PHPT for If-Modified-Since,
  telemetry counter for zero-coroutine, bench vs entry.php,
  valgrind on a million-request run, TLS hard-zero (needs
  non-suspending TLS write helper), headers↔sendfile ordering on
  EAGAIN (TCP_CORK or wait-for-completion).

  Future PRs untouched: H2/H3 (#2), Range (#3), precompressed (#4),
  browse (#6). Note that PR #5 (sendfile) effectively merged into
  #1 because zero-copy sendfile required the zend_async API
  extension anyway, and it's cleaner than gating it behind a flag.

Mark acceptance checklist items in §6 with [x] / [~] / [ ] so the
status is at-a-glance. Open questions §7 picks up TLS hard-zero,
headers/sendfile ordering, and SYMLINKS_OWNER as deferred items
the next iteration owns.
EdmondDantes added a commit that referenced this pull request May 8, 2026
Bounded per-server cache of recent static-handler path lookups.
On a warm cache, http_static_try_serve skips the realpath/stat
walk that runs on every request — the dominant cost on hot
serving when the dentry cache is cold.

Defaults: 512 entries, 60-second TTL (matches nginx
open_file_cache / open_file_cache_valid). Both tunable per worker
via env: TRUE_ASYNC_STATIC_CACHE_ENTRIES=0 disables; TTL=0 also
disables. No locking — single-thread per event loop is the
invariant; worker pool gives each worker its own snapshot.

Per-entry: zend_string key (the absolute resolved path), full
struct stat copy, content_type pointer (borrowed from the
persistent MIME table), pemalloc'd ETag and Last-Modified
buffers, LRU links. HashTable destructor frees the entry; cache
destroy fires at server free.

Wire-up in http_static_try_serve: lookup before
resolved_under_root; on hit skip the realpath. On miss + realpath
success: insert. Hit-side reuse is currently realpath-only — st,
etag, content_type fields in the view are NULL/zero. Full
metadata reuse (skip stat / etag / mime lookup) is a separate
follow-up that needs a one-shot stat at insert time inside the
async chain.

Bench (debug-zts build, 16 cores WSL2, 64 KiB file, wrk -c64 -t4 -d5s,
warm dentry cache, 3-run average):
  - cache on  : ~11017 req/s
  - cache off : ~11748 req/s
On warm dentry cache realpath is essentially free, so the
HashTable lookup is pure overhead in this microbench. The cache
earns its keep on cold-cache scenarios and once follow-up #1
lands (skip stat/etag/mime on hit). Tests: 19/20 PASS + 1
pre-existing SKIP across static + tls suites.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant