mirror of
https://github.com/Cacti/rrdproxy.git
synced 2026-10-23 03:26:43 +09:00
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>
This commit is contained in:
1 parent
7456c5e697
commit
629971cc73
19 files changed
+2731
-558
No files matched your search
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
@@ -0,0 +1,57 @@
|
||||
# rrdproxy Copilot Instructions
|
||||
|
||||
RRDtool Proxy Server (rrdp) is a standalone PHP daemon that lets Cacti offload RRDtool
|
||||
file access to a remote host, with RSA + fresh-session-AES encrypted client traffic. See
|
||||
[README.md](../README.md) for the full feature/protocol overview before making
|
||||
security-sensitive changes.
|
||||
|
||||
## Architecture
|
||||
|
||||
- `rrdtool-proxy.php` — main daemon entrypoint: forks the master process, sets up admin
|
||||
(CLI), client, and replication sockets, and contains most `rrdp_cmd__*` (admin CLI) and
|
||||
`rrdp_system__*` (internal plumbing) functions.
|
||||
- `lib/functions.php` — shared helpers used by every process: `encrypt()`/`decrypt()`
|
||||
(RSA+AES wire protocol), path-confinement helpers (`rrdp_resolve_rra_path`,
|
||||
`rrdp_command_has_unsafe_path`), atomic file/key-pair writers, logging.
|
||||
- `lib/client.php` — per-client child process (`interact()`), forked once per connection.
|
||||
- `lib/__master__.php` and `lib/replicator.php` — near-duplicate implementations of the
|
||||
cluster peer / multi-server-replication (MSR) roles (master listens only; replicator also
|
||||
dials out to peers). Keep changes to peer-auth/fingerprint logic in sync between the two.
|
||||
- `lib/wizard.php` — interactive first-run/reconfiguration setup wizard.
|
||||
- `cli/removespikes.php` — standalone RRD spike-removal tool, also invoked as a child
|
||||
process by the client handler.
|
||||
- `include/global.php` — constants, severity levels, and CLI help-tree config arrays.
|
||||
|
||||
## Build, Lint, and Test
|
||||
|
||||
- Install deps: `composer install` (add `--ignore-platform-req=ext-pcntl
|
||||
--ignore-platform-req=ext-posix` on Windows dev machines — those extensions are Unix-only
|
||||
and only present on the Linux production target).
|
||||
- `composer run lint` — phplint.
|
||||
- `composer run phpstan` — `phpstan analyse --level 6` (may need
|
||||
`php -d memory_limit=-1` locally; do not lower the configured level to silence findings).
|
||||
- `composer run phpcsfixer` — dry-run diff; `composer run phpcsfixit` to apply.
|
||||
- Tests are a plain assertion script, not PHPUnit: `php tests/run.php` (expects
|
||||
`OK (<n> checks)`; failures print to STDERR and exit 1).
|
||||
- A handful of checks (file permission bits, `AF_UNIX` `socket_create_pair`, path
|
||||
separators) are known to fail on Windows but pass on Linux — don't "fix" those unless
|
||||
asked; verify by reasoning about the platform difference, not by relaxing the assertion.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Indent with tabs. Every PHP file starts with the standard Cacti Group GPL header
|
||||
comment block (`Copyright (C) 2004-<year> The Cacti Group ...`); `include/global.php`
|
||||
computes the display copyright dynamically via `date('Y')`, but the header comments
|
||||
themselves are static and need bumping only when explicitly asked.
|
||||
- Every function has a PHPDoc block: one-line (or short) description, blank `*` line,
|
||||
`@param` list, blank `*` line, `@return`. Function signatures are intentionally left
|
||||
untyped in code — types live in PHPDoc only, and PHPStan is the source of truth for
|
||||
catching mismatches. Don't add native param/return type hints unless asked.
|
||||
- Encryption uses `phpseclib4\Crypt\*` (namespace root `phpseclib4`, not `phpseclib3`).
|
||||
Notable v4 gotchas: there is no `Random` class (use `random_bytes()`); `Rijndael`/AES
|
||||
requires an explicit `setIV()` and validates key length strictly (16/20/24/28/32 bytes);
|
||||
load keys via `RSA::loadPublicKey()`/`loadPrivateKey()` (or `PublicKeyLoader`) and always
|
||||
pass `'md5'` to `getFingerprint()` — the wire protocol and existing client/proxy configs
|
||||
store md5 fingerprints, and v4's default changed to sha256.
|
||||
- New feature/security/behavior changes should get an entry in `CHANGELOG.md` under
|
||||
`[Unreleased]`, prefixed `feature:`, `issue:`, or `security:`.
|
||||
@@ -1,4 +1,6 @@
|
||||
.git*
|
||||
!.github/
|
||||
!.gitattributes
|
||||
|
||||
locales/po/*.mo
|
||||
vendor/**
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
parameters:
|
||||
ignoreErrors:
|
||||
-
|
||||
message: '#^Function removeComments\(\) should return array\<int, string\>\|null but return statement is missing\.$#'
|
||||
identifier: return.missing
|
||||
count: 1
|
||||
path: cli/removespikes.php
|
||||
|
||||
-
|
||||
message: '#^If condition is always false\.$#'
|
||||
identifier: if.alwaysFalse
|
||||
count: 3
|
||||
path: cli/removespikes.php
|
||||
|
||||
-
|
||||
message: '#^Offset ''cacti_server_os'' does not exist on array\{\}\.$#'
|
||||
identifier: offsetAccess.notFound
|
||||
count: 1
|
||||
path: cli/removespikes.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#2 \$descriptor_spec of function proc_open expects array\<1\|2, list\<string\>\|resource\>, array\{1\: array\{''pipe'', ''w''\}, 2\: array\{''redirect'', 1\}\} given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: cli/removespikes.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$num of function abs expects float\|int, string given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: lib/client.php
|
||||
|
||||
-
|
||||
message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#'
|
||||
identifier: function.alreadyNarrowedType
|
||||
count: 1
|
||||
path: lib/functions.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#2 \$descriptor_spec of function proc_open expects array\<1\|2, list\<string\>\|resource\>, array\{1\: array\{''pipe'', ''w''\}, 2\: array\{''redirect'', 1\}\} given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: lib/functions.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#2 \$enable of function stream_set_blocking expects bool, int given\.$#'
|
||||
identifier: argument.type
|
||||
count: 2
|
||||
path: lib/functions.php
|
||||
|
||||
-
|
||||
message: '#^Unreachable statement \- code above always terminates\.$#'
|
||||
identifier: deadCode.unreachable
|
||||
count: 2
|
||||
path: lib/functions.php
|
||||
|
||||
-
|
||||
message: '#^Comparison operation "\<" between int\<80200, 80599\> and 80200 is always false\.$#'
|
||||
identifier: smaller.alwaysFalse
|
||||
count: 1
|
||||
path: lib/wizard.php
|
||||
|
||||
-
|
||||
message: '#^Strict comparison using \=\=\= between false and false will always evaluate to true\.$#'
|
||||
identifier: identical.alwaysTrue
|
||||
count: 1
|
||||
path: lib/wizard.php
|
||||
|
||||
-
|
||||
message: '#^Unreachable statement \- code above always terminates\.$#'
|
||||
identifier: deadCode.unreachable
|
||||
count: 1
|
||||
path: lib/wizard.php
|
||||
|
||||
-
|
||||
message: '#^Call to function in_array\(\) with arguments numeric\-string, array\{0, 1, 2, 3, 4, 5, 6, 7\} and true will always evaluate to false\.$#'
|
||||
identifier: function.impossibleType
|
||||
count: 1
|
||||
path: rrdtool-proxy.php
|
||||
|
||||
-
|
||||
message: '#^Call to function in_array\(\) with arguments numeric\-string, array\{0\: 0, 1\: 1, 2\: 2, 3\: 3, 4\: 4, 5\: 5, 6\: 6, 7\: 7, \.\.\.\} and true will always evaluate to false\.$#'
|
||||
identifier: function.impossibleType
|
||||
count: 1
|
||||
path: rrdtool-proxy.php
|
||||
|
||||
-
|
||||
message: '#^Call to function is_array\(\) with list\<string\> will always evaluate to true\.$#'
|
||||
identifier: function.alreadyNarrowedType
|
||||
count: 1
|
||||
path: rrdtool-proxy.php
|
||||
|
||||
-
|
||||
message: '#^Call to function is_array\(\) with non\-empty\-list\<string\> will always evaluate to true\.$#'
|
||||
identifier: function.alreadyNarrowedType
|
||||
count: 3
|
||||
path: rrdtool-proxy.php
|
||||
|
||||
-
|
||||
message: '#^Comparison operation "\<" between int\<80200, 80599\> and 80200 is always false\.$#'
|
||||
identifier: smaller.alwaysFalse
|
||||
count: 1
|
||||
path: rrdtool-proxy.php
|
||||
|
||||
-
|
||||
message: '#^Comparison operation "\>\=" between int\<80200, 80599\> and 80000 is always true\.$#'
|
||||
identifier: greaterOrEqual.alwaysTrue
|
||||
count: 2
|
||||
path: rrdtool-proxy.php
|
||||
|
||||
-
|
||||
message: '#^Function handle_child_processes\(\) has parameter \$ipc_sockets with no value type specified in iterable type array\.$#'
|
||||
identifier: missingType.iterableValue
|
||||
count: 1
|
||||
path: rrdtool-proxy.php
|
||||
|
||||
-
|
||||
message: '#^If condition is always true\.$#'
|
||||
identifier: if.alwaysTrue
|
||||
count: 1
|
||||
path: rrdtool-proxy.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$socket of function socket_close expects Socket, bool given\.$#'
|
||||
identifier: argument.type
|
||||
count: 3
|
||||
path: rrdtool-proxy.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$socket of function socket_shutdown expects Socket, bool given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: rrdtool-proxy.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$socket_client of function interact expects resource\|Socket, bool given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: rrdtool-proxy.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#3 \$ssock of function handle_child_processes expects bool, Socket given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: rrdtool-proxy.php
|
||||
|
||||
-
|
||||
message: '#^Result of && is always false\.$#'
|
||||
identifier: booleanAnd.alwaysFalse
|
||||
count: 1
|
||||
path: rrdtool-proxy.php
|
||||
|
||||
-
|
||||
message: '#^Strict comparison using \!\=\= between ''undefined'' and ''undefined'' will always evaluate to false\.$#'
|
||||
identifier: notIdentical.alwaysFalse
|
||||
count: 1
|
||||
path: rrdtool-proxy.php
|
||||
|
||||
-
|
||||
message: '#^Strict comparison using \!\=\= between string and false will always evaluate to true\.$#'
|
||||
identifier: notIdentical.alwaysTrue
|
||||
count: 1
|
||||
path: rrdtool-proxy.php
|
||||
|
||||
-
|
||||
message: '#^Strict comparison using \!\=\= between true and false will always evaluate to true\.$#'
|
||||
identifier: notIdentical.alwaysTrue
|
||||
count: 1
|
||||
path: rrdtool-proxy.php
|
||||
|
||||
-
|
||||
message: '#^Strict comparison using \=\=\= between Socket and false will always evaluate to false\.$#'
|
||||
identifier: identical.alwaysFalse
|
||||
count: 1
|
||||
path: rrdtool-proxy.php
|
||||
|
||||
-
|
||||
message: '#^Ternary operator condition is always false\.$#'
|
||||
identifier: ternary.alwaysFalse
|
||||
count: 1
|
||||
path: rrdtool-proxy.php
|
||||
|
||||
-
|
||||
message: '#^Unreachable statement \- code above always terminates\.$#'
|
||||
identifier: deadCode.unreachable
|
||||
count: 1
|
||||
path: rrdtool-proxy.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$arg in isset\(\) always exists and is not nullable\.$#'
|
||||
identifier: isset.variable
|
||||
count: 1
|
||||
path: rrdtool-proxy.php
|
||||
|
||||
-
|
||||
message: '#^While loop condition is always true\.$#'
|
||||
identifier: while.alwaysTrue
|
||||
count: 1
|
||||
path: rrdtool-proxy.php
|
||||
@@ -1,3 +1,26 @@
|
||||
<?php
|
||||
|
||||
define('PHP_STAN', true);
|
||||
|
||||
/**
|
||||
* PHPStan stub: Cacti's real settings-table accessor, unavailable outside a
|
||||
* Cacti install, so analysis always sees it return null here.
|
||||
*/
|
||||
function read_config_option(string $name): mixed {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* PHPStan stub: Cacti's real logging function, unavailable outside a Cacti
|
||||
* install, so analysis sees a no-op here.
|
||||
*/
|
||||
function cacti_log(string $message, bool $output = false, string $facility = ''): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* PHPStan stub: the Spikekill plugin's real version accessor, unavailable
|
||||
* outside a Cacti install, so analysis always sees an 'unknown' version here.
|
||||
*/
|
||||
function spikekill_version(): array {
|
||||
return ['version' => 'unknown'];
|
||||
}
|
||||
+24
-37
@@ -1,43 +1,30 @@
|
||||
includes:
|
||||
- .phpstan-baseline.neon
|
||||
|
||||
parameters:
|
||||
parallel:
|
||||
maximumNumberOfProcesses: 8
|
||||
scanDirectories:
|
||||
- include
|
||||
maximumNumberOfProcesses: 1
|
||||
level: 6
|
||||
paths:
|
||||
- include/global.php
|
||||
- lib/client.php
|
||||
- lib/functions.php
|
||||
- lib/wizard.php
|
||||
- cli/removespikes.php
|
||||
- rrdtool-proxy.php
|
||||
bootstrapFiles:
|
||||
- vendor/autoload.php
|
||||
- .phpstan-bootstrap.php
|
||||
excludePaths:
|
||||
analyse:
|
||||
- vendor/*
|
||||
- plugins/*
|
||||
- rector.php
|
||||
paths:
|
||||
- include
|
||||
- lib
|
||||
- install
|
||||
- .
|
||||
bootstrapFiles:
|
||||
- .phpstan-bootstrap.php
|
||||
- vendor/autoload.php
|
||||
- include/config.php.dist
|
||||
- include/global_constants.php
|
||||
- include/global.php
|
||||
- lib/functions.php
|
||||
- include/global_languages.php
|
||||
- include/global_arrays.php
|
||||
- include/global_form.php
|
||||
- include/global_settings.php
|
||||
# Replication is not started by the current daemon and remains unfinished.
|
||||
- lib/__master__.php
|
||||
- lib/replicator.php
|
||||
- lib/master.php
|
||||
- lib/slave.php
|
||||
reportUnmatchedIgnoredErrors: false
|
||||
ignoreErrors:
|
||||
- identifier: missingType.iterableValue
|
||||
# PHPStan gets confused about dynamic constants
|
||||
- identifier: if.alwaysFalse
|
||||
- identifier: notEqual.alwaysFalse
|
||||
- identifier: notEqual.alwaysTrue
|
||||
- identifier: equal.alwaysTrue
|
||||
- identifier: if.alwaysTrue
|
||||
- identifier: while.alwaysTrue
|
||||
- identifier: greater.alwaysFalse
|
||||
- identifier: identical.alwaysTrue
|
||||
- identifier: identical.alwaysFalse
|
||||
- identifier: booleanAnd.leftAlwaysTrue
|
||||
- identifier: booleanAnd.leftAlwaysFalse
|
||||
# L9 - '/^Parameter #1 \$value of function strval expects bool\|float\|int\|resource\|string\|null, mixed given.$/'
|
||||
# L9 - '/^Parameter #1 \$value of function intval expects array\|bool\|float\|int\|resource\|string\|null, mixed given.$/'
|
||||
# These files are intentionally generated by the interactive setup wizard.
|
||||
- identifier: include.fileNotFound
|
||||
- identifier: includeOnce.fileNotFound
|
||||
- identifier: requireOnce.fileNotFound
|
||||
@@ -0,0 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to RRDtool Proxy Server are documented in this file.
|
||||
Entries are prefixed `security:`, `issue:`, or `feature:`.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- feature: Bump the minimum supported PHP version to 8.2.
|
||||
- feature: Upgrade the phpseclib dependency from phpseclib 3 to phpseclib 4 (Composer-managed).
|
||||
- feature: Migrate encryption from the unmaintained, manually vendored phpseclib 1.x/2.x copy to the Composer-managed phpseclib 3 package.
|
||||
- issue: Fix `lib/__master__.php` and `lib/replicator.php` calling phpseclib 1.x/2.x-only methods (`loadKey()`, `getPublicKeyFingerprint()`) that do not exist in phpseclib 3/4, which fataled the replicator/MSR peer-authentication code path at runtime.
|
||||
- security: Encrypt the "request too large" error response for authenticated clients instead of returning it in plaintext.
|
||||
- issue: Inherit the full child-process environment for `removespikes` instead of the subset populated in `$_ENV` (was silently dropping variables such as `TEMP` on Windows).
|
||||
- issue: Add the missing space in the `RRDTOOL_PIPE_UNAVAILABLE` error frame so it matches other error responses.
|
||||
- issue: Check the `symlink()` return value in the test fixtures instead of risking a false positive when symlinks are unavailable.
|
||||
- security: Use constant-time `hash_equals()` comparisons for RSA public-key fingerprint verification.
|
||||
- security: Bound encrypted and compressed request frames and fail closed on malformed or oversized input.
|
||||
- security: Confine RRDtool and filesystem operations to the configured RRA root; reject path traversal, absolute paths, and embedded command framing.
|
||||
- security: Remove shell interpolation from RRDtool and `removespikes` execution in favor of argv-based process invocation.
|
||||
- security: Make RSA key writes atomic, validate matching key pairs, and enforce `0600` permissions on private key files.
|
||||
- issue: Fix partial socket writes, RRDtool pipe startup failures, admin disconnect cleanup, and IPv6 admin binding.
|
||||
@@ -8,10 +8,18 @@ replication, support of Cacti Boost and, if needed, the RRDcached daemon. It
|
||||
allows system administrators to split Cacti's access resources across machines
|
||||
without the need for NFS.
|
||||
|
||||
Due the fact that updates to files may contain sensitive data and to prevent
|
||||
undesired updates, RRDtool proxy utilizes high encrypted connections (RSA2048
|
||||
and AES192) with continuously changing keys. This ensures a secured connection
|
||||
between the proxy and registered clients and/or other proxies.
|
||||
Because updates may contain sensitive data, RRDtool Proxy encrypts client
|
||||
traffic using RSA-2048 key exchange and a fresh 256-bit symmetric key for each
|
||||
message. Client IP addresses and public-key fingerprints must also be explicitly
|
||||
allowlisted.
|
||||
|
||||
The current compatibility protocol verifies the presented public-key
|
||||
fingerprint, but does not yet perform a signed challenge proving possession of
|
||||
the corresponding private key. Its legacy CBC message format also lacks an
|
||||
authenticated integrity tag. Deploy the client port only on a trusted,
|
||||
firewalled management network. A future protocol version must add private-key
|
||||
proof and authenticated encryption in coordination with the Cacti client
|
||||
before these limitations can be removed.
|
||||
|
||||
A local service port is provided to allow administrators are able to access a
|
||||
separate command line interface which provides easy access to configure and
|
||||
@@ -30,7 +38,7 @@ higher). The most recent version of RRDProxy requires composer
|
||||
|
||||
### Minimum Requirements
|
||||
|
||||
- PHP Version 8.1 or higher
|
||||
- PHP Version 8.2 or higher
|
||||
|
||||
Installation must include and enable the following modules:
|
||||
|
||||
@@ -48,7 +56,7 @@ higher). The most recent version of RRDProxy requires composer
|
||||
|
||||
All minimum requirements must be met, though the following are more optional:
|
||||
|
||||
- PHP Version 8.1 or higher
|
||||
- PHP Version 8.2 or higher
|
||||
|
||||
Include all minimum recommendation modules
|
||||
|
||||
@@ -200,4 +208,3 @@ rrdp#
|
||||
|
||||
Currently, the RRDproxy wizard and command line interface port are both
|
||||
offering only english as the default language.
|
||||
|
||||
+164
-20
@@ -56,6 +56,8 @@ $std_kills = false;
|
||||
$var_kills = false;
|
||||
$html = false;
|
||||
$backup = false;
|
||||
$config = [];
|
||||
$new_output = [];
|
||||
|
||||
if ($using_cacti) {
|
||||
$method = read_config_option('spikekill_method');
|
||||
@@ -205,14 +207,17 @@ if ($rrdfile == '') {
|
||||
}
|
||||
|
||||
// let's see if we can find rrdtool
|
||||
if (!$using_cacti) {
|
||||
if (substr_count(PHP_OS, 'WIN')) {
|
||||
$response = shell_exec('rrdtool.exe');
|
||||
} else {
|
||||
$response = shell_exec('rrdtool');
|
||||
$rrdtool_path = getenv('RRDP_RRDTOOL_PATH');
|
||||
|
||||
if ($rrdtool_path === false || $rrdtool_path === '') {
|
||||
$rrdtool_path = $using_cacti ? read_config_option('path_rrdtool') : (substr_count(PHP_OS, 'WIN') ? 'rrdtool.exe' : 'rrdtool');
|
||||
}
|
||||
|
||||
if (strlen($response)) {
|
||||
if (!$using_cacti) {
|
||||
$response = removespikes_run_rrdtool($rrdtool_path, []);
|
||||
|
||||
if ($response !== false && strlen($response['stdout'] . $response['stderr'])) {
|
||||
$response = $response['stdout'] . $response['stderr'];
|
||||
$response_array = explode(' ', $response);
|
||||
print 'NOTE: Using ' . $response_array[0] . ' Version ' . $response_array[1] . "\n";
|
||||
} else {
|
||||
@@ -227,23 +232,28 @@ $seed = mt_rand();
|
||||
if ($using_cacti) {
|
||||
if ($config['cacti_server_os'] == 'win32') {
|
||||
$tempdir = getenv('TEMP');
|
||||
$xmlfile = $tempdir . '/' . str_replace('.rrd', '', basename($rrdfile)) . '.dump.' . $seed;
|
||||
$xmlfile = tempnam($tempdir, 'rrdproxy-dump-');
|
||||
$bakfile = $tempdir . '/' . str_replace('.rrd', '', basename($rrdfile)) . '.backup.' . $seed . '.rrd';
|
||||
} else {
|
||||
$tempdir = '/tmp';
|
||||
$xmlfile = '/tmp/' . str_replace('.rrd', '', basename($rrdfile)) . '.dump.' . $seed;
|
||||
$xmlfile = tempnam($tempdir, 'rrdproxy-dump-');
|
||||
$bakfile = '/tmp/' . str_replace('.rrd', '', basename($rrdfile)) . '.backup.' . $seed . '.rrd';
|
||||
}
|
||||
} elseif (substr_count(PHP_OS, 'WIN')) {
|
||||
$tempdir = getenv('TEMP');
|
||||
$xmlfile = $tempdir . '/' . str_replace('.rrd', '', basename($rrdfile)) . '.dump.' . $seed;
|
||||
$xmlfile = tempnam($tempdir, 'rrdproxy-dump-');
|
||||
$bakfile = $tempdir . '/' . str_replace('.rrd', '', basename($rrdfile)) . '.backup.' . $seed . '.rrd';
|
||||
} else {
|
||||
$tempdir = '/tmp';
|
||||
$xmlfile = '/tmp/' . str_replace('.rrd', '', basename($rrdfile)) . '.dump.' . $seed;
|
||||
$xmlfile = tempnam($tempdir, 'rrdproxy-dump-');
|
||||
$bakfile = '/tmp/' . str_replace('.rrd', '', basename($rrdfile)) . '.backup.' . $seed . '.rrd';
|
||||
}
|
||||
|
||||
if ($xmlfile === false) {
|
||||
print "FATAL: Unable to create a secure temporary file.\n";
|
||||
exit(-12);
|
||||
}
|
||||
|
||||
if ($html) {
|
||||
print "<table cellpadding='3' cellspacing='0' class='spikekill_data' id='spikekill_data'>";
|
||||
}
|
||||
@@ -255,10 +265,12 @@ if ($using_cacti) {
|
||||
// execute the dump command
|
||||
print ($html ? "<tr><td colspan='20' class='spikekill_note'>" : '') . "NOTE: Creating XML file '$xmlfile' from '$rrdfile'" . ($html ? "</td></tr>\n" : "\n");
|
||||
|
||||
if ($using_cacti) {
|
||||
shell_exec(read_config_option('path_rrdtool') . " dump $rrdfile > $xmlfile");
|
||||
$dump_result = removespikes_run_rrdtool($rrdtool_path, ['dump', $rrdfile]);
|
||||
|
||||
if ($dump_result !== false && $dump_result['status'] === 0 && $dump_result['stdout'] !== '') {
|
||||
file_put_contents($xmlfile, $dump_result['stdout'], LOCK_EX);
|
||||
} else {
|
||||
shell_exec("rrdtool dump $rrdfile > $xmlfile");
|
||||
unlink($xmlfile);
|
||||
}
|
||||
|
||||
// read the xml file into an array
|
||||
@@ -454,7 +466,10 @@ if (!$dryrun) {
|
||||
if ($total_kills) {
|
||||
if (writeXMLFile($new_output, $xmlfile)) {
|
||||
if (backupRRDFile($rrdfile)) {
|
||||
createRRDFileFromXML($xmlfile, $rrdfile);
|
||||
if (!createRRDFileFromXML($xmlfile, $rrdfile)) {
|
||||
print ($html ? "<tr><td colspan='20' class='spikekill_note'>" : '') . "FATAL: Unable to restore '$rrdfile' from '$xmlfile'" . ($html ? "</td></tr>\n" : "\n");
|
||||
exit(-14);
|
||||
}
|
||||
} else {
|
||||
print ($html ? "<tr><td colspan='20' class='spikekill_note'>" : '') . "FATAL: Unable to backup '$rrdfile'" . ($html ? "</td></tr>\n" : "\n");
|
||||
}
|
||||
@@ -471,27 +486,78 @@ if ($html) {
|
||||
}
|
||||
|
||||
// All Functions
|
||||
/**
|
||||
* Restores an RRD file from its (spike-cleaned) XML dump via `rrdtool restore`.
|
||||
*
|
||||
* @param string $xmlfile
|
||||
* @param string $rrdfile
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function createRRDFileFromXML($xmlfile, $rrdfile) {
|
||||
global $using_cacti, $html;
|
||||
global $html, $rrdtool_path;
|
||||
|
||||
// execute the dump command
|
||||
print ($html ? "<tr><td colspan='20' class='spikekill_note'>" : '') . "NOTE: Re-Importing '$xmlfile' to '$rrdfile'" . ($html ? "</td></tr>\n" : "\n");
|
||||
|
||||
if ($using_cacti) {
|
||||
$response = shell_exec(read_config_option('path_rrdtool') . " restore -f -r $xmlfile $rrdfile");
|
||||
} else {
|
||||
$response = shell_exec("rrdtool restore -f -r $xmlfile $rrdfile");
|
||||
}
|
||||
$result = removespikes_run_rrdtool($rrdtool_path, ['restore', '-f', '-r', $xmlfile, $rrdfile]);
|
||||
$response = $result === false ? 'Unable to start RRDtool' : $result['stdout'] . $result['stderr'];
|
||||
|
||||
if (strlen($response)) {
|
||||
print ($html ? "<tr><td colspan='20' class='spikekill_note'>" : '') . $response . ($html ? "</td></tr>\n" : "\n");
|
||||
}
|
||||
|
||||
return $result !== false && $result['status'] === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the rrdtool binary with the given argv-style arguments (no shell
|
||||
* interpolation) and captures its combined stdout/stderr and exit status.
|
||||
*
|
||||
* @param string $binary
|
||||
* @param array<int, string> $arguments
|
||||
*
|
||||
* @return array{status: int, stdout: string, stderr: string}|false
|
||||
*/
|
||||
function removespikes_run_rrdtool($binary, $arguments) {
|
||||
$descriptors = [
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['redirect', 1],
|
||||
];
|
||||
$process = proc_open(array_merge([$binary], $arguments), $descriptors, $pipes);
|
||||
|
||||
if (!is_resource($process)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$stdout = stream_get_contents($pipes[1]);
|
||||
fclose($pipes[1]);
|
||||
$status = proc_close($process);
|
||||
|
||||
return ['status' => $status, 'stdout' => $stdout, 'stderr' => ''];
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the (spike-cleaned) XML dump lines out to $xmlfile.
|
||||
*
|
||||
* @param array<int, string> $output
|
||||
* @param string $xmlfile
|
||||
*
|
||||
* @return int|false
|
||||
*/
|
||||
function writeXMLFile($output, $xmlfile) {
|
||||
return file_put_contents($xmlfile, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies $rrdfile into the configured (or temp) backup directory before it is
|
||||
* modified, appending a random seed to the filename if a file of that name
|
||||
* already exists there.
|
||||
*
|
||||
* @param string $rrdfile
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function backupRRDFile($rrdfile) {
|
||||
global $using_cacti, $tempdir, $seed, $html;
|
||||
|
||||
@@ -516,6 +582,16 @@ function backupRRDFile($rrdfile) {
|
||||
return copy($rrdfile, $backupdir . '/' . $newfile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes each RRA/data-source's outlier-trimmed "variance" average (the mean
|
||||
* of the samples after removing the top/bottom $outliers extremes), storing it
|
||||
* into $rra[$rra_num][$ds_num]['variance_avg'].
|
||||
*
|
||||
* @param array<int, array<int, array<string, mixed>>> $rra
|
||||
* @param array<int, array<int, array<int, float|int>>> $samples
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function calculateVarianceAverages(&$rra, &$samples) {
|
||||
global $outliers;
|
||||
|
||||
@@ -540,6 +616,17 @@ function calculateVarianceAverages(&$rra, &$samples) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For every RRA/data-source, computes the standard deviation, average, and
|
||||
* min/max cutoffs from the collected samples, then counts (and flags via
|
||||
* $std_kills/$var_kills) how many samples fall outside the standard-deviation
|
||||
* and variance thresholds.
|
||||
*
|
||||
* @param array<int, array<int, array<string, mixed>>> $rra
|
||||
* @param array<int, array<int, array<int, float|int>>> $samples
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function calculateOverallStatistics(&$rra, &$samples) {
|
||||
global $percent, $stddev, $ds_min, $ds_max, $var_kills, $std_kills;
|
||||
|
||||
@@ -631,6 +718,14 @@ function calculateOverallStatistics(&$rra, &$samples) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints a per-data-source summary table (plain text or HTML) of the computed
|
||||
* statistics and kill counts for each RRA.
|
||||
*
|
||||
* @param array<int, array<int, array<string, mixed>>> $rra
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function outputStatistics($rra) {
|
||||
global $rra_cf, $rra_name, $ds_name, $rra_pdp, $html;
|
||||
|
||||
@@ -718,6 +813,16 @@ function outputStatistics($rra) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites the dumped XML's <row> value lines, replacing any sample outside
|
||||
* the computed thresholds with the RRA's average (or 'NaN'), up to $numspike
|
||||
* replacements per RRA.
|
||||
*
|
||||
* @param array<int, string> $output
|
||||
* @param array<int, array<int, array<string, mixed>>> $rra
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
function updateXML(&$output, &$rra) {
|
||||
global $numspike, $percent, $avgnan, $method, $total_kills;
|
||||
|
||||
@@ -725,6 +830,7 @@ function updateXML(&$output, &$rra) {
|
||||
$rra_num = 0;
|
||||
$ds_num = 0;
|
||||
$kills = 0;
|
||||
$new_array = [];
|
||||
|
||||
if (sizeof($output)) {
|
||||
foreach ($output as $line) {
|
||||
@@ -797,7 +903,17 @@ function updateXML(&$output, &$rra) {
|
||||
return $new_array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips blank lines and `<!-- ... -->` XML comments from a dumped RRD file's
|
||||
* lines.
|
||||
*
|
||||
* @param array<int, string> $output
|
||||
*
|
||||
* @return array<int, string>|null
|
||||
*/
|
||||
function removeComments(&$output) {
|
||||
$new_array = [];
|
||||
|
||||
if (sizeof($output)) {
|
||||
foreach ($output as $line) {
|
||||
$line = trim($line);
|
||||
@@ -831,6 +947,14 @@ function removeComments(&$output) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a PDP (primary data point) count as a human-readable duration
|
||||
* (seconds, minutes, hours, or days) based on the RRD's step size.
|
||||
*
|
||||
* @param int $pdp
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function displayTime($pdp) {
|
||||
global $step;
|
||||
|
||||
@@ -857,6 +981,13 @@ function displayTime($pdp) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints $string prefixed with "DEBUG: " when debug output is enabled.
|
||||
*
|
||||
* @param string $string
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function debug($string) {
|
||||
global $debug;
|
||||
|
||||
@@ -865,8 +996,16 @@ function debug($string) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the (population) standard deviation of a set of numeric samples.
|
||||
*
|
||||
* @param array<int, float|int> $samples
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
function standard_deviation($samples) {
|
||||
$sample_count = count($samples);
|
||||
$sample_square = [];
|
||||
|
||||
for ($current_sample = 0; $sample_count > $current_sample; ++$current_sample) {
|
||||
$sample_square[$current_sample] = pow($samples[$current_sample], 2);
|
||||
@@ -876,6 +1015,11 @@ function standard_deviation($samples) {
|
||||
}
|
||||
|
||||
// display_help - displays the usage of the function
|
||||
/**
|
||||
* Prints removespikes' version banner and command-line usage/help text.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function display_help() {
|
||||
global $using_cacti;
|
||||
|
||||
|
||||
+4
-3
@@ -26,7 +26,7 @@
|
||||
"type": "composer-plugin",
|
||||
"license": "GPL-2.0-only",
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"php": "^8.2",
|
||||
"ext-gmp": "*",
|
||||
"ext-json": "*",
|
||||
"ext-ldap": "*",
|
||||
@@ -36,7 +36,7 @@
|
||||
"ext-posix": "*",
|
||||
"ext-sockets": "*",
|
||||
"pear/net_dns2": "^2.0",
|
||||
"phpseclib/phpseclib": "^3.0"
|
||||
"phpseclib/phpseclib": "^4.0"
|
||||
},
|
||||
"suggest": {
|
||||
"friendsofphp/php-cs-fixer": "^3.13"
|
||||
@@ -63,8 +63,9 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "php tests/run.php",
|
||||
"lint": "phplint --no-cache --exclude=vendor",
|
||||
"phpstan": "phpstan analyse --level 6 ",
|
||||
"phpstan": "phpstan analyse --no-progress --debug",
|
||||
"phpcsfixer": "php-cs-fixer fix --allow-unsupported-php-version=yes --dry-run --diff --config=./.php-cs-fixer.php ",
|
||||
"phpcsfixit": "php-cs-fixer fix "
|
||||
},
|
||||
|
||||
Generated
+636
-335
File diff suppressed because it is too large.
Load diff
+5
-2
@@ -27,11 +27,15 @@ define('COPYRIGHT_YEARS_SHORT', '(c) 2004-' . date('Y') . ' - The Cacti Group');
|
||||
|
||||
define('RRDP_VERSION', '1.2.17');
|
||||
define('RRDP_VERSION_FULL', 'RRDtool Proxy Server v' . RRDP_VERSION . ', ' . COPYRIGHT_YEARS . "\r\n");
|
||||
define('RRDP_PHP_VERSION_REQUIRED', 70200);
|
||||
define('RRDP_PHP_VERSION_REQUIRED', 80200);
|
||||
|
||||
define('RRD_OK', 'OK u:0.00');
|
||||
define('RRD_ERROR', 'ERROR:');
|
||||
|
||||
define('RRDP_MAX_KEY_SIZE', 16384);
|
||||
define('RRDP_MAX_REQUEST_SIZE', 1048576);
|
||||
define('RRDP_MAX_DECOMPRESSED_REQUEST_SIZE', 8388608);
|
||||
|
||||
define('SEVERITY_LEVEL_NONE', 0);
|
||||
define('SEVERITY_LEVEL_EMERGENCY', 1);
|
||||
define('SEVERITY_LEVEL_ALERT', 2);
|
||||
@@ -174,7 +178,6 @@ $rrdtool_custom_cmds = [
|
||||
|
||||
$rrdp_client_cnn_params = [
|
||||
'timeout',
|
||||
'encryption',
|
||||
];
|
||||
|
||||
$rrdtool_cmds = [
|
||||
|
||||
+97
-34
@@ -22,6 +22,16 @@
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
use phpseclib4\Crypt\RSA;
|
||||
|
||||
/**
|
||||
* Main loop of the replication master process: listens for and authenticates
|
||||
* inbound cluster peer connections (verifying each peer's RSA public-key
|
||||
* fingerprint), services IPC requests from the parent process, and drives the
|
||||
* persistent RRDtool pipe used to answer replication requests.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function interact() {
|
||||
global $rrdp_remoteproxies,
|
||||
$ipc_socket_parent,
|
||||
@@ -53,9 +63,6 @@ function interact() {
|
||||
$rrdp_remoteproxies = [];
|
||||
$rrdp_replicator_state = 'running';
|
||||
|
||||
// enable message encryption
|
||||
$rsa = new \phpseclib\Crypt\RSA();
|
||||
|
||||
// start listening for cluster peers
|
||||
$rrdp_server = @socket_create((($rrdp_config['ipv6']) ? AF_INET : AF_INET), SOCK_STREAM, SOL_TCP);
|
||||
|
||||
@@ -98,6 +105,13 @@ function interact() {
|
||||
// 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];
|
||||
}
|
||||
@@ -128,11 +142,14 @@ function interact() {
|
||||
// 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);
|
||||
}
|
||||
@@ -250,9 +267,13 @@ function interact() {
|
||||
$ip = $rrdp_remoteproxies[$index]['ip'];
|
||||
$rsa_finger_print = isset($rrdp_remote_proxies[$ip]) ? $rrdp_remote_proxies[$ip]['fingerprint'] : 'unknown';
|
||||
|
||||
$rsa->loadKey($client_public_key);
|
||||
try {
|
||||
$remote_fingerprint = RSA::loadPublicKey($client_public_key)->getFingerprint('md5');
|
||||
} catch (Throwable $e) {
|
||||
$remote_fingerprint = false;
|
||||
}
|
||||
|
||||
if ($rsa_finger_print == $rsa->getPublicKeyFingerprint()) {
|
||||
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;
|
||||
@@ -288,6 +309,7 @@ function interact() {
|
||||
|
||||
if (substr_count($output, 'END_OF_MSG')) {
|
||||
$output = str_replace('END_OF_MSG', '', $output);
|
||||
|
||||
$rrdp_remoteproxies[$index]['last_seen'] = time();
|
||||
|
||||
break 2;
|
||||
@@ -338,9 +360,11 @@ function interact() {
|
||||
|
||||
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 {
|
||||
@@ -366,12 +390,19 @@ function interact() {
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// enable message encryption
|
||||
$rsa = $rsa = new \phpseclib\Crypt\RSA();
|
||||
|
||||
$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) {
|
||||
@@ -404,8 +435,11 @@ function __remote_connect($remote_ip, $remote_port, $remote_fingerprint) {
|
||||
}
|
||||
}
|
||||
|
||||
$rsa->loadKey($rrdp_public_key);
|
||||
$fingerprint = $rsa->getPublicKeyFingerprint();
|
||||
try {
|
||||
$fingerprint = RSA::loadPublicKey($rrdp_public_key)->getFingerprint('md5');
|
||||
} catch (Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($remote_fingerprint != $fingerprint) {
|
||||
// fingerprint mismatch
|
||||
@@ -419,6 +453,15 @@ function __remote_connect($remote_ip, $remote_port, $remote_fingerprint) {
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -460,6 +503,7 @@ function __remote_read($read_socket) {
|
||||
|
||||
if ($first_packet) {
|
||||
[$cmd, $status, $payload] = explode(' ', $packet, 3);
|
||||
|
||||
$first_packet = false;
|
||||
$packet = $payload;
|
||||
}
|
||||
@@ -485,6 +529,15 @@ function __remote_read($read_socket) {
|
||||
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, $replicator_system_start, $rrdtool_process, $rrdtool_pipes;
|
||||
|
||||
@@ -660,13 +713,20 @@ function handle__request($input, $read_socket) {
|
||||
$file_size = $file_settings[2];
|
||||
$mtime = $file_settings[3];
|
||||
|
||||
if ($folder != './') {
|
||||
$rra_subfolder = rtrim($rrdp_config['path_rra'], '/') . '/' . ltrim($folder, './');
|
||||
$rra_file_path_absolute = $rra_subfolder . '/' . $file;
|
||||
$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 (!is_dir($rra_subfolder)) {
|
||||
if (mkdir($rra_subfolder) === false) {
|
||||
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);
|
||||
@@ -678,16 +738,9 @@ function handle__request($input, $read_socket) {
|
||||
__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);
|
||||
}
|
||||
} else {
|
||||
$rra_file_path_absolute = rtrim($rrdp_config['path_rra'], '/') . '/' . $file;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -696,14 +749,20 @@ function handle__request($input, $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 ' . $folder . '/' . $file . "\r\n", $rrdtool_pipes, false, false, false, true);
|
||||
|
||||
$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);
|
||||
@@ -714,7 +773,7 @@ function handle__request($input, $read_socket) {
|
||||
/* 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 ' . rtrim($folder, '/') . '/' . $file . "\r\n", $rrdtool_pipes, false, false, false);
|
||||
$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')));
|
||||
@@ -747,9 +806,12 @@ function handle__request($input, $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 ' . $folder . '/' . $file . ".tmp\r\n", $rrdtool_pipes, false, false, false, true);
|
||||
|
||||
$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);
|
||||
@@ -779,11 +841,11 @@ function handle__request($input, $read_socket) {
|
||||
break;
|
||||
case 'RRDLAST':
|
||||
if ($cmd_options && is_array($cmd_options) && substr($cmd_options[0],-3) == 'rrd') {
|
||||
$rra_path_absolute = rtrim($rrdp_config['path_rra'], '/') . '/' . ltrim($cmd_options[0], './');
|
||||
$rra_path_absolute = rrdp_resolve_rra_path($cmd_options[0]);
|
||||
|
||||
if (file_exists($rra_path_absolute)) {
|
||||
if ($rra_path_absolute !== false) {
|
||||
if ($rrdcached_pid) {
|
||||
$rrd_cmd = 'flushcached ' . $cmd_options[0];
|
||||
$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) {
|
||||
@@ -794,7 +856,7 @@ function handle__request($input, $read_socket) {
|
||||
}
|
||||
}
|
||||
|
||||
$rrd_cmd = 'last ' . $cmd_options[0];
|
||||
$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');
|
||||
@@ -815,11 +877,12 @@ function handle__request($input, $read_socket) {
|
||||
break;
|
||||
case 'RRDDUMP':
|
||||
if ($cmd_options && is_array($cmd_options) && substr($cmd_options[0],-3) == 'rrd') {
|
||||
$rra_path_absolute = rtrim($rrdp_config['path_rra'], '/') . '/' . ltrim($cmd_options[0], './');
|
||||
$rra_path_absolute = rrdp_resolve_rra_path($cmd_options[0]);
|
||||
|
||||
if (file_exists($rra_path_absolute)) {
|
||||
if ($rra_path_absolute !== false) {
|
||||
if ($rrdcached_pid) {
|
||||
$rrd_cmd = 'flushcached ' . $cmd_options[0];
|
||||
$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) {
|
||||
@@ -830,7 +893,7 @@ function handle__request($input, $read_socket) {
|
||||
}
|
||||
}
|
||||
|
||||
$rrd_cmd = 'dump ' . $cmd_options[0];
|
||||
$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');
|
||||
|
||||
+142
-45
@@ -22,8 +22,18 @@
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
use phpseclib\Crypt\RSA;
|
||||
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.
|
||||
@@ -45,7 +55,9 @@ function interact($socket_client) {
|
||||
$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";
|
||||
|
||||
@@ -59,9 +71,6 @@ function interact($socket_client) {
|
||||
$real_client_resource_id = rrdp_system__get_resource_id($socket_client);
|
||||
$ipc_parent_resource_id = rrdp_system__get_resource_id($ipc_socket_parent);
|
||||
|
||||
// enable message encryption
|
||||
$rsa = new RSA();
|
||||
|
||||
$input = '';
|
||||
|
||||
while (1) {
|
||||
@@ -110,6 +119,7 @@ function interact($socket_client) {
|
||||
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));
|
||||
@@ -118,6 +128,14 @@ function interact($socket_client) {
|
||||
$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 !!!
|
||||
@@ -130,9 +148,15 @@ function interact($socket_client) {
|
||||
$client_public_key = $transaction;
|
||||
socket_getpeername($socket_client, $ip);
|
||||
$rsa_finger_print = isset($rrdp_config['remote_clients'][$ip]) ? $rrdp_config['remote_clients'][$ip] : 'unknown';
|
||||
$rsa->loadKey($client_public_key);
|
||||
|
||||
if ($rsa_finger_print == $rsa->getPublicKeyFingerprint()) {
|
||||
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
|
||||
@@ -161,7 +185,14 @@ function interact($socket_client) {
|
||||
}
|
||||
|
||||
if (strpos($transaction, "\x1f\x8b") === 0) {
|
||||
$transaction = gzdecode($transaction);
|
||||
$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
|
||||
@@ -184,11 +215,37 @@ function interact($socket_client) {
|
||||
__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];
|
||||
}
|
||||
@@ -203,13 +260,19 @@ function interact($socket_client) {
|
||||
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) {
|
||||
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")) {
|
||||
@@ -243,7 +306,8 @@ function interact($socket_client) {
|
||||
case 'setenv':
|
||||
if (__sizeof($options) >= 2 && in_array($options[0], $rrdtool_env_vars, true)) {
|
||||
$enviro_var = array_shift($options);
|
||||
putenv("'" . $enviro_var . "'='" . str_replace("'", '', implode(' ', $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 {
|
||||
@@ -278,19 +342,6 @@ function interact($socket_client) {
|
||||
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);
|
||||
}
|
||||
} elseif ($options[0] == 'encryption') {
|
||||
if (in_array($options[1], ['null', 'off', 'disabled', '-1'], true)) {
|
||||
rrdp_system__socket_write($socket_client, encrypt("% Encryption will be disabled.\n" . RRD_OK, $client_public_key) . $end_of_sequence);
|
||||
$encryption = false;
|
||||
__logging(LOGGING_LOCATION_BUFFERED, 'RESPONSE: ' . RRD_OK, 'IPC', SEVERITY_LEVEL_DEBUG);
|
||||
} elseif (in_array($options[1], ['true', 'on', 'enabled', '1'], true)) {
|
||||
rrdp_system__socket_write($socket_client, encrypt("% Encryption will be enabled.\n" . RRD_OK, $client_public_key) . $end_of_sequence);
|
||||
$encryption = true;
|
||||
__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);
|
||||
@@ -308,55 +359,82 @@ function interact($socket_client) {
|
||||
// scan RRA folder
|
||||
clearstatcache();
|
||||
$buffer = '';
|
||||
$rra_path_absolute = rtrim($rrdp_config['path_rra'], '/');
|
||||
$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) {
|
||||
if (substr($file->getPathname(),-3) == 'rrd') {
|
||||
$buffer .= str_replace($rra_path_absolute . '/', '', $file->getPathname()) . ',' . $file->getSize() . ',' . $file->getMTime() . "\r\n";
|
||||
$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':
|
||||
$rrdp_exec_return = call_user_func_array($cmd, $options);
|
||||
$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':
|
||||
$rrdp_exec_status = call_user_func_array($cmd, $options);
|
||||
$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':
|
||||
$options[1] = 0700;
|
||||
$options[2] = true;
|
||||
$rrdp_exec_status = call_user_func_array($cmd, $options);
|
||||
$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':
|
||||
if (__sizeof($options) == 1 && (substr($options[0],-3) == 'rrd') && file_exists($options[0]) && $rrdp_config['path_rra_archive']) {
|
||||
$source_file = $options[0];
|
||||
$target_dir = $rrdp_config['path_rra_archive'] . '/' . ltrim(dirname($source_file), './');
|
||||
$target_file = $rrdp_config['path_rra_archive'] . '/' . $source_file;
|
||||
$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);
|
||||
mkdir($target_dir, 0700, true);
|
||||
}
|
||||
|
||||
$rrdp_exec_status = rename($source_file, $target_file);
|
||||
}
|
||||
} else {
|
||||
$rrdp_exec_status = false;
|
||||
}
|
||||
@@ -366,22 +444,26 @@ function interact($socket_client) {
|
||||
|
||||
break;
|
||||
case 'unlink':
|
||||
if (__sizeof($options) == 1 && (substr($options[0],-3) == 'rrd') & file_exists($options[0])) {
|
||||
$rrdp_exec_status = call_user_func_array($cmd, $options);
|
||||
} else {
|
||||
$rrdp_exec_status = false;
|
||||
}
|
||||
$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':
|
||||
$cmd_options = str_replace('-R=.', '-R=' . $rrdp_config['path_rra'], $cmd_options);
|
||||
$rrdp_exec_return = shell_exec(PHP_BINARY . ' -q ' . $rrdp_config['path_cli'] . '/removespikes.php ' . $cmd_options);
|
||||
$parsed_options = rrdp_parse_removespikes_options($cmd_options);
|
||||
|
||||
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);
|
||||
// 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':
|
||||
@@ -397,13 +479,19 @@ function interact($socket_client) {
|
||||
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) {
|
||||
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")) {
|
||||
@@ -419,12 +507,21 @@ function interact($socket_client) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+575
-19
@@ -22,17 +22,23 @@
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
use phpseclib\Crypt\Random;
|
||||
use phpseclib\Crypt\Rijndael;
|
||||
use phpseclib\Crypt\RSA;
|
||||
use phpseclib4\Crypt\Rijndael;
|
||||
use phpseclib4\Crypt\RSA;
|
||||
|
||||
/**
|
||||
* Starts an RRDtool pipe-mode process and returns its process handle and stdio pipes.
|
||||
*
|
||||
* @param array<string, mixed> $rrdp_config
|
||||
*
|
||||
* @return array{0: resource, 1: array<int, resource>}|false
|
||||
*/
|
||||
function rrdtool_pipe_init($rrdp_config) {
|
||||
$fds = [
|
||||
0 => ['pipe', 'r'], // stdin
|
||||
1 => ['pipe', 'w'], // stdout
|
||||
2 => ['file', '/dev/null', 'a'] // stderr
|
||||
];
|
||||
$process = @proc_open($rrdp_config['path_rrdtool'] . ' - ' . $rrdp_config['path_rra'], $fds, $pipes);
|
||||
$process = @proc_open([$rrdp_config['path_rrdtool'], '-', $rrdp_config['path_rra']], $fds, $pipes);
|
||||
|
||||
if ($process === false) {
|
||||
return false;
|
||||
@@ -45,10 +51,31 @@ function rrdtool_pipe_init($rrdp_config) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminates a previously opened RRDtool pipe-mode process.
|
||||
*
|
||||
* @param resource $process
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdtool_pipe_close($process) {
|
||||
proc_close($process);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a command to an open RRDtool pipe and streams the (optionally compressed,
|
||||
* encrypted) response back over the client socket in chunks as it arrives.
|
||||
*
|
||||
* @param string $command
|
||||
* @param array<int, resource> $pipes
|
||||
* @param resource|\Socket|false $socket
|
||||
* @param string $client_public_key
|
||||
* @param bool $compression
|
||||
* @param bool $silent_mode
|
||||
* @param string $terminator
|
||||
*
|
||||
* @return bool|string|null
|
||||
*/
|
||||
function rrdtool_pipe_execute($command, $pipes, $socket, $client_public_key, $compression, $silent_mode = false, $terminator = "_EOT_\r\n") {
|
||||
$return_code = fwrite($pipes[0], $command);
|
||||
|
||||
@@ -160,47 +187,533 @@ function rrdtool_pipe_execute($command, $pipes, $socket, $client_public_key, $co
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts $output for the holder of $rsa_key: a random AES-256-CBC session key
|
||||
* encrypts the payload, and that session key is itself RSA-encrypted and prepended.
|
||||
*
|
||||
* @param string $output
|
||||
* @param string $rsa_key
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
function encrypt($output, $rsa_key) {
|
||||
global $encryption;
|
||||
|
||||
if ($encryption) {
|
||||
$rsa = new RSA();
|
||||
$aes = new Rijndael();
|
||||
$aes_key = Random::string(192);
|
||||
if (!$encryption) {
|
||||
return $output;
|
||||
}
|
||||
|
||||
try {
|
||||
$public = RSA::loadPublicKey($rsa_key);
|
||||
|
||||
if (!$public instanceof phpseclib4\Crypt\RSA\PublicKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$aes = new Rijndael('cbc');
|
||||
$aes_key = random_bytes(32);
|
||||
|
||||
$aes->setKey($aes_key);
|
||||
$aes->setIV(str_repeat("\0", 16));
|
||||
$ciphertext = base64_encode($aes->encrypt($output));
|
||||
$rsa->loadKey($rsa_key);
|
||||
$aes_key = base64_encode($rsa->encrypt($aes_key));
|
||||
$aes_key = base64_encode($public->encrypt($aes_key));
|
||||
$aes_key_length = str_pad(dechex(strlen($aes_key)), 3, '0', STR_PAD_LEFT);
|
||||
|
||||
return $aes_key_length . $aes_key . $ciphertext;
|
||||
} else {
|
||||
return $output;
|
||||
} catch (Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses encrypt(): RSA-decrypts the embedded AES session key with this proxy's
|
||||
* private key, then AES-256-CBC decrypts the remaining ciphertext.
|
||||
*
|
||||
* @param string $input
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
function decrypt($input) {
|
||||
global $rrdp_config, $encryption;
|
||||
|
||||
if ($encryption) {
|
||||
$rsa = new RSA();
|
||||
$aes = new Rijndael();
|
||||
if (!$encryption) {
|
||||
return $input;
|
||||
}
|
||||
|
||||
if (strlen($input) < 4 || !ctype_xdigit(substr($input, 0, 3))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$aes_key_length = hexdec(substr($input, 0, 3));
|
||||
|
||||
if ($aes_key_length < 1 || strlen($input) <= 3 + $aes_key_length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$aes_key = base64_decode(substr($input, 3, $aes_key_length), true);
|
||||
$ciphertext = base64_decode(substr($input, 3 + $aes_key_length), true);
|
||||
|
||||
$rsa->loadKey($rrdp_config['encryption']['private_key']);
|
||||
$aes_key = $rsa->decrypt($aes_key);
|
||||
if ($aes_key === false || $aes_key === '' || $ciphertext === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$private = RSA::loadPrivateKey($rrdp_config['encryption']['private_key']);
|
||||
|
||||
if (!$private instanceof phpseclib4\Crypt\RSA\PrivateKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$aes = new Rijndael('cbc');
|
||||
$aes_key = $private->decrypt($aes_key);
|
||||
|
||||
if (!is_string($aes_key) || $aes_key === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// phpseclib 2 truncated oversized Rijndael keys to 256 bits.
|
||||
if (strlen($aes_key) > 32) {
|
||||
$aes_key = substr($aes_key, 0, 32);
|
||||
}
|
||||
|
||||
$aes->setKey($aes_key);
|
||||
$aes->setIV(str_repeat("\0", 16));
|
||||
|
||||
return $aes->decrypt($ciphertext);
|
||||
} else {
|
||||
return $input;
|
||||
} catch (Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a path is absolute (POSIX-rooted, UNC/backslash-rooted, or
|
||||
* Windows drive-letter-rooted).
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function rrdp_path_is_absolute($path) {
|
||||
return str_starts_with($path, '/') || str_starts_with($path, '\\') || preg_match('/^[A-Za-z]:[\\\\\/]/', $path) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a client-supplied path against the configured RRA root, rejecting it
|
||||
* if it would escape that root.
|
||||
*
|
||||
* @param string $path
|
||||
* @param bool $must_exist
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
function rrdp_resolve_rra_path($path, $must_exist = true) {
|
||||
global $rrdp_config;
|
||||
|
||||
return rrdp_resolve_path_within($rrdp_config['path_rra'], $path, $must_exist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves $path relative to $base_path and confirms the result (or, for paths that
|
||||
* don't need to exist yet, its nearest existing ancestor) stays within $base_path,
|
||||
* rejecting traversal, absolute paths, null bytes, and symlink escapes.
|
||||
*
|
||||
* @param string $base_path
|
||||
* @param mixed $path
|
||||
* @param bool $must_exist
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
function rrdp_resolve_path_within($base_path, $path, $must_exist = true) {
|
||||
if (!is_string($path) || $path === '' || str_contains($path, "\0") || rrdp_path_is_absolute($path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$segments = preg_split('~[\\\\/]+~', $path, -1, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
if ($segments === false || in_array('..', $segments, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$base = realpath($base_path);
|
||||
|
||||
if ($base === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($must_exist) {
|
||||
$resolved = realpath($base . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $segments));
|
||||
|
||||
return $resolved !== false && rrdp_path_is_within($resolved, $base) ? $resolved : false;
|
||||
}
|
||||
|
||||
// Walk the path one segment at a time (instead of only checking the nearest
|
||||
// existing ancestor) so that a symlink anywhere along a not-yet-fully-existing
|
||||
// path is resolved and confined, even if the symlink itself is dangling.
|
||||
$walked = $base;
|
||||
|
||||
for ($i = 0; $i < count($segments); $i++) {
|
||||
$next = $walked . DIRECTORY_SEPARATOR . $segments[$i];
|
||||
|
||||
if (is_link($next)) {
|
||||
$resolved_link = realpath($next);
|
||||
|
||||
if ($resolved_link === false || !rrdp_path_is_within($resolved_link, $base)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$walked = $resolved_link;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!file_exists($next)) {
|
||||
// the remaining segments are the not-yet-created portion of the path
|
||||
return $walked . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, array_slice($segments, $i));
|
||||
}
|
||||
|
||||
$walked = $next;
|
||||
}
|
||||
|
||||
// the full path already exists; confirm its canonical form is still confined
|
||||
$resolved = realpath($walked);
|
||||
|
||||
return $resolved !== false && rrdp_path_is_within($resolved, $base) ? $resolved : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a canonical path is equal to, or nested under, a canonical base path.
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $base
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function rrdp_path_is_within($path, $base) {
|
||||
return $path === $base || str_starts_with($path, $base . DIRECTORY_SEPARATOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects RRDtool command arguments that attempt path traversal, absolute paths, or
|
||||
* embedded command framing (via null bytes/newlines) so such commands can be rejected.
|
||||
* This is a lexical, framing-level check only: it does not resolve operands against
|
||||
* the filesystem, so it cannot by itself catch a symlink placed inside the RRA root
|
||||
* that points outside it. Callers must also pass the command through
|
||||
* rrdp_resolve_command_paths() before dispatching it to RRDtool.
|
||||
*
|
||||
* @param string $command
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function rrdp_command_has_unsafe_path($command) {
|
||||
return str_contains($command, "\0")
|
||||
|| str_contains($command, "\r")
|
||||
|| str_contains($command, "\n")
|
||||
|| preg_match('~(^|[[:space:]=:,\\\\/])\.\.([\\\\/]|$)~', $command) === 1
|
||||
|| preg_match('~(^|[[:space:]=:,])(?:/|\\\\|[A-Za-z]:[\\\\/])~', $command) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves every RRD file path operand referenced by an RRDtool command (the
|
||||
* leading bare file argument(s), e.g. for update/fetch/dump/restore, and any
|
||||
* DEF:/SDEF: clauses used by graph/graphv/xport) against the RRA root, and
|
||||
* rewrites the command to use the canonical resolved paths. This closes the gap
|
||||
* left by rrdp_command_has_unsafe_path(): resolving through realpath() follows
|
||||
* (and thereby confines) symlinks instead of only rejecting lexical traversal.
|
||||
*
|
||||
* @param string $cmd
|
||||
* @param string $cmd_options
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
function rrdp_resolve_command_paths($cmd, $cmd_options) {
|
||||
static $leading_file_operands = [
|
||||
'create' => [false],
|
||||
'update' => [true],
|
||||
'updatev' => [true],
|
||||
'dump' => [true],
|
||||
'restore' => [true, false],
|
||||
'last' => [true],
|
||||
'lastupdate' => [true],
|
||||
'first' => [true],
|
||||
'info' => [true],
|
||||
'fetch' => [true],
|
||||
'tune' => [true],
|
||||
'resize' => [true],
|
||||
'graph' => [false],
|
||||
'graphv' => [false],
|
||||
'flushcached' => [true],
|
||||
];
|
||||
|
||||
$tokens = preg_split('/\s+/', trim((string) $cmd_options), -1, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
if ($tokens === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$must_exist_list = $leading_file_operands[$cmd] ?? [];
|
||||
$operand_index = 0;
|
||||
|
||||
foreach ($tokens as $index => $token) {
|
||||
if (preg_match('/^((?:DEF|SDEF):[^=]+=)([^:]+)(:.*)$/i', $token, $matches) === 1) {
|
||||
$resolved = rrdp_resolve_rra_path($matches[2]);
|
||||
|
||||
if ($resolved === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tokens[$index] = $matches[1] . $resolved . $matches[3];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($token[0] === '-' || preg_match('/^(?:CDEF|VDEF):/i', $token) === 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($operand_index < count($must_exist_list)) {
|
||||
$resolved = rrdp_resolve_rra_path($token, $must_exist_list[$operand_index]);
|
||||
|
||||
if ($resolved === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tokens[$index] = $resolved;
|
||||
$operand_index++;
|
||||
}
|
||||
}
|
||||
|
||||
return implode(' ', $tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses and whitelists removespikes CLI options from a client request, resolving
|
||||
* the RRD file argument against the RRA root and rejecting anything unrecognized,
|
||||
* malformed, or containing more than one RRD file argument.
|
||||
*
|
||||
* @param string $input
|
||||
*
|
||||
* @return array<int, string>|false
|
||||
*/
|
||||
function rrdp_parse_removespikes_options($input) {
|
||||
$options = str_getcsv($input, ' ', '"', '\\');
|
||||
$result = [];
|
||||
$has_rrdfile = false;
|
||||
|
||||
foreach ($options as $option) {
|
||||
if ($option === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($option, ['--backup', '--html', '--debug', '-d', '--dryrun', '-D'], true)) {
|
||||
$result[] = $option;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^(?:-M|--method)=(?:stddev|variance)$/', $option)
|
||||
|| preg_match('/^(?:-A|--avgnan)=(?:avg|nan)$/', $option)
|
||||
|| preg_match('/^(?:-S|--stddev|-P|--percent|-N|--number|-n|-O|--outliers)=[0-9]+(?:\.[0-9]+)?$/', $option)) {
|
||||
$result[] = $option;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^(?:-R|--rrdfile)=(.+)$/', $option, $matches)) {
|
||||
if ($has_rrdfile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$path = rrdp_resolve_rra_path($matches[1]);
|
||||
|
||||
if ($path === false || !str_ends_with(strtolower($path), '.rrd')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result[] = $option[1] === 'R' ? '-R=' . $path : '--rrdfile=' . $path;
|
||||
$has_rrdfile = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $has_rrdfile ? $result : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs an argv-style child process (no shell interpolation) and captures its
|
||||
* combined stdout/stderr and exit status.
|
||||
*
|
||||
* @param array<int, string> $command
|
||||
* @param array<string, string>|null $environment
|
||||
*
|
||||
* @return array{status: int, stdout: string, stderr: string}|false
|
||||
*/
|
||||
function rrdp_run_process($command, $environment = null) {
|
||||
$descriptors = [
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['redirect', 1],
|
||||
];
|
||||
$process = proc_open($command, $descriptors, $pipes, null, $environment);
|
||||
|
||||
if (!is_resource($process)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$stdout = stream_get_contents($pipes[1]);
|
||||
fclose($pipes[1]);
|
||||
$status = proc_close($process);
|
||||
|
||||
return ['status' => $status, 'stdout' => $stdout, 'stderr' => ''];
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically writes $contents to $path (via a temp file in the same directory,
|
||||
* chmod'd before the rename) so the file never appears with the wrong permissions.
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $contents
|
||||
* @param int $mode
|
||||
*
|
||||
* @return int|false
|
||||
*/
|
||||
function rrdp_write_secure_file($path, $contents, $mode = 0600) {
|
||||
$directory = dirname($path);
|
||||
$temporary = tempnam($directory, '.rrdp-');
|
||||
|
||||
if ($temporary === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@chmod($temporary, $mode);
|
||||
$written = file_put_contents($temporary, $contents, LOCK_EX);
|
||||
|
||||
if ($written === false || !@rename($temporary, $path)) {
|
||||
@unlink($temporary);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@chmod($path, $mode);
|
||||
|
||||
return $written;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a public/private key pair actually match (by comparing
|
||||
* fingerprints) and then atomically writes both files, restoring the previous
|
||||
* private key if the public key write fails partway through.
|
||||
*
|
||||
* @param string $public_path
|
||||
* @param string $public_key
|
||||
* @param string $private_path
|
||||
* @param string $private_key
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function rrdp_write_key_pair($public_path, $public_key, $private_path, $private_key) {
|
||||
try {
|
||||
$public_fingerprint = RSA::loadPublicKey($public_key)->getFingerprint('sha256');
|
||||
$private_fingerprint = RSA::loadPrivateKey($private_key)->getPublicKey()->getFingerprint('sha256');
|
||||
} catch (Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hash_equals($public_fingerprint, $private_fingerprint)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$public_temporary = tempnam(dirname($public_path), '.rrdp-public-');
|
||||
$private_temporary = tempnam(dirname($private_path), '.rrdp-private-');
|
||||
|
||||
if ($public_temporary === false || $private_temporary === false) {
|
||||
if ($public_temporary !== false) {
|
||||
@unlink($public_temporary);
|
||||
}
|
||||
|
||||
if ($private_temporary !== false) {
|
||||
@unlink($private_temporary);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@chmod($public_temporary, 0644);
|
||||
@chmod($private_temporary, 0600);
|
||||
|
||||
if (file_put_contents($public_temporary, $public_key, LOCK_EX) === false
|
||||
|| file_put_contents($private_temporary, $private_key, LOCK_EX) === false) {
|
||||
@unlink($public_temporary);
|
||||
@unlink($private_temporary);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$previous_private = file_exists($private_path) ? file_get_contents($private_path) : false;
|
||||
|
||||
if (!@rename($private_temporary, $private_path)) {
|
||||
@unlink($public_temporary);
|
||||
@unlink($private_temporary);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!@rename($public_temporary, $public_path)) {
|
||||
@unlink($public_temporary);
|
||||
|
||||
if ($previous_private === false) {
|
||||
@unlink($private_path);
|
||||
} else {
|
||||
rrdp_write_secure_file($private_path, $previous_private, 0600);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@chmod($public_path, 0644);
|
||||
@chmod($private_path, 0600);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes $output to a socket in a loop, retrying until every byte has been sent
|
||||
* (socket_write() may perform a short write).
|
||||
*
|
||||
* @param resource|\Socket $socket
|
||||
* @param string $output
|
||||
*
|
||||
* @return int|false
|
||||
*/
|
||||
function rrdp_socket_write_all($socket, $output) {
|
||||
$length = strlen($output);
|
||||
$written = 0;
|
||||
|
||||
while ($written < $length) {
|
||||
$result = @socket_write($socket, substr($output, $written), $length - $written);
|
||||
|
||||
if ($result === false || $result === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$written += $result;
|
||||
}
|
||||
|
||||
return $written;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwards a log message to the parent process over the IPC socket if it meets
|
||||
* the configured severity/category thresholds for its logging location.
|
||||
*
|
||||
* @param int $location
|
||||
* @param string $msg
|
||||
* @param string $category
|
||||
* @param int $severity
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function __logging($location, $msg, $category, $severity) {
|
||||
global $rrdp_config, $ipc_socket_parent, $ipc_global_resource_id, $c_pid;
|
||||
|
||||
@@ -212,14 +725,39 @@ function __logging($location, $msg, $category, $severity) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Null/false-safe wrapper around sizeof().
|
||||
*
|
||||
* @param mixed $array
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
function __sizeof($array) {
|
||||
return ($array === false || !is_array($array)) ? 0 : sizeof($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Null/false-safe wrapper around count().
|
||||
*
|
||||
* @param mixed $array
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
function __count($array) {
|
||||
return ($array === false || !is_array($array)) ? 0 : count($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom error handler: logs PHP user-level errors/warnings/notices via __logging()
|
||||
* and terminates the process on E_USER_ERROR.
|
||||
*
|
||||
* @param int $code
|
||||
* @param string $text
|
||||
* @param string $file
|
||||
* @param int $line
|
||||
*
|
||||
* @return bool|null
|
||||
*/
|
||||
function __errorHandler($code, $text, $file, $line) {
|
||||
if (!($code & error_reporting())) {
|
||||
return null;
|
||||
@@ -249,6 +787,13 @@ function __errorHandler($code, $text, $file, $line) {
|
||||
}
|
||||
|
||||
// signal handler for master, slave and client processes
|
||||
/**
|
||||
* Exits cleanly on SIGTERM; ignores SIGUSR1/SIGHUP and any other signal.
|
||||
*
|
||||
* @param int $signo
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function __sig_handler($signo) {
|
||||
switch ($signo) {
|
||||
case SIGTERM:
|
||||
@@ -262,12 +807,23 @@ function __sig_handler($signo) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks (via `ps`) whether another rrdtool-proxy.php process is already running.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function is_rrdtool_proxy_running() {
|
||||
exec('ps -ef | grep -v grep | grep -E "php .*rrdtool-proxy.php"', $output);
|
||||
|
||||
return (__sizeof($output) >= 2) ? false : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks (via `ps`) whether an rrdcached process is already running, unless
|
||||
* $force is set.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function is_rrdcached_running() {
|
||||
global $force;
|
||||
|
||||
|
||||
+83
-29
@@ -22,6 +22,17 @@
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
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;
|
||||
@@ -41,9 +52,6 @@ function interact() {
|
||||
$rrdp_remoteproxies = [];
|
||||
$rrdp_replicator_state = 'running';
|
||||
|
||||
// enable message encryption
|
||||
$rsa = new \phpseclib\Crypt\RSA();
|
||||
|
||||
// start listening for cluster peers
|
||||
$rrdp_server = @socket_create((($rrdp_config['ipv6']) ? AF_INET : AF_INET), SOCK_STREAM, SOL_TCP);
|
||||
|
||||
@@ -87,6 +95,13 @@ function interact() {
|
||||
// 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];
|
||||
}
|
||||
@@ -117,12 +132,14 @@ function interact() {
|
||||
// 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);
|
||||
}
|
||||
@@ -237,9 +254,13 @@ function interact() {
|
||||
$ip = $rrdp_remoteproxies[$index]['ip'];
|
||||
$rsa_finger_print = isset($rrdp_remote_proxies[$ip]) ? $rrdp_remote_proxies[$ip]['fingerprint'] : 'unknown';
|
||||
|
||||
$rsa->loadKey($client_public_key);
|
||||
try {
|
||||
$remote_fingerprint = RSA::loadPublicKey($client_public_key)->getFingerprint('md5');
|
||||
} catch (Throwable $e) {
|
||||
$remote_fingerprint = false;
|
||||
}
|
||||
|
||||
if ($rsa_finger_print == $rsa->getPublicKeyFingerprint()) {
|
||||
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;
|
||||
@@ -324,9 +345,11 @@ function interact() {
|
||||
|
||||
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 {
|
||||
@@ -353,12 +376,19 @@ function interact() {
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// enable message encryption
|
||||
$rsa = $rsa = new \phpseclib\Crypt\RSA();
|
||||
|
||||
$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) {
|
||||
@@ -392,8 +422,11 @@ function __remote_connect($remote_ip, $remote_port, $remote_fingerprint) {
|
||||
}
|
||||
}
|
||||
|
||||
$rsa->loadKey($rrdp_public_key);
|
||||
$fingerprint = $rsa->getPublicKeyFingerprint();
|
||||
try {
|
||||
$fingerprint = RSA::loadPublicKey($rrdp_public_key)->getFingerprint('md5');
|
||||
} catch (Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($remote_fingerprint != $fingerprint) {
|
||||
// fingerprint mismatch
|
||||
@@ -407,6 +440,15 @@ function __remote_connect($remote_ip, $remote_port, $remote_fingerprint) {
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -474,6 +516,15 @@ function __remote_read($read_socket) {
|
||||
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;
|
||||
@@ -656,14 +707,20 @@ function handle__request($input, $read_socket) {
|
||||
$file_size = $file_settings[2];
|
||||
$mtime = $file_settings[3];
|
||||
|
||||
if ($folder != './') {
|
||||
$rra_subfolder = rtrim($rrdp_config['path_rra'], '/') . '/' . ltrim($folder, './');
|
||||
$rra_relative_path = ($folder !== './' ? ltrim($folder, './') . '/' : '') . $file;
|
||||
$rra_file_path_absolute = rrdp_resolve_rra_path($rra_relative_path, false);
|
||||
|
||||
$rra_file_path_absolute = $rra_subfolder . '/' . $file;
|
||||
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 (!is_dir($rra_subfolder)) {
|
||||
if (mkdir($rra_subfolder) === false) {
|
||||
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);
|
||||
@@ -677,9 +734,6 @@ function handle__request($input, $read_socket) {
|
||||
|
||||
__logging(LOGGING_LOCATION_BUFFERED, 'SYNC: restored: ' . $rra_subfolder , 'MSR', SEVERITY_LEVEL_INFORMATION);
|
||||
}
|
||||
} else {
|
||||
$rra_file_path_absolute = rtrim($rrdp_config['path_rra'], '/') . '/' . $file;
|
||||
}
|
||||
|
||||
if (!file_exists($rra_file_path_absolute)) {
|
||||
__logging(LOGGING_LOCATION_BUFFERED, 'SYNC: file not found: ' . $rra_file_path_absolute , 'MSR', SEVERITY_LEVEL_DEBUG);
|
||||
@@ -698,7 +752,7 @@ function handle__request($input, $read_socket) {
|
||||
|
||||
file_put_contents($rra_file_path_absolute . '.xml', $rrd_data);
|
||||
|
||||
$rrd_exec_status = rrdtool_pipe_execute('restore ' . $rra_file_path_absolute . '.xml ' . $folder . '/' . $file . "\r\n", $rrdtool_pipes, false, false, false, true);
|
||||
$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);
|
||||
@@ -714,7 +768,7 @@ function handle__request($input, $read_socket) {
|
||||
/* 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 ' . rtrim($folder, '/') . '/' . $file . "\r\n", $rrdtool_pipes, false, false, false);
|
||||
$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')));
|
||||
@@ -750,7 +804,7 @@ function handle__request($input, $read_socket) {
|
||||
[$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 ' . $folder . '/' . $file . ".tmp\r\n", $rrdtool_pipes, false, false, false, true);
|
||||
$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);
|
||||
@@ -780,11 +834,11 @@ function handle__request($input, $read_socket) {
|
||||
break;
|
||||
case 'RRDLAST':
|
||||
if ($cmd_options && is_array($cmd_options) && substr($cmd_options[0],-3) == 'rrd') {
|
||||
$rra_path_absolute = rtrim($rrdp_config['path_rra'], '/') . '/' . ltrim($cmd_options[0], './');
|
||||
$rra_path_absolute = rrdp_resolve_rra_path($cmd_options[0]);
|
||||
|
||||
if (file_exists($rra_path_absolute)) {
|
||||
if ($rra_path_absolute !== false) {
|
||||
if ($rrdcached_pid) {
|
||||
$rrd_cmd = 'flushcached ' . $cmd_options[0];
|
||||
$rrd_cmd = 'flushcached ' . $rra_path_absolute;
|
||||
|
||||
$rrd_exec_status = rrdtool_pipe_execute($rrd_cmd . "\r\n", $rrdtool_pipes, false, false, false, true);
|
||||
|
||||
@@ -796,7 +850,7 @@ function handle__request($input, $read_socket) {
|
||||
}
|
||||
}
|
||||
|
||||
$rrd_cmd = 'last ' . $cmd_options[0];
|
||||
$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');
|
||||
@@ -817,11 +871,11 @@ function handle__request($input, $read_socket) {
|
||||
break;
|
||||
case 'RRDDUMP':
|
||||
if ($cmd_options && is_array($cmd_options) && substr($cmd_options[0],-3) == 'rrd') {
|
||||
$rra_path_absolute = rtrim($rrdp_config['path_rra'], '/') . '/' . ltrim($cmd_options[0], './');
|
||||
$rra_path_absolute = rrdp_resolve_rra_path($cmd_options[0]);
|
||||
|
||||
if (file_exists($rra_path_absolute)) {
|
||||
if ($rra_path_absolute !== false) {
|
||||
if ($rrdcached_pid) {
|
||||
$rrd_cmd = 'flushcached ' . $cmd_options[0];
|
||||
$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) {
|
||||
@@ -832,7 +886,7 @@ function handle__request($input, $read_socket) {
|
||||
}
|
||||
}
|
||||
|
||||
$rrd_cmd = 'dump ' . $cmd_options[0];
|
||||
$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');
|
||||
|
||||
+108
-5
@@ -26,7 +26,7 @@ global $active_config;
|
||||
|
||||
include('../vendor/autoload.php');
|
||||
|
||||
use phpseclib3\Crypt\RSA;
|
||||
use phpseclib4\Crypt\RSA;
|
||||
|
||||
$active_config = [
|
||||
'version' => RRDP_VERSION,
|
||||
@@ -57,11 +57,23 @@ $active_config = [
|
||||
'rrdcache_write_threads' => 4,
|
||||
'enable_password' => '',
|
||||
];
|
||||
$rrdp_config = [];
|
||||
$rrdp_config_tmp = [];
|
||||
$rrdp_remote_clients = [];
|
||||
$rrdp_remote_proxies = [];
|
||||
|
||||
wizard();
|
||||
|
||||
/**
|
||||
* Runs the interactive command-line configuration wizard: checks system
|
||||
* requirements, prompts for and validates each configuration setting, generates
|
||||
* or reuses the RSA key pair, and writes out the resulting configuration file.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function wizard() {
|
||||
global $microtime_start, $active_config;
|
||||
global $microtime_start, $active_config, $rrdp_config, $rrdp_config_tmp;
|
||||
global $rrdp_remote_clients, $rrdp_remote_proxies;
|
||||
|
||||
define('WIZARD_RUNNING', 1);
|
||||
|
||||
@@ -218,8 +230,7 @@ function wizard() {
|
||||
$public = $private->getPublicKey();
|
||||
|
||||
rrd_system__system_boolean_message('create: Generate RSA key-pair (2048Bit)', [$private, $public], true);
|
||||
rrd_system__system_boolean_message(' save: New RSA public key', file_put_contents('./include/public.key', $public), true);
|
||||
rrd_system__system_boolean_message(' save: New RSA private key', file_put_contents('./include/private.key', $private), true);
|
||||
rrd_system__system_boolean_message(' save: New RSA key pair', rrdp_write_key_pair('./include/public.key', (string) $public, './include/private.key', (string) $private), true);
|
||||
}
|
||||
|
||||
$filter_options = ['options' => ['regexp' => '/[\s]*/']];
|
||||
@@ -707,10 +718,20 @@ function wizard() {
|
||||
wizard_handle_output(wizard_prompt_wordwrap('Example', $msg, 75), true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that $path (or the configured RRDtool path) is an absolute,
|
||||
* executable RRDtool binary of at least version 1.5, printing an error and
|
||||
* returning false otherwise.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
function wizard_verify_rrdtool($path) {
|
||||
global $active_config;
|
||||
|
||||
$valid_path = false;
|
||||
$msg = 'Invalid RRDtool path.';
|
||||
|
||||
if (!$path) {
|
||||
$path = $active_config['path_rrdtool'];
|
||||
@@ -753,10 +774,19 @@ function wizard_verify_rrdtool($path) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that $path (or the configured rrdcached path) is an absolute,
|
||||
* executable file, printing an error and returning false otherwise.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
function wizard_verify_rrdcached($path) {
|
||||
global $active_config;
|
||||
|
||||
$valid_path = false;
|
||||
$msg = 'Invalid RRDCached path.';
|
||||
|
||||
if (!$path) {
|
||||
$path = $active_config['path_rrdcached'];
|
||||
@@ -782,6 +812,14 @@ function wizard_verify_rrdcached($path) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that $path (or the configured RRA path) is an absolute, readable,
|
||||
* writable directory, printing an error and returning false otherwise.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
function wizard_verify_path($path) {
|
||||
global $active_config;
|
||||
|
||||
@@ -812,10 +850,37 @@ function wizard_verify_path($path) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a wizard prompt/message to STDOUT, optionally clearing the screen first
|
||||
* and/or surrounding the message with newlines.
|
||||
*
|
||||
* @param string $msg
|
||||
* @param bool $new_line
|
||||
* @param bool $new_line_after
|
||||
* @param bool $clear_commandline_screen
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function wizard_handle_output($msg, $new_line = false, $new_line_after = false, $clear_commandline_screen = false) {
|
||||
fwrite(STDOUT, ($clear_commandline_screen ? ANSI_ERASE_SCREEN . ANSI_ERASE_BUFFER . ANSI_POS_TOP_LEFT : '') . ($new_line ? PHP_EOL : '') . $msg . ($new_line_after ? PHP_EOL : ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts for and reads a line from STDIN, re-prompting (with an optional help
|
||||
* message) until the input passes the given filter_var() filter or, if
|
||||
* $require_value is set, is non-empty.
|
||||
*
|
||||
* @param string $msg
|
||||
* @param int $filter
|
||||
* @param array<mixed>|false $filter_options
|
||||
* @param string|false $filter_help_msg
|
||||
* @param bool $new_line
|
||||
* @param bool $new_line_after
|
||||
* @param bool $clear_commandline_screen
|
||||
* @param bool $require_value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
function wizard_handle_input($msg, $filter = FILTER_DEFAULT, $filter_options = false, $filter_help_msg = false, $new_line = false, $new_line_after = false, $clear_commandline_screen = false, $require_value = false) {
|
||||
$filtered_value = false;
|
||||
|
||||
@@ -843,6 +908,14 @@ function wizard_handle_input($msg, $filter = FILTER_DEFAULT, $filter_options = f
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the next lettered attribute prompt (a, b, ... z, aa, ab, ...), advancing
|
||||
* $attribute_count for the following call.
|
||||
*
|
||||
* @param int $attribute_count
|
||||
*
|
||||
* @return array{text: string, ansi: string, padding: string}
|
||||
*/
|
||||
function wizard_get_prompt(&$attribute_count) {
|
||||
$attr_part1 = $attribute_count % 26;
|
||||
$attr_part2 = ($attribute_count - $attr_part1) / 26;
|
||||
@@ -856,6 +929,14 @@ function wizard_get_prompt(&$attribute_count) {
|
||||
return $attr_prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the wizard's page banner/title bar (e.g. "Page 2/9") for the given step.
|
||||
*
|
||||
* @param int $page
|
||||
* @param string $title
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function wizard_handle_title($page, $title = '') {
|
||||
$title_prefix = ' RRDtool Proxy Server Wizard';
|
||||
|
||||
@@ -867,13 +948,25 @@ function wizard_handle_title($page, $title = '') {
|
||||
$title_spaces = strlen($title_prefix) + strlen($title_suffix);
|
||||
|
||||
if ($title_spaces > 75) {
|
||||
$title_prefix = substr($title_prefix, 1, 75 - $title_suffix);
|
||||
$title_prefix = substr($title_prefix, 1, 75 - strlen($title_suffix));
|
||||
$title_spaces = 75;
|
||||
}
|
||||
|
||||
wizard_handle_output(ANSI_BOLD . ANSI_YELLOW_FG . ANSI_BLUE_BG . $title_prefix .
|
||||
str_repeat(' ', 80 - $title_spaces) . $title_suffix . ANSI_RESET, false, true, true);
|
||||
}
|
||||
/**
|
||||
* Prompts for and applies a group of configuration settings, updating
|
||||
* $active_config in place, persisting a temporary config file after each
|
||||
* accepted value, and advancing the shared $attribute_count prompt counter.
|
||||
*
|
||||
* @param array<string, array<string, mixed>> $inputs
|
||||
* @param array<string, mixed> $active_config
|
||||
* @param int $attribute_count
|
||||
* @param string $title
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function wizard_handle_settings($inputs, &$active_config, &$attribute_count, $title = '') {
|
||||
if (empty($title)) {
|
||||
$title = '';
|
||||
@@ -918,6 +1011,16 @@ function wizard_handle_settings($inputs, &$active_config, &$attribute_count, $ti
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Word-wraps $msg to $length columns, indenting wrapped lines to align under an
|
||||
* optional bold/colored "$prompt: " prefix.
|
||||
*
|
||||
* @param string $prompt
|
||||
* @param string $msg
|
||||
* @param int $length
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function wizard_prompt_wordwrap($prompt, $msg, $length) {
|
||||
$prompt = (empty($prompt) ? '' : $prompt . ': ');
|
||||
$prompt_len = strlen($prompt);
|
||||
|
||||
+419
-22
@@ -25,7 +25,7 @@
|
||||
|
||||
// do NOT run this script through a web browser
|
||||
|
||||
use phpseclib3\Crypt\RSA;
|
||||
use phpseclib4\Crypt\RSA;
|
||||
|
||||
if (!isset($_SERVER['argv'][0]) || isset($_SERVER['REQUEST_METHOD']) || isset($_SERVER['REMOTE_ADDR'])) {
|
||||
die('<br><strong>This script is only meant to run at the command line.</strong>');
|
||||
@@ -38,6 +38,7 @@ chdir(__DIR__);
|
||||
require_once('./vendor/autoload.php');
|
||||
require_once('./include/global.php');
|
||||
require_once('./lib/functions.php');
|
||||
$rrdp_help_messages = $rrdp_help_messages ?? [];
|
||||
|
||||
// process calling arguments
|
||||
$parms = $_SERVER['argv'];
|
||||
@@ -213,6 +214,7 @@ $rrdp_msr_buffer = [];
|
||||
$rrdp_buffers = ['logging_buffered' => [], 'logging_snmp' => [] ];
|
||||
|
||||
// set up an admin socket for proxy administration
|
||||
$rrdp_admin_resource_id = 0;
|
||||
$rrdp_admin = @socket_create(($rrdp_config['ip_version'] == 4) ? AF_INET : AF_INET6 , SOCK_STREAM, SOL_TCP);
|
||||
|
||||
if ($rrdp_admin === false) {
|
||||
@@ -222,7 +224,7 @@ if ($rrdp_admin === false) {
|
||||
}
|
||||
@socket_set_option($rrdp_admin, SOL_SOCKET, SO_REUSEADDR, 1);
|
||||
|
||||
if (!@socket_bind($rrdp_admin, (($rrdp_config['ipv6']) ? '::1' : '127.0.0.1'), $rrdp_config['port_admin'])) {
|
||||
if (!@socket_bind($rrdp_admin, (($rrdp_config['ip_version'] == 6) ? '::1' : '127.0.0.1'), $rrdp_config['port_admin'])) {
|
||||
rrd_system__system_die(NL . 'Unable to bind socket to \'' . $rrdp_config['address'] . ':' . $rrdp_config['port_admin'] . '\'' . NL . 'Error: ' . socket_strerror(socket_last_error()) . NL);
|
||||
}
|
||||
socket_set_nonblock($rrdp_admin);
|
||||
@@ -231,6 +233,7 @@ rrd_system__system_boolean_message('init: tcp admin socket #' . $rrdp_admin_reso
|
||||
rrdp_system__logging(LOGGING_LOCATION_BUFFERED, 'Start listening to port ' . $rrdp_config['address'] . ':' . $rrdp_config['port_admin'], 'SYS', SEVERITY_LEVEL_NOTIFICATION);
|
||||
|
||||
// set up a client socket handling requests against RRDtool
|
||||
$rrdp_client_resource_id = 0;
|
||||
$rrdp_client = @socket_create((($rrdp_config['ip_version'] == 4) ? AF_INET : AF_INET6), SOCK_STREAM, SOL_TCP);
|
||||
|
||||
if ($rrdp_client === false) {
|
||||
@@ -318,7 +321,7 @@ exec("ls -l /proc/$pid_of_php/fd/ | wc -l", $open_files);
|
||||
if ($max_open_files[0] == 'unlimited') {
|
||||
$max_open_files[0] = 1048576;
|
||||
}
|
||||
$rrdp_config['max_cnn'] = intval(($max_open_files[0] - $open_files[0]) / 2 - $rrdp_config['max_admin_cnn'] * 2 - 100); // use a buffer of 100 open files
|
||||
$rrdp_config['max_cnn'] = intval(((int) $max_open_files[0] - (int) $open_files[0]) / 2 - (int) $rrdp_config['max_admin_cnn'] * 2 - 100); // use a buffer of 100 open files
|
||||
|
||||
// return system info
|
||||
fwrite(STDOUT, rrdp_cmd__show_version($systemd) . NL);
|
||||
@@ -679,8 +682,9 @@ while ($__server_listening) {
|
||||
// return prompt
|
||||
rrdp_system__return_prompt($read_socket);
|
||||
} else {
|
||||
socket_close($rrdp_admin_client['socket']);
|
||||
socket_close($read_socket);
|
||||
unset($rrdp_admin_clients[$read_socket_resource_id]);
|
||||
unset($rrdp_admin_sockets[$read_socket_resource_id]);
|
||||
rrdp_system__count('aborted_clients');
|
||||
|
||||
continue;
|
||||
@@ -695,6 +699,13 @@ while ($__server_listening) {
|
||||
|
||||
// #################################### MSR FUNCTIONS ####################################
|
||||
|
||||
/**
|
||||
* Flushes any buffered MSR (multi-server replication) commands whose 10-second
|
||||
* timeframe has elapsed to per-peer files under ./msr/, or clears the buffer
|
||||
* entirely if there are no configured remote proxies.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_msr__block_write() {
|
||||
global $rrdp_config, $rrdp_msr_buffer;
|
||||
|
||||
@@ -735,6 +746,13 @@ function rrdp_msr__block_write() {
|
||||
|
||||
// #################################### INTERNAL FUNCTIONS ####################################
|
||||
|
||||
/**
|
||||
* Runs periodic (every 30s) health checks: monitors RRA/MSR filesystem disk
|
||||
* usage and logs warning/alert/critical messages as free space thresholds are
|
||||
* crossed.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_system__check() {
|
||||
global $rrdp_config, $rrdp_status;
|
||||
|
||||
@@ -813,10 +831,16 @@ function rrdp_system__check() {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates (if missing) and loads this proxy's RSA key pair, populating
|
||||
* $rrdp_config['encryption'] with the public/private keys and the public key's
|
||||
* md5 fingerprint.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_system__encryption_init() {
|
||||
global $rrdp_config;
|
||||
|
||||
|
||||
if (!file_exists('./include/public.key') || !file_exists('./include/private.key')) {
|
||||
$private = RSA::createKey(2048);
|
||||
$public = $private->getPublicKey();
|
||||
@@ -824,9 +848,10 @@ function rrdp_system__encryption_init() {
|
||||
$rrdp_config['encryption']['public_key'] = $public;
|
||||
$rrdp_config['encryption']['private_key'] = $private;
|
||||
|
||||
file_put_contents('./include/public.key', $rrdp_config['encryption']['public_key']);
|
||||
file_put_contents('./include/private.key', $rrdp_config['encryption']['private_key']);
|
||||
$keys_written = rrdp_write_key_pair('./include/public.key', (string) $public, './include/private.key', (string) $private);
|
||||
rrd_system__system_boolean_message('init: write RSA key pair', $keys_written, true);
|
||||
}
|
||||
@chmod('./include/private.key', 0600);
|
||||
|
||||
$rrdp_config['encryption']['public_key'] = file_get_contents('./include/public.key');
|
||||
rrd_system__system_boolean_message('init: RSA public key', $rrdp_config['encryption']['public_key'], true);
|
||||
@@ -834,10 +859,29 @@ function rrdp_system__encryption_init() {
|
||||
$rrdp_config['encryption']['private_key'] = file_get_contents('./include/private.key');
|
||||
rrd_system__system_boolean_message('init: RSA private key', $rrdp_config['encryption']['private_key'], true);
|
||||
|
||||
$private = RSA::loadPublicKey($rrdp_config['encryption']['public_key']);
|
||||
$rrdp_config['encryption']['public_key_fingerprint'] = $private->getFingerprint();
|
||||
try {
|
||||
$public_fingerprint = RSA::loadPublicKey($rrdp_config['encryption']['public_key'])->getFingerprint('sha256');
|
||||
$private_fingerprint = RSA::loadPrivateKey($rrdp_config['encryption']['private_key'])->getPublicKey()->getFingerprint('sha256');
|
||||
} catch (Throwable $e) {
|
||||
$public_fingerprint = $private_fingerprint = false;
|
||||
}
|
||||
|
||||
$keys_match = $public_fingerprint !== false && $private_fingerprint !== false && hash_equals($public_fingerprint, $private_fingerprint);
|
||||
rrd_system__system_boolean_message('init: RSA public/private key pair matches', $keys_match, true);
|
||||
|
||||
$private = RSA::loadPublicKey($rrdp_config['encryption']['public_key']);
|
||||
$rrdp_config['encryption']['public_key_fingerprint'] = $private->getFingerprint('md5');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a serialized IPC status update received from the replicator child
|
||||
* process: forwards debug log entries and starts/pauses the client listener
|
||||
* socket depending on the replicator's reported synchronization state.
|
||||
*
|
||||
* @param string $input
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_system__replicator($input) {
|
||||
global $rrdp_client, $rrd_config;
|
||||
|
||||
@@ -867,6 +911,15 @@ function rrdp_system__replicator($input) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a serialized IPC status update received from a client-handling child
|
||||
* process: forwards debug log entries, merges any pending MSR commands into the
|
||||
* shared buffer, and rolls the reported counters/status into the global stats.
|
||||
*
|
||||
* @param string $input
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_system__client(&$input) {
|
||||
global $rrdp_msr_buffer;
|
||||
|
||||
@@ -909,6 +962,14 @@ function rrdp_system__client(&$input) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a fatal message, signals the parent process to terminate, and
|
||||
* terminates this process.
|
||||
*
|
||||
* @param string $msg
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
function rrd_system__system_die($msg = '') {
|
||||
global $ppid;
|
||||
|
||||
@@ -923,6 +984,18 @@ function rrd_system__system_die($msg = '') {
|
||||
die($msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints a timestamped "[OK]"/"[FAILED]"/"[SKIPPED]" startup status line for
|
||||
* $msg, and (when $boolean_state is falsy and $exit is true) logs a critical
|
||||
* error and terminates the process via rrd_system__system_die().
|
||||
*
|
||||
* @param string $msg
|
||||
* @param mixed $boolean_state
|
||||
* @param bool $exit
|
||||
* @param bool $skip
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrd_system__system_boolean_message($msg, $boolean_state, $exit = false, $skip = false) {
|
||||
global $colors, $microtime_start, $systemd;
|
||||
|
||||
@@ -954,10 +1027,27 @@ function rrd_system__system_boolean_message($msg, $boolean_state, $exit = false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the byte length of $str, using mbstring's 8-bit-safe strlen() when
|
||||
* mbstring function overloading is active.
|
||||
*
|
||||
* @param string $str
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
function rrdp_system__calc_bytes($str) {
|
||||
return (ini_get('mbstring.func_overload') ? mb_strlen($str, '8bit') : strlen($str));
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments a named counter in $rrdp_status by $value, wrapping it back to 0
|
||||
* before it would exceed a signed 32-bit integer.
|
||||
*
|
||||
* @param string $variable
|
||||
* @param int $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function rrdp_system__count($variable, $value = 1) {
|
||||
global $rrdp_status;
|
||||
|
||||
@@ -971,6 +1061,14 @@ function rrdp_system__count($variable, $value = 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recomputes a "max" style status counter (max_admin_connections or
|
||||
* max_client_connections) from the current number of connected clients.
|
||||
*
|
||||
* @param string $variable
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_system__update($variable) {
|
||||
global $rrdp_admin_clients, $rrdp_clients, $rrdp_status;
|
||||
|
||||
@@ -992,6 +1090,14 @@ function rrdp_system__update($variable) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the admin CLI prompt (reflecting privileged/debug mode) back to a
|
||||
* connected admin socket.
|
||||
*
|
||||
* @param resource|\Socket $socket
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_system__return_prompt($socket) {
|
||||
global $rrdp_admin_clients, $rrdp_config, $colors;
|
||||
|
||||
@@ -1003,16 +1109,41 @@ function rrdp_system__return_prompt($socket) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes $output to a socket (retrying on short writes) and, if $counter is
|
||||
* given, tallies the bytes sent against that named counter.
|
||||
*
|
||||
* @param resource|\Socket $socket
|
||||
* @param string $output
|
||||
* @param string $counter
|
||||
*
|
||||
* @return int|false
|
||||
*/
|
||||
function rrdp_system__socket_write($socket, $output, $counter = '') {
|
||||
if ($return = @socket_write($socket, $output, strlen($output))) {
|
||||
$written = rrdp_socket_write_all($socket, $output);
|
||||
|
||||
if ($written === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($counter) {
|
||||
rrdp_system__count($counter, rrdp_system__calc_bytes($output));
|
||||
}
|
||||
|
||||
return $written;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally writes a final message, then closes a socket, either gracefully
|
||||
* (shutdown + close) or forcibly (SO_LINGER with a zero timeout, discarding
|
||||
* unsent data).
|
||||
*
|
||||
* @param resource|\Socket $socket
|
||||
* @param string|false $msg
|
||||
* @param bool $force
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function rrdp_system__socket_close($socket, $msg = false, $force = false) {
|
||||
if ($msg) {
|
||||
rrdp_system__socket_write($socket, $msg);
|
||||
@@ -1030,6 +1161,15 @@ function rrdp_system__socket_close($socket, $msg = false, $force = false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a live (non-cached) value for a status variable such as
|
||||
* threads_connected, uptime, memory_usage, memory_peak_usage, or
|
||||
* connections_open.
|
||||
*
|
||||
* @param string $variable
|
||||
*
|
||||
* @return int|float|string
|
||||
*/
|
||||
function rrdp_system__status_live($variable) {
|
||||
global $rrdp_admin_clients, $rrdp_ipc_sockets, $rrdp_config;
|
||||
|
||||
@@ -1061,6 +1201,14 @@ function rrdp_system__status_live($variable) {
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a human-readable size string (e.g. "512M", "2G") such as php.ini's
|
||||
* memory_limit into a byte count.
|
||||
*
|
||||
* @param string $val
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
function rrdp_system__convert2bytes($val) {
|
||||
preg_match('/^\s*([0-9.]+)\s*([KMGTPE])B?\s*$/i', $val, $matches);
|
||||
$num = (float)$matches[1];
|
||||
@@ -1083,6 +1231,18 @@ function rrdp_system__convert2bytes($val) {
|
||||
return intval($num);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a log entry into the in-memory buffered/SNMP logging ring buffers
|
||||
* (subject to their configured severity thresholds) and streams it live to any
|
||||
* connected admin console with console logging enabled.
|
||||
*
|
||||
* @param int $location
|
||||
* @param string $msg
|
||||
* @param string $category
|
||||
* @param int $severity
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_system__logging($location, $msg, $category, $severity) {
|
||||
global $rrdp_config, $rrdp_admin_clients, $rrdp_admin_sockets, $colors, $rrdp_buffers, $severity_levels;
|
||||
|
||||
@@ -1128,6 +1288,16 @@ function rrdp_system__logging($location, $msg, $category, $severity) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies an admin CLI output filter/pipe (`| i|e|b|I|E|B|exp <pattern>`) to
|
||||
* $output, appending a match count, or returns an error message for an
|
||||
* unrecognized filter.
|
||||
*
|
||||
* @param string $output
|
||||
* @param array<int, string> $args
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function rrdp_system__filter($output, $args) {
|
||||
if (!$output || !$args) {
|
||||
return $output;
|
||||
@@ -1187,6 +1357,13 @@ function rrdp_system__filter($output, $args) {
|
||||
return '% Unrecognized arguments: \'' . implode(' ', $args) . '\'' . NL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recomputes the effective global console logging severity/category (the
|
||||
* highest/broadest setting across all privileged admin sessions) and notifies
|
||||
* the replication master/slave child processes of the updated running config.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_system__global_console_logging_update() {
|
||||
global $rrdp_config, $rrdp_admin_clients, $rrdp_clients, $rrdp_ipc_sockets, $rrdp_repl_master_pid, $rrdp_repl_slave_pid;
|
||||
|
||||
@@ -1251,16 +1428,41 @@ function rrdp_system__global_console_logging_update() {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a stable per-connection identifier for a socket/resource, using
|
||||
* spl_object_id() on PHP 8's \Socket objects or intval() on legacy resources.
|
||||
*
|
||||
* @param resource|\Socket $object
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
function rrdp_system__get_resource_id($object) {
|
||||
return (PHP_VERSION_ID >= 80000) ? spl_object_id($object) : intval($object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether $object is a valid socket handle, accounting for sockets
|
||||
* being \Socket objects (PHP 8+) rather than resources.
|
||||
*
|
||||
* @param mixed $object
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function rrdp_system__is_resource($object) {
|
||||
return (PHP_VERSION_ID >= 80000) ? is_object($object) : is_resource($object);
|
||||
}
|
||||
|
||||
// #################################### SYSTEM COMMANDS ####################################
|
||||
|
||||
/**
|
||||
* Admin CLI "clear" command: dispatches to the matching rrdp_cmd__clear_*
|
||||
* subcommand handler based on the first argument.
|
||||
*
|
||||
* @param resource|\Socket|string $socket
|
||||
* @param array<int, string> $args
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_cmd__clear($socket, $args) {
|
||||
$arg = array_shift($args);
|
||||
|
||||
@@ -1276,6 +1478,12 @@ function rrdp_cmd__clear($socket, $args) {
|
||||
rrdp_system__socket_write($socket, '% Incomplete command. Type "clear ?" for a list of subcommands' . NL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin CLI "clear counters" command: resets all non-"live" entries in
|
||||
* $rrdp_status back to zero.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_cmd__clear_counters() {
|
||||
global $rrdp_status;
|
||||
|
||||
@@ -1287,6 +1495,15 @@ function rrdp_cmd__clear_counters() {
|
||||
rrdp_system__logging(LOGGING_LOCATION_BUFFERED, 'Counters have been cleared', 'SYS', SEVERITY_LEVEL_NOTIFICATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin CLI "clear logging buffered|snmp" command: empties the selected
|
||||
* in-memory logging ring buffer.
|
||||
*
|
||||
* @param resource|\Socket|string $socket
|
||||
* @param array<int, string> $args
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_cmd__clear_logging($socket, $args) {
|
||||
global $rrdp_buffers;
|
||||
|
||||
@@ -1314,11 +1531,28 @@ function rrdp_cmd__clear_logging($socket, $args) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin CLI "reset" command: clears the client terminal's screen buffer.
|
||||
*
|
||||
* @param resource|\Socket|string $socket
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_cmd__reset($socket) {
|
||||
// client would like to regularly clear and reset terminal screen
|
||||
rrdp_system__socket_write($socket, ANSI_ERASE_SCREEN . ANSI_ERASE_BUFFER . ANSI_POS_TOP_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin CLI "enable" command: promotes the calling (localhost-only) admin
|
||||
* session to privileged mode, after verifying the enable password if one is
|
||||
* configured.
|
||||
*
|
||||
* @param resource|\Socket $socket
|
||||
* @param array<int, string> $args
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_cmd__enable($socket, $args) {
|
||||
global $rrdp_admin_clients, $rrdp_config;
|
||||
$arg = array_shift($args);
|
||||
@@ -1347,6 +1581,15 @@ function rrdp_cmd__enable($socket, $args) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin CLI "disable" command: returns the calling admin session to
|
||||
* unprivileged mode.
|
||||
*
|
||||
* @param resource|\Socket $socket
|
||||
* @param array<int, string> $args
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_cmd__disable($socket, $args) {
|
||||
global $rrdp_admin_clients;
|
||||
|
||||
@@ -1362,6 +1605,15 @@ function rrdp_cmd__disable($socket, $args) {
|
||||
rrdp_system__socket_write($socket, '% Unrecognized command' . NL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin CLI "quit" command: drops back to unprivileged mode and closes the
|
||||
* calling admin connection.
|
||||
*
|
||||
* @param resource|\Socket $socket
|
||||
* @param array<int, string> $args
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_cmd__quit($socket, $args) {
|
||||
global $rrdp_admin_clients, $rrdp_admin_sockets;
|
||||
|
||||
@@ -1384,6 +1636,17 @@ function rrdp_cmd__quit($socket, $args) {
|
||||
unset($rrdp_admin_sockets[$socket_resource_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin CLI "shutdown" command (also invoked directly with $socket ===
|
||||
* 'SIGTERM' from the signal handler): gracefully stops accepting new clients,
|
||||
* stops RRDCached, terminates all child processes, closes every socket, and
|
||||
* exits the process.
|
||||
*
|
||||
* @param resource|\Socket|string $socket
|
||||
* @param array<int, string>|false $args
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_cmd__shutdown($socket, $args) {
|
||||
global $rrdp_clients, $rrdp_admin_clients, $rrdp_ipc_sockets, $rrdp_client, $rrdp_admin, $rrdcached_pid, $microtime_start, $rrdp_process_types, $systemd;
|
||||
|
||||
@@ -1471,6 +1734,17 @@ function rrdp_cmd__shutdown($socket, $args) {
|
||||
rrdp_system__socket_write($socket, '% Unrecognized command' . NL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin CLI "?"/"list" command: prints the available subcommands (and their
|
||||
* help text) for the current privilege level, or for a specific command's `?`
|
||||
* subtree when $root is given.
|
||||
*
|
||||
* @param resource|\Socket $socket
|
||||
* @param array<int, string>|false $args
|
||||
* @param array<string, mixed>|false $root
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_cmd__list($socket, $args = false, $root = false) {
|
||||
global $rrdp_admin_clients, $rrdp_help_messages;
|
||||
|
||||
@@ -1494,6 +1768,16 @@ function rrdp_cmd__list($socket, $args = false, $root = false) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin CLI "show" command: dispatches to the appropriate report (threads,
|
||||
* processes, counters, variables, clients, cluster, rsa, version, logging, or
|
||||
* msr) and writes the (optionally filtered) output back to the socket.
|
||||
*
|
||||
* @param resource|\Socket $socket
|
||||
* @param array<int, string> $args
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_cmd__show($socket, $args) {
|
||||
global $rrdp_admin_clients, $rrdp_config, $rrdp_status, $rrdp_clients, $rrdp_process_types;
|
||||
|
||||
@@ -1591,6 +1875,14 @@ function rrdp_cmd__show($socket, $args) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the report text for "show logging buffered|snmp": the current
|
||||
* contents of the selected in-memory logging ring buffer.
|
||||
*
|
||||
* @param string|false $arg
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function rrdp_cmd__show_logging($arg) {
|
||||
global $rrdp_buffers;
|
||||
|
||||
@@ -1621,6 +1913,14 @@ function rrdp_cmd__show_logging($arg) {
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the report text for "show msr buffer|health|status": the pending
|
||||
* multi-server-replication command buffer, keyed by timeframe.
|
||||
*
|
||||
* @param string|false $arg
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function rrdp_cmd__show_msr($arg) {
|
||||
global $rrdp_msr_buffer;
|
||||
|
||||
@@ -1657,6 +1957,15 @@ function rrdp_cmd__show_msr($arg) {
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the "show version" report: banner/logo, version, uptime, memory
|
||||
* usage, RSA fingerprint, running process IDs, and listening sockets (a
|
||||
* condensed socket-only summary is returned when $systemd is true).
|
||||
*
|
||||
* @param bool $systemd
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function rrdp_cmd__show_version($systemd = false) {
|
||||
global $rrdp_config, $rrdp_clients, $rrdp_repl_master_pid, $rrdp_repl_slave_pid;
|
||||
|
||||
@@ -1700,6 +2009,16 @@ function rrdp_cmd__show_version($systemd = false) {
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin CLI "debug <process> on|off" command: toggles verbose debug logging
|
||||
* for a given process (forwarding the toggle to the replicator over IPC when
|
||||
* applicable) once no other admin session still needs it enabled.
|
||||
*
|
||||
* @param resource|\Socket $socket
|
||||
* @param array<int, string> $args
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_cmd__debug($socket, $args) {
|
||||
global $rrdp_replicator_pid, $rrdp_admin_clients, $rrdp_help_messages, $rrdp_config, $rrdp_clients, $rrdp_ipc_sockets;
|
||||
|
||||
@@ -1774,6 +2093,15 @@ function rrdp_cmd__debug($socket, $args) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin CLI "set" command: dispatches to the matching rrdp_cmd__set_*
|
||||
* subcommand handler based on the first argument.
|
||||
*
|
||||
* @param resource|\Socket $socket
|
||||
* @param array<int, string> $args
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_cmd__set($socket, $args) {
|
||||
$arg = array_shift($args);
|
||||
|
||||
@@ -1789,6 +2117,16 @@ function rrdp_cmd__set($socket, $args) {
|
||||
rrdp_system__socket_write($socket, '% Incomplete command. Type "set ?" for a list of subcommands' . NL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin CLI "set cluster add|remove|update" command: validates and persists a
|
||||
* registered peer's IP/port/fingerprint to ./include/proxies, creates its MSR
|
||||
* data subfolder, and notifies the replication master/slave of the change.
|
||||
*
|
||||
* @param resource|\Socket $socket
|
||||
* @param array<int, string> $args
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_cmd__set_cluster($socket, $args) {
|
||||
global $rrdp_config, $rrdp_clients, $rrdp_repl_master_pid, $rrdp_repl_slave_pid, $rrdp_ipc_sockets;
|
||||
|
||||
@@ -1923,6 +2261,15 @@ function rrdp_cmd__set_cluster($socket, $args) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin CLI "set client add|remove" command: validates and persists a trusted
|
||||
* client's IP/fingerprint to ./include/clients.
|
||||
*
|
||||
* @param resource|\Socket $socket
|
||||
* @param array<int, string> $args
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_cmd__set_client($socket, $args) {
|
||||
global $rrdp_config;
|
||||
|
||||
@@ -1984,6 +2331,15 @@ function rrdp_cmd__set_client($socket, $args) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin CLI "set rsa keys" command: generates and atomically writes a new RSA
|
||||
* key pair, refreshing the running config's key material and fingerprint.
|
||||
*
|
||||
* @param resource|\Socket $socket
|
||||
* @param array<int, string> $args
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_cmd__set_rsa($socket, $args) {
|
||||
global $rrdp_config;
|
||||
|
||||
@@ -1992,18 +2348,20 @@ function rrdp_cmd__set_rsa($socket, $args) {
|
||||
if (!is_null($arg)) {
|
||||
switch($arg) {
|
||||
case 'keys':
|
||||
$rsa = new phpseclib3\Crypt\RSA();
|
||||
$private = $rsa->createKey(2048);
|
||||
$public = $rsa->getPublicKey();
|
||||
$private = RSA::createKey(2048);
|
||||
$public = $private->getPublicKey();
|
||||
|
||||
$rrdp_config['encryption']['public_key'] = $public;
|
||||
$rrdp_config['encryption']['private_key'] = $private;
|
||||
if (!rrdp_write_key_pair('./include/public.key', (string) $public, './include/private.key', (string) $private)) {
|
||||
rrdp_system__socket_write($socket, '% Unable to write new RSA key pair' . NL);
|
||||
|
||||
file_put_contents('./include/public.key', $rrdp_config['encryption']['public_key']);
|
||||
file_put_contents('./include/private.key', $rrdp_config['encryption']['private_key']);
|
||||
break;
|
||||
}
|
||||
|
||||
$public_key = $rsa->loadPublicKey($rrdp_config['encryption']['public_key']);
|
||||
$rrdp_config['encryption']['public_key_fingerprint'] = $public_key->getFingerprint();
|
||||
$rrdp_config['encryption']['public_key'] = (string) $public;
|
||||
$rrdp_config['encryption']['private_key'] = (string) $private;
|
||||
|
||||
$public_key = RSA::loadPublicKey($rrdp_config['encryption']['public_key']);
|
||||
$rrdp_config['encryption']['public_key_fingerprint'] = $public_key->getFingerprint('md5');
|
||||
|
||||
rrdp_cmd__show($socket, [ 0=>'rsa', 1=>'publickey']);
|
||||
|
||||
@@ -2020,6 +2378,15 @@ function rrdp_cmd__set_rsa($socket, $args) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin CLI "set logging buffered|snmp|terminal|console" command: updates the
|
||||
* global or per-session logging severity/category thresholds.
|
||||
*
|
||||
* @param resource|\Socket $socket
|
||||
* @param array<int, string> $args
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_cmd__set_logging($socket, $args) {
|
||||
global $rrdp_config, $rrdp_admin_clients, $logging_categories;
|
||||
|
||||
@@ -2220,12 +2587,22 @@ function handle_child_processes($ipc_sockets, $type, $ssock = false, $arg1 = fal
|
||||
}
|
||||
|
||||
// display_version - displays the version of the RRDproxy
|
||||
/**
|
||||
* Writes the proxy's version banner to STDOUT.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function display_version() {
|
||||
$output = RRDP_VERSION_FULL;
|
||||
fwrite(STDOUT, $output);
|
||||
}
|
||||
|
||||
// display_help - displays the usage of the RRDproxy
|
||||
/**
|
||||
* Writes the command-line usage/help text (and version banner) to STDOUT.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function display_help() {
|
||||
display_version();
|
||||
$output = NL . 'Usage: rrdtool-proxy.php [-w|--wizard] [-v|--version] [-h|--help] [-f|--force] [-s|--systemd]' . NL
|
||||
@@ -2240,6 +2617,12 @@ function display_help() {
|
||||
}
|
||||
|
||||
// wizard - starts the wizard for system setup
|
||||
/**
|
||||
* Disables system logging and hands off execution to the interactive setup
|
||||
* wizard, which exits the process when finished.
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
function init_wizard() {
|
||||
define('SYSTEM_LOGGING', false);
|
||||
include_once('./lib/wizard.php');
|
||||
@@ -2247,6 +2630,14 @@ function init_wizard() {
|
||||
}
|
||||
|
||||
// signal handler
|
||||
/**
|
||||
* Process signal handler: triggers a graceful shutdown on SIGTERM and ignores
|
||||
* SIGHUP/SIGUSR1 and any other signal.
|
||||
*
|
||||
* @param int $signo
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rrdp_sig_handler($signo) {
|
||||
switch ($signo) {
|
||||
case SIGTERM:
|
||||
@@ -2261,6 +2652,12 @@ function rrdp_sig_handler($signo) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ASCII-art Cacti/RRDtool Proxy banner used in startup, shutdown,
|
||||
* and "show version" output.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function rrdp_get_cacti_proxy_logo() {
|
||||
return
|
||||
NL . '# ' . ANSI_BOLD . ANSI_GREEN_FG . ' ___ _ _ ' . ANSI_RESET . ' __ __ ___ ___ '
|
||||
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
<?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/ |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
require dirname(__DIR__) . '/include/global.php';
|
||||
require dirname(__DIR__) . '/lib/functions.php';
|
||||
|
||||
$failures = [];
|
||||
$checks = 0;
|
||||
|
||||
/**
|
||||
* Records a single test assertion: increments the running check count and, if
|
||||
* $condition is falsy, appends $message to the list of failures.
|
||||
*
|
||||
* @param mixed $condition
|
||||
* @param string $message
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function check($condition, $message) {
|
||||
global $checks, $failures;
|
||||
|
||||
$checks++;
|
||||
|
||||
if (!$condition) {
|
||||
$failures[] = $message;
|
||||
}
|
||||
}
|
||||
|
||||
$test_root = sys_get_temp_dir() . '/rrdproxy-tests-' . bin2hex(random_bytes(8));
|
||||
$rra_root = $test_root . '/rra';
|
||||
$outside = $test_root . '/outside';
|
||||
mkdir($rra_root, 0700, true);
|
||||
mkdir($outside, 0700, true);
|
||||
file_put_contents($rra_root . '/sample.rrd', 'rrd');
|
||||
file_put_contents($outside . '/outside.rrd', 'outside');
|
||||
$symlink_created = @symlink($outside, $rra_root . '/escape');
|
||||
|
||||
if (!$symlink_created) {
|
||||
fwrite(STDERR, 'SKIP: symlink() is not available in this environment; skipping the symlink escape check.' . PHP_EOL);
|
||||
}
|
||||
|
||||
$rrdp_config = ['path_rra' => $rra_root];
|
||||
$canonical_rra_root = realpath($rra_root);
|
||||
|
||||
check(rrdp_resolve_rra_path('sample.rrd') === realpath($rra_root . '/sample.rrd'), 'A valid RRA path should resolve');
|
||||
check(rrdp_resolve_rra_path('nested/new', false) === $canonical_rra_root . '/nested/new', 'A future path beneath the RRA root should resolve');
|
||||
check(rrdp_resolve_rra_path('../outside/outside.rrd') === false, 'Parent traversal must be rejected');
|
||||
check(rrdp_resolve_rra_path($outside . '/outside.rrd') === false, 'Absolute paths must be rejected');
|
||||
|
||||
if ($symlink_created) {
|
||||
check(rrdp_resolve_rra_path('escape/outside.rrd') === false, 'Symlink escapes must be rejected');
|
||||
} else {
|
||||
fwrite(STDERR, 'SKIP: Symlink escapes must be rejected (no symlink() support)' . PHP_EOL);
|
||||
}
|
||||
|
||||
check(rrdp_command_has_unsafe_path("update foo.rrd\ncreate escaped.rrd"), 'Embedded commands must be rejected');
|
||||
check(rrdp_command_has_unsafe_path('update ../outside.rrd'), 'RRDtool traversal must be rejected');
|
||||
check(rrdp_command_has_unsafe_path('graph graph.png DEF:x=../outside.rrd:value:AVERAGE'), 'RRDtool option traversal must be rejected');
|
||||
check(rrdp_command_has_unsafe_path('update /tmp/outside.rrd'), 'Absolute RRDtool paths must be rejected');
|
||||
check(rrdp_command_has_unsafe_path('graph graph.png --font TITLE:12:/tmp/font.ttf'), 'Embedded absolute RRDtool paths must be rejected');
|
||||
|
||||
$options = rrdp_parse_removespikes_options('-R=sample.rrd --method=stddev --dryrun');
|
||||
check(is_array($options) && str_starts_with($options[0], '-R=' . $canonical_rra_root), 'Valid removespikes options should be normalized');
|
||||
check(rrdp_parse_removespikes_options('-R=sample.rrd ;id') === false, 'Shell metacharacter arguments must be rejected');
|
||||
check(rrdp_parse_removespikes_options('-R=../outside/outside.rrd') === false, 'removespikes traversal must be rejected');
|
||||
check(rrdp_parse_removespikes_options('--dryrun') === false, 'removespikes must require exactly one RRD file');
|
||||
check(rrdp_resolve_path_within($outside, 'future.rrd', false) === realpath($outside) . '/future.rrd', 'Generic containment should resolve safe future paths');
|
||||
check(rrdp_resolve_path_within($outside, '../rra/sample.rrd', false) === false, 'Generic containment must reject traversal');
|
||||
|
||||
$process = rrdp_run_process([PHP_BINARY, '-r', 'echo $argv[1];', ';touch-not-executed']);
|
||||
check($process !== false && $process['stdout'] === ';touch-not-executed', 'Process arguments must not be interpreted by a shell');
|
||||
|
||||
$rsa_class = 'phpseclib4\\Crypt\\RSA';
|
||||
set_error_handler(static function ($severity, $message) {
|
||||
return str_contains($message, 'Unable to write random state');
|
||||
});
|
||||
$private = $rsa_class::createKey(2048);
|
||||
restore_error_handler();
|
||||
$encryption = true;
|
||||
$rrdp_config['encryption']['private_key'] = (string) $private;
|
||||
$frame = encrypt('round trip', (string) $private->getPublicKey());
|
||||
check(is_string($frame) && decrypt($frame) === 'round trip', 'phpseclib 4 encryption must round-trip');
|
||||
check(decrypt('not-a-frame') === false, 'Malformed encrypted frames must fail closed');
|
||||
|
||||
$secret = $test_root . '/private.key';
|
||||
check(rrdp_write_secure_file($secret, 'secret', 0600) !== false, 'Private key writes should succeed');
|
||||
check((fileperms($secret) & 0777) === 0600, 'Private keys must use mode 0600');
|
||||
$public_path = $test_root . '/public.key';
|
||||
$private_path = $test_root . '/pair-private.key';
|
||||
check(rrdp_write_key_pair($public_path, (string) $private->getPublicKey(), $private_path, (string) $private), 'Validated key pairs should be written together');
|
||||
check((fileperms($private_path) & 0777) === 0600, 'Key-pair private keys must use mode 0600');
|
||||
check(!rrdp_write_key_pair($public_path, 'invalid', $private_path, (string) $private), 'Invalid key pairs must fail closed');
|
||||
|
||||
$sockets = [];
|
||||
check(socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $sockets), 'A socket pair should be available');
|
||||
|
||||
if (count($sockets) === 2) {
|
||||
check(rrdp_socket_write_all($sockets[0], 'complete') === 8, 'Socket writes should report the complete byte count');
|
||||
check(socket_read($sockets[1], 8) === 'complete', 'Socket writes should deliver the complete payload');
|
||||
socket_close($sockets[0]);
|
||||
socket_close($sockets[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively deletes a file, symlink, or directory tree used by the test
|
||||
* fixtures.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function remove_test_tree($path) {
|
||||
if (is_link($path) || is_file($path)) {
|
||||
unlink($path);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (scandir($path) as $entry) {
|
||||
if ($entry !== '.' && $entry !== '..') {
|
||||
remove_test_tree($path . DIRECTORY_SEPARATOR . $entry);
|
||||
}
|
||||
}
|
||||
|
||||
rmdir($path);
|
||||
}
|
||||
|
||||
remove_test_tree($test_root);
|
||||
|
||||
if ($failures) {
|
||||
fwrite(STDERR, implode(PHP_EOL, $failures) . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
print "OK ($checks checks)" . PHP_EOL;
|
||||
Reference in new issue
Block a user