From 42963cb71590b7341c54e741d53d2a5535abcdf8 Mon Sep 17 00:00:00 2001 From: flashwave Date: Sat, 25 Feb 2023 21:04:27 +0000 Subject: [PATCH] Include script that RemoteV2 interacts with. --- private/remote.php | 294 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 private/remote.php diff --git a/private/remote.php b/private/remote.php new file mode 100644 index 0000000..2fe1e19 --- /dev/null +++ b/private/remote.php @@ -0,0 +1,294 @@ + ':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"}';