Include script that RemoteV2 interacts with.
This commit is contained in:
parent
cddfe1b904
commit
42963cb715
1 changed files with 294 additions and 0 deletions
294
private/remote.php
Normal file
294
private/remote.php
Normal file
|
@ -0,0 +1,294 @@
|
|||
<?php
|
||||
// this is the source for the script that RemoteV2 interacts with
|
||||
|
||||
define('SRV_REQ_SEC', 'secret key goes here');
|
||||
define('SRV_REQ_LIFE', 60);
|
||||
define('SRV_DIR_FMT', '/srv/minecraft/%s');
|
||||
|
||||
function base64url_encode(string $input): string {
|
||||
return rtrim(strtr(base64_encode($input), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
function base64url_decode(string $input): string {
|
||||
return base64_decode(str_pad(strtr($input, '-_', '+/'), strlen($input) % 4, '=', STR_PAD_RIGHT));
|
||||
}
|
||||
|
||||
function rcon_open(int $port) {
|
||||
$conn = fsockopen('localhost', $port, $code, $message, 2);
|
||||
return $conn;
|
||||
}
|
||||
|
||||
function rcon_close($conn) {
|
||||
if(is_resource($conn))
|
||||
fclose($conn);
|
||||
}
|
||||
|
||||
function rcon_recv($conn): array {
|
||||
if(!is_resource($conn))
|
||||
return ['error' => ':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"}';
|
Loading…
Reference in a new issue