mirror of
https://github.com/Cacti/rrdproxy.git
synced 2026-10-26 21:17:18 +09:00
* Harden RRDtool proxy request handling * Address PR review feedback: encrypt oversized-request errors, fix env inheritance, spacing, and symlink test Addresses review comments on PR #17: - lib/client.php: encrypt the 'Request too large' error frame once the client is authenticated, matching the existing encrypted-error pattern - lib/client.php: build the removespikes child-process environment from getenv() instead of $_ENV so the full current environment (e.g. TEMP on Windows) is inherited, with RRDP_RRDTOOL_PATH merged on top - lib/client.php: add the missing space in the RRDTOOL_PIPE_UNAVAILABLE error frame so it matches the other 'ERROR: ...' responses - tests/run.php: check the symlink() fixture return value and skip the symlink-escape assertion (instead of producing a false positive) when symlink() is unavailable * Upgrade phpseclib 3.x to phpseclib/phpseclib ^4.0 - composer.json/composer.lock: require phpseclib/phpseclib ^4.0 (was ^3.0) - Update phpseclib3\\... namespace usages to phpseclib4\\... across rrdtool-proxy.php, lib/client.php, lib/functions.php, lib/wizard.php, and tests/run.php - lib/functions.php: phpseclib4 removed the Random class, so Random::string(32) is replaced with random_bytes(32) - Fix lib/__master__.php and lib/replicator.php, which PR #17 never migrated off the old pre-Composer phpseclib API (new \\phpseclib\\Crypt\\RSA(), ->loadKey(), ->getPublicKeyFingerprint() -- none of which exist in phpseclib 3.x or 4.x, so these MSR/replicator peer-authentication code paths would fatal at runtime). Both now use RSA::loadPublicKey()->getFingerprint('md5') wrapped in try/catch, matching the pattern already used in lib/client.php, plus a constant-time hash_equals() fingerprint comparison - tests/run.php: relabel the phpseclib round-trip check from 'phpseclib 3' to 'phpseclib 4' * Bump minimum PHP version to 8.2 and add CHANGELOG.md - composer.json: require php ^8.2 (was ^8.1) - include/global.php: bump RRDP_PHP_VERSION_REQUIRED to 80200 to match - README.md: update minimum/recommended PHP version to 8.2 - composer.lock: refreshed for the new php platform constraint - Add CHANGELOG.md tracking changes prefixed security:/issue:/feature: * Add PHPDoc blocks to all functions Adds a PHPDoc block above every function in rrdtool-proxy.php, lib/functions.php, lib/client.php, lib/__master__.php, lib/replicator.php, lib/wizard.php, cli/removespikes.php, tests/run.php, and .phpstan-bootstrap.php, each with a brief description and inferred @param/@return types. Function signatures themselves are unchanged; PHPStan will flag any inferred types that turn out to be inaccurate. * Add .github/copilot-instructions.md Generated from README.md and this session's learnings: project architecture (daemon entrypoint, shared lib helpers, client/master/replicator roles, wizard, removespikes CLI), build/lint/test commands (composer scripts, php tests/run.php, Windows platform-req caveats), and conventions (tabs, GPL header block, PHPDoc-only typing, phpseclib4 gotchas, CHANGELOG entry prefixes). Also fix .gitignore: the '.git*' pattern was unintentionally also matching and ignoring the .github/ directory; added a '!.github/' exception so files under .github/ (like this one) are tracked. * Add .gitattributes enforcing LF line endings; normalize existing CRLF files - Add .gitattributes with '* text=auto eol=lf' so Git normalizes line endings to LF on checkout/commit regardless of local core.autocrlf settings. - .gitignore: add '!.gitattributes' alongside the existing '!.github/' exception, since '.git*' was also unintentionally ignoring .gitattributes itself. - Normalize the two tracked files that had CRLF line endings (.github/copilot-instructions.md, CHANGELOG.md) to LF; no other tracked files were affected. * Manual PSR coding-style adjustments Manual formatting/coding-style cleanup in cli/removespikes.php, lib/__master__.php, lib/client.php, and lib/replicator.php. * Address automated review feedback: symlink confinement, key-pair validation, and misc fixes Addresses 10 unresolved copilot-pull-request-reviewer comments on PR #20: - lib/functions.php: rewrite rrdp_resolve_path_within() to walk the path one segment at a time (instead of only checking the nearest existing ancestor via file_exists()), so a symlink anywhere along a not-yet-fully-existing path is resolved and confined even if the symlink itself is dangling. - lib/functions.php: add rrdp_resolve_command_paths(), which resolves every RRD file path operand referenced by an RRDtool command (leading bare file arguments and DEF:/SDEF: clauses) against the RRA root and rewrites the command to use the canonical resolved paths, closing the symlink-escape gap left by the lexical-only rrdp_command_has_unsafe_path() check. - lib/client.php: wire rrdp_resolve_command_paths() into the RRDtool command dispatch path, rejecting/rewriting unsafe operands before they reach the RRDtool pipe. - lib/client.php: enforce the request-size limit on each completed transaction and the trailing unfinished remainder, instead of on the aggregate receive buffer before splitting on _EOT_, so multiple legitimate transactions in one read no longer trip a false 'Request too large'. - rrdtool-proxy.php: validate at startup that the loaded public/private key files actually form a matching pair (comparing derived fingerprints) instead of only deriving a fingerprint from the public key, failing startup instead of accepting requests that will all fail to decrypt. - lib/__master__.php, lib/replicator.php: resolve peer-supplied FULLSCAN folder/file names (and RRDLAST/RRDDUMP request filenames) through rrdp_resolve_rra_path() before using them in mkdir()/touch()/ file_put_contents()/RRDtool restore-dump-last-flushcached operations, instead of concatenating raw peer-supplied text; reject and skip entries that fail confinement. - lib/replicator.php, lib/__master__.php: guard rrdtool_pipe_init() failures in the main replication loop the same way lib/client.php already does, instead of indexing into a false return value and never retrying. - cli/removespikes.php: make createRRDFileFromXML() return whether the RRDtool restore actually succeeded, and exit non-zero when it fails instead of silently exiting 0. - .phpstan.neon: restore level 6 (was lowered to 2, contradicting .github/copilot-instructions.md) and add .phpstan-baseline.neon to capture the 42 pre-existing findings surfaced at level 6 without hiding future regressions. - tests/run.php: add the standard Cacti Group GPL header block required for every PHP file. --------- Co-authored-by: Thomas Vincent <thomasvincent@gmail.com>
583 lines
26 KiB
PHP
583 lines
26 KiB
PHP
<?php
|
|
/*
|
|
+-------------------------------------------------------------------------+
|
|
| Copyright (C) 2004-2026 The Cacti Group |
|
|
| |
|
|
| This program is free software; you can redistribute it and/or |
|
|
| modify it under the terms of the GNU General Public License |
|
|
| as published by the Free Software Foundation; either version 2 |
|
|
| of the License, or (at your option) any later version. |
|
|
| |
|
|
| This program is distributed in the hope that it will be useful, |
|
|
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
|
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
|
| GNU General Public License for more details. |
|
|
+-------------------------------------------------------------------------+
|
|
| Cacti: The Complete RRDTool-based Graphing Solution |
|
|
+-------------------------------------------------------------------------+
|
|
| This code is designed, written, and maintained by the Cacti Group. See |
|
|
| about.php and/or the AUTHORS file for specific developer information. |
|
|
+-------------------------------------------------------------------------+
|
|
| http://www.cacti.net/ |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
use phpseclib4\Crypt\RSA;
|
|
|
|
/**
|
|
* Handles a single client connection for its entire lifetime: performs the RSA
|
|
* public-key handshake, then authenticates, decrypts, and dispatches each request
|
|
* (RRDtool commands, custom commands, and removespikes) to its handler, encrypting
|
|
* and writing back every response until the client disconnects or times out.
|
|
*
|
|
* @param resource|\Socket $socket_client
|
|
*
|
|
* @return void
|
|
*/
|
|
function interact($socket_client) {
|
|
/*
|
|
Clients using the default port are only allowed to talk to RRDtool directly.
|
|
By using IPC this child process will interact with the main process.
|
|
*/
|
|
global $rrdcached_pid, $rrdp_config, $ipc_socket_parent, $rrdtool_cmds, $rrdtool_custom_cmds;
|
|
global $rrdtool_msr_cmds, $rrdp_status, $rrdtool_env_vars, $rrdp_client_cnn_params;
|
|
global $encryption;
|
|
|
|
if ($rrdcached_pid) {
|
|
putenv('RRDCACHED_ADDRESS=unix:' . realpath('') . '/run/rrdcached.sock');
|
|
}
|
|
|
|
// switch into the rra directory
|
|
chdir($rrdp_config['path_rra']);
|
|
|
|
set_time_limit(0);
|
|
|
|
$encryption = true;
|
|
$rrdp_status_backup = $rrdp_status;
|
|
$rrdtool_process = false;
|
|
$rrdtool_pipes = [];
|
|
$client_authenticated = false;
|
|
$client_public_key = '';
|
|
$end_of_packet = "_EOP_\r\n";
|
|
$end_of_sequence = "_EOT_\r\n";
|
|
|
|
$tv_sec = $rrdp_config['remote_cnn_timeout'];
|
|
|
|
// avoid zombies
|
|
socket_set_option($socket_client,SOL_SOCKET, SO_RCVTIMEO, ['sec'=>600, 'usec'=>0]);
|
|
socket_set_block($socket_client);
|
|
socket_set_block($ipc_socket_parent);
|
|
|
|
$real_client_resource_id = rrdp_system__get_resource_id($socket_client);
|
|
$ipc_parent_resource_id = rrdp_system__get_resource_id($ipc_socket_parent);
|
|
|
|
$input = '';
|
|
|
|
while (1) {
|
|
// setup clients listening to socket for reading
|
|
$read = [];
|
|
$read[0] = $socket_client;
|
|
$read[1] = $ipc_socket_parent;
|
|
|
|
$ready = socket_select($read, $write, $except, $tv_sec);
|
|
|
|
switch ($err = socket_last_error()) {
|
|
case 0:
|
|
socket_clear_error();
|
|
|
|
break;
|
|
default:
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'Critical error occurred: ' . $err, 'IPC', SEVERITY_LEVEL_CRITICAL);
|
|
|
|
break 2;
|
|
}
|
|
|
|
if ($ready > 0) {
|
|
foreach ($read as $read_socket_index => $read_socket) {
|
|
$socket_resource_id = rrdp_system__get_resource_id($read_socket);
|
|
|
|
if ($socket_resource_id == $real_client_resource_id) {
|
|
// RRDtool client is talking to us
|
|
|
|
while (1) {
|
|
$recv = @socket_read($read_socket, 100000, PHP_BINARY_READ);
|
|
|
|
if ($recv === false) {
|
|
// timeout
|
|
rrdp_system__socket_write($socket_client, (($client_authenticated) ? encrypt(RRD_ERROR . 'Timeout', $client_public_key) : RRD_ERROR . 'Timeout') . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'Client connection timeout detected', 'IPC', SEVERITY_LEVEL_WARNING);
|
|
$rrdp_status['status'] = 'CLOSEDOWN_BY_TIMEOUT';
|
|
|
|
break 3;
|
|
}
|
|
|
|
if ($recv === '') {
|
|
// socket session dropped by client
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'Connection dropped by client', 'IPC', SEVERITY_LEVEL_NOTIFICATION);
|
|
$rrdp_status['status'] = 'CLOSEDOWN_BY_CLIENT_DROP';
|
|
|
|
break 3;
|
|
} else {
|
|
$input .= $recv;
|
|
$max_input_size = $client_authenticated ? RRDP_MAX_REQUEST_SIZE : RRDP_MAX_KEY_SIZE;
|
|
|
|
if (strpos($input, $end_of_sequence) !== false) { // end of one or more transactions detected
|
|
rrdp_system__count('bytes_received', rrdp_system__calc_bytes($input));
|
|
|
|
$transactions = explode($end_of_sequence, $input);
|
|
$input = array_pop($transactions);
|
|
|
|
foreach ($transactions as $transaction) {
|
|
if (strlen($transaction) > $max_input_size) {
|
|
rrdp_system__socket_write($socket_client, (($client_authenticated) ? encrypt(RRD_ERROR . ' Request too large', $client_public_key) : RRD_ERROR . ' Request too large') . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'Client request exceeded the maximum frame size', 'IPC', SEVERITY_LEVEL_WARNING);
|
|
$rrdp_status['status'] = 'CLOSEDOWN_BY_VIOLATION';
|
|
|
|
break 4;
|
|
}
|
|
|
|
if (!$client_authenticated) {
|
|
if (strpos($transaction, '-----BEGIN PUBLIC KEY-----') === false) {
|
|
// authentication failed !!!
|
|
rrdp_system__socket_write($socket_client, 'Authentication failed' . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'Client Authentication failed by invalid key format', 'IPC', SEVERITY_LEVEL_ERROR);
|
|
$rrdp_status['status'] = 'CLOSEDOWN_BY_CLIENT_AUTH';
|
|
|
|
break 4;
|
|
} else {
|
|
$client_public_key = $transaction;
|
|
socket_getpeername($socket_client, $ip);
|
|
$rsa_finger_print = isset($rrdp_config['remote_clients'][$ip]) ? $rrdp_config['remote_clients'][$ip] : 'unknown';
|
|
|
|
try {
|
|
$public_key = RSA::loadPublicKey($client_public_key);
|
|
$client_fingerprint = $public_key->getFingerprint('md5');
|
|
} catch (Throwable $e) {
|
|
$client_fingerprint = false;
|
|
}
|
|
|
|
if ($client_fingerprint !== false && hash_equals(strtolower($rsa_finger_print), strtolower($client_fingerprint))) {
|
|
// registered public key has been received
|
|
$client_authenticated = true;
|
|
// send out proxy's public key
|
|
rrdp_system__socket_write($socket_client, $rrdp_config['encryption']['public_key'] . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'Client Authentication successful', 'IPC', SEVERITY_LEVEL_DEBUG);
|
|
|
|
continue;
|
|
} else {
|
|
// authentication failed !!!
|
|
rrdp_system__socket_write($socket_client, 'Authentication failed' . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'Client Authentication failed by invalid public key', 'IPC', SEVERITY_LEVEL_ERROR);
|
|
$rrdp_status['status'] = 'CLOSEDOWN_BY_CLIENT_AUTH';
|
|
|
|
break 4;
|
|
}
|
|
}
|
|
} else {
|
|
$transaction = decrypt($transaction);
|
|
|
|
if ($transaction === false) {
|
|
rrdp_system__socket_write($socket_client, 'Decryption error' . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'Client message decryption failed.', 'IPC', SEVERITY_LEVEL_ERROR);
|
|
$rrdp_status['status'] = 'CLOSEDOWN_BY_DECRYPTION';
|
|
|
|
break 4;
|
|
}
|
|
|
|
if (strpos($transaction, "\x1f\x8b") === 0) {
|
|
$transaction = gzdecode($transaction, RRDP_MAX_DECOMPRESSED_REQUEST_SIZE);
|
|
|
|
if ($transaction === false) {
|
|
rrdp_system__socket_write($socket_client, encrypt(RRD_ERROR . ' Invalid compressed request', $client_public_key) . $end_of_sequence);
|
|
$rrdp_status['status'] = 'CLOSEDOWN_BY_VIOLATION';
|
|
|
|
break 4;
|
|
}
|
|
}
|
|
|
|
// take care of invalid blanks
|
|
$transaction = trim($transaction);
|
|
|
|
if (strpos($transaction, ' ') != false) {
|
|
$options = explode(' ', $transaction, 2);
|
|
$cmd = $options[0];
|
|
$cmd_options = $options[1];
|
|
} else {
|
|
$cmd = $transaction;
|
|
$cmd_options = '';
|
|
}
|
|
|
|
// try to react as similar as possible
|
|
if (!$cmd) {
|
|
$cmd = 'info';
|
|
}
|
|
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'REQUEST: ' . $cmd . ' ' . $cmd_options , 'IPC', SEVERITY_LEVEL_DEBUG);
|
|
|
|
if (in_array($cmd, $rrdtool_cmds, true) === true) {
|
|
if (rrdp_command_has_unsafe_path($cmd_options)) {
|
|
rrdp_system__socket_write($socket_client, encrypt(RRD_ERROR . ' Unsafe path or command framing', $client_public_key) . $end_of_sequence);
|
|
rrdp_system__count('queries_rrdtool_invalid');
|
|
|
|
continue;
|
|
}
|
|
|
|
$resolved_cmd_options = rrdp_resolve_command_paths($cmd, $cmd_options);
|
|
|
|
if ($resolved_cmd_options === false) {
|
|
rrdp_system__socket_write($socket_client, encrypt(RRD_ERROR . ' Unsafe path or command framing', $client_public_key) . $end_of_sequence);
|
|
rrdp_system__count('queries_rrdtool_invalid');
|
|
|
|
continue;
|
|
}
|
|
|
|
$cmd_options = $resolved_cmd_options;
|
|
|
|
rrdp_system__count('queries_rrdtool_total');
|
|
|
|
// open a persistent RRDtool pipe only on demand
|
|
if ($rrdtool_process === false) {
|
|
$rrdtool_process_pipes = rrdtool_pipe_init($rrdp_config);
|
|
|
|
if ($rrdtool_process_pipes === false) {
|
|
rrdp_system__count('rrd_pipe_broken');
|
|
rrdp_system__socket_write($socket_client, encrypt(RRD_ERROR . ' RRDTOOL_PIPE_UNAVAILABLE', $client_public_key) . $end_of_sequence);
|
|
|
|
continue;
|
|
}
|
|
|
|
$rrdtool_process = $rrdtool_process_pipes[0];
|
|
$rrdtool_pipes = $rrdtool_process_pipes[1];
|
|
}
|
|
|
|
$auto_compression = in_array($cmd, ['xport', 'fetch', 'dump', 'graph', 'graphv', 'updatev ', 'info'], true) && strpos($cmd_options, '--imgformat=PNG') === false;
|
|
$rrd_exec_status = rrdtool_pipe_execute($cmd . ' ' . $cmd_options . "\r\n", $rrdtool_pipes, $socket_client, $client_public_key, $auto_compression, false, $end_of_sequence);
|
|
|
|
if ($rrd_exec_status === true) {
|
|
rrdp_system__count('queries_rrdtool_valid');
|
|
|
|
// update local cache by valid RRDtool commands for MSR
|
|
if (__sizeof($rrdp_config['remote_proxies']) > 0 && in_array($cmd , $rrdtool_msr_cmds, true) === true) {
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'MSR: ' . $cmd . ' ' . $cmd_options , 'MSR', SEVERITY_LEVEL_DEBUG);
|
|
[$mtime,$time] = explode(' ',microtime());
|
|
|
|
$mtime = (float) $mtime;
|
|
$time = (int) $time;
|
|
$offset = $time % 10;
|
|
$block = $time - $offset + 10;
|
|
|
|
$rrdp_status['msr_commands'][$block][$offset + $mtime . '_' . $socket_resource_id] = $cmd . ' ' . $cmd_options;
|
|
|
|
if (__sizeof($rrdp_status['msr_commands']) > 1 || $time >= $block) {
|
|
$msr_block = key($rrdp_status['msr_commands']);
|
|
|
|
$msr_message['type'] = 'msr';
|
|
|
|
$msr_message['msr_commands'][$msr_block] = $rrdp_status['msr_commands'][$msr_block];
|
|
|
|
if (@socket_write($ipc_socket_parent, serialize($msr_message) . "\r\n")) {
|
|
unset($rrdp_status['msr_commands'][$msr_block]);
|
|
unset($msr_message);
|
|
$rrdp_status = $rrdp_status_backup;
|
|
usleep(10000);
|
|
}
|
|
}
|
|
}
|
|
} elseif ($rrd_exec_status === null) {
|
|
// RRDtool pipe broken
|
|
rrdp_system__count('rrd_pipe_broken');
|
|
rrdp_system__socket_write($socket_client, encrypt(RRD_ERROR . 'RRDTOOL_PIPE_BROKEN', $client_public_key) . $end_of_sequence);
|
|
rrdtool_pipe_close($rrdtool_process);
|
|
$rrdtool_process = false;
|
|
} else {
|
|
// client tried to execute an invalid or unsupported RRDtool command
|
|
rrdp_system__count('queries_rrdtool_invalid');
|
|
}
|
|
} elseif (in_array($cmd, ['exit', 'quit', 'shutdown'], true) === true) {
|
|
// client wants to quit on a regular basis
|
|
$rrdp_status['status'] = 'CLOSEDOWN_BY_CLIENT_CMD';
|
|
|
|
break 4;
|
|
} elseif (in_array($cmd, $rrdtool_custom_cmds, true)) {
|
|
$rrdp_exec_status = false;
|
|
$options = explode(' ', $cmd_options);
|
|
|
|
switch ($cmd) {
|
|
case 'setenv':
|
|
if (__sizeof($options) >= 2 && in_array($options[0], $rrdtool_env_vars, true)) {
|
|
$enviro_var = array_shift($options);
|
|
$enviro_value = str_replace(["\0", "\r", "\n"], '', implode(' ', $options));
|
|
putenv($enviro_var . '=' . $enviro_value);
|
|
rrdp_system__socket_write($socket_client, encrypt(RRD_OK, $client_public_key) . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'RESPONSE: ' . RRD_OK, 'IPC', SEVERITY_LEVEL_DEBUG);
|
|
} else {
|
|
rrdp_system__socket_write($socket_client, encrypt(RRD_ERROR . ' % setenv <name> <value>', $client_public_key) . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'RESPONSE: ' . RRD_ERROR, 'IPC', SEVERITY_LEVEL_DEBUG);
|
|
}
|
|
|
|
break;
|
|
case 'getenv':
|
|
if (__sizeof($options) == 1 && in_array($options[0], $rrdtool_env_vars, true)) {
|
|
$output = getenv($options[0]);
|
|
rrdp_system__socket_write($socket_client, encrypt($output . "\n" . RRD_OK, $client_public_key) . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'RESPONSE: ' . $output . "\n" . RRD_OK, 'IPC', SEVERITY_LEVEL_DEBUG);
|
|
} else {
|
|
rrdp_system__socket_write($socket_client, encrypt(RRD_ERROR . ' % getenv <name>', $client_public_key) . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'RESPONSE: ' . RRD_ERROR, 'IPC', SEVERITY_LEVEL_DEBUG);
|
|
}
|
|
|
|
break;
|
|
case 'setcnn':
|
|
if (__sizeof($options) == 2 && in_array($options[0], $rrdp_client_cnn_params, true)) {
|
|
if ($options[0] == 'timeout') {
|
|
if (in_array($options[1], ['null', 'off', 'disabled', '-1'], true)) {
|
|
$tv_sec = null;
|
|
rrdp_system__socket_write($socket_client, encrypt("% Timeout disabled.\n" . RRD_OK, $client_public_key) . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'RESPONSE: ' . RRD_OK, 'IPC', SEVERITY_LEVEL_DEBUG);
|
|
} elseif (is_numeric($options[1])) {
|
|
$tv_sec = abs($options[1]);
|
|
rrdp_system__socket_write($socket_client, encrypt('% Timeout ' . $tv_sec . "s.\n" . RRD_OK, $client_public_key) . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'RESPONSE: ' . RRD_OK, 'IPC', SEVERITY_LEVEL_DEBUG);
|
|
} else {
|
|
rrdp_system__socket_write($socket_client, encrypt(RRD_ERROR . ' % Out of range.', $client_public_key) . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'RESPONSE: ' . RRD_ERROR, 'IPC', SEVERITY_LEVEL_DEBUG);
|
|
}
|
|
}
|
|
} else {
|
|
rrdp_system__socket_write($socket_client, encrypt(RRD_ERROR . ' % setcnn <parameter> <value>', $client_public_key) . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'RESPONSE: ' . RRD_ERROR, 'IPC', SEVERITY_LEVEL_DEBUG);
|
|
}
|
|
|
|
break;
|
|
case 'fc-list':
|
|
$output = shell_exec('fc-list');
|
|
rrdp_system__socket_write($socket_client, encrypt($output . "\n" . RRD_OK, $client_public_key) . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'RESPONSE: ' . RRD_OK, 'IPC', SEVERITY_LEVEL_DEBUG);
|
|
|
|
break;
|
|
case 'rrd-list':
|
|
// scan RRA folder
|
|
clearstatcache();
|
|
$buffer = '';
|
|
$rra_path_absolute = realpath($rrdp_config['path_rra']);
|
|
|
|
if ($rra_path_absolute === false) {
|
|
rrdp_system__socket_write($socket_client, encrypt(RRD_ERROR . ' RRA path is unavailable', $client_public_key) . $end_of_sequence);
|
|
|
|
break;
|
|
}
|
|
|
|
$dir_iterator = new RecursiveDirectoryIterator($rra_path_absolute);
|
|
$iterator = new RecursiveIteratorIterator($dir_iterator, RecursiveIteratorIterator::SELF_FIRST);
|
|
|
|
foreach ($iterator as $file) {
|
|
$resolved_path = realpath($file->getPathname());
|
|
|
|
if ($resolved_path !== false
|
|
&& rrdp_path_is_within($resolved_path, $rra_path_absolute)
|
|
&& str_ends_with(strtolower($resolved_path), '.rrd')) {
|
|
$buffer .= ltrim(substr($resolved_path, strlen($rra_path_absolute)), DIRECTORY_SEPARATOR) . ',' . $file->getSize() . ',' . $file->getMTime() . "\r\n";
|
|
}
|
|
}
|
|
|
|
$buffer .= RRD_OK;
|
|
|
|
$buffer_length = strlen($buffer);
|
|
$buffer = gzencode($buffer,1);
|
|
$buffer_length_new = strlen($buffer);
|
|
|
|
rrdp_system__socket_write($read_socket, encrypt($buffer, $client_public_key) . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'RESPONSE: ' . RRD_OK . ', payload: ' . $buffer_length_new . ' Bytes, compression: on , ratio: ' . round(($buffer_length / $buffer_length_new),2) . ' , packets: 1', 'IPC', SEVERITY_LEVEL_DEBUG);
|
|
|
|
break;
|
|
case 'filemtime':
|
|
$path = __sizeof($options) === 1 ? rrdp_resolve_rra_path($options[0]) : false;
|
|
|
|
$rrdp_exec_return = $path !== false ? filemtime($path) : false;
|
|
|
|
rrdp_system__socket_write($socket_client, encrypt($rrdp_exec_return . "\r\n" . RRD_OK, $client_public_key) . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'RESPONSE: ' . $rrdp_exec_return . "\r\n" . RRD_OK, 'IPC', SEVERITY_LEVEL_DEBUG);
|
|
|
|
break;
|
|
case 'file_exists':
|
|
case 'is_dir':
|
|
$path = __sizeof($options) === 1 ? rrdp_resolve_rra_path($options[0]) : false;
|
|
|
|
$rrdp_exec_status = $path !== false && $cmd($path);
|
|
|
|
rrdp_system__socket_write($socket_client, encrypt(($rrdp_exec_status === true) ? RRD_OK : RRD_ERROR , $client_public_key) . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'RESPONSE: ' . (($rrdp_exec_status === true) ? RRD_OK : RRD_ERROR), 'IPC', SEVERITY_LEVEL_DEBUG);
|
|
|
|
break;
|
|
case 'mkdir':
|
|
$path = __sizeof($options) === 1 ? rrdp_resolve_rra_path($options[0], false) : false;
|
|
|
|
$rrdp_exec_status = $path !== false && mkdir($path, 0700, true);
|
|
|
|
rrdp_system__socket_write($socket_client, encrypt(($rrdp_exec_status === true) ? RRD_OK : RRD_ERROR , $client_public_key) . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'RESPONSE: ' . (($rrdp_exec_status === true) ? RRD_OK : RRD_ERROR), 'IPC', SEVERITY_LEVEL_DEBUG);
|
|
|
|
break;
|
|
case 'archive':
|
|
$source_file = __sizeof($options) === 1 ? rrdp_resolve_rra_path($options[0]) : false;
|
|
$archive_base = $rrdp_config['path_rra_archive'] ? realpath($rrdp_config['path_rra_archive']) : false;
|
|
|
|
if ($source_file !== false && str_ends_with(strtolower($source_file), '.rrd') && $archive_base !== false) {
|
|
$relative = ltrim(substr($source_file, strlen(realpath($rrdp_config['path_rra']))), DIRECTORY_SEPARATOR);
|
|
$target_file = rrdp_resolve_path_within($archive_base, $relative, false);
|
|
|
|
if ($target_file !== false) {
|
|
$target_dir = dirname($target_file);
|
|
|
|
if (!is_dir($target_dir)) {
|
|
mkdir($target_dir, 0700, true);
|
|
}
|
|
|
|
$rrdp_exec_status = rename($source_file, $target_file);
|
|
}
|
|
} else {
|
|
$rrdp_exec_status = false;
|
|
}
|
|
|
|
rrdp_system__socket_write($socket_client, encrypt((($rrdp_exec_status === true) ? RRD_OK : RRD_ERROR), $client_public_key) . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'RESPONSE: ' . (($rrdp_exec_status === true) ? RRD_OK : RRD_ERROR), 'IPC', SEVERITY_LEVEL_DEBUG);
|
|
|
|
break;
|
|
case 'unlink':
|
|
$path = __sizeof($options) === 1 ? rrdp_resolve_rra_path($options[0]) : false;
|
|
|
|
$rrdp_exec_status = $path !== false && str_ends_with(strtolower($path), '.rrd') && unlink($path);
|
|
|
|
rrdp_system__socket_write($socket_client, encrypt((($rrdp_exec_status === true) ? RRD_OK : RRD_ERROR), $client_public_key) . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'RESPONSE: ' . (($rrdp_exec_status === true) ? RRD_OK : RRD_ERROR), 'IPC', SEVERITY_LEVEL_DEBUG);
|
|
|
|
break;
|
|
case 'removespikes':
|
|
$parsed_options = rrdp_parse_removespikes_options($cmd_options);
|
|
|
|
// inherit the full process environment (getenv() unlike $_ENV isn't gated by variables_order)
|
|
$environment = array_merge(getenv(), ['RRDP_RRDTOOL_PATH' => $rrdp_config['path_rrdtool']]);
|
|
$result = $parsed_options === false ? false : rrdp_run_process(array_merge([PHP_BINARY, '-q', $rrdp_config['path_cli'] . '/removespikes.php'], $parsed_options), $environment);
|
|
$rrdp_exec_status = $result !== false && $result['status'] === 0;
|
|
$rrdp_exec_return = $result !== false ? $result['stdout'] . $result['stderr'] : 'Invalid removespikes options';
|
|
|
|
$response = $rrdp_exec_return . "\r\n" . ($rrdp_exec_status ? RRD_OK : RRD_ERROR);
|
|
rrdp_system__socket_write($socket_client, encrypt($response, $client_public_key) . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'RESPONSE: ' . $response, 'IPC', SEVERITY_LEVEL_DEBUG);
|
|
|
|
break;
|
|
case 'version':
|
|
rrdp_system__socket_write($socket_client, encrypt(RRDP_VERSION_FULL . "\r\n" . RRD_OK, $client_public_key) . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'RESPONSE: ' . RRDP_VERSION_FULL . "\r\n" . RRD_OK, 'IPC', SEVERITY_LEVEL_DEBUG);
|
|
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// update local cache for MSR
|
|
if ($rrdp_exec_status === true) {
|
|
if (in_array($cmd , $rrdtool_msr_cmds, true) === true) {
|
|
[$mtime,$time] = explode(' ',microtime());
|
|
|
|
$mtime = (float) $mtime;
|
|
$time = (int) $time;
|
|
$offset = $time % 10;
|
|
$block = $time - $offset + 10;
|
|
|
|
$rrdp_status['msr_commands'][$block][$offset + $mtime . '_' . $socket_resource_id] = $cmd . ' ' . $cmd_options;
|
|
|
|
if (__sizeof($rrdp_status['msr_commands']) > 1 || $time >= $block) {
|
|
$msr_block = key($rrdp_status['msr_commands']);
|
|
|
|
$msr_message['type'] = 'msr';
|
|
|
|
$msr_message['msr_commands'][$msr_block] = $rrdp_status['msr_commands'][$msr_block];
|
|
|
|
if (@socket_write($ipc_socket_parent, serialize($msr_message) . "\r\n")) {
|
|
unset($rrdp_status['msr_commands'][$msr_block]);
|
|
unset($msr_message);
|
|
$rrdp_status = $rrdp_status_backup;
|
|
usleep(10000);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// kick that client if it does not know how to talk to me
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'Client is using invalid commands.', 'IPC', SEVERITY_LEVEL_ERROR);
|
|
rrdp_system__socket_write($socket_client, encrypt(RRD_ERROR . ' % Invalid input detected', $client_public_key) . $end_of_sequence);
|
|
|
|
$rrdp_status['status'] = 'CLOSEDOWN_BY_VIOLATION';
|
|
|
|
break 4;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (strlen($input) > $max_input_size) {
|
|
rrdp_system__socket_write($socket_client, (($client_authenticated) ? encrypt(RRD_ERROR . ' Request too large', $client_public_key) : RRD_ERROR . ' Request too large') . $end_of_sequence);
|
|
__logging(LOGGING_LOCATION_BUFFERED, 'Client request exceeded the maximum frame size', 'IPC', SEVERITY_LEVEL_WARNING);
|
|
$rrdp_status['status'] = 'CLOSEDOWN_BY_VIOLATION';
|
|
|
|
break 3;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} elseif ($socket_resource_id == $ipc_parent_resource_id) {
|
|
// RRDtool proxy is talking to us
|
|
|
|
while (1) {
|
|
$recv = @socket_read($ipc_socket_parent, 8192, PHP_BINARY_READ);
|
|
|
|
if ($recv === false) {
|
|
// timeout
|
|
break;
|
|
}
|
|
|
|
if ($recv == '') {
|
|
// socket session dropped by proxy
|
|
break;
|
|
} else {
|
|
$input .= $recv;
|
|
|
|
if (substr($input, -1) == "\n") {
|
|
rrdp_system__count('bytes_received', rrdp_system__calc_bytes($input));
|
|
|
|
$status = unserialize(trim($input));
|
|
|
|
if ($status !== false && $status['type']) {
|
|
switch($status['type']) {
|
|
case 'shutdown':
|
|
rrdp_system__socket_write($socket_client, (($client_authenticated) ? encrypt(RRD_ERROR . 'Shutting down ...', $client_public_key) : RRD_ERROR . 'Shutting down ...') . $end_of_sequence);
|
|
socket_close($ipc_socket_parent);
|
|
exit;
|
|
case 'status':
|
|
socket_write($ipc_socket_parent, "running\r\n");
|
|
|
|
break 4;
|
|
default:
|
|
break 3;
|
|
}
|
|
}
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
rrdp_system__socket_write($socket_client, 'Timeout' . $end_of_sequence);
|
|
$rrdp_status['status'] = 'CLOSEDOWN_BY_TIMEOUT';
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($rrdtool_process) {
|
|
rrdtool_pipe_close($rrdtool_process);
|
|
}
|
|
}
|