mirror of
https://github.com/p1neappleXpress/OpenFlux.git
synced 2026-10-21 06:20:22 +08:00
* phpbox: survive any host's limits and keep streams alive across handovers Found on a local emulation of a free host (Apache + PHP-FPM, 60 s CPU per request, the host's disabled functions, a gateway that gives up on the request): - disabled set_time_limit/ignore_user_abort/getmypid/getenv/ini_set ended the node at once under PHP 8 (a disabled function is undefined): guarded everywhere; ping lists what the host took away - time budget per generation on both clocks (CPU on Linux, wall on Windows/macOS, RLIMIT_CPU), handover early when CPU runs short, and learning: a generation that died early leaves its age in host.json and the next ones plan below it; on a host not yet proven, handovers start at 40 s and grow as generations end well - cups: the carrier slept 18 ms per message inside the loop (a 64 KB frame ~0.4 s), so under a download new connections timed out and acks waited; now a paced outbox in the mux: control frames first, streams in turn, whole packets per message, at most ~72 KB/s (the server drops a member that sends faster) - two generations in one room: each now ignores the other's messages (and its own echoes, before decoding them), and messages carry whole packets, so a packet cut across messages is never mixed with another member's bytes; the Go cupsonline receiver keeps one reassembly buffer per member - a successor takes streams only after its link stayed up 8 s (a Mail.ru document drops new connections right after they join, and streams handed over then died) - a repeated OPEN is answered again instead of redialled; the client asks again after 4 s without an answer - the page restarts a node that ended without Stop while it is open - --mode=stream without --inbound is SOCKS5 on every OS again (macOS took utun) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * phpbox: send to cups only as fast as the server confirms; release rebuilds an existing tag - the cups server shows every member's message back to it; our own coming back is its confirmation. At most 6 unconfirmed messages: past that the server is behind, and the client's OPENs and its pings queued behind our echoes (close 3012 'no pong' under a download). The link's close code and reason go to the log. - release.yml: a moved tag replaces the release's assets and notes, as the apps do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * phpbox: count the cups server's echoes reliably; learn a host limit only from two deaths - echoes were matched with a regex on the raw line that never matched the server's JSON, so the window waited out its 3 s timeout each time (~6 KB/s under load): a substring test on the member uuid instead; a live room confirms 10 of 10 - one sudden end (a host restart) no longer teaches a limit that shortens every run for a week: two ends at about the same age do Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * phpbox: hand over early and consistently; never reach for a longer run than the host allows The emulated hosts exposed the real cause of the node dying: the 'hold idle to prove a longer run' recorded a lifetime (107s) longer than a host's hidden wall-clock kill (90s), so later generations set their handover past that limit and were killed before they could start a successor - the chain broke with no generation left to carry the lesson. - a generation now hands over at ~2/3 of the known limit, never later than 45s, even on a host that looks generous: a free host's real limit is unknown until a generation dies by it, and that death must happen after a successor exists. Deaths only lower it. - removed the idle hold and the lifetime-growth; a learned wall/CPU limit still lowers the handover, and a wall limit is believed only after two deaths at a similar age - while a successor shares the room, the predecessor caps its reading (OVERLAP_RATE): the document server copies our download to the successor too, and at full speed the client's OPENs to the successor waited behind it Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * CHANGELOG: the node-survival and stream-proxy fixes Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * CHANGELOG: fold the node-survival fixes into 0.3.0 (rebuilt in place) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: p1neappleXpress <a@a.a> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
5.1 KiB
PHP
145 lines
5.1 KiB
PHP
<?php
|
|
// Shared helpers for the phpbox exits (cups, mailru, ...).
|
|
|
|
final class PhpboxUtil
|
|
{
|
|
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
|
|
|
|
/** One HTTP request with a shared cookie jar file. Returns [body, status]. */
|
|
public static function http(string $url, string $cookieFile, string $method, ?string $body, array $headers): array
|
|
{
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_USERAGENT => self::UA,
|
|
CURLOPT_COOKIEJAR => $cookieFile,
|
|
CURLOPT_COOKIEFILE => $cookieFile,
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|
CURLOPT_TIMEOUT => 30,
|
|
CURLOPT_HTTPHEADER => $headers,
|
|
]);
|
|
if ($method === 'POST') {
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
|
}
|
|
$out = curl_exec($ch);
|
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
return [$out !== false ? $out : '', $code];
|
|
}
|
|
|
|
public static function scrape(string $re, string $html): string
|
|
{
|
|
return preg_match($re, $html, $m) ? $m[1] : '';
|
|
}
|
|
|
|
/** Netscape cookie-jar value lookup by name. */
|
|
public static function cookie(string $file, string $name): string
|
|
{
|
|
foreach (@file($file) ?: [] as $line) {
|
|
$p = explode("\t", trim($line));
|
|
if (count($p) === 7 && $p[5] === $name) {
|
|
return $p[6];
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
public static function cookieHeader(string $file): string
|
|
{
|
|
$out = [];
|
|
foreach (@file($file) ?: [] as $line) {
|
|
$c = explode("\t", trim($line));
|
|
if (count($c) === 7) {
|
|
$out[] = $c[5] . '=' . $c[6];
|
|
}
|
|
}
|
|
return implode('; ', $out);
|
|
}
|
|
|
|
public static function originOf(string $u): string
|
|
{
|
|
$p = parse_url($u);
|
|
return ($p['scheme'] ?? 'https') . '://' . ($p['host'] ?? '');
|
|
}
|
|
|
|
/** Host -> every address it has (IPv4 first), looked up once per run; [] when it does not resolve. */
|
|
public static function resolveAll(string $host): array
|
|
{
|
|
static $cache = [];
|
|
if (!isset($cache[$host])) {
|
|
if (filter_var($host, FILTER_VALIDATE_IP)) {
|
|
$cache[$host] = [$host];
|
|
} else {
|
|
$ips = @gethostbynamel($host) ?: [];
|
|
$cache[$host] = array_values(array_filter($ips, fn($ip) => filter_var($ip, FILTER_VALIDATE_IP)));
|
|
}
|
|
}
|
|
return $cache[$host];
|
|
}
|
|
|
|
/** Host -> its first address ('' when it does not resolve). */
|
|
public static function resolve(string $host): string
|
|
{
|
|
return self::resolveAll($host)[0] ?? '';
|
|
}
|
|
|
|
/** Local testing only: lifts the private-address and port guards. Never set on a real host. */
|
|
public static function testMode(): bool
|
|
{
|
|
return self::env('PHPBOX_ALLOW_PRIVATE') === '1' || (defined('PHPBOX_ALLOW_PRIVATE') && PHPBOX_ALLOW_PRIVATE === '1');
|
|
}
|
|
|
|
/** Refuse loopback / private / reserved targets (no SSRF into the host LAN): any address that is one blocks the host. */
|
|
public static function isPrivate(string $host): bool
|
|
{
|
|
if (self::testMode()) {
|
|
return false;
|
|
}
|
|
$ips = self::resolveAll($host);
|
|
if (!$ips) {
|
|
return true;
|
|
}
|
|
foreach ($ips as $ip) {
|
|
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Where run state and logs live: a writable dir that is not web-served when the host has one. */
|
|
/** getenv() where the host has it: a disabled function is gone in PHP 8, and calling it is a fatal error. */
|
|
public static function env(string $name)
|
|
{
|
|
return function_exists('getenv') ? getenv($name) : false;
|
|
}
|
|
|
|
/** This process's id, or a random stand-in where getmypid() is disabled. */
|
|
public static function pid(): int
|
|
{
|
|
static $pid = null;
|
|
if ($pid === null) { $pid = function_exists('getmypid') ? (int)getmypid() : random_int(100000, 999999); }
|
|
return $pid;
|
|
}
|
|
|
|
/**
|
|
* Ask the host to keep running when the request's connection is gone, and to lift the time limit, where those
|
|
* calls exist. Hosts disable them; in PHP 8 a disabled function is undefined and calling it ends the script.
|
|
*/
|
|
public static function keepRunning(bool $afterClose = true): void
|
|
{
|
|
if (function_exists('ignore_user_abort')) { @ignore_user_abort($afterClose); }
|
|
if (function_exists('set_time_limit')) { @set_time_limit(0); }
|
|
}
|
|
|
|
public static function stateDir(): string
|
|
{
|
|
$base = (is_dir('/home/tmp') && is_writable('/home/tmp')) ? '/home/tmp' : sys_get_temp_dir();
|
|
$dir = $base . '/phpbox-state';
|
|
if (!is_dir($dir)) {
|
|
@mkdir($dir, 0700, true);
|
|
}
|
|
return $dir;
|
|
}
|
|
}
|