-- * bin/client/zagan-bet-client---.exe * bin/server/zagan-bet-server--- * * is MAJOR.MINOR.PATCH, is linux|windows|macos and * is x86_64|i686|aarch64. Example: * * bin/client/zagan-bet-client-3.1.1-linux-x86_64 * bin/server/zagan-bet-server-3.1.1-linux-x86_64 * * SHA-256 values are cached in a JSON file keyed on size and mtime, so a file * is only re-read when it changes. The cache lives in the system temp * directory by default (the web root is often not writable by the PHP user); * set ZAGAN_HASH_CACHE to put it somewhere else. */ const KINDS = [ 'client' => [ 'title' => 'Client', 'hint' => 'Needs a terminal of at least 80×24 cells.', ], 'server' => [ 'title' => 'Server', 'hint' => 'Generates its own TLS identity on first run.', ], ]; const PLATFORMS = [ 'linux' => 'Linux', 'windows' => 'Windows', 'macos' => 'macOS', ]; const ARCHES = [ 'x86_64' => '64-bit', 'i686' => '32-bit', 'aarch64' => 'ARM64', ]; // Newest version first, then 64-bit builds before 32-bit. const PLATFORM_ORDER = ['linux' => 0, 'windows' => 1, 'macos' => 2]; const ARCH_ORDER = ['x86_64' => 0, 'aarch64' => 1, 'i686' => 2]; function e(?string $value): string { return htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function human_size(int $bytes): string { if ($bytes >= 1048576) { return number_format($bytes / 1048576, 1) . ' MB'; } if ($bytes >= 1024) { return number_format($bytes / 1024) . ' KB'; } return $bytes . ' B'; } /** * Hash to use for a published file, reusing the cached value when the file has * not changed since it was hashed. * * A cached hash is valid only when the size and mtime still match and the hash * was taken at or after the file's mtime, so a restored or touched file is * re-read. * * @param array{files:array} $cache */ function cached_hash( string $key, string $path, int $size, int $mtime, array &$cache, bool &$dirty ): ?string { $entry = $cache['files'][$key] ?? null; if ( is_array($entry) && ($entry['size'] ?? null) === $size && ($entry['mtime'] ?? null) === $mtime && (int) ($entry['hashed_at'] ?? 0) >= $mtime && isset($entry['sha256']) && is_string($entry['sha256']) && preg_match('/\A[0-9a-f]{64}\z/', $entry['sha256']) === 1 ) { return $entry['sha256']; } $hash = hash_file('sha256', $path); if ($hash === false) { return null; } $cache['files'][$key] = [ 'size' => $size, 'mtime' => $mtime, 'sha256' => $hash, 'hashed_at' => time(), ]; $dirty = true; return $hash; } /** @return array{files:array} */ function load_hash_cache(string $path): array { $raw = @file_get_contents($path); if ($raw === false || $raw === '') { return ['files' => []]; } $decoded = json_decode($raw, true); if (!is_array($decoded) || !isset($decoded['files']) || !is_array($decoded['files'])) { return ['files' => []]; } return ['files' => $decoded['files']]; } /** * Best effort: a read-only web root, a full disk or a racing writer must never * break the page, so failures are ignored and the next request recomputes. * * @param array{files:array} $cache */ function save_hash_cache(string $path, array $cache): void { $json = json_encode($cache, JSON_UNESCAPED_SLASHES); if ($json === false) { return; } $directory = dirname($path); $temporary = @tempnam($directory, '.zagan-hashes-'); if ($temporary === false) { return; } if (@file_put_contents($temporary, $json, LOCK_EX) === false || !@rename($temporary, $path)) { @unlink($temporary); } } /** * @return array{version:string,platform:string,arch:string,file:string,size:int,mtime:int,sha256:string}|null */ function read_release( string $kind, string $directory, string $entry, array &$cache, bool &$dirty ): ?array { $pattern = '/^zagan-bet-' . preg_quote($kind, '/') . '-v?(\d+\.\d+\.\d+)-([a-z]+)-([a-z0-9_]+)(\.exe)?$/i'; if (preg_match($pattern, $entry, $matches) !== 1) { return null; } $realDirectory = realpath($directory); $real = realpath($directory . '/' . $entry); if ($realDirectory === false || $real === false) { return null; } // Never serve anything that resolves outside the published directory. if (!str_starts_with($real, $realDirectory . DIRECTORY_SEPARATOR)) { return null; } if (!is_file($real)) { return null; } $size = filesize($real); $mtime = filemtime($real); if ($size === false || $mtime === false) { return null; } $hash = cached_hash($kind . '/' . $entry, $real, $size, $mtime, $cache, $dirty); if ($hash === null) { return null; } return [ 'version' => $matches[1], 'platform' => strtolower($matches[2]), 'arch' => strtolower($matches[3]), 'file' => $entry, 'size' => $size, 'mtime' => $mtime, 'sha256' => $hash, ]; } $base = rtrim(str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME'] ?? '/')), '/'); if ($base === '.' || $base === '/') { $base = ''; } $cachePath = getenv('ZAGAN_HASH_CACHE'); if (!is_string($cachePath) || $cachePath === '') { $cachePath = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'zagan-bet-hashes-' . substr(hash('sha256', __DIR__), 0, 16) . '.json'; } $cache = load_hash_cache($cachePath); $cacheDirty = false; $seen = []; $releases = []; $newest = 0; foreach (KINDS as $kind => $meta) { $found = []; $directory = __DIR__ . '/bin/' . $kind; if (is_dir($directory)) { $entries = scandir($directory); if ($entries !== false) { foreach ($entries as $entry) { if ($entry === '.' || $entry === '..') { continue; } $release = read_release($kind, $directory, $entry, $cache, $cacheDirty); if ($release === null) { continue; } $seen[$kind . '/' . $entry] = true; $release['href'] = $base . '/bin/' . $kind . '/' . rawurlencode($release['file']); $found[] = $release; $newest = max($newest, $release['mtime']); } } } usort($found, static function (array $a, array $b): int { $byVersion = version_compare($b['version'], $a['version']); if ($byVersion !== 0) { return $byVersion; } $byPlatform = (PLATFORM_ORDER[$a['platform']] ?? 9) <=> (PLATFORM_ORDER[$b['platform']] ?? 9); if ($byPlatform !== 0) { return $byPlatform; } $byArch = (ARCH_ORDER[$a['arch']] ?? 9) <=> (ARCH_ORDER[$b['arch']] ?? 9); if ($byArch !== 0) { return $byArch; } return strcmp($a['file'], $b['file']); }); $releases[$kind] = $found; } // Drop cache entries for files that are no longer published. foreach (array_keys($cache['files']) as $key) { if (!isset($seen[$key])) { unset($cache['files'][$key]); $cacheDirty = true; } } if ($cacheDirty) { save_hash_cache($cachePath, $cache); } ?> żagań.bet

żagań.bet

Official client and server builds.

0): ?>

Builds published UTC.

Client

Needs a terminal of at least 80×24 cells.

Nothing published yet.

Server

Generates its own TLS identity on first run.

Nothing published yet.

SHA-256

On Linux use sha256sum <file>. On Windows use Get-FileHash <file>.

Then simply check if the checksum matches with your file.

Running it

Client:

chmod +x <downloaded-client>
./<downloaded-client>

Server, first run creates identity.p12, key.pem and cert.pem:

chmod +x <downloaded-server>
./<downloaded-server> --init
./<downloaded-server> -i 0.0.0.0 -p 11525

Windows: run the .exe from Windows Terminal or another real terminal.

When lost, execute help command inside zagan.bet client. Server binary supports --help flag