PHP Classes

File: tests/test_apache_buffering.php

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   tests/test_apache_buffering.php   Download  
File: tests/test_apache_buffering.php
Role: Example script
Content type: text/plain
Description: Example script
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 13,115 bytes
 

Contents

Class file image Download
#!/usr/bin/env php <?php /** * Apache Buffering Test * * Makes a raw HTTP/1.1 request directly to Apache (bypassing any CDN/proxy), * authenticates as a real user by pulling a session from the database, * and streams the response body as it arrives so buffering is visible * in real time. Timing summary is printed at the end. * * Usage: * php tests/test_apache_buffering.php [options] * * Options: * --host Apache host (default: 127.0.0.1) * --port Apache port (default: 81; 443 when --tls is set) * --tls Use TLS/HTTPS (sets default port to 443) * --no-verify Skip TLS certificate verification (for self-signed certs) * --uri Request URI to test (default: /) * --session Use this session ID directly (skips DB lookup) * --user Username to use (pulls their most recent active session) * --vhost HTTP Host header value (default: claudes.lovelybits.org) * --help Show this help * * Examples: * php tests/test_apache_buffering.php --uri /echomail * php tests/test_apache_buffering.php --user admin --uri /admin * php tests/test_apache_buffering.php --tls --uri /echomail * php tests/test_apache_buffering.php --tls --no-verify --uri /echomail * php tests/test_apache_buffering.php --tls --host claudes.lovelybits.org --uri / */ // ?? Argument parser ??????????????????????????????????????????????????????????? function parseArgs(array $argv): array { $result = []; $i = 1; while ($i < count($argv)) { $arg = $argv[$i]; if (str_starts_with($arg, '--')) { $key = substr($arg, 2); if (str_contains($key, '=')) { [$key, $val] = explode('=', $key, 2); $result[$key] = $val; } elseif (isset($argv[$i + 1]) && !str_starts_with($argv[$i + 1], '--')) { $result[$key] = $argv[$i + 1]; $i++; } else { $result[$key] = true; } } $i++; } return $result; } $opts = parseArgs($argv); if (isset($opts['help'])) { $lines = file(__FILE__); foreach (array_slice($lines, 1, 20) as $l) echo $l; exit(0); } $useTls = isset($opts['tls']); $noVerify = isset($opts['no-verify']); $apacheHost = $opts['host'] ?? '127.0.0.1'; $apachePort = (int)($opts['port'] ?? ($useTls ? 443 : 81)); $uri = $opts['uri'] ?? '/'; $vhost = $opts['vhost'] ?? 'claudes.lovelybits.org'; $wantUser = $opts['user'] ?? null; $wantSessId = $opts['session'] ?? null; // ?? Load .env ????????????????????????????????????????????????????????????????? $envFile = __DIR__ . '/../.env'; $env = []; if (file_exists($envFile)) { foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { $line = trim($line); if ($line === '' || $line[0] === '#' || !str_contains($line, '=')) continue; [$k, $v] = explode('=', $line, 2); $env[trim($k)] = trim($v); } } // ?? Resolve session ID ???????????????????????????????????????????????????????? if ($wantSessId !== null) { // Use the supplied session ID directly ? no DB needed $sessionId = $wantSessId; $username = '(supplied)'; $isAdmin = ''; echo "Using supplied session : " . substr($sessionId, 0, 16) . "...\n\n"; } else { $dbHost = $env['DB_HOST'] ?? 'localhost'; $dbPort = $env['DB_PORT'] ?? '5432'; $dbName = $env['DB_NAME'] ?? 'binktest'; $dbUser = $env['DB_USER'] ?? 'binktest'; $dbPass = $env['DB_PASS'] ?? ''; echo "Connecting to PostgreSQL {$dbHost}:{$dbPort}/{$dbName}...\n"; try { $dsn = "pgsql:host={$dbHost};port={$dbPort};dbname={$dbName}"; $db = new PDO($dsn, $dbUser, $dbPass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); } catch (PDOException $e) { die("DB connection failed: " . $e->getMessage() . "\n"); } if ($wantUser !== null) { $stmt = $db->prepare(" SELECT s.session_id, u.username, u.is_admin FROM user_sessions s JOIN users u ON s.user_id = u.id WHERE LOWER(u.username) = LOWER(?) AND s.expires_at > NOW() AND s.service = 'web' AND u.is_active = TRUE ORDER BY s.last_activity DESC LIMIT 1 "); $stmt->execute([$wantUser]); } else { $stmt = $db->query(" SELECT s.session_id, u.username, u.is_admin FROM user_sessions s JOIN users u ON s.user_id = u.id WHERE s.expires_at > NOW() AND s.service = 'web' AND u.is_active = TRUE ORDER BY s.last_activity DESC LIMIT 1 "); } $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row) { $who = $wantUser ? "user '{$wantUser}'" : "any active user"; die("No active web session found for {$who}. Log in via the browser first.\n"); } $sessionId = $row['session_id']; $username = $row['username']; $isAdmin = $row['is_admin'] ? ' (admin)' : ''; echo "Using session for : {$username}{$isAdmin}\n"; echo "Session ID : " . substr($sessionId, 0, 16) . "...\n\n"; } // ?? Build HTTP request ???????????????????????????????????????????????????????? $parsedUri = parse_url($uri); $path = $parsedUri['path'] ?? '/'; $query = isset($parsedUri['query']) ? '?' . $parsedUri['query'] : ''; $requestUri = $path . $query; $request = implode("\r\n", [ "GET {$requestUri} HTTP/1.1", "Host: {$vhost}", "Cookie: binktermphp_session={$sessionId}", "User-Agent: ApacheBufferingTest/1.0", "Accept: text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8", "Accept-Encoding: identity", // disable compression so we see raw bytes "Connection: close", "", "", ]); // ?? Connect and send ?????????????????????????????????????????????????????????? $scheme = $useTls ? 'https' : 'http'; echo "Apache target : {$scheme}://{$apacheHost}:{$apachePort}\n"; echo "Request URI : {$requestUri}\n"; echo "Virtual host : {$vhost}\n"; if ($useTls && $noVerify) echo "TLS verify : disabled\n"; echo str_repeat('-', 60) . "\n"; $tConnect = microtime(true); $streamProto = $useTls ? 'ssl' : 'tcp'; $context = stream_context_create($useTls ? [ 'ssl' => [ 'verify_peer' => !$noVerify, 'verify_peer_name' => !$noVerify, 'allow_self_signed' => $noVerify, 'SNI_enabled' => true, 'peer_name' => $vhost, ], ] : []); $sock = @stream_socket_client("{$streamProto}://{$apacheHost}:{$apachePort}", $errno, $errstr, 5.0, STREAM_CLIENT_CONNECT, $context); if (!$sock) { die("Cannot connect to Apache ({$scheme}://{$apacheHost}:{$apachePort}): [{$errno}] {$errstr}\n"); } stream_set_timeout($sock, 30); // Disable read buffering so we see data as it arrives stream_set_read_buffer($sock, 0); $tConnected = microtime(true); printf("Connected in : %.1f ms\n", ($tConnected - $tConnect) * 1000); fwrite($sock, $request); $tSent = microtime(true); printf("Request sent at : %.1f ms\n", ($tSent - $tConnect) * 1000); // ?? Read response ????????????????????????????????????????????????????????????? $firstByte = null; $responseHeaders = ''; $headersDone = false; $headerBuf = ''; $chunkBuf = ''; // residual buffer for chunked decoding across fread() calls $bodyBytes = 0; $isChunked = false; $contentLength = null; $statusLine = ''; while (!feof($sock)) { $raw = fread($sock, 4096); if ($raw === false || $raw === '') { $info = stream_get_meta_data($sock); if ($info['timed_out']) { echo "\n[read timeout]\n"; } break; } if ($firstByte === null) { $firstByte = microtime(true); } if (!$headersDone) { $headerBuf .= $raw; $sep = strpos($headerBuf, "\r\n\r\n"); if ($sep !== false) { $responseHeaders = substr($headerBuf, 0, $sep); $body = substr($headerBuf, $sep + 4); $headersDone = true; // Extract status line $statusLine = strtok($responseHeaders, "\n"); // Detect transfer encoding and content-length $lowerHeaders = strtolower($responseHeaders); $isChunked = str_contains($lowerHeaders, 'transfer-encoding: chunked'); if (preg_match('/content-length:\s*(\d+)/i', $responseHeaders, $m)) { $contentLength = (int)$m[1]; } if ($body !== '') { if ($isChunked) { $chunkBuf .= $body; $body = decodeChunked($chunkBuf); } echo $body; $bodyBytes += strlen($body); } } } else { if ($isChunked) { $chunkBuf .= $raw; $decoded = decodeChunked($chunkBuf); echo $decoded; $bodyBytes += strlen($decoded); } else { echo $raw; $bodyBytes += strlen($raw); } } } fclose($sock); $tEnd = microtime(true); // ?? Summary ??????????????????????????????????????????????????????????????????? echo "\n" . str_repeat('=', 60) . "\n"; echo "Status : " . trim($statusLine) . "\n"; if ($firstByte !== null) { printf("Time to first byte : %.1f ms\n", ($firstByte - $tConnect) * 1000); } printf("Total response time : %.1f ms\n", ($tEnd - $tConnect) * 1000); if ($firstByte !== null) { printf("Body transfer time : %.1f ms\n", ($tEnd - $firstByte) * 1000); $bufferingGap = ($firstByte - $tSent) * 1000; printf("Send?first-byte gap : %.1f ms %s\n", $bufferingGap, $bufferingGap > 200 ? ' ? possible buffering delay' : ' ? looks fine' ); } printf("Body size : %d bytes%s\n", $bodyBytes, $contentLength !== null ? " (Content-Length: {$contentLength})" : ($isChunked ? ' (chunked)' : '') ); echo str_repeat('-', 60) . "\n"; echo "Response headers:\n"; foreach (explode("\n", $responseHeaders) as $hLine) { echo " " . rtrim($hLine) . "\n"; } // Note any buffering-related headers $notable = []; foreach (explode("\n", $responseHeaders) as $hLine) { $lower = strtolower(trim($hLine)); if (str_starts_with($lower, 'x-accel-buffering:') || str_starts_with($lower, 'transfer-encoding:') || str_starts_with($lower, 'x-php-ob-level:')) { $notable[] = ' ' . trim($hLine); } } if ($notable) { echo "\nNotable buffering headers:\n"; foreach ($notable as $h) echo $h . "\n"; } exit(0); // ?? Helpers ??????????????????????????????????????????????????????????????????? /** * Decode all complete chunks from $buf (passed by reference). * Leaves any incomplete trailing data in $buf for the next call. * Returns the decoded bytes ready to print. */ function decodeChunked(string &$buf): string { $out = ''; $len = strlen($buf); $pos = 0; while ($pos < $len) { // Need at least a chunk-size line ending in \r\n $crlf = strpos($buf, "\r\n", $pos); if ($crlf === false) break; // incomplete chunk header ? wait for more data $sizeLine = substr($buf, $pos, $crlf - $pos); $chunkSize = hexdec(strtok($sizeLine, ';')); // strip chunk extensions if ($chunkSize === 0) { // Terminal chunk ? consume it and stop (connection will close) $pos = $crlf + 2; // Skip optional trailing headers break; } $dataStart = $crlf + 2; $dataEnd = $dataStart + $chunkSize; if ($dataEnd + 2 > $len) { // Chunk data (or its trailing \r\n) not fully received yet ? wait break; } $out .= substr($buf, $dataStart, $chunkSize); $pos = $dataEnd + 2; // skip trailing \r\n } // Keep only the unconsumed remainder for the next fread() call $buf = substr($buf, $pos); return $out; }