':rcon:conn']; extract(unpack('Vlength', fread($conn, 4))); if($length < 10) return ['error' => ':rcon:length']; extract(unpack('VreqId/Vopcode', fread($conn, 8))); $body = substr(fread($conn, $length - 8), 0, -2); return compact('reqId', 'opcode', 'body'); } function rcon_send($conn, int $opcode, string $text): int { if(!is_resource($conn)) return -1; $length = 10 + strlen($text); $reqId = random_int(1, 0x7FFFFFFF); fwrite($conn, pack('VVV', $length, $reqId, $opcode) . $text . "\0\0"); return $reqId; } function rcon_send_large($conn, int $opcode, string $text): array { if(!is_resource($conn)) return ['error' => ':rcon:conn']; $reqId = rcon_send($conn, $opcode, $text); if($reqId < 1) return ['error' => ':rcon:request']; $trailer = rcon_send($conn, 2, 'time query gametime'); if($trailer < 1) return ['error' => ':rcon:trailer']; $opcode = 0; $body = ''; for(;;) { $resp = rcon_recv($conn); if(!empty($resp['error'])) return $resp; if($resp['reqId'] === $trailer) break; if($resp['reqId'] !== $reqId) continue; if($resp['opcode'] !== 0) return ['error' => ':rcon:opcode']; $body .= $resp['body']; } return compact('reqId', 'trailer', 'opcode', 'body'); } function rcon_open_props(array $props) { if(empty($props['rcon.port'])) die('{"error":":conf:rcon-port"}'); if(empty($props['rcon.password'])) die('{"error":":conf:rcon-passwd"}'); return rcon_open($props['rcon.port']); } function rcon_auth_props($conn, array $props): void { if(empty($props['rcon.password'])) die('{"error":":conf:rcon-passwd"}'); rcon_send($conn, 3, $props['rcon.password']); $resp = rcon_recv($conn); if(!empty($resp['error'])) die(json_encode(['error' => $resp['error']])); } function rcon_get_whitelist($conn): array { $resp = rcon_send_large($conn, 2, 'whitelist list'); if(!empty($resp['error'])) return $resp; $halfs = explode(':', $resp['body'], 2); if(empty($halfs[1])) return []; $names = explode(',', $halfs[1]); foreach($names as &$name) $name = trim($name); return array_values(array_filter($names)); } function rcon_add_whitelist($conn, array $userNames): array { $results = []; foreach($userNames as $name) { rcon_send($conn, 2, 'whitelist add ' . $name); // todo: sanitise username $resp = rcon_recv($conn); $results[$name] = [ 'success' => str_starts_with($resp['body'], 'Added '), 'message' => $resp['body'], ]; } rcon_send($conn, 2, 'whitelist reload'); rcon_recv($conn); // discard reload message return compact('results'); } function rcon_remove_whitelist($conn, array $userNames): array { $results = []; foreach($userNames as $name) { rcon_send($conn, 2, 'whitelist remove ' . $name); // todo: sanitise username $resp = rcon_recv($conn); $results[$name] = [ 'success' => str_starts_with($resp['body'], 'Removed '), 'message' => $resp['body'], ]; } rcon_send($conn, 2, 'whitelist reload'); rcon_recv($conn); // discard reload message return compact('results'); } function read_server_props(string $serverPath, string $fileName = 'server.properties'): array { $props = []; $path = realpath($serverPath . '/' . $fileName); if(is_file($path)) { $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach($lines as $line) { if(empty($line) || str_starts_with($line, '#')) continue; $parts = explode('=', $line, 2); if(count($parts) == 2) { $value = $parts[1]; if($value === 'false' || $value === 'true') $value = $value === 'true'; elseif(ctype_digit($value)) $value = (int)$value; elseif(is_numeric($value)) $value = (float)$value; $props[$parts[0]] = $value; } } } return $props; } function create_request_hash(int $time = -1, ?array $params = null, ?string $method = null, ?string $path = null): string { if($time < 0) $time = time(); if($params === null) $params = $_REQUEST ?? []; if($method === null) $method = $GLOBALS['reqMethod'] ?? ''; if($path === null) $path = $GLOBALS['reqPath'] ?? ''; ksort($params); $compare = []; $stringify = null; // use() gets sad if it's not defined yet $stringify = function(array $arr, string $prefix = '') use(&$compare, &$stringify) { foreach($arr as $name => $value) { if(is_array($value)) $stringify($value, $name . ';'); else $compare[] = "{$name}:{$value}"; } }; $stringify($params); $input = "{$time}%{$method} {$path}%" . implode('#', $compare); return hash_hmac('sha256', $input, SRV_REQ_SEC, true); } function verify_request_hash(): bool { $realTime = time(); $lifeTimeHalf = (int)ceil(SRV_REQ_LIFE / 2); $userHash = base64url_decode((string)filter_input(INPUT_SERVER, 'HTTP_X_MINCE_SIGNATURE')); $userTime = (int)filter_input(INPUT_SERVER, 'HTTP_X_MINCE_TIMESTAMP', FILTER_SANITIZE_NUMBER_INT); if(strlen($userHash) !== 32 || $userTime < ($realTime - $lifeTimeHalf) || $userTime > ($realTime + $lifeTimeHalf)) return false; return hash_equals(create_request_hash($userTime), $userHash); } function get_server_path(string $serverId): string { if(empty($serverId) || !ctype_alnum($serverId)) die('{"error":":req:id"}'); $path = sprintf(SRV_DIR_FMT, $serverId); if(!is_dir($path)) die('{"error":":req:server"}'); return $path; } $reqMethod = (string)filter_input(INPUT_SERVER, 'REQUEST_METHOD'); $reqPath = '/' . trim(parse_url((string)filter_input(INPUT_SERVER, 'REQUEST_URI'), PHP_URL_PATH), '/'); header('Content-Type: application/json; charset=utf-8'); if(!verify_request_hash()) die('{"error":":request:verification"}'); if($reqMethod === 'GET' && $reqPath === '/') { $dirs = glob(__DIR__ . '/../*'); $servers = []; foreach($dirs as $dir) if(is_file($dir . '/server.properties') && !is_file($dir . '/.dead')) $servers[] = basename($dir); echo json_encode(compact('servers')); return; } if($reqMethod === 'GET' && $reqPath === '/whitelist') { $sPath = get_server_path((string)filter_input(INPUT_GET, 'server')); $sProps = read_server_props($sPath); $rcon = rcon_open_props($sProps); try { rcon_auth_props($rcon, $sProps); echo json_encode(['list' => rcon_get_whitelist($rcon)]); } finally { rcon_close($rcon); } return; } if($reqMethod === 'POST' && $reqPath === '/whitelist') { $wNames = json_decode((string)filter_input(INPUT_POST, 'names')); $sPath = get_server_path((string)filter_input(INPUT_POST, 'server')); $sProps = read_server_props($sPath); $rcon = rcon_open_props($sProps); try { rcon_auth_props($rcon, $sProps); echo json_encode(rcon_add_whitelist($rcon, $wNames)); } finally { rcon_close($rcon); } return; } if($reqMethod === 'DELETE' && $reqPath === '/whitelist') { $wNames = json_decode((string)filter_input(INPUT_GET, 'names')); $sPath = get_server_path((string)filter_input(INPUT_GET, 'server')); $sProps = read_server_props($sPath); $rcon = rcon_open_props($sProps); try { rcon_auth_props($rcon, $sProps); echo json_encode(rcon_remove_whitelist($rcon, $wNames)); } finally { rcon_close($rcon); } return; } echo '{"error":":request:notfound"}';