<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*/
namespace seekquarry\yioop;
use seekquarry\yioop\configs as C;
use seekquarry\yioop\library as L;
use seekquarry\atto as A;
/**
* Used to make using Yioop through this index page rather than as part of
* a library
*/
const BASE_INDEX_ENTRY = true;
/**
* This will in turn load Config.php
*/
require_once __DIR__ . "/src/library/Utility.php";
/**
* Sends a request from the outer Yioop index.php file on to the inner one
* Marks that redirects are on
*
* @return bool always true once the inner index page has handled the
* request
*/
function passthruYioopRequest()
{
$web_site = $GLOBALS['web_site'];
$uri = $_SERVER['REQUEST_URI'];
$new_uri = preg_replace("@/(\.?/)+@", "/", $uri);
if (!empty($web_site) && $new_uri != $uri) {
$web_site->header("Location: $new_uri");
A\webExit();
}
require_once __DIR__ . "/src/index.php";
bootstrap($web_site);
return true;
}
/**
* Used to process the command line arguments to yioop when run in CLI
* mode as its own web server
*/
function processCommandLine()
{
global $argv;
define("seekquarry\\yioop\\configs\\IS_OWN_WEB_SERVER", true);
/*
secure-terminal / secure-start / secure-stop drive the TLS
launcher (bind 80 and 443 with a managed certificate) instead
of the single plain port. secure-terminal runs in this
process; secure-start daemonizes by re-spawning index.php as a
'child' (see CrawlDaemon::start), which drops the secure-*
word. So the secure listen address is set to the self-naming
marker 'secure-80-443' below: CrawlDaemon forwards it to the
child, which reads it back here as argv[3] to re-enable secure
mode, and it also names the daemon (startup line reads
"secure-80-443-index") so it is clear this is the secure
server and on which ports. The secure block ignores the
address otherwise. secure-stop maps to the plain stop, which
signals the secure daemon through the same lock and stop file.
The choice is recorded in a constant the listen-construction
step reads.
*/
$secure_marker = 'secure-80-443';
$is_secure_server = (!empty($argv[1]) && in_array($argv[1],
['secure-terminal', 'secure-start', 'secure-stop',
'secure-restart'], true)) ||
(!empty($argv[1]) && $argv[1] === $secure_marker) ||
(isset($argv[3]) && $argv[3] === $secure_marker);
define("seekquarry\\yioop\\configs\\SECURE_SERVER_MARKER",
$secure_marker);
if (!empty($argv[1])) {
if ($argv[1] === 'secure-terminal') {
$argv[1] = 'terminal';
} else if ($argv[1] === 'secure-start') {
$argv[1] = 'start';
} else if ($argv[1] === 'secure-stop') {
$argv[1] = 'stop';
} else if ($argv[1] === 'secure-restart') {
$argv[1] = 'restart';
}
}
$address = 8080;
$sleep = 0;
$session_file = "";
if (empty($argv[1])) {
echo "Yioop web server commands:\n\n";
echo " php index.php start [port] " .
"start in the background on a port\n";
echo " (default 8080)\n";
echo " php index.php terminal [port] " .
"run in this terminal, not in the\n";
echo " background\n";
echo " php index.php restart [port] " .
"restart a running server, or start\n";
echo " one if none is running\n";
echo " php index.php stop " .
"stop the running server\n";
echo " php index.php status " .
"show which Yioop processes are\n";
echo " running and on which " .
"ports\n\n";
echo "Add 443 as the port to serve HTTPS on the standard " .
"secure port.\n";
echo "The secure- forms bind both 80 and 443 with a managed " .
"certificate:\n";
echo " sudo php index.php secure-start\n";
echo " sudo php index.php secure-terminal\n";
echo " sudo php index.php secure-restart\n";
echo " sudo php index.php secure-stop\n";
exit();
} else {
if ($argv[1] == 'stop' || $argv[1] == 'status') {
return;
}
$possible_address = $argv[1];
$possible_sleep = 0;
if (empty($argv[2])) {
if (in_array($argv[1], ['start', 'terminal', 'child', 'restart'])) {
$possible_address = 8080;
} else {
$argv[1] = 'start';
}
} else {
$possible_sleep = $argv[2];
if (in_array($argv[1], ['start', 'terminal', 'child', 'restart'])) {
$possible_address = $argv[2];
if (empty($argv[3])) {
$possible_sleep = 0;
if ($argv[2] == 'none') {
$possible_address = 8080;
}
} else {
$possible_sleep = $argv[3];
if ($argv[2] == 'none') {
$possible_address = $argv[3];
if (empty($argv[4])) {
$possible_sleep = 0;
} else {
$possible_sleep = $argv[4];
}
if (!empty($argv[5])) {
$session_file = $argv[5];
}
} else if (!empty($argv[4])) {
$session_file = $argv[4];
}
}
} else if (!empty($argv[3])) {
if (intval($possible_sleep) > 0) {
$argv[1] = 'restart';
} else {
$argv[1] = 'start';
}
$session_file = $argv[3];
}
}
$tmp_address = intval($possible_address);
if ($tmp_address > 0 && $tmp_address < 65535) {
$address = $possible_address;
} else {
$url_parts = @parse_url($possible_address);
if(!empty($url_parts)) {
$address = $possible_address;
}
}
if (intval($possible_sleep) > 0 ) {
$sleep = $possible_sleep;
}
}
/* A bare "443" port (terminal 443 / start 443) means the user
wants HTTPS on the standard secure port. Turn on secure serving
for that one port: a self-signed certificate is written if none
exists and the port is served over TLS. This is the single-port
counterpart to secure-terminal / secure-start, which instead
bind both 80 and 443 for the full public set-up (HTTP-to-HTTPS
redirect and the ACME challenge on 80). Here only the one port
the user named is served. */
$is_single_secure_port = !$is_secure_server &&
intval($address) === 443;
define("seekquarry\\yioop\\configs\\IS_SECURE_SERVER",
$is_secure_server);
define("seekquarry\\yioop\\configs\\SECURE_SINGLE_PORT",
$is_single_secure_port);
$argv[2] = 'none';
$argv[3] = ($is_secure_server) ? $secure_marker : $address;
$argv[4] = $sleep;
$argv[5] = $session_file;
}
/**
* Creates a website object and specify some common routes.
* If run in CLI mode this will start the server linstening for connect
* web connections
*/
$web_site = new A\WebSite('.');
$web_site->get('/*', 'seekquarry\\yioop\\passthruYioopRequest');
$web_site->head('/*', 'seekquarry\\yioop\\passthruYioopRequest');
$web_site->post('/*', 'seekquarry\\yioop\\passthruYioopRequest');
$web_site->put('/*', 'seekquarry\\yioop\\passthruYioopRequest');
$web_site->delete('/*', 'seekquarry\\yioop\\passthruYioopRequest');
$web_site->options('/*', 'seekquarry\\yioop\\passthruYioopRequest');
$web_site->propfind('/*', 'seekquarry\\yioop\\passthruYioopRequest');
$web_site->mkcol('/*', 'seekquarry\\yioop\\passthruYioopRequest');
$web_site->move('/*', 'seekquarry\\yioop\\passthruYioopRequest');
$web_site->lock('/*', 'seekquarry\\yioop\\passthruYioopRequest');
$web_site->unlock('/*', 'seekquarry\\yioop\\passthruYioopRequest');
if ($web_site->isCli()) {
if ($argv[1] == 'status') {
/* Report what Yioop processes are running and, for the web
server, which port(s) it is listening on. Reads the same
lock files the server uses to track running daemons, so the
report reflects what is actually alive right now. */
$running = L\CrawlDaemon::statuses();
$labels = ["index" => "Web server",
"MediaUpdater" => "Media Updater",
"MailServer" => "Mail Server",
"QueueServer" => "Queue Server", "Fetcher" => "Fetcher"];
if (empty($running)) {
echo "No Yioop processes appear to be running.\n";
} else {
echo "Running Yioop processes:\n";
foreach ($running as $name => $instances) {
$label = $labels[$name] ?? $name;
$line = " " . $label;
if ($name == "index") {
$port_file = C\DATA_DIR . "/IndexServerPort.txt";
if (file_exists($port_file)) {
$line .= " (listening on port " . trim(
(string)file_get_contents($port_file)) .
")";
}
}
$count = count($instances);
if ($count > 1) {
$line .= " x" . $count;
}
echo $line . "\n";
}
}
exit();
}
$web_site->setTimer(10, function () use ($web_site) {
if (!C\PROFILE) {
return;
}
static $start_time = 0;
$global_stop_file = L\CrawlDaemon::GLOBAL_STOP_FILE_NAME;
if ($start_time == 0) {
$start_time = time();
}
if (file_exists($global_stop_file)) {
$stop_time = intval(file_get_contents($global_stop_file));
if ($stop_time > $start_time) {
echo "Stopping Server...";
$web_site->stop = true;
$web_site->restart = false;
/* clear this server's lock as part of honoring the
stop, so a follow-up start is not refused for
PROCESS_TIMEOUT because a request handled between
the stop command's unlink and shutdown recreated
the lock with a fresh timestamp
*/
L\CrawlDaemon::releaseLock();
$port_file = C\DATA_DIR . "/IndexServerPort.txt";
if (file_exists($port_file)) {
unlink($port_file);
}
} else {
unlink($global_stop_file);
}
}
$restart_request_file = C\DATA_DIR . "/RestartServer.txt";
if (file_exists($restart_request_file)) {
$request_time = intval(
file_get_contents($restart_request_file));
unlink($restart_request_file);
if ($request_time > $start_time) {
/* A command-line restart asked for this. Take the
background daemons down and record them for relaunch,
then restart this server the same way the admin
Restart Server action does, so there is one restart
path to maintain. */
echo "Restarting Server...";
L\CrawlDaemon::saveDaemonStateForRestart();
$session_path = C\DATA_DIR . "/PreviousSession.txt";
$web_site->saveSessionsForRestart($session_path);
$web_site->restart = "restart " . $session_path;
$web_site->stop = true;
}
}
$log_msg = "Current Memory Usage: " . memory_get_usage() .
" Peak usage:" . memory_get_peak_usage() . " " .
L\IndexManager::cacheReport() . " max-posting-read=" .
L\IndexDocumentBundle::$max_postings_read . "B";
L\crawlLog($log_msg);
});
if (file_exists(C\LOG_DIR)) {
// don't use logging until after log directory built on first run
$web_site->middleware(function () use ($web_site)
{
$lock_file = L\CrawlDaemon::getLockFileName("index", '');
/* in first run case, lock file won't be created by outer
instance as folder not created yet; skip the recreate
while stopping so a shutdown-window request does not
resurrect a lock the stop command just removed
*/
if (empty($web_site->stop) && !file_exists($lock_file)) {
L\CrawlDaemon::writeLockFile($lock_file, time());
}
$log_msg = $_SERVER['REMOTE_ADDR'] . " " .
$_SERVER['REQUEST_URI'] . " " . $web_site->requestProtocol() .
":" . ($_SERVER['SERVER_PORT'] ?? "");
L\crawlLog($log_msg);
});
$web_site->logResponses(function ($status, $size) use ($web_site) {
/* The arrival line above shows a request came in; this
companion line records how it turned out, so an oversized
or failing reply is recognizable by its status and byte
size rather than only by the path requested. */
$access_line = $_SERVER['REMOTE_ADDR'] . " " .
$_SERVER['REQUEST_URI'] . " " .
$web_site->requestProtocol() . ":" .
($_SERVER['SERVER_PORT'] ?? "") . " " . $status . " " .
$size . "B";
L\crawlLog($access_line);
});
register_shutdown_function(function () {
/* If the always-on server dies on a fatal error, a memory
exhaustion most of all, record which request was in flight
and the peak memory reached. A request that exhausts memory
while still building its reply never reaches the response
logger above, so without this the crashing request can only
be guessed at from the surrounding arrival lines. */
$fatal = error_get_last();
if ($fatal === null || !in_array($fatal['type'],
[E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
return;
}
$remote = $_SERVER['REMOTE_ADDR'] ?? "?";
$uri = $_SERVER['REQUEST_URI'] ?? "?";
L\crawlLog("Fatal while serving " . $remote . " " . $uri .
" peak_memory=" . memory_get_peak_usage() . " " .
$fatal['message']);
});
}
$session_info = null;
if (!empty($argv[5]) && file_exists($argv[5])) {
$session_info = unserialize(file_get_contents($argv[5]));
if ($argv[1] == 'restart') {
if(!empty($session_info['SESSIONS']['TERMINAL'])) {
$argv[1] = 'start';
} else {
$argv[1] = 'child';
}
}
} else if ($argv[1] == 'restart') {
/* Command-line restart. If a server is already running, leave
the work to it: drop a request file that it picks up on its
next tick and then restarts itself the same way the admin
Restart Server action does (stopping and relaunching the
background daemons, handing sessions across so people stay
signed in). Talking to it through a file rather than a web
request avoids opening an unauthenticated restart endpoint.
If nothing is running, say so and start a fresh server. */
$index_lock = L\CrawlDaemon::getLockFileName("index", '');
$index_lock_alt = L\CrawlDaemon::getLockFileName("index", '0');
$running = false;
foreach ([$index_lock, $index_lock_alt] as $lock) {
if (file_exists($lock)) {
$lock_time = intval(file_get_contents($lock));
$lock_process = L\CrawlDaemon::lockProcessId($lock);
if (time() - $lock_time < C\PROCESS_TIMEOUT &&
L\CrawlDaemon::isProcessAlive($lock_process)) {
$running = true;
break;
}
}
}
if ($running) {
$restart_request_file = C\DATA_DIR . "/RestartServer.txt";
file_put_contents($restart_request_file, time());
chmod($restart_request_file, 0777);
echo "Asked the running server to restart.\n";
/* Follow along until the restart finishes. First wait for
the running server to notice and remove the request file
(it polls on a timer, so this can take a few seconds);
that confirms the restart is under way. Then print each
restart phase the server records (stopping services,
waiting for and restarting the mail server, and so on)
as it changes, and finish once the phase has settled
back to empty, which the server does when it is serving
again. A generous overall cap stops the wait if no
progress is seen. */
$last_phase = "";
$waited = 0;
$max_wait = 120;
$picked_up = false;
$clear_streak = 0;
$done = false;
$relaunched = [];
$daemon_labels = ["MediaUpdater" => "Media Updater",
"MailServer" => "Mail Server"];
while ($waited < $max_wait) {
sleep(1);
$waited++;
clearstatcache(true, $restart_request_file);
if (!$picked_up) {
if (!file_exists($restart_request_file)) {
$picked_up = true;
echo " Restart under way...\n";
/* The server has recorded which background
services it is bringing back; read that list
now so every one can be named at the end,
even any whose progress message flicks past
between these once-a-second checks. */
$marker = C\DATA_DIR . "/RestartDaemons.txt";
if (file_exists($marker)) {
foreach (array_filter(array_map('trim',
explode("\n", (string)file_get_contents(
$marker)))) as $entry) {
$name = explode(" ", $entry)[0];
$relaunched[] = $daemon_labels[$name] ??
$name;
}
}
}
continue;
}
$phase = L\CrawlDaemon::getRestartStatus();
if ($phase !== "") {
$clear_streak = 0;
if ($phase !== $last_phase) {
echo " " . $phase . "\n";
$last_phase = $phase;
}
} else {
$clear_streak++;
/* a couple of clear readings in a row means the
server has finished and is serving again */
if ($clear_streak >= 3) {
$done = true;
break;
}
}
}
if ($done) {
echo "Restart complete; the web server is back up.\n";
if (!empty($relaunched)) {
echo "Services brought back up: " .
implode(", ", $relaunched) . ".\n";
}
} else if (!$picked_up) {
echo "The running server did not pick up the restart " .
"request; check the server log.\n";
} else {
echo "Restart did not confirm within " . $max_wait .
" seconds; check the server log.\n";
}
exit();
}
echo "No server appears to be running; starting a fresh one.\n";
$argv[1] = 'start';
}
if ($argv[1] == 'start' || $argv[1] == 'terminal') {
/* Yioop runs a single index web server. If one is already up, a
second start or terminal run (even on a different port) cannot
bind alongside it, so say so clearly and point at the right
next step rather than leaving the impression that two servers
on two ports are now running. A lock left behind by a server
that crashed or was stopped with Ctrl-C is recognized as stale
through its recorded process id and removed, so the next start
or terminal run is not blocked and needs no separate stop
first. */
$index_lock = L\CrawlDaemon::getLockFileName("index", '');
$index_lock_alt = L\CrawlDaemon::getLockFileName("index", '0');
foreach ([$index_lock, $index_lock_alt] as $lock) {
if (!file_exists($lock)) {
continue;
}
if (L\CrawlDaemon::lockHeldByLiveProcess($lock)) {
$port_file = C\DATA_DIR . "/IndexServerPort.txt";
$where = (file_exists($port_file)) ? trim(
(string)file_get_contents($port_file)) : "";
$on_port = ($where === "") ? "" :
" on port " . $where;
echo "An index web server is already running" .
$on_port . ". Yioop runs one index web server " .
"at a time, so it cannot also start on " .
"another port.\n";
echo "To move it to a different port, stop it " .
"first (php index.php stop), then start on " .
"the port you want. To restart it in place, " .
"use php index.php restart.\n";
exit();
}
unlink($lock);
}
}
L\CrawlDaemon::init($argv, "index");
L\crawlLog("\n\nInitialize logger..", "index", true);
/* Write this server's lock now, at startup. The per-request
middleware also writes it, but a server that has just
(re)started in the background as a daemon may sit with no
inbound request for several seconds; the periodic crawlLog
in the stop-watch timer calls CrawlDaemon::processHandler,
which exits a daemon whose lock file is missing. Creating the
lock here keeps the freshly restarted server alive until the
first request arrives. */
if (file_exists(C\SCHEDULES_DIR)) {
$index_lock_file = L\CrawlDaemon::getLockFileName("index", '');
if (!file_exists($index_lock_file)) {
L\CrawlDaemon::writeLockFile($index_lock_file, time());
}
/* Record which port(s) this server is listening on so that a
later start attempt can name them when it explains that one
index server is already running. */
if (C\nsdefined("IS_SECURE_SERVER") && C\IS_SECURE_SERVER) {
$listening_on = "80 and 443";
} else if (C\nsdefined("SECURE_SINGLE_PORT") &&
C\SECURE_SINGLE_PORT) {
$listening_on = "443";
} else {
$listening_on = (string)$argv[3];
}
file_put_contents(C\DATA_DIR . "/IndexServerPort.txt",
$listening_on);
}
if ($argv[4] > 0) {
sleep($argv[4]);
}
if (empty($session_info)) {
$session_info = ['SESSIONS' => []];
}
if ($argv[1] == 'terminal') {
$session_info['SESSIONS']['TERMINAL'] = true;
}
$context = [];
/* Let the server accept a request body as large as PHP itself is
configured to accept, rather than capping it at the atto
default (10MB). Without this, a file upload bigger than 10MB —
well within a typical PHP post_max_size — is refused before it
reaches Yioop and the browser shows a bare 400 Bad Request.
PHP's post_max_size is read and converted to a byte count; a
value of 0 means "no limit" in PHP, and any value below the
atto default is raised to it so a small or unset ini setting
cannot shrink what the server will take. */
$php_post_max = L\metricToInt(ini_get('post_max_size'));
$atto_default_max_request = 10000000;
if ($php_post_max <= 0) {
$context['MAX_REQUEST_LEN'] = PHP_INT_MAX;
} else if ($php_post_max < $atto_default_max_request) {
$context['MAX_REQUEST_LEN'] = $atto_default_max_request;
} else {
$context['MAX_REQUEST_LEN'] = $php_post_max;
}
if (C\nsdefined("SERVER_CONTEXT")) {
$context['SERVER_CONTEXT'] = C\SERVER_CONTEXT;
}
/* The names this server answers to, so the WebSite can set
$_SERVER['SERVER_NAME'] per request to the domain a visitor
actually used (validated against this list) rather than the
single name it bound to. Outbound mail links read SERVER_NAME,
so a visitor on one of several served domains gets links back
to that same domain. Passed as a plain list to keep the atto
WebSite free of Yioop config; an empty list leaves the
per-request naming off. */
$configured_domains = trim((string)C\SECURE_DOMAINS);
if ($configured_domains !== "") {
$context['SECURE_DOMAINS'] = array_values(array_filter(
array_map('trim', preg_split('/[\s,]+/',
$configured_domains))));
}
/* How often (in whole seconds) the running web server should write
a memory-usage line to the error log, or 0 to leave it off. It
is a switchable diagnostic for tracking slow memory growth in the
always-on server. Passed as a plain server value so the atto
WebSite stays free of Yioop config; the WebSite takes it back out
of its globals so it does not leak into $_SERVER. */
$context['WEBSITE_MEMORY_SAMPLE_PERIOD'] =
(int)C\WEBSITE_MEMORY_SAMPLE_PERIOD;
$context['SESSION_INFO'] = $session_info;
/* On a restart the WebSite re-spawns itself; for the secure
server it must come back binding 80 and 443, which the
launcher selects from this marker as argv[1] rather than the
single parsed address. Passing it as a server global lets the
atto WebSite reuse it without knowing anything Yioop-specific
about secure mode. */
if (C\IS_SECURE_SERVER) {
$context['RESTART_ADDRESS'] = C\SECURE_SERVER_MARKER;
}
$listen_address = $argv[3];
if (C\IS_SECURE_SERVER || C\SECURE_SINGLE_PORT) {
/*
Secure launcher: bind 80 and 443 from this one process
rather than the single port the plain modes use. Port
443 carries the managed certificate from SECURITY_DIR
(not SERVER_CONTEXT); port 80 is plain HTTP, used to
answer the ACME HTTP-01 challenge and otherwise redirect
to HTTPS. The privileged binds need root; WWW_USER (when
set) is the user the server drops to afterward, so the
long-running process does not hold root. Cert reload on
renewal updates the live listener context in place and
needs no rebind, so the drop does not block renewal.
*/
unset($context['SERVER_CONTEXT']);
$cert_file = C\SECURE_CERT_FILE;
$key_file = C\SECURE_KEY_FILE;
$has_cert = file_exists($cert_file) && file_exists($key_file);
if (!$has_cert) {
/* No managed certificate yet (for example a fresh
install). Generate a self-signed bootstrap certificate
so port 443 binds with a working certificate rather
than a missing one, which would accept connections and
then fail every TLS handshake. The server runs (with a
browser trust warning); when ACME is on, the HTTP-01
challenge on port 80 then replaces it with a real
certificate. Covers the configured secure domains so
the placeholder at least names the right hosts. */
$secure_domains = C\SECURE_DOMAINS;
$boot_domains = ($secure_domains === '') ? [] :
array_filter(array_map('trim',
explode(',', $secure_domains)));
$self_signed = L\CsrBuilder::selfSigned($boot_domains);
if ($self_signed !== null) {
$installer = new L\CertInstaller($cert_file,
$key_file);
$has_cert = $installer->writeFiles(
$self_signed['cert_pem'],
$self_signed['key_pem']);
}
if ($has_cert) {
L\crawlLog("Secure server: no managed certificate " .
"found; wrote a self-signed bootstrap " .
"certificate so 443 can bind. Browsers will warn " .
"until a real certificate is installed" .
(C\ACME_ON ? " (ACME will obtain one)." : "."));
} else {
L\crawlLog("Secure server: no managed certificate " .
"and could not write a self-signed bootstrap " .
"certificate; TLS on 443 will fail until a " .
"certificate is installed.");
}
}
$alpn = C\ALPN_PROTOCOLS;
$alpn_list = ($alpn === '') ? [] :
array_filter(array_map('trim', explode(',', $alpn)));
$ssl_context = [
'local_cert' => $cert_file,
'local_pk' => $key_file,
'allow_self_signed' => true,
'verify_peer' => false,
'alpn_protocols' => $alpn,
];
if (C\SECURE_SINGLE_PORT) {
/* single secure port: serve TLS on 443 only, with no
port-80 companion, since the user asked for one port. */
$listen_address = [
['address' => 'tcp://0.0.0.0:443',
'context' => ['ssl' => $ssl_context]],
];
} else {
$listen_address = [
['address' => 'tcp://0.0.0.0:443',
'context' => ['ssl' => $ssl_context]],
['address' => 'tcp://0.0.0.0:80'],
];
}
/* When h3 is among the selected protocols, add a QUIC
listener on UDP 443 carrying the same managed certificate.
It coexists with the TCP 443 listener: HTTP/2 and
HTTP/1.1 over TCP, HTTP/3 over QUIC, all advertised in the
TCP listener's ALPN list. The H3 listener opens only if
the optional QUIC pieces are present; otherwise it falls
through and the server keeps serving over TCP. */
$h3_on = in_array('h3', $alpn_list);
if ($h3_on) {
$listen_address[] = [
'address' => 'tcp://0.0.0.0:443',
'context' => ['ssl' => $ssl_context],
'protocol' => 'h3',
];
}
if (!empty(C\WWW_USER)) {
$context['USER'] = C\WWW_USER;
}
if (!C\ACME_ON) {
L\crawlLog("Secure server: ACME is OFF (ACME_ON is " .
"false); certificates will not be obtained or renewed " .
"automatically.");
} else {
L\crawlLog("Secure server: ACME is ON. Certificate " .
"renewal runs as a media job, so MediaUpdater must be " .
"running for certificates to update.");
}
if ($h3_on) {
L\crawlLog("Secure server: HTTP/3 selected; opening a " .
"QUIC listener on UDP 443. If the optional QUIC " .
"extensions are missing the server keeps serving " .
"over TCP (HTTP/2 and HTTP/1.1).");
}
}
$gc_window = max(intval(C\AUTOLOGOUT),
intval(ini_get('session.gc_maxlifetime')));
ini_set('session.gc_maxlifetime', (string)$gc_window);
if (C\IS_SECURE_SERVER || C\SECURE_SINGLE_PORT) {
/*
Watch the managed certificate file and refresh the live
TLS listener context whenever it changes, so a renewal
written by the AcmeRenew media job (which runs in the
MediaUpdater process, not this one) is served without a
restart. The once-a-minute stat is cheap; new handshakes
pick up the refreshed certificate while in-flight
connections finish on the one they started with.
*/
$secure_cert_file = C\SECURE_CERT_FILE;
$secure_key_file = C\SECURE_KEY_FILE;
$last_cert_mtime = file_exists($secure_cert_file)
? filemtime($secure_cert_file) : 0;
$web_site->setTimer(C\ONE_MINUTE,
function () use ($web_site, $secure_cert_file,
$secure_key_file, &$last_cert_mtime) {
clearstatcache(true, $secure_cert_file);
if (!file_exists($secure_cert_file)) {
return;
}
$mtime = filemtime($secure_cert_file);
if ($mtime <= $last_cert_mtime) {
return;
}
$last_cert_mtime = $mtime;
$web_site->reloadSecureCertificate($secure_cert_file,
$secure_key_file);
L\crawlLog("Secure server: certificate file changed; " .
"refreshed the live TLS listener context. New " .
"connections use the renewed certificate.");
});
}
$restart_daemons_marker = C\DATA_DIR . "/RestartDaemons.txt";
if (file_exists($restart_daemons_marker)) {
/* a Restart Server action or profile change stopped the
background daemons (MediaUpdater, MailServer) that had
been started through Manage Machines and recorded them
here; wait for each to finish exiting then relaunch it.
The waiting is done across repeating-timer ticks rather
than in one blocking loop so the freshly bound web server
keeps serving (and answering the restart-status poll)
while a daemon that is mid-download takes its time to
stop. The marker is removed up front so this fires once. */
$wanted = array_filter(array_map('trim',
explode("\n", (string) file_get_contents(
$restart_daemons_marker))));
unlink($restart_daemons_marker);
$relaunch_state = ["index" => 0, "waited" => 0, "id" => null];
$relaunch_timer = $web_site->setTimer(1,
function () use ($web_site, $wanted, &$relaunch_state) {
while ($relaunch_state["index"] < count($wanted)) {
$entry_parts = explode(" ",
$wanted[$relaunch_state["index"]]);
$daemon = $entry_parts[0];
$old_process_id = isset($entry_parts[1]) ?
intval($entry_parts[1]) : 0;
if (!in_array($daemon,
["MediaUpdater", "MailServer"], true)) {
$relaunch_state["index"]++;
$relaunch_state["waited"] = 0;
continue;
}
$daemon_label = ($daemon == "MediaUpdater") ?
"Media Updater" : "Mail Server";
$step = L\CrawlDaemon::drainDaemonStep($daemon,
$old_process_id, $relaunch_state["waited"]);
if ($step == "waiting") {
L\CrawlDaemon::setRestartStatus(
"Waiting for " . $daemon_label .
" to stop...");
$relaunch_state["waited"]++;
return;
}
$statuses = L\CrawlDaemon::statuses();
if (empty($statuses[$daemon])) {
L\CrawlDaemon::setRestartStatus(
"Starting " . $daemon_label . "...");
L\CrawlDaemon::start($daemon, "none", "", -1);
}
$relaunch_state["index"]++;
$relaunch_state["waited"] = 0;
}
L\CrawlDaemon::setRestartStatus("");
if ($relaunch_state["id"] !== null) {
$web_site->clearTimer($relaunch_state["id"]);
}
});
$relaunch_state["id"] = $relaunch_timer;
}
$web_site->listen($listen_address, $context);
} else {
$web_site->process();
}