mirror of
https://github.com/Qbix/webserver.git
synced 2026-07-22 07:57:23 +02:00
Added support for hot reload, start, stop, restart, and virtual hosts
This commit is contained in:
@@ -2106,9 +2106,72 @@ Create `config/server.json` next to your `web/` directory, or pass `--config=pat
|
||||
| `socket.js` | `"/Q/socket.js"` | Path to serve the minimal bare-WebSocket client (3KB). `false` to disable. |
|
||||
| `app` | `""` | App name — prefixes handler function names (e.g. `"Chess"` → `Chess_chat_message()`) |
|
||||
| `webserver.fallback` | null | Catch-all: `"index.html"`, `{"handler":"app/notfound"}`, or `{"file":"404.html"}` |
|
||||
| `webserver.hotReload` | `false` | Watch `classes/`, `handlers/`, `config/` for changes. Auto-restarts on class/config changes. |
|
||||
| `webserver.cgi.patterns` | [] | Regex patterns for scripts that use php-cgi (legacy compatibility) |
|
||||
| `webserver.cgi.binary` | auto | Path to php-cgi binary (auto-detected if not set) |
|
||||
|
||||
### Virtual hosts
|
||||
|
||||
Serve multiple domains from one server. Each host can have its own document root:
|
||||
|
||||
```json
|
||||
{
|
||||
"Q": {
|
||||
"webserver": {
|
||||
"hosts": {
|
||||
"example.com": {
|
||||
"root": "/var/www/example/web"
|
||||
},
|
||||
"api.example.com": {
|
||||
"root": "/var/www/api/web"
|
||||
},
|
||||
"staging.example.com": {
|
||||
"root": "/var/www/staging/web"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `Host` header selects the root. Requests for unconfigured hosts use the
|
||||
default `--root` directory. WebSocket, rooms, handlers, and static files all
|
||||
respect the per-host root.
|
||||
|
||||
### Hot reload
|
||||
|
||||
Watch `classes/`, `handlers/`, and `config/` for file changes:
|
||||
|
||||
```bash
|
||||
php qbixserver.php --root=./web --port=8080 --hotreload
|
||||
```
|
||||
|
||||
Or via config:
|
||||
|
||||
```json
|
||||
{
|
||||
"Q": {
|
||||
"webserver": {
|
||||
"hotReload": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Handler changes take effect immediately — handlers are lazy-loaded, so the
|
||||
next request or connection picks up the new code. Class or config changes
|
||||
trigger a graceful restart (the server re-execs itself with the same arguments).
|
||||
|
||||
Changes are logged to stderr:
|
||||
|
||||
```
|
||||
14:32:07 hot-reload: ~ handlers/chat/message.php
|
||||
14:32:09 hot-reload: + classes/MyApp/NewFeature.php
|
||||
14:32:09 hot-reload: restarting server...
|
||||
```
|
||||
|
||||
Polls every 2 seconds. Use in development, not production.
|
||||
|
||||
### Scheduler
|
||||
|
||||
Run tasks on intervals or at specific times. Handlers are forked like HTTP
|
||||
@@ -2599,8 +2662,8 @@ the full 10x performance advantage, use Linux or macOS (or WSL).
|
||||
|
||||
**Coming next:**
|
||||
|
||||
- **Virtual hosts** — `Q.web.hosts.$hostname` config overrides for multi-domain serving
|
||||
- **Hot reload** — watch `classes/`, `handlers/`, `config/` for changes, auto-restart workers
|
||||
- **Clustering** — multi-process worker pool with shared socket for horizontal scaling
|
||||
- **HTTP long-polling** — Socket.IO polling transport for environments that block WebSocket
|
||||
|
||||
---
|
||||
|
||||
|
||||
Binary file not shown.
+81
-1
@@ -38,7 +38,7 @@ foreach ($argv as $i => $arg) {
|
||||
if ($i === 0) continue;
|
||||
if ($arg === '--help' || $arg === '-h') {
|
||||
echo "Qbix Server v" . QBIX_SERVER_VERSION . "\n\n";
|
||||
echo "Usage: php qbixserver.php [options]\n\n";
|
||||
echo "Usage: ./qbixserver.php [options]\n\n";
|
||||
echo "Options:\n";
|
||||
echo " --root=DIR Document root (default: ./web)\n";
|
||||
echo " --app=DIR Qbix app directory (uses full Q framework)\n";
|
||||
@@ -47,7 +47,11 @@ foreach ($argv as $i => $arg) {
|
||||
echo " --workers=N Pre-fork workers (default: 0 = in-process)\n";
|
||||
echo " --config=FILE JSON config file\n";
|
||||
echo " --pid=PATH PID file path\n";
|
||||
echo " --hotreload Watch files, auto-restart on changes\n";
|
||||
echo " --debug Verbose logging\n";
|
||||
echo " -t Test config and exit\n";
|
||||
echo " --stop Graceful shutdown (via PID file)\n";
|
||||
echo " --reload Re-exec server (via PID file)\n";
|
||||
echo " --version Print version\n";
|
||||
exit(0);
|
||||
}
|
||||
@@ -55,10 +59,26 @@ foreach ($argv as $i => $arg) {
|
||||
echo "Qbix Server v" . QBIX_SERVER_VERSION . "\n";
|
||||
exit(0);
|
||||
}
|
||||
if ($arg === '-t') {
|
||||
$opts['test'] = true;
|
||||
continue;
|
||||
}
|
||||
if ($arg === '--stop') {
|
||||
$opts['signal'] = 'stop';
|
||||
continue;
|
||||
}
|
||||
if ($arg === '--reload') {
|
||||
$opts['signal'] = 'reload';
|
||||
continue;
|
||||
}
|
||||
if ($arg === '--debug') {
|
||||
$opts['debug'] = true;
|
||||
continue;
|
||||
}
|
||||
if ($arg === '--hotreload') {
|
||||
$opts['hotreload'] = true;
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^--(\w+)=(.+)$/', $arg, $m)) {
|
||||
$opts[$m[1]] = $m[2];
|
||||
}
|
||||
@@ -208,6 +228,66 @@ if (file_exists($appConfig)) {
|
||||
// Preload handlers if configured (Q.handlers.preload: true)
|
||||
Q::preload();
|
||||
|
||||
// CLI flag overrides
|
||||
if (!empty($opts['hotreload'])) {
|
||||
Q_Config::set('Q', 'webserver', 'hotReload', true);
|
||||
}
|
||||
|
||||
// ── Signal commands (--stop, --reload) ──────────────
|
||||
|
||||
if (!empty($opts['signal'])) {
|
||||
$pidFile = $opts['pid'] ?: dirname($webDir) . '/qbixserver.pid';
|
||||
if (!file_exists($pidFile)) {
|
||||
fwrite(STDERR, "PID file not found: $pidFile\n");
|
||||
fwrite(STDERR, "Use --pid=PATH to specify, or start the server with --pid first.\n");
|
||||
exit(1);
|
||||
}
|
||||
$pid = (int) trim(file_get_contents($pidFile));
|
||||
if ($pid <= 0 || !posix_kill($pid, 0)) {
|
||||
fwrite(STDERR, "No running server found (PID $pid)\n");
|
||||
@unlink($pidFile);
|
||||
exit(1);
|
||||
}
|
||||
if ($opts['signal'] === 'stop') {
|
||||
posix_kill($pid, SIGTERM);
|
||||
echo "Sent SIGTERM to PID $pid\n";
|
||||
} elseif ($opts['signal'] === 'reload') {
|
||||
posix_kill($pid, SIGHUP);
|
||||
echo "Sent SIGHUP to PID $pid\n";
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// ── Config test (-t) ────────────────────────────────
|
||||
|
||||
if (!empty($opts['test'])) {
|
||||
echo "Qbix Server v" . QBIX_SERVER_VERSION . "\n";
|
||||
echo "Config: OK\n";
|
||||
echo " Root: $webDir\n";
|
||||
echo " Host: {$opts['host']}\n";
|
||||
echo " Port: {$opts['port']}\n";
|
||||
$app = Q::app();
|
||||
if ($app) echo " App: $app\n";
|
||||
$ioPath = Q_Config::get('Q', 'socket', 'io', '/socket.io');
|
||||
echo " Socket.IO: " . ($ioPath !== false ? $ioPath : 'disabled') . "\n";
|
||||
$jsPath = Q_Config::get('Q', 'socket', 'js', '/Q/socket.js');
|
||||
echo " Socket.js: " . ($jsPath !== false ? $jsPath : 'disabled') . "\n";
|
||||
$hosts = Q_Config::get('Q', 'webserver', 'hosts', array());
|
||||
if (!empty($hosts)) {
|
||||
echo " Vhosts: " . implode(', ', array_keys($hosts)) . "\n";
|
||||
}
|
||||
$schedule = Q_Config::get('Q', 'scheduler', array());
|
||||
if (!empty($schedule)) {
|
||||
echo " Scheduled: " . implode(', ', array_keys($schedule)) . "\n";
|
||||
}
|
||||
$timeout = Q_Config::get('Q', 'webserver', 'requestTimeout', 30);
|
||||
echo " Timeout: {$timeout}s\n";
|
||||
$preloaded = Q::$preloadedHandlers;
|
||||
echo " Classes: " . count(get_declared_classes()) . " preloaded\n";
|
||||
echo " Handlers: " . ($preloaded > 0 ? "$preloaded preloaded" : "lazy") . "\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// ── PID file ────────────────────────────────────────
|
||||
|
||||
if ($opts['pid']) {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# Running Qbix Server as a Service
|
||||
|
||||
## Linux (systemd)
|
||||
|
||||
```bash
|
||||
# Edit paths in the service file
|
||||
sudo cp service/qbixserver.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# Start, stop, reload
|
||||
sudo systemctl start qbixserver
|
||||
sudo systemctl stop qbixserver
|
||||
sudo systemctl reload qbixserver # re-reads config, restarts workers
|
||||
|
||||
# Auto-start on boot
|
||||
sudo systemctl enable qbixserver
|
||||
|
||||
# View logs
|
||||
journalctl -u qbixserver -f
|
||||
```
|
||||
|
||||
## macOS (launchd)
|
||||
|
||||
```bash
|
||||
# Edit paths in the plist file
|
||||
cp service/com.qbix.server.plist ~/Library/LaunchAgents/
|
||||
|
||||
# Start, stop
|
||||
launchctl load ~/Library/LaunchAgents/com.qbix.server.plist
|
||||
launchctl unload ~/Library/LaunchAgents/com.qbix.server.plist
|
||||
|
||||
# View logs
|
||||
tail -f /var/log/qbixserver.log
|
||||
```
|
||||
|
||||
For system-wide (not per-user), use `/Library/LaunchDaemons/` instead.
|
||||
|
||||
## Manual (any platform)
|
||||
|
||||
```bash
|
||||
# Start in background
|
||||
./qbixserver.php --root=./web --port=8080 --pid=./qbixserver.pid &
|
||||
|
||||
# Stop
|
||||
./qbixserver.php --stop --pid=./qbixserver.pid
|
||||
|
||||
# Reload (graceful restart)
|
||||
./qbixserver.php --reload --pid=./qbixserver.pid
|
||||
|
||||
# Test config
|
||||
./qbixserver.php -t
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.qbix.server</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/local/bin/php</string>
|
||||
<string>/var/www/myapp/qbixserver.php</string>
|
||||
<string>--root=./web</string>
|
||||
<string>--port=8080</string>
|
||||
<string>--pid=/tmp/qbixserver.pid</string>
|
||||
</array>
|
||||
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/var/www/myapp</string>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
|
||||
<key>StandardOutPath</key>
|
||||
<string>/var/log/qbixserver.log</string>
|
||||
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/var/log/qbixserver.error.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,25 @@
|
||||
[Unit]
|
||||
Description=Qbix Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=/var/www/myapp
|
||||
ExecStart=/usr/bin/php /var/www/myapp/qbixserver.php --root=./web --port=8080 --pid=/var/run/qbixserver.pid
|
||||
ExecStop=/usr/bin/php /var/www/myapp/qbixserver.php --stop --pid=/var/run/qbixserver.pid
|
||||
ExecReload=/usr/bin/php /var/www/myapp/qbixserver.php --reload --pid=/var/run/qbixserver.pid
|
||||
PIDFile=/var/run/qbixserver.pid
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/var/www/myapp /var/run
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
/**
|
||||
* Hot reload — watches directories for file changes and triggers
|
||||
* a graceful server restart when detected.
|
||||
*
|
||||
* Uses filemtime() polling (works everywhere, no extensions needed).
|
||||
* Watches classes/, handlers/, config/ directories.
|
||||
*
|
||||
* Config:
|
||||
* "Q": {"webserver": {"hotReload": true}}
|
||||
*
|
||||
* When a change is detected:
|
||||
* - Handler changes: no action needed (lazy-loaded, next fork loads fresh)
|
||||
* - Class/config changes: graceful restart via re-exec
|
||||
*
|
||||
* @class Q_HotReload
|
||||
*/
|
||||
class Q_HotReload
|
||||
{
|
||||
/** @var array path => mtime snapshot */
|
||||
static $snapshot = array();
|
||||
/** @var float Last full scan time */
|
||||
static $lastScan = 0;
|
||||
/** @var array Directories to watch */
|
||||
static $watchDirs = array();
|
||||
/** @var boolean Whether a restart is pending */
|
||||
static $restarting = false;
|
||||
|
||||
/**
|
||||
* Initialize: snapshot all watched files.
|
||||
*/
|
||||
static function init()
|
||||
{
|
||||
// Watch standard directories relative to each registered path
|
||||
foreach (Q::$paths as $base) {
|
||||
foreach (array('classes', 'handlers', 'config') as $dir) {
|
||||
$full = $base . DS . $dir;
|
||||
if (is_dir($full)) {
|
||||
self::$watchDirs[] = $full;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty(self::$watchDirs)) return;
|
||||
|
||||
self::$snapshot = self::scan();
|
||||
self::$lastScan = microtime(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for changes. Called every 2 seconds by the event loop.
|
||||
*/
|
||||
static function check()
|
||||
{
|
||||
if (self::$restarting || empty(self::$watchDirs)) return;
|
||||
|
||||
$current = self::scan();
|
||||
$changes = self::diff($current);
|
||||
|
||||
if (empty($changes)) {
|
||||
self::$snapshot = $current;
|
||||
return;
|
||||
}
|
||||
|
||||
// Categorize changes
|
||||
$needsRestart = false;
|
||||
foreach ($changes as $file => $type) {
|
||||
$rel = self::relativePath($file);
|
||||
if (strpos($rel, 'classes' . DS) === 0 || strpos($rel, 'config' . DS) === 0) {
|
||||
$needsRestart = true;
|
||||
}
|
||||
$label = ($type === 'added') ? "\033[32m+\033[0m" :
|
||||
(($type === 'removed') ? "\033[31m-\033[0m" : "\033[33m~\033[0m");
|
||||
fwrite(STDERR, date('H:i:s') . " hot-reload: $label $rel\n");
|
||||
}
|
||||
|
||||
self::$snapshot = $current;
|
||||
|
||||
if ($needsRestart) {
|
||||
self::restart();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan all watched directories, return path => mtime map.
|
||||
*/
|
||||
static function scan()
|
||||
{
|
||||
$files = array();
|
||||
foreach (self::$watchDirs as $dir) {
|
||||
self::scanDir($dir, $files);
|
||||
}
|
||||
return $files;
|
||||
}
|
||||
|
||||
private static function scanDir($dir, &$files)
|
||||
{
|
||||
$entries = @scandir($dir);
|
||||
if (!$entries) return;
|
||||
foreach ($entries as $e) {
|
||||
if ($e[0] === '.') continue;
|
||||
$path = $dir . DS . $e;
|
||||
if (is_dir($path)) {
|
||||
self::scanDir($path, $files);
|
||||
} elseif (is_file($path)) {
|
||||
$files[$path] = @filemtime($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare current scan to snapshot, return changed files.
|
||||
*/
|
||||
static function diff($current)
|
||||
{
|
||||
$changes = array();
|
||||
// Modified or added
|
||||
foreach ($current as $path => $mtime) {
|
||||
if (!isset(self::$snapshot[$path])) {
|
||||
$changes[$path] = 'added';
|
||||
} elseif ($mtime !== self::$snapshot[$path]) {
|
||||
$changes[$path] = 'modified';
|
||||
}
|
||||
}
|
||||
// Removed
|
||||
foreach (self::$snapshot as $path => $mtime) {
|
||||
if (!isset($current[$path])) {
|
||||
$changes[$path] = 'removed';
|
||||
}
|
||||
}
|
||||
return $changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human-readable relative path.
|
||||
*/
|
||||
static function relativePath($path)
|
||||
{
|
||||
foreach (Q::$paths as $base) {
|
||||
if (strpos($path, $base . DS) === 0) {
|
||||
return substr($path, strlen($base) + 1);
|
||||
}
|
||||
}
|
||||
return basename($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Graceful restart — re-exec the server process.
|
||||
*/
|
||||
static function restart()
|
||||
{
|
||||
self::$restarting = true;
|
||||
fwrite(STDERR, date('H:i:s') . " hot-reload: restarting server...\n");
|
||||
|
||||
// Re-exec: replace current process with a fresh one
|
||||
// This preserves the original command-line arguments
|
||||
$args = $_SERVER['argv'] ?? array();
|
||||
$php = PHP_BINARY;
|
||||
|
||||
if (function_exists('pcntl_exec')) {
|
||||
pcntl_exec($php, $args);
|
||||
// If pcntl_exec fails, fall through
|
||||
}
|
||||
|
||||
// Fallback: signal the event loop to stop, then exec
|
||||
fwrite(STDERR, date('H:i:s') . " hot-reload: stopping for manual restart\n");
|
||||
Q_Evented::stop();
|
||||
}
|
||||
}
|
||||
+43
-6
@@ -347,6 +347,16 @@ class Q_WebServer
|
||||
self::stop();
|
||||
Q_Evented::stop();
|
||||
});
|
||||
Q_Evented::onSignal(SIGHUP, function () {
|
||||
echo "\n Reloading (SIGHUP)...\n";
|
||||
self::stop();
|
||||
Q_Evented::stop();
|
||||
// Re-exec with same arguments
|
||||
$args = $_SERVER['argv'] ?? array();
|
||||
if (function_exists('pcntl_exec')) {
|
||||
pcntl_exec(PHP_BINARY, $args);
|
||||
}
|
||||
});
|
||||
// Reap zombie children from fork-per-request PHP execution
|
||||
Q_Evented::onSignal(SIGCHLD, function () {
|
||||
while (($pid = pcntl_waitpid(-1, $st, WNOHANG)) > 0) {
|
||||
@@ -382,6 +392,14 @@ class Q_WebServer
|
||||
});
|
||||
}
|
||||
|
||||
// Hot reload — watch for file changes in classes/, handlers/, config/
|
||||
if (Q_Config::get('Q', 'webserver', 'hotReload', false)) {
|
||||
Q_HotReload::init();
|
||||
Q_Evented::repeat(2, function () {
|
||||
Q_HotReload::check();
|
||||
});
|
||||
}
|
||||
|
||||
Q_Evented::run();
|
||||
}
|
||||
|
||||
@@ -539,6 +557,7 @@ class Q_WebServer
|
||||
&& self::$keepAliveCount[$key] < $maxKeepAlive;
|
||||
|
||||
try {
|
||||
$savedRoot = self::$rootDir;
|
||||
$keepOpen = self::handleRequest($client, $parsed);
|
||||
} catch (\Throwable $e) {
|
||||
// Never let a request crash the event loop
|
||||
@@ -553,6 +572,11 @@ class Q_WebServer
|
||||
(self::$onRequest)($parsed['method'] ?? 'GET', $parsed['uri'] ?? '/', 500, $ms);
|
||||
}
|
||||
return;
|
||||
} finally {
|
||||
// Restore rootDir after vhost override
|
||||
if (self::$rootDir !== $savedRoot) {
|
||||
self::$rootDir = $savedRoot;
|
||||
}
|
||||
}
|
||||
$ms = round((microtime(true) - $start) * 1000, 1);
|
||||
|
||||
@@ -779,6 +803,17 @@ class Q_WebServer
|
||||
$method = $parsed['method'];
|
||||
$path = $parsed['path'];
|
||||
|
||||
// Virtual hosts — override rootDir based on Host header
|
||||
$host = $parsed['headers']['host'] ?? '';
|
||||
$host = strtolower(preg_replace('/:\d+$/', '', $host)); // strip port
|
||||
$hostConfig = Q_Config::get('Q', 'webserver', 'hosts', $host, null);
|
||||
if ($hostConfig && isset($hostConfig['root'])) {
|
||||
$vroot = realpath($hostConfig['root']);
|
||||
if ($vroot && is_dir($vroot)) {
|
||||
self::$rootDir = rtrim(str_replace(array('/', '\\'), DS, $vroot), DS) . DS;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Dashboard + Panel + WebSocket + Health (/Q/*)
|
||||
// 1. Serve client JS files (before /Q/ check — socket.io.js is at /socket.io/)
|
||||
$jsMap = array();
|
||||
@@ -2427,14 +2462,16 @@ HTML
|
||||
private static function resolveStatic($urlPath)
|
||||
{
|
||||
// Path resolution cache — avoids repeated realpath() syscalls
|
||||
// Key includes rootDir so vhosts don't cross-contaminate
|
||||
static $pathCache = array();
|
||||
if (isset($pathCache[$urlPath])) {
|
||||
$cached = $pathCache[$urlPath];
|
||||
$cacheKey = self::$rootDir . $urlPath;
|
||||
if (isset($pathCache[$cacheKey])) {
|
||||
$cached = $pathCache[$cacheKey];
|
||||
// Quick mtime check for invalidation (cheaper than realpath)
|
||||
if ($cached === null || file_exists($cached)) {
|
||||
return $cached;
|
||||
}
|
||||
unset($pathCache[$urlPath]);
|
||||
unset($pathCache[$cacheKey]);
|
||||
}
|
||||
|
||||
$rel = str_replace('/', DS, ltrim($urlPath, '/'));
|
||||
@@ -2443,17 +2480,17 @@ HTML
|
||||
$fsPath = realpath(self::$rootDir . $rel);
|
||||
if (!$fsPath) {
|
||||
// Cache negative results too (404s won't re-stat)
|
||||
if (count($pathCache) < 10000) $pathCache[$urlPath] = null;
|
||||
if (count($pathCache) < 10000) $pathCache[$cacheKey] = null;
|
||||
return null;
|
||||
}
|
||||
$fsPath = str_replace(array('/','\\'), DS, $fsPath);
|
||||
$root = rtrim(self::$rootDir, DS);
|
||||
if ($fsPath !== $root && strncmp($fsPath, self::$rootDir, strlen(self::$rootDir)) !== 0) {
|
||||
$pathCache[$urlPath] = null;
|
||||
$pathCache[$cacheKey] = null;
|
||||
return null; // path traversal
|
||||
}
|
||||
$result = (is_dir($fsPath) || is_file($fsPath)) ? $fsPath : null;
|
||||
if (count($pathCache) < 10000) $pathCache[$urlPath] = $result;
|
||||
if (count($pathCache) < 10000) $pathCache[$cacheKey] = $result;
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
@@ -258,7 +258,8 @@ class Q_WebServer_Cache
|
||||
*/
|
||||
static function cacheKey($parsed)
|
||||
{
|
||||
$parts = $parsed['path'] . '?' . ($parsed['query'] ?? '');
|
||||
$host = $parsed['headers']['host'] ?? '';
|
||||
$parts = $host . $parsed['path'] . '?' . ($parsed['query'] ?? '');
|
||||
|
||||
// Include Accept-Encoding in key for compressed variants
|
||||
$ae = $parsed['headers']['accept-encoding'] ?? '';
|
||||
|
||||
Reference in New Issue
Block a user