Files
rrdproxy/lib/replicator.php
TheWitnessandThomas Vincent 629971cc73 Harden RRDtool proxy request handling (#20)
* 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>
2026-09-19 09:04:05 -04:00

917 lines
34 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;
/**
* Main loop of the replicator engine: connects out to registered cluster peers
* and listens for inbound peer connections, authenticating each by RSA public-key
* fingerprint, then drives full and incremental (DIFF-based) synchronization with
* the peer that reports the highest uptime, servicing IPC requests from the
* parent process along the way.
*
* @return void
*/
function interact() {
global $rrdp_remoteproxies, $ipc_socket_parent, $debug_mode, $rrdp_config, $rrdp_remote_proxies;
global $rrdp_encryption, $rrdtool_cmds, $rrdtool_custom_cmds, $rrdp_replicator_cmds;
global $rrdtool_msr_cmds, $rrdp_remote_clients, $rrdp_status, $rrdtool_env_vars, $rrdp_client_cnn_params;
global $rrdcached_pid, $rrdtool_process, $rrdtool_pipes, $rrdp_encryption;
if ($rrdcached_pid) {
putenv('RRDCACHED_ADDRESS=unix:' . realpath('') . '/run/rrdcached.sock');
}
$memory_table = [];
$rrdp_status_backup = $rrdp_status;
$rrdtool_process = false;
$full_synchronized = false;
$shutdown_triggered = false;
$rrdp_remoteproxies = [];
$rrdp_replicator_state = 'running';
// start listening for cluster peers
$rrdp_server = @socket_create((($rrdp_config['ipv6']) ? AF_INET : AF_INET), SOCK_STREAM, SOL_TCP);
if (!$rrdp_server) {
rrd_system__system_die(PHP_EOL . 'Unable to create socket. Error: ' . socket_strerror(socket_last_error()) . PHP_EOL);
}
@socket_set_option($rrdp_server, SOL_SOCKET, SO_REUSEADDR, 1);
if (!@socket_bind($rrdp_server, $rrdp_config['address'], $rrdp_config['port_server'])) {
rrd_system__system_die(PHP_EOL . "Unable to bind socket to '" . $rrdp_config['address'] . ':' . $rrdp_config['port_server'] . "'" . PHP_EOL);
}
socket_set_option($rrdp_server,SOL_SOCKET, SO_RCVTIMEO, ['sec'=>15, 'usec'=>0]);
socket_set_nonblock($rrdp_server);
socket_set_nonblock($ipc_socket_parent);
rrd_system__system_boolean_message('init: replicator engine [PID: ' . getmypid() . ']', $rrdp_server, true);
// replicator startup - connect to registered peers automatically
if ($rrdp_remote_proxies && sizeof($rrdp_remote_proxies) > 0) {
foreach ($rrdp_remote_proxies as $proxy_ip => $proxy_settings) {
// first off verify that replication subfolders have been created for the case that a new server has been setup with an old configuration
if (!is_dir('./msr/' . $proxy_ip)) {
mkdir('./msr/' . $proxy_ip);
}
$remote_socket = __remote_connect($proxy_ip, $proxy_settings['port'], $proxy_settings['fingerprint']);
if ($remote_socket !== false) {
$rrdp_remoteproxies[intval($remote_socket['socket'])] = $remote_socket;
}
}
}
// start listening for incoming requests by other new peers
socket_listen($rrdp_server);
$__replicator_listening = true;
while ($__replicator_listening) {
// setup a dedicated persistent RRD_PIPE
if ($rrdtool_process === false) {
$rrdtool_process_pipes = rrdtool_pipe_init($rrdp_config);
if ($rrdtool_process_pipes === false) {
rrdp_system__count('rrd_pipe_broken');
continue;
}
$rrdtool_process = $rrdtool_process_pipes[0];
$rrdtool_pipes = $rrdtool_process_pipes[1];
}
$write = [];
$except = [];
$tv_sec = 1;
// setup clients listening to socket for reading
$read = [];
$read[0] = $rrdp_server;
$read[1] = $ipc_socket_parent;
// all remote proxy connections need to be monitored for changes, too
foreach ($rrdp_remoteproxies as $rrdp_remoteproxy) {
$read[] = $rrdp_remoteproxy['socket'];
}
$ready = socket_select($read, $write, $except, $tv_sec);
if ($ready) {
foreach ($read as $read_socket_index => $read_socket) {
if ($read_socket == $rrdp_server) {
// a proxy server is trying to connect
$socket_descriptor = socket_accept($read_socket);
socket_getpeername($socket_descriptor, $ip);
// verify authorization
if (array_key_exists($ip, $rrdp_remote_proxies) === true) {
$key = intval($socket_descriptor);
$rrdp_remoteproxies[$key] = [ 'socket' => $socket_descriptor, 'ip' => $ip, 'public_key' => false, 'authenticated' => false, 'last_seen' => time()];
__logging(LOGGING_LOCATION_BUFFERED, 'Remote Proxy connection request #' . $key . '[IP: ' . $ip . '] granted', 'ACL', SEVERITY_LEVEL_DEBUG);
} else {
@socket_write($socket_descriptor, "ERROR: Access denied.\r\n");
@socket_close($socket_descriptor);
rrdp_system__count('connections_refused');
__logging(LOGGING_LOCATION_BUFFERED, 'Remote Proxy connection request [IP: ' . $ip . '] rejected.', 'ACL', SEVERITY_LEVEL_WARNING);
}
} elseif ($read_socket == $ipc_socket_parent) {
// ===> Mom is calling <=== so shut up and listen :):):)
$input = '';
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") {
$status = unserialize(trim($input));
if ($status !== false && $status['type']) {
switch($status['type']) {
case 'reload_running_config':
if (isset($status['rrdp_config'])) {
$rrdp_config = $status['rrdp_config'];
__logging(LOGGING_LOCATION_BUFFERED, 'Running config reloaded.', 'SYS', SEVERITY_LEVEL_DEBUG);
}
break 2;
case 'reload_proxy_list':
$reloaded = @include('./include/proxies');
if ($reloaded) {
__logging(LOGGING_LOCATION_BUFFERED, 'Remote proxies list successfully reloaded.', 'SYS', SEVERITY_LEVEL_DEBUG);
} else {
__logging(LOGGING_LOCATION_BUFFERED, 'Unable to refresh remote proxies list.', 'SYS', SEVERITY_LEVEL_ERROR);
}
break 2;
case 'status':
// TODO - Provide more information about our connection status
socket_write($ipc_socket_parent, "running\r\n");
break 2;
case 'shutdown':
$shutdown_triggered = true;
rrdp_system__socket_write($socket_client, (($client_authenticated) ? encrypt(RRD_ERROR . 'Shutting down ...', $client_public_key) : RRD_ERROR . 'Shutting down ...') . "\r\n");
socket_close($ipc_socket_parent);
exit;
default:
break;
}
}
} else {
continue;
}
}
}
} else {
/* handle established remote proxy connections
- established does here not automatically mean authenticated.
*/
$index = intval($read_socket);
if (isset($rrdp_remoteproxies[$index])) {
$input = '';
$output = '';
// Remote proxy is talking to us
while (1) {
$recv = @socket_read($read_socket, 100000, PHP_BINARY_READ);
if ($recv === false) {
// keep the connection persistent
break;
}
if ($recv == '') {
// socket session dropped by client
rrdp_system__socket_close($read_socket, false, true);
unset($rrdp_remoteproxies[$index]);
$rrdp_status['status'] = 'CLOSEDOWN_BY_CLIENT_DROP';
__logging(LOGGING_LOCATION_BUFFERED, 'Connection dropped by remote client #' . intval($read_socket), 'MSR', SEVERITY_LEVEL_WARNING);
break;
} else {
$input .= $recv;
if (strpos($input, "\n") !== false) {
rrdp_system__count('bytes_received', rrdp_system__calc_bytes($input));
if ($rrdp_remoteproxies[$index]['authenticated'] === false) {
if (strpos($input, '-----BEGIN PUBLIC KEY-----') === false) {
// authentication failed !!!
rrdp_system__socket_close($read_socket, "Authentication failed\r\n");
unset($rrdp_remoteproxies[$index]);
$rrdp_status['status'] = 'CLOSEDOWN_BY_CLIENT_AUTH';
__logging(LOGGING_LOCATION_BUFFERED, 'Authentication failed - Invalid communication sequence #' . intval($read_socket), 'MSR', SEVERITY_LEVEL_WARNING);
break;
}
if (strpos($input, '-----END PUBLIC KEY-----') !== false) {
$client_public_key = trim($input);
$ip = $rrdp_remoteproxies[$index]['ip'];
$rsa_finger_print = isset($rrdp_remote_proxies[$ip]) ? $rrdp_remote_proxies[$ip]['fingerprint'] : 'unknown';
try {
$remote_fingerprint = RSA::loadPublicKey($client_public_key)->getFingerprint('md5');
} catch (Throwable $e) {
$remote_fingerprint = false;
}
if ($remote_fingerprint !== false && hash_equals(strtolower($rsa_finger_print), strtolower($remote_fingerprint))) {
// registered public key has been received
$rrdp_remoteproxies[$index]['authenticated'] = true;
$rrdp_remoteproxies[$index]['public_key'] = $client_public_key;
$rrdp_remoteproxies[$index]['last_seen'] = time();
__logging(LOGGING_LOCATION_BUFFERED, 'Authentication successful #' . intval($read_socket), 'MSR', SEVERITY_LEVEL_DEBUG);
// send out proxy's public key
rrdp_system__socket_write($read_socket, $rrdp_encryption['public_key'] . "\r\n");
$input = '';
continue 2;
} else {
// authentication failed !!!
rrdp_system__socket_close($read_socket, "Authentication failed\r\n");
__logging(LOGGING_LOCATION_BUFFERED, 'Authentication failed - Fingerprints not matching #' . intval($read_socket), 'MSR', SEVERITY_LEVEL_WARNING);
unset($rrdp_remoteproxies[$index]);
$rrdp_status['status'] = 'CLOSEDOWN_BY_CLIENT_AUTH';
break;
}
} else {
continue;
}
} else {
$chunks = explode("\n", $input);
$input = array_pop($chunks);
foreach ($chunks as $chunk) {
$output .= decrypt(trim($chunk), $rrdp_encryption);
if (strpos($output, "\x1f\x8b") === 0) {
$output = gzdecode($output);
}
if (substr_count($output, 'END_OF_MSG')) {
$output = str_replace('END_OF_MSG', '', $output);
$rrdp_remoteproxies[$index]['last_seen'] = time();
break 2;
}
}
}
}
}
}
$time = time();
__logging(LOGGING_LOCATION_BUFFERED, $time . 'RECEIVED #[IP: ' . $rrdp_remoteproxies[intval($read_socket)]['ip'] . ']: ' . $output, 'MSR', SEVERITY_LEVEL_DEBUG);
handle__request($output, $read_socket);
__logging(LOGGING_LOCATION_BUFFERED, $time . 'RECEIVED #[IP: ' . $rrdp_remoteproxies[intval($read_socket)]['ip'] . ']: ' . $output, 'MSR', SEVERITY_LEVEL_DEBUG);
// sleep(1);
}
}
}
} else {
// non-recurring full synchronization
if ($full_synchronized === false & $rrdp_replicator_state == 'running') {
if (microtime(true) - $rrdp_config['start'] >= 15) {
$remote_uptime_max = 0;
$remote_index = false;
// ask every peer for status
foreach ($rrdp_remoteproxies as $index => $rrdp_remoteproxy) {
$remote_socket = $rrdp_remoteproxy['socket'];
$public_key = $rrdp_remoteproxy['public_key'];
rrdp_system__socket_write($remote_socket, encrypt('STATUS END_OF_MSG', $public_key) . "\r\n", 'msr_bytes_sent');
$response = __remote_read($remote_socket);
if ($response !== false) {
$remote_uptime = $response[2];
$local_uptime = microtime(true) - $rrdp_config['start'];
// TODO -- race condition - better you focus on delta instead of using absolute uptime
if ($remote_uptime > $local_uptime && $remote_uptime > $remote_uptime_max) {
$remote_uptime_max = $remote_uptime;
$remote_index = $index;
}
} else {
// remote proxy did not answer :|
}
}
if ($remote_index) {
$rrdp_replicator_state = 'synchronizing';
$read_socket = $rrdp_remoteproxies[$remote_index]['socket'];
$public_key = $rrdp_remoteproxies[$remote_index]['public_key'];
$ip = $rrdp_remoteproxies[$remote_index]['ip'];
__logging(LOGGING_LOCATION_BUFFERED, 'Start full synchronisation process with #' . intval($read_socket) . ' [IP: ' . $ip . ']', 'MSR', SEVERITY_LEVEL_NOTIFICATION);
rrdp_system__socket_write($read_socket, encrypt('FULLSCAN END_OF_MSG', $public_key) . "\r\n", 'msr_bytes_sent');
} else {
// I am a legend or the only bastard who is working
$full_synchronized = true;
}
}
} else {
// recurring incremental synchronization
if ($shutdown_triggered === false) {
// ask every cluster node for DIFF updates
foreach ($rrdp_remoteproxies as $index => $rrdp_remoteproxy) {
if ((time() - $rrdp_remoteproxy['last_seen']) > 5) {
// rrdp_system__socket_write( $rrdp_remoteproxy['socket'], encrypt( 'DIFF 0 END_OF_MSG', $rrdp_remoteproxy['public_key'] ) . "\r\n", 'msr_bytes_sent');
}
}
}
}
}
// sleep(10);
// break;
}
}
// internal function to establish an encrypted remote connection to registered peers
/**
* Opens an outbound connection to a registered cluster peer, exchanges RSA public
* keys, and verifies the peer's key against its configured fingerprint.
*
* @param string $remote_ip
* @param int $remote_port
* @param string $remote_fingerprint
*
* @return array{socket: resource|\Socket, ip: string, public_key: string, authenticated: bool, last_seen: int}|false
*/
function __remote_connect($remote_ip, $remote_port, $remote_fingerprint) {
global $rrdp_config, $rrdp_encryption;
$rrdp_socket = @socket_create((($rrdp_config['ipv6']) ? AF_INET6 : AF_INET), SOCK_STREAM, SOL_TCP);
if (@socket_connect($rrdp_socket, $remote_ip, $remote_port) === true) {
socket_write($rrdp_socket, $rrdp_encryption['public_key'] . "\r\n");
// read public key being returned by the proxy server
$rrdp_public_key = '';
while (true) {
$recv = socket_read($rrdp_socket, 1000, PHP_BINARY_READ);
if ($recv === false) {
// timeout
$rrdp_public_key = false;
break;
}
if ($recv == '') {
// session closed by remote Proxy
break;
} else {
$rrdp_public_key .= $recv;
if (substr($rrdp_public_key, -1) == "\n") {
$rrdp_public_key = trim($rrdp_public_key);
break;
}
}
}
try {
$fingerprint = RSA::loadPublicKey($rrdp_public_key)->getFingerprint('md5');
} catch (Throwable $e) {
return false;
}
if ($remote_fingerprint != $fingerprint) {
// fingerprint mismatch
return false;
} else {
return [ 'socket' => $rrdp_socket, 'ip' => $remote_ip, 'public_key' => $rrdp_public_key, 'authenticated' => true, 'last_seen' => time()];
}
}
return false;
}
// internal function to read the response of a registered peer to a previous request
/**
* Reads and decrypts a registered peer's newline-delimited response packets,
* reassembling them until an END_OF_MSG marker is seen, and returns the parsed
* [command, status, payload] triple.
*
* @param resource|\Socket $read_socket
*
* @return array{0: string, 1: string, 2: string}|false
*/
function __remote_read($read_socket) {
global $rrdp_remoteproxies, $rrdp_encryption;
$index = intval($read_socket);
$input = '';
$first_packet = true;
while (1) {
$recv = @socket_read($read_socket, 100000, PHP_BINARY_READ);
if ($recv === false) {
// keep the connection persistent
break;
}
if ($recv == '') {
// socket session dropped by client
rrdp_system__socket_close($read_socket, false, true);
unset($rrdp_remoteproxies[$index]);
$rrdp_status['status'] = 'CLOSEDOWN_BY_CLIENT_DROP';
__logging(LOGGING_LOCATION_BUFFERED, 'Connection dropped by remote client #' . intval($read_socket), 'MSR', SEVERITY_LEVEL_WARNING);
break;
} else {
$input .= $recv;
if (strpos($input, "\n") !== false) {
rrdp_system__count('bytes_received', rrdp_system__calc_bytes($input));
$chunks = explode("\n", $input);
$input = array_pop($chunks);
foreach ($chunks as $chunk) {
$packet = decrypt(trim($chunk), $rrdp_encryption);
if (strpos($packet, "\x1f\x8b") === 0) {
$packet = gzdecode($packet);
}
if ($first_packet) {
[$cmd, $status, $payload] = explode(' ', $packet, 3);
$first_packet = false;
$packet = $payload;
}
if (substr_count($packet, 'END_OF_MSG')) {
$payload .= str_replace('END_OF_MSG', '', $packet);
$rrdp_remoteproxies[$index]['last_seen'] = time();
return [$cmd, $status, $payload];
}
if (!$first_packet) {
$payload .= $packet;
}
}
}
}
}
return false;
}
/**
* Dispatches a decoded replication request (STATUS, DIFF, etc.) received from a
* registered peer and writes the encrypted response back to that peer's socket.
*
* @param string $input
* @param resource|\Socket $read_socket
*
* @return void
*/
function handle__request($input, $read_socket) {
global $rrdp_config, $rrdp_remoteproxies, $rrdp_encryption, $rrdcached_pid;
global $replicator_system_start, $rrdtool_process, $rrdtool_pipes;
__logging(LOGGING_LOCATION_BUFFERED, 'INPUT: ' . $input, 'MSR', SEVERITY_LEVEL_DEBUG);
$options = explode(' ', $input, 2);
$type = $options[0];
$cmd_options = isset($options[1]) ? explode(' ', $options[1]) : false;
$index = intval($read_socket);
$ip = $rrdp_remoteproxies[$index]['ip'];
$public_key = $rrdp_remoteproxies[$index]['public_key'];
switch($type) {
case 'STATUS':
// status request will return the UNIX uptime
$runtime = microtime(true) - $rrdp_config['start'];
rrdp_system__socket_write($read_socket, encrypt('__STATUS 200 ' . $runtime . ' END_OF_MSG', $public_key) . "\r\n", 'msr_bytes_sent');
__logging(LOGGING_LOCATION_BUFFERED, 'SENT [IP: ' . $ip . ']: __STATUS 200 ' . $runtime, 'MSR', SEVERITY_LEVEL_DEBUG);
break;
case '__STATUS':
break;
case 'DIFF':
// request the oldest DIFF file if existing
if ($cmd_options && is_array($cmd_options) && is_numeric($cmd_options[0])) {
/*
first argument will be used to confirm that a previous file transfer has been completed successful.
If this procedure will be called up for the first time, remote proxy will send "0" as arg1.
*/
$path = './msr/' . $rrdp_remoteproxies[$index]['ip'] . '/';
if ($cmd_options[0]) {
$last_file = $path . $cmd_options[0]; // cmd_option[0] has already been validated by is_numeric() above
if (is_file($last_file)) {
unlink($last_file);
}
}
$content = scandir($path, SCANDIR_SORT_ASCENDING);
if (cacti_sizeof($content) >= 3) {
$current_file = $content[2];
$content_file = file_get_contents($path . $current_file);
$buffer = '__DIFF 200 ' . $current_file . '::' . $content_file . 'END_OF_MSG';
if (strlen($buffer) > 65536) {
$buffer = gzencode($buffer,1);
}
rrdp_system__socket_write($read_socket, encrypt($buffer, $public_key) . "\r\n", 'msr_bytes_sent');
} else {
// no new replication logs found - synchronization complete
rrdp_system__socket_write($read_socket, encrypt('__DIFF 204 END_OF_MSG', $public_key) . "\r\n", 'msr_bytes_sent');
__logging(LOGGING_LOCATION_BUFFERED, 'SENT [IP: ' . $ip . ']: __DIFF 204 ' . $runtime, 'MSR', SEVERITY_LEVEL_DEBUG);
}
} else {
// invalid request
__logging(LOGGING_LOCATION_BUFFERED, 'Invalid request from [IP: ' . $ip . ']: ' . $input, 'MSR', SEVERITY_LEVEL_CRITICAL);
rrdp_system__socket_write($read_socket, encrypt('__DIFF 400 END_OF_MSG', $public_key) . "\r\n", 'msr_bytes_sent');
__logging(LOGGING_LOCATION_BUFFERED, 'SENT [IP: ' . $ip . ']: __DIFF 400 ' . $runtime, 'MSR', SEVERITY_LEVEL_DEBUG);
}
break;
case '__DIFF':
__logging(LOGGING_LOCATION_BUFFERED, 'SERIALIZED: ' . serialize($cmd_options), 'MSR', SEVERITY_LEVEL_DEBUG);
if ($cmd_options && is_array($cmd_options) && is_numeric($cmd_options[0])) {
__logging(LOGGING_LOCATION_BUFFERED, 'READ #1: ', 'MSR', SEVERITY_LEVEL_DEBUG);
$status = array_shift($cmd_options);
__logging(LOGGING_LOCATION_BUFFERED, 'Status ' . $status, 'MSR', SEVERITY_LEVEL_DEBUG);
switch($status) {
case 200:
__logging(LOGGING_LOCATION_BUFFERED, 'READ #200: ', 'MSR', SEVERITY_LEVEL_DEBUG);
$diff_content = implode(' ', $cmd_options);
__logging(LOGGING_LOCATION_BUFFERED, "What we have : $diff_content" , 'MSR', SEVERITY_LEVEL_CRITICAL);
[$period, $transactions] = explode('::', $diff_content);
__logging(LOGGING_LOCATION_BUFFERED, 'TRANSACTIONS: ' . $transactions, 'MSR', SEVERITY_LEVEL_DEBUG);
$transactions = explode("\r", $transactions);
foreach ($transactions as $transaction) {
[$time, $transaction] = explode("\t", $transaction);
$time = trim($time);
$transaction = trim($transaction);
if ($time) {
__logging(LOGGING_LOCATION_BUFFERED, '#1 ' . $transaction, 'MSR', SEVERITY_LEVEL_DEBUG);
}
}
// foreach ($msr_commands as $id => $cmd) {
// $buffer .= $id . "\t" . $cmd . "\r\n";
// }
// request next transaction log
rrdp_system__socket_write($read_socket, encrypt('DIFF ' . $period . ' END_OF_MSG', $public_key) . "\r\n", 'msr_bytes_sent');
break;
case 204:
__logging(LOGGING_LOCATION_BUFFERED, 'READ #204: ', 'MSR', SEVERITY_LEVEL_DEBUG);
__logging(LOGGING_LOCATION_BUFFERED, 'synchronization complete ... ;) ', 'MSR', SEVERITY_LEVEL_DEBUG);
break;
case 400:
// what have we done wrong ???
break;
}
}
break;
case 'FULLSCAN':
// fullscan makes existing(!) incremental synchronization files obsolete
$diff_path = './msr/' . $rrdp_remoteproxies[$index]['ip'] . '/';
$dir_iterator = new RecursiveDirectoryIterator($diff_path, FilesystemIterator::SKIP_DOTS);
$iterator = new RecursiveIteratorIterator($dir_iterator, RecursiveIteratorIterator::CHILD_FIRST);
foreach ($iterator as $file) {
unlink($file);
}
// scan RRA folder
$rra_path_absolute = rtrim($rrdp_config['path_rra'], '/');
$dir_iterator = new RecursiveDirectoryIterator($rra_path_absolute);
$iterator = new RecursiveIteratorIterator($dir_iterator, RecursiveIteratorIterator::SELF_FIRST);
$buffer = false;
foreach ($iterator as $file) {
if ($buffer === false) {
$buffer = '__FULLSCAN 200 ';
}
if (substr($file->getPathname(),-3) == 'rrd') {
$buffer .= './' . ltrim(str_replace($rra_path_absolute, '', $file->getPath()),'/') . ',' . $file->getBasename() . ',' . $file->getSize() . ',' . $file->getMTime() . "\r\n";
}
if (strlen($buffer) <= 65536) {
continue;
} else {
__logging(LOGGING_LOCATION_BUFFERED, $buffer, 'MSR', SEVERITY_LEVEL_DEBUG);
rrdp_system__socket_write($read_socket, encrypt(gzencode($buffer,1), $public_key) . "\r\n", 'msr_bytes_sent');
$buffer = '';
}
}
if ($buffer === false) {
$buffer = '__FULLSCAN 204 ';
}
__logging(LOGGING_LOCATION_BUFFERED, $buffer, 'MSR', SEVERITY_LEVEL_DEBUG);
rrdp_system__socket_write($read_socket, encrypt(gzencode($buffer . ' END_OF_MSG',1), $public_key) . "\r\n", 'msr_bytes_sent');
break;
case '__FULLSCAN':
if ($cmd_options && is_array($cmd_options)) {
$status = $cmd_options[0];
if ($status == 200) {
$scan = explode(PHP_EOL, trim($cmd_options[1]));
if (cacti_sizeof($scan) > 0) {
foreach ($scan as $line) {
$file_settings = explode(',', $line);
$folder = $file_settings[0];
$file = $file_settings[1];
$file_size = $file_settings[2];
$mtime = $file_settings[3];
$rra_relative_path = ($folder !== './' ? ltrim($folder, './') . '/' : '') . $file;
$rra_file_path_absolute = rrdp_resolve_rra_path($rra_relative_path, false);
if ($rra_file_path_absolute === false) {
__logging(LOGGING_LOCATION_BUFFERED, 'SYNC: rejected unsafe peer-supplied path: ' . $rra_relative_path, 'MSR', SEVERITY_LEVEL_CRITICAL);
continue;
}
$rra_subfolder = dirname($rra_file_path_absolute);
// create subfolder if not already existing
if ($folder !== './' && !is_dir($rra_subfolder)) {
if (mkdir($rra_subfolder, 0755, true) === false) {
__logging(LOGGING_LOCATION_BUFFERED, 'Cannot create RRA subfolder: ' . $rra_subfolder , 'MSR', SEVERITY_LEVEL_CRITICAL);
} else {
__logging(LOGGING_LOCATION_BUFFERED, 'SYNC: created: ' . $rra_subfolder , 'MSR', SEVERITY_LEVEL_DEBUG);
}
if (touch($rra_subfolder, $mtime) === false) {
__logging(LOGGING_LOCATION_BUFFERED, 'Cannot set modification time for RRA subfolder: ' . $rra_subfolder , 'MSR', SEVERITY_LEVEL_CRITICAL);
} else {
__logging(LOGGING_LOCATION_BUFFERED, 'SYNC: set mtime: ' . $rra_subfolder , 'MSR', SEVERITY_LEVEL_DEBUG);
}
__logging(LOGGING_LOCATION_BUFFERED, 'SYNC: restored: ' . $rra_subfolder , 'MSR', SEVERITY_LEVEL_INFORMATION);
}
if (!file_exists($rra_file_path_absolute)) {
__logging(LOGGING_LOCATION_BUFFERED, 'SYNC: file not found: ' . $rra_file_path_absolute , 'MSR', SEVERITY_LEVEL_DEBUG);
// request the complete dump of that RRDfile
$msg = 'RRDDUMP ' . rtrim($folder, '/') . '/' . $file;
rrdp_system__socket_write($read_socket, encrypt($msg . ' END_OF_MSG', $public_key) . "\r\n", 'msr_bytes_sent');
__logging(LOGGING_LOCATION_BUFFERED, 'SYNC: sent data request: ' . $msg , 'MSR', SEVERITY_LEVEL_DEBUG);
$response = __remote_read($read_socket);
if ($response !== false) {
if (substr_count($response[2], 'OK u')) {
[$payload,$mtime] = explode(':__filemtime__:', $response[2]);
$rrd_data = substr($payload, 0, strpos($payload, 'OK u'));
file_put_contents($rra_file_path_absolute . '.xml', $rrd_data);
$rrd_exec_status = rrdtool_pipe_execute('restore ' . $rra_file_path_absolute . '.xml ' . $rra_file_path_absolute . "\r\n", $rrdtool_pipes, false, false, false, true);
if ($rrd_exec_status) {
touch($rra_file_path_absolute, $mtime);
$memory_table[$rra_file_path_absolute] = $mtime;
unlink($rra_file_path_absolute . '.xml');
__logging(LOGGING_LOCATION_BUFFERED, 'SYNC: file restored: ' . $rra_file_path_absolute , 'MSR', SEVERITY_LEVEL_DEBUG);
} else {
__logging(LOGGING_LOCATION_BUFFERED, 'SYNC: file restoral failed: ' . $rra_file_path_absolute , 'MSR', SEVERITY_LEVEL_CRITICAL);
}
}
}
} else {
/* file is already existing - in this case we have to compare
* local and remote last update time of this file */
$rrd_return = rrdtool_pipe_execute('last ' . $rra_file_path_absolute . "\r\n", $rrdtool_pipes, false, false, false);
if (substr_count($rrd_return, 'OK u')) {
$local_last = rtrim(substr($rrd_return, 0, strpos($rrd_return, 'OK u')));
} else {
$local_last = 0;
}
__logging(LOGGING_LOCATION_BUFFERED, 'LAST: local:' . rtrim($folder, '/') . '/' . $file . ' - ' . $local_last, 'MSR', SEVERITY_LEVEL_DEBUG);
// request the local remote update time of the RRDfile
rrdp_system__socket_write($read_socket, encrypt('RRDLAST ' . rtrim($folder, '/') . '/' . $file . ' END_OF_MSG', $public_key) . "\r\n", 'msr_bytes_sent');
$response = __remote_read($read_socket);
if ($response !== false) {
if (substr_count($response[2], 'OK u')) {
$remote_last = rtrim(substr($response[2], 0, strpos($response[2], 'OK u')));
} else {
$remote_last = 0;
}
__logging(LOGGING_LOCATION_BUFFERED, 'LAST: remote:' . rtrim($folder, '/') . '/' . $file . ' - ' . $remote_last, 'MSR', SEVERITY_LEVEL_DEBUG);
if ($local_last > $remote_last) {
// local file seems to be more up-to-date
} elseif ($local_last < $remote_last) {
// remote file is more accurate than local copy
rrdp_system__socket_write($read_socket, encrypt('RRDDUMP ' . rtrim($folder, '/') . '/' . $file . ' END_OF_MSG', $public_key) . "\r\n", 'msr_bytes_sent');
$response = __remote_read($read_socket);
if ($response !== false) {
if (substr_count($response[2], 'OK u')) {
[$payload,$mtime] = explode(':__filemtime__:', $response[2]);
$rrd_data = substr($payload, 0, strpos($payload, 'OK u'));
file_put_contents($rra_file_path_absolute . '.xml', $rrd_data);
$rrd_exec_status = rrdtool_pipe_execute('restore ' . $rra_file_path_absolute . '.xml ' . $rra_file_path_absolute . ".tmp\r\n", $rrdtool_pipes, false, false, false, true);
if ($rrd_exec_status) {
unlink($rra_file_path_absolute);
unlink($rra_file_path_absolute . '.xml');
rename($rra_file_path_absolute . '.tmp', $rra_file_path_absolute);
touch($rra_file_path_absolute, $mtime);
$memory_table[$rra_file_path_absolute] = $mtime;
__logging(LOGGING_LOCATION_BUFFERED, 'SYNC: file restored: ' . $rra_file_path_absolute , 'MSR', SEVERITY_LEVEL_DEBUG);
} else {
__logging(LOGGING_LOCATION_BUFFERED, 'SYNC: file restoral failed: ' . $rra_file_path_absolute , 'MSR', SEVERITY_LEVEL_CRITICAL);
}
}
}
} else {
}
}
}
}
} elseif ($status == 204) {
} else {
}
}
}
break;
case 'RRDLAST':
if ($cmd_options && is_array($cmd_options) && substr($cmd_options[0],-3) == 'rrd') {
$rra_path_absolute = rrdp_resolve_rra_path($cmd_options[0]);
if ($rra_path_absolute !== false) {
if ($rrdcached_pid) {
$rrd_cmd = 'flushcached ' . $rra_path_absolute;
$rrd_exec_status = rrdtool_pipe_execute($rrd_cmd . "\r\n", $rrdtool_pipes, false, false, false, true);
if (!$rrd_exec_status) {
__logging(LOGGING_LOCATION_BUFFERED, 'RRDTOOL: "' . $rrd_cmd . '"' . ' failed', 'MSR', SEVERITY_LEVEL_WARNING);
rrdp_system__socket_write($read_socket, encrypt('__RRDDUMP 424 END_OF_MSG', $public_key) . "\r\n", 'msr_bytes_sent');
} else {
__logging(LOGGING_LOCATION_BUFFERED, 'RRDTOOL: "' . $rrd_cmd . '"' . ' executed', 'MSR', SEVERITY_LEVEL_DEBUG);
}
}
$rrd_cmd = 'last ' . $rra_path_absolute;
rrdp_system__socket_write($read_socket, encrypt('__RRDLAST 201 ', $public_key) . "\r\n", 'msr_bytes_sent');
$rrd_exec_status = rrdtool_pipe_execute($rrd_cmd . "\r\n", $rrdtool_pipes, $read_socket, $public_key, 1);
rrdp_system__socket_write($read_socket, encrypt(' END_OF_MSG', $public_key) . "\r\n", 'msr_bytes_sent');
if ($rrd_exec_status) {
__logging(LOGGING_LOCATION_BUFFERED, 'RRDTOOL: "' . $rrd_cmd . '"' . ' executed', 'MSR', SEVERITY_LEVEL_DEBUG);
} else {
__logging(LOGGING_LOCATION_BUFFERED, 'RRDTOOL: "' . $rrd_cmd . '"' . ' failed', 'MSR', SEVERITY_LEVEL_WARNING);
}
} else {
rrdp_system__socket_write($read_socket, encrypt('__RRDLAST 404 END_OF_MSG', $public_key) . "\r\n", 'msr_bytes_sent');
}
} else {
// invalid request
rrdp_system__socket_write($read_socket, encrypt('__RRDLAST 400 END_OF_MSG', $public_key) . "\r\n", 'msr_bytes_sent');
}
break;
case 'RRDDUMP':
if ($cmd_options && is_array($cmd_options) && substr($cmd_options[0],-3) == 'rrd') {
$rra_path_absolute = rrdp_resolve_rra_path($cmd_options[0]);
if ($rra_path_absolute !== false) {
if ($rrdcached_pid) {
$rrd_cmd = 'flushcached ' . $rra_path_absolute;
$rrd_exec_status = rrdtool_pipe_execute($rrd_cmd . "\r\n", $rrdtool_pipes, false, false, false, true);
if (!$rrd_exec_status) {
__logging(LOGGING_LOCATION_BUFFERED, 'RRDTOOL: "' . $rrd_cmd . '"' . ' failed', 'MSR', SEVERITY_LEVEL_WARNING);
rrdp_system__socket_write($read_socket, encrypt('__RRDDUMP 424 END_OF_MSG', $public_key) . "\r\n", 'msr_bytes_sent');
} else {
__logging(LOGGING_LOCATION_BUFFERED, 'RRDTOOL: "' . $rrd_cmd . '"' . ' executed', 'MSR', SEVERITY_LEVEL_DEBUG);
}
}
$rrd_cmd = 'dump ' . $rra_path_absolute;
rrdp_system__socket_write($read_socket, encrypt('__RRDDUMP 201 ', $public_key) . "\r\n", 'msr_bytes_sent');
$rrd_exec_status = rrdtool_pipe_execute($rrd_cmd . "\r\n", $rrdtool_pipes, $read_socket, $public_key, 1);
rrdp_system__socket_write($read_socket, encrypt(':__filemtime__:' . filemtime($rra_path_absolute) . ' END_OF_MSG', $public_key) . "\r\n", 'msr_bytes_sent');
if ($rrd_exec_status) {
__logging(LOGGING_LOCATION_BUFFERED, 'RRDTOOL: "' . $rrd_cmd . '"' . ' executed', 'MSR', SEVERITY_LEVEL_DEBUG);
} else {
__logging(LOGGING_LOCATION_BUFFERED, 'RRDTOOL: "' . $rrd_cmd . '"' . ' failed', 'MSR', SEVERITY_LEVEL_WARNING);
}
} else {
rrdp_system__socket_write($read_socket, encrypt('__RRDDUMP 404 END_OF_MSG', $public_key) . "\r\n", 'msr_bytes_sent');
}
} else {
// invalid request
rrdp_system__socket_write($read_socket, encrypt('__RRDDUMP 400 END_OF_MSG', $public_key) . "\r\n", 'msr_bytes_sent');
}
break;
case '__RRDDUMP':
break;
default:
// invalid request
// rrdp_system__socket_write( $read_socket, encrypt( '404 END_OF_MSG', $public_key ) . "\r\n", 'msr_bytes_sent');
break;
}
}