mirror of
https://github.com/Cacti/cacti.git
synced 2026-11-02 01:57:17 +09:00
* PHPStan Level 8 - Phase 1 Conversion
Reduces the real (no-baseline) PHPStan level 8 error count from ~2,724 to 425
and regenerates phpstan-baseline.neon accordingly (8,941 -> 1,921 lines).
Core type-safety changes
- lib/database.php: narrow db_fetch_row()/db_fetch_row_prepared() to return
array (was bool|array) and db_fetch_assoc()/db_fetch_assoc_prepared() to
return array (was mixed); the _prepared workers coerce the connection-failure
path to an empty array so the declared type is honest. empty()/cacti_sizeof()
checks are unaffected.
- lib/database.php: narrow $db_conn guards from is_object() to instanceof PDO in
the transaction/quote/insert-id helpers so the PDO calls resolve.
- lib/mib_cache.php: the fluent MibCache methods mib()/object()/table()/row()
now return self instead of a bare object so chained calls type-check.
Type-safety casts and guards (mechanical, behaviour-preserving)
- Cast string|false/|null builtin results feeding string parameters
(json_decode, base64_encode, trim, htmle, preg_replace, explode, etc.).
- Cast int|string / int|false values feeding int id/pid parameters.
- Guard resource|false fopen()/popen() handles before fwrite/fclose/fread, and
wrap foreach over array|false/list|false results with `?: []`.
Genuine bugs found and fixed
- cli/add_tree.php / lib/api_tree.php: site tree nodes could not be created via
the CLI - the --site-id option was never parsed, $nodeTypes had no 'site'
entry (passing null to api_tree_item_save(int $type) -> TypeError), and
api_tree_item_save() never persisted site_id. All three fixed.
- tree.php: leaves_exist() is typed : int but returned db_fetch_assoc_prepared()
(an array of rows) for a SELECT COUNT(*); now uses db_fetch_cell_prepared()
and returns the scalar count.
- lib/mib_cache.php: $entries_per_object was a float (division / ceil) used as
an array key (PHP 8.1 implicit float-to-int key deprecation); now cast to int.
Config
- .phpstan.neon: add notIdentical.alwaysTrue and booleanAnd.alwaysFalse to the
existing always-true/false ignore family (same "PHPStan gets confused"
dynamic-constant cause already covered by identical.alwaysTrue,
notEqual.alwaysTrue, booleanAnd.leftAlwaysTrue, etc.).
- phpstan-baseline.neon: regenerated; the remaining 425 entries are the
untouched long tail for Phase 2.
* Remove redundant @phpstan-ignore comments
Follow-up cleanup for the level 8 Phase 1 work. Removes 14 inline
@phpstan-ignore comments that no longer suppress any reported error:
- cli/repair_database.php: `@phpstan-ignore booleanAnd.alwaysFalse` is now
covered by the global booleanAnd.alwaysFalse ignore added to .phpstan.neon.
- 13 others (host.php, include/global.php, include/session.php,
lib/api_automation.php x2, lib/functions.php x2, lib/html_utility.php x2,
lib/installer.php, lib/package.php, lib/reports.php, lib/rrd.php) no longer
match any reported error after the type-safety fixes (e.g. the
db_fetch_row_prepared() return-type narrowing removed the offset-assign
errors the api_automation ignores were suppressing).
Verified with reportUnmatchedIgnoredErrors enabled: 0 unmatched ignores
remain and the full level 8 run still reports 0 errors.
* ci: satisfy validator ratchet and refresh security sink baselines
- lib/api_tree.php: give the new tree-item site_id a real numeric
constraint ('^[-0-9]+$', matching html_reports.php) instead of an
empty regex, so the validator constraint ratchet passes without
bumping its baseline.
- tests/security/baselines: refresh the three sink/hotspot signatures
whose code text changed from the (string) casts added in this branch
(lib/poller.php setsid, cli/rrdresize.php json_encode, lib/import.php
simplexml_load_string).
* ci(js): allow DOMPurify 3.4.16 and update sync_js pin test
The dependabot bump (#8134) moved package.json/package-lock.json to
DOMPurify 3.4.16 on develop but left tests/js/sync_js.test.mjs pinned to
3.4.15, so the PR merge commit fails the 'DOMPurify is pinned' contract.
Bring package.json/package-lock.json to 3.4.16 (matching develop) and
update the pin test to expect 3.4.16.
* test: update sql.php null-handling assertion for the (string) cast
This branch changed scripts/sql.php from trim(\ ?? '') to
trim((string) \) (the phpstan-safe form that coerces both null and
false from shell_exec()). Update SqlScriptsTest's null-return assertion
to match the new cast instead of the removed '?? ''.
* fix: address Copilot review - ownership IDs, fopen guards, mib_cache bound
- chown()/chgrp(): pass integer UID/GID from fileowner()/filegroup()
(guarding the false lookup) instead of string-casting them, which made
them resolve account/group names like '1001' and fail. Covers
lib/api_device.php, lib/boost.php and poller_maintenance.php (x2).
- fopen() failure now short-circuits instead of reporting success:
lib/package.php returns false, lib/clog_webapi.php and utilities.php
render an error, and snmpagent_mibcachechild.php skips the cache write
and sentinel unlink so a concurrent writer's lock is preserved.
- lib/mib_cache.php: keep the float quotient as the loop bound and cast
only the array offset, so a non-even entries/columns split no longer
truncates the loop and drops a cached row.
* revert: keep db_fetch_* returning false on error; baseline Level 8 instead
Per review, restoring the intentional fail-safe: db_fetch_row[_prepared]()
return bool|array and db_fetch_assoc[_prepared]() return mixed again, so a
genuine SQL error (not just a missing connection) is signalled as false and
the === false checks in cdef_is_in_use(), boost_process_* and aggregate.php
work as before. Dropped the is_array() ? : [] coercion.
Level 8 stays green by regenerating phpstan-baseline.neon to capture the
re-introduced bool|array/mixed call-site errors (the ~1,850 the narrowing
had eliminated). Tightening those callers to drop them from the baseline is
Phase 2 work.
---------
Co-authored-by: TheWitness <copilot@users.noreply.github.com>
806 lines
27 KiB
PHP
806 lines
27 KiB
PHP
<?php
|
|
/*
|
|
+-------------------------------------------------------------------------+
|
|
| Copyright (C) 2004-2026 The Cacti Group |
|
|
| |
|
|
| This program is free software; you can redistribute it and/or |
|
|
| modify it under the terms of the GNU General Public License |
|
|
| as published by the Free Software Foundation; either version 2 |
|
|
| of the License, or (at your option) any later version. |
|
|
| |
|
|
| This program is distributed in the hope that it will be useful, |
|
|
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
|
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
|
| GNU General Public License for more details. |
|
|
+-------------------------------------------------------------------------+
|
|
| Cacti: The Complete RRDtool-based Graphing Solution |
|
|
+-------------------------------------------------------------------------+
|
|
| This code is designed, written, and maintained by the Cacti Group. See |
|
|
| about.php and/or the AUTHORS file for specific developer information. |
|
|
+-------------------------------------------------------------------------+
|
|
| http://www.cacti.net/ |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
class Net_Ping {
|
|
public socket $socket;
|
|
public array $host;
|
|
public int $port;
|
|
public mixed $ping_status;
|
|
public string $ping_response;
|
|
public mixed $snmp_status;
|
|
public string $snmp_response;
|
|
public string $request;
|
|
public int $request_len;
|
|
public mixed $reply;
|
|
public int $timeout;
|
|
public int $retries;
|
|
public int $precision;
|
|
public int $time;
|
|
public float $timer_start_time;
|
|
public string $sqn;
|
|
public int $avail_method;
|
|
public int $ping_type;
|
|
|
|
function __construct() {
|
|
$this->port = 33439;
|
|
}
|
|
|
|
function __destruct() {
|
|
}
|
|
|
|
function close_socket() : void {
|
|
@socket_shutdown($this->socket, 2);
|
|
socket_close($this->socket);
|
|
}
|
|
|
|
function start_time() : void {
|
|
$this->timer_start_time = microtime(true);
|
|
}
|
|
|
|
function get_time(int $acc = 2) : mixed {
|
|
// format start time
|
|
$start_time = $this->timer_start_time;
|
|
// get and format end time
|
|
$end_time = microtime(true);
|
|
|
|
return number_format($end_time - $start_time, $acc);
|
|
}
|
|
|
|
function build_udp_packet() : void {
|
|
$data = 'cacti-monitoring-system'; // the actual test data
|
|
|
|
// now lets build the actual UDP packet
|
|
$this->request = chr(0) . chr(1) . chr(0) . $data . chr(0);
|
|
$this->request_len = strlen($this->request);
|
|
}
|
|
|
|
function ping_error_handler(int $errno, string $errmsg, string $filename, int $linenum, array $vars = []) : bool {
|
|
return true;
|
|
}
|
|
|
|
function set_ping_error_handler() : void {
|
|
set_error_handler([$this, 'ping_error_handler']);
|
|
}
|
|
|
|
function restore_cacti_error_handler() : void {
|
|
restore_error_handler();
|
|
}
|
|
|
|
function build_icmp_packet() : void {
|
|
$seq_low = random_int(0,255);
|
|
$seq_high = random_int(0,255);
|
|
|
|
$data = 'cacti-monitoring-system'; // the actual test data
|
|
$type = "\x08"; // 8 echo message; 0 echo reply message
|
|
$code = "\x00"; // always 0 for this program
|
|
$chksm = "\x00\x00"; // generate checksum for icmp request
|
|
$id = chr($seq_high) . chr($seq_low);
|
|
$sqn = chr($seq_high) . chr($seq_low);
|
|
$this->sqn = $sqn;
|
|
|
|
// now lets build the actual icmp packet
|
|
$this->request = $type . $code . $chksm . $id . $sqn . $data;
|
|
$chksm = $this->get_checksum($this->request);
|
|
|
|
$this->request = $type . $code . $chksm . $id . $sqn . $data;
|
|
$this->request_len = strlen($this->request);
|
|
}
|
|
|
|
function get_checksum(string $data) : string {
|
|
if (strlen($data) % 2) {
|
|
$data .= "\x00";
|
|
}
|
|
|
|
$bit = unpack('n*', $data);
|
|
$sum = array_sum($bit);
|
|
|
|
while ($sum >> 16) {
|
|
$sum = ($sum >> 16) + ($sum & 0xffff);
|
|
}
|
|
|
|
return pack('n*', ~$sum);
|
|
}
|
|
|
|
function ping_icmp() : bool {
|
|
// ping me
|
|
if ($this->host['hostname']) {
|
|
// initialize variables
|
|
$this->ping_status = 'down';
|
|
$this->ping_response = __('ICMP Ping timed out');
|
|
|
|
// establish timeout variables
|
|
$to_sec = floor($this->timeout / 1000);
|
|
$to_usec = ($this->timeout % 1000) * 1000;
|
|
|
|
// clean up hostname if specifying snmp_transport
|
|
$this->host['hostname'] = $this->strip_ip_address($this->host['hostname']);
|
|
|
|
/* determine the host's ip address
|
|
* this prevents from command injection as well*/
|
|
if ($this->is_ipaddress($this->host['hostname'])) {
|
|
$host_ip = $this->host['hostname'];
|
|
} else {
|
|
// again, as a side effect, prevention from command injection
|
|
$host_ip = cacti_gethostbyname($this->host['hostname']);
|
|
|
|
if (!$this->is_ipaddress($host_ip)) {
|
|
cacti_log('WARNING: ICMP Ping Error: cacti_gethostbyname failed for ' . $this->host['hostname']);
|
|
$this->ping_response = 'ICMP Ping Error: cacti_gethostbyname failed for ' . $this->host['hostname'];
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// we have to use the real ping, in cases where windows failed or while using UNIX/Linux
|
|
$pattern = bin2hex('cacti-monitoring-system'); // the actual test data
|
|
|
|
$fping = read_config_option('path_fping');
|
|
|
|
if ($fping != '' && file_exists($fping) && is_executable($fping)) {
|
|
$using_fping = true;
|
|
|
|
if (filter_var($this->host['hostname'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
|
if (file_exists('/usr/sbin/fping6')) {
|
|
$fping = '/usr/sbin/fping6';
|
|
} elseif (file_exists('/usr/bin/fping6')) {
|
|
$fping = '/usr/bin/fping6';
|
|
} elseif (file_exists('/usr/local/sbin/fping6')) {
|
|
$fping = '/usr/local/sbin/fping6';
|
|
} elseif (file_exists('/usr/local/bin/fping6')) {
|
|
$fping = '/usr/local/bin/fping6';
|
|
}
|
|
}
|
|
|
|
$result = shell_exec(cacti_escapeshellarg($fping) . ' -q -t ' . $this->timeout . ' -c 1 -r ' . $this->retries . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true) . ' 2>&1');
|
|
} else {
|
|
$using_fping = false;
|
|
|
|
/**
|
|
* The host timeout is given in ms; recalculate to seconds.
|
|
* All numeric fields are safe. The hostname is quoted via
|
|
* cacti_escapeshellarg() as defense-in-depth alongside the
|
|
* DNS resolution check above.
|
|
*/
|
|
if (substr_count(cacti_strtolower(PHP_OS), 'sun')) {
|
|
$result = shell_exec('ping ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true));
|
|
} elseif (substr_count(cacti_strtolower(PHP_OS), 'hpux')) {
|
|
$result = shell_exec('ping -m ' . ceil($this->timeout / 1000) . ' -n ' . $this->retries . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true));
|
|
} elseif (substr_count(cacti_strtolower(PHP_OS), 'mac')) {
|
|
$result = shell_exec('ping -t ' . ceil($this->timeout / 1000) . ' -c ' . $this->retries . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true));
|
|
} elseif (substr_count(cacti_strtolower(PHP_OS), 'freebsd')) {
|
|
if (filter_var($host_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
|
$result = shell_exec('ping6 -t ' . ceil($this->timeout / 1000) . ' -c ' . $this->retries . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true));
|
|
} else {
|
|
$result = shell_exec('ping -t ' . ceil($this->timeout / 1000) . ' -c ' . $this->retries . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true));
|
|
}
|
|
} elseif (substr_count(cacti_strtolower(PHP_OS), 'darwin')) {
|
|
$result = shell_exec('ping -t ' . ceil($this->timeout / 1000) . ' -c ' . $this->retries . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true));
|
|
} elseif (substr_count(cacti_strtolower(PHP_OS), 'bsd')) {
|
|
$result = shell_exec('ping -w ' . ceil($this->timeout / 1000) . ' -c ' . $this->retries . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true));
|
|
} elseif (substr_count(cacti_strtolower(PHP_OS), 'aix')) {
|
|
$result = shell_exec('ping -i ' . ceil($this->timeout / 1000) . ' -c ' . $this->retries . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true));
|
|
} elseif (cacti_strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
|
|
$result = shell_exec('chcp 437 && ping -w ' . $this->timeout . ' -n ' . $this->retries . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true));
|
|
} else {
|
|
/**
|
|
* Please know, that when running SELinux, httpd will throw
|
|
* ping: cap_set_proc: Permission denied
|
|
* as it now tries to open an ICMP socket and fails
|
|
* $result will be empty, then.
|
|
*/
|
|
if (filter_var($host_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
|
$result = shell_exec('ping -6 -W ' . ceil($this->timeout / 1000) . ' -c ' . $this->retries . ' -p ' . $pattern . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true));
|
|
} else {
|
|
$result = shell_exec('ping -W ' . ceil($this->timeout / 1000) . ' -c ' . $this->retries . ' -p ' . $pattern . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true) . ' 2>&1');
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($using_fping) {
|
|
/**
|
|
* Prototype fping output below:
|
|
* www.google.com : xmt/rcv/%loss = 1/1/0%, min/avg/max = 25.6/25.6/25.6
|
|
* 172.24.254.2 : xmt/rcv/%loss = 1/1/0%, min/avg/max = 7.01/7.01/7.01
|
|
*/
|
|
$position = strpos((string) $result, 'min/avg/max =');
|
|
|
|
if ($position > 0) {
|
|
$output = substr((string) $result, $position);
|
|
$results = explode('/', $output);
|
|
$this->ping_status = $results[3]; // avg
|
|
$this->ping_response = __('ICMP Ping Success (fping.exe) (%s ms)', $results[1]);
|
|
|
|
return true;
|
|
} else {
|
|
$this->ping_status = 'down';
|
|
$this->ping_response = __('ICMP Ping Timed Out (fping.exe) (' . $this->host['hostname'] . '), Result [' . $result . ']');
|
|
|
|
return false;
|
|
}
|
|
} elseif (str_starts_with(PHP_OS, 'WIN')) {
|
|
/**
|
|
* The native ping command was used for windows platform has output
|
|
* as follows.
|
|
*
|
|
* ping -w 500 -n 3 abc
|
|
*
|
|
* Pinging abc [216.239.38.120] with 32 bytes of data:
|
|
* Reply from 216.239.38.120: bytes=32 time=30ms TTL=53
|
|
* Reply from 216.239.38.120: bytes=32 time=22ms TTL=53
|
|
* Reply from 216.239.38.120: bytes=32 time=25ms TTL=53
|
|
*
|
|
* Ping statistics for 216.239.38.120:
|
|
* Packets: Sent = 3, Received = 3, Lost = 0 (0% loss),
|
|
* Approximate round trip times in milli-seconds:
|
|
* Minimum = 22ms, Maximum = 30ms, Average = 25ms
|
|
*/
|
|
$position = strpos((string) $result, 'Minimum');
|
|
|
|
if ($position > 0) {
|
|
$output = trim(substr((string) $result, $position));
|
|
$pieces = explode(',', $output);
|
|
$results = explode('=', $pieces[2]); // Average
|
|
|
|
$this->ping_status = trim(str_replace('ms', '', $output));
|
|
$this->ping_response = __('ICMP Ping Success (ping.exe) (%s ms)', $this->ping_status);
|
|
|
|
return true;
|
|
} else {
|
|
$this->ping_status = 'down';
|
|
$this->ping_response = __('ICMP Ping Timed Out (ping.exe) (' . $this->host['hostname'] . '), Result [' . $result . ']');
|
|
|
|
return false;
|
|
}
|
|
} else {
|
|
$position = strpos((string) $result, 'min/avg/max');
|
|
|
|
if ($position > 0) {
|
|
$output = trim(str_replace(' ms', '', substr((string) $result, $position)));
|
|
$pieces = explode('=', $output);
|
|
$results = explode('/', $pieces[1]);
|
|
|
|
$this->ping_status = $results[1];
|
|
$this->ping_response = __('ICMP Ping Success (%s ms)', $results[1]);
|
|
|
|
return true;
|
|
} else {
|
|
$this->ping_status = 'down';
|
|
$this->ping_response = __('ICMP ping Timed out');
|
|
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
$this->ping_status = 'down';
|
|
$this->ping_response = __('Destination address not specified');
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function seteuid() : int {
|
|
$cacti_user = '';
|
|
|
|
// if we are unix, set the effective userid to root and then create
|
|
if ((CACTI_SERVER_OS == 'unix') &&
|
|
(function_exists('posix_getuid'))) {
|
|
$cacti_user = posix_getuid();
|
|
posix_seteuid(0);
|
|
}
|
|
|
|
return $cacti_user;
|
|
}
|
|
|
|
function setuid(int $cacti_poller_account) : void {
|
|
// if we are unix, set the effective userid to root and then create
|
|
if ((CACTI_SERVER_OS == 'unix') &&
|
|
(function_exists('posix_getuid'))) {
|
|
posix_seteuid($cacti_poller_account);
|
|
}
|
|
}
|
|
|
|
function ping_snmp() : mixed {
|
|
// initialize variables
|
|
$this->snmp_status = 'down';
|
|
$this->snmp_response = 'Device did not respond to SNMP';
|
|
$output = '';
|
|
|
|
// get start time
|
|
$this->start_time();
|
|
|
|
// by default, we look at sysUptime
|
|
if ($this->avail_method == AVAIL_SNMP_GET_NEXT) {
|
|
$oid = '.1.3.6.1.2.1.1.3.0';
|
|
} elseif ($this->avail_method == AVAIL_SNMP_GET_SYSDESC) {
|
|
$oid = '.1.3.6.1.2.1.1.1.0';
|
|
} else {
|
|
$oid = '.1.3.6.1.2.1.1.3.0';
|
|
}
|
|
|
|
$session = cacti_snmp_session($this->host['hostname'], $this->host['snmp_community'],
|
|
$this->host['snmp_version'], $this->host['snmp_username'],
|
|
$this->host['snmp_password'], $this->host['snmp_auth_protocol'],
|
|
$this->host['snmp_priv_passphrase'], $this->host['snmp_priv_protocol'],
|
|
$this->host['snmp_context'], $this->host['snmp_engine_id'],
|
|
$this->host['snmp_port'], $this->host['snmp_timeout'],
|
|
$this->retries, read_config_option('max_get_size'));
|
|
|
|
if ($session === false) {
|
|
$this->snmp_status = 'down';
|
|
$this->snmp_response = 'Failed to make SNMP session';
|
|
|
|
return false;
|
|
}
|
|
|
|
$result = $this->get_snmp_result($session, $oid);
|
|
|
|
if (!$result && $oid == '.1.3.6.1.2.1.1.3.0') {
|
|
$result = $this->get_snmp_result($session, '.1.3.6.1.6.3.10.2.1.3.0');
|
|
}
|
|
|
|
$session->close();
|
|
|
|
return $result;
|
|
}
|
|
|
|
function get_snmp_result(object $session, string $oid) : bool {
|
|
// getnext does not work in php versions less than 5
|
|
if (($this->avail_method == AVAIL_SNMP_GET_NEXT) &&
|
|
(version_compare('5', phpversion(), '<'))) {
|
|
$output = cacti_snmp_session_getnext($session, $oid);
|
|
} else {
|
|
$output = cacti_snmp_session_get($session, $oid);
|
|
}
|
|
|
|
// determine total time +- ~10%
|
|
$this->time = $this->get_time($this->precision);
|
|
|
|
// check result for uptime
|
|
if ($output !== false && $output != 'U' && strlen($output)) {
|
|
// calculate total time
|
|
$this->snmp_status = $this->time * 1000;
|
|
$this->snmp_response = 'Device responded to SNMP';
|
|
|
|
return true;
|
|
} else {
|
|
$this->snmp_status = 'down';
|
|
$this->snmp_response = 'Device did not respond to SNMP';
|
|
|
|
return false;
|
|
}
|
|
} // ping_snmp
|
|
|
|
function ping_udp() : bool {
|
|
$this->set_ping_error_handler();
|
|
|
|
// hostname must be nonblank
|
|
if ($this->host['hostname'] != '') {
|
|
// initialize variables
|
|
$this->ping_status = 'down';
|
|
$this->ping_response = __('default');
|
|
|
|
// establish timeout variables
|
|
$to_sec = intval(floor($this->timeout / 1000));
|
|
$to_usec = intval(($this->timeout % 1000) * 1000);
|
|
|
|
// clean up hostname if specifying snmp_transport
|
|
$this->host['hostname'] = $this->strip_ip_address($this->host['hostname']);
|
|
|
|
// determine the host's ip address
|
|
if ($this->is_ipaddress($this->host['hostname'])) {
|
|
$host_ip = $this->host['hostname'];
|
|
} else {
|
|
$host_ip = cacti_gethostbyname($this->host['hostname']);
|
|
|
|
if (!$this->is_ipaddress($host_ip)) {
|
|
cacti_log('WARNING: UDP Ping Error: cacti_gethostbyname failed for ' . $this->host['hostname']);
|
|
$this->ping_response = 'UDP Ping Error: cacti_gethostbyname failed for ' . $this->host['hostname'];
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// initialize the socket
|
|
if (filter_var($host_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
|
if (defined('AF_INET6')) {
|
|
$this->socket = socket_create(AF_INET6, SOCK_DGRAM, SOL_UDP);
|
|
} else {
|
|
$this->ping_response = __('IPv6 support seems to be missing!');
|
|
$this->ping_status = 'down';
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return false;
|
|
}
|
|
} else {
|
|
$this->socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
|
|
}
|
|
|
|
socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, ['sec' => $to_sec, 'usec' => $to_usec]);
|
|
socket_set_option($this->socket, SOL_SOCKET, SO_SNDTIMEO, ['sec' => $to_sec, 'usec' => $to_usec]);
|
|
|
|
socket_connect($this->socket, $host_ip, $this->port);
|
|
|
|
// format packet
|
|
$this->build_udp_packet();
|
|
|
|
$error = '';
|
|
$retry_count = 0;
|
|
|
|
while (true) {
|
|
if ($retry_count >= $this->retries) {
|
|
$this->ping_status = 'down';
|
|
$this->ping_response = __('UDP ping error: %s', $error);
|
|
$this->close_socket();
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return false;
|
|
}
|
|
|
|
// get start time
|
|
$this->start_time();
|
|
|
|
// write to the socket
|
|
socket_write($this->socket, $this->request, $this->request_len);
|
|
|
|
// get the socket response
|
|
$r = [$this->socket];
|
|
$w = [$this->socket];
|
|
$f = [$this->socket];
|
|
|
|
$num_changed_sockets = socket_select($r, $w, $f, $to_sec, $to_usec);
|
|
|
|
if ($num_changed_sockets === false) {
|
|
$error = 'UDP ping: socket_select(), reason: ' . socket_strerror(socket_last_error($this->socket));
|
|
} else {
|
|
switch($num_changed_sockets) {
|
|
case 2: // response received, so host is available
|
|
case 1:
|
|
// get packet response
|
|
// $code = socket_recv($this->socket, $this->reply, 256, 0);
|
|
$code = socket_recv($this->socket, $this->reply, 256, 0);
|
|
|
|
// get the end time after the packet was received
|
|
$this->time = $this->get_time($this->precision);
|
|
|
|
$errno = socket_last_error($this->socket);
|
|
socket_clear_error($this->socket);
|
|
|
|
if (($code == -1 || empty($code)) &&
|
|
($errno == EHOSTUNREACH || $errno == ECONNRESET || $errno == ECONNREFUSED)) {
|
|
// set the return message
|
|
$this->ping_status = $this->time * 1000;
|
|
$this->ping_response = __('UDP Ping Success (%s ms)', $this->time * 1000);
|
|
|
|
$this->close_socket();
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return true;
|
|
} else {
|
|
$error = socket_strerror($errno);
|
|
}
|
|
|
|
break;
|
|
case 0:
|
|
// timeout
|
|
$error = 'timeout';
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
$retry_count++;
|
|
}
|
|
} else {
|
|
$this->ping_response = __('Destination address not specified');
|
|
$this->ping_status = 'down';
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return false;
|
|
}
|
|
} // end ping_udp
|
|
|
|
function ping_tcp() : bool {
|
|
$this->set_ping_error_handler();
|
|
|
|
// hostname must be nonblank
|
|
if ($this->host['hostname'] != '') {
|
|
// initialize variables
|
|
$this->ping_status = 'down';
|
|
$this->ping_response = __('default');
|
|
|
|
// establish timeout variables
|
|
$to_sec = intval(floor($this->timeout / 1000));
|
|
$to_usec = intval(($this->timeout % 1000) * 1000);
|
|
|
|
// clean up hostname if specifying snmp_transport
|
|
$this->host['hostname'] = $this->strip_ip_address($this->host['hostname']);
|
|
|
|
// determine the host's ip address
|
|
if ($this->is_ipaddress($this->host['hostname'])) {
|
|
$host_ip = $this->host['hostname'];
|
|
} else {
|
|
$host_ip = cacti_gethostbyname($this->host['hostname']);
|
|
|
|
if (!$this->is_ipaddress($host_ip)) {
|
|
cacti_log('WARNING: TCP Ping Error: cacti_gethostbyname failed for ' . $this->host['hostname']);
|
|
$this->ping_response = 'TCP Ping Error: cacti_gethostbyname failed for ' . $this->host['hostname'];
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// initialize the socket
|
|
if (filter_var($host_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
|
if (defined('AF_INET6')) {
|
|
$this->socket = socket_create(AF_INET6, SOCK_STREAM, SOL_TCP);
|
|
} else {
|
|
$this->ping_response = __('IPv6 support appears to be missing!');
|
|
$this->ping_status = 'down';
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return false;
|
|
}
|
|
} else {
|
|
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
|
|
}
|
|
|
|
while (1) {
|
|
// set start time
|
|
$this->start_time();
|
|
|
|
socket_set_block($this->socket);
|
|
socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, ['sec' => $to_sec, 'usec' => $to_usec]);
|
|
socket_set_option($this->socket, SOL_SOCKET, SO_SNDTIMEO, ['sec' => $to_sec, 'usec' => $to_usec]);
|
|
|
|
socket_connect($this->socket, $host_ip, $this->port);
|
|
|
|
$errno = socket_last_error($this->socket);
|
|
|
|
if ($errno > 0) {
|
|
if ($errno == 111 && $this->ping_type == PING_TCP_CLOSED) {
|
|
$this->time = $this->get_time($this->precision);
|
|
$this->ping_status = 'up';
|
|
$this->ping_response = __('TCP Ping Success Connection Refused (%s ms)', $this->time * 1000);
|
|
$this->ping_status = $this->time * 1000;
|
|
} else {
|
|
$this->ping_response = __('TCP Ping Failed: socket_connect(), reason: %s', socket_strerror($errno));
|
|
$this->ping_status = 'down';
|
|
}
|
|
|
|
socket_clear_error($this->socket);
|
|
|
|
$this->close_socket();
|
|
$this->restore_cacti_error_handler();
|
|
|
|
if ($this->ping_status == 'down') {
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
$r = [$this->socket];
|
|
$w = [$this->socket];
|
|
$f = [$this->socket];
|
|
|
|
$num_changed_sockets = socket_select($r, $w, $f, $to_sec, $to_usec);
|
|
|
|
if ($num_changed_sockets === false) {
|
|
$this->ping_response = __('TCP Ping Failed: socket_select() failed, reason: %s', socket_strerror(socket_last_error()));
|
|
$this->ping_status = 'down';
|
|
|
|
$this->close_socket();
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return false;
|
|
} else {
|
|
switch($num_changed_sockets) {
|
|
case 2: // response received, so host is available
|
|
case 1:
|
|
// connected, so calculate the total time and return
|
|
$this->time = $this->get_time($this->precision);
|
|
|
|
if (($this->time * 1000) <= $this->timeout) {
|
|
$this->ping_response = __('TCP Ping Success (%s ms)', $this->time * 1000);
|
|
$this->ping_status = $this->time * 1000;
|
|
}
|
|
|
|
$this->close_socket();
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return true;
|
|
case 0:
|
|
// timeout
|
|
$this->ping_response = __('TCP ping timed out');
|
|
$this->ping_status = 'down';
|
|
|
|
$this->close_socket();
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
$this->ping_response = __('Destination address not specified');
|
|
$this->ping_status = 'down';
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return false;
|
|
}
|
|
} // end ping_tcp
|
|
|
|
function ping(int $avail_method = AVAIL_SNMP_AND_PING, int $ping_type = PING_ICMP, int $timeout = 500, int $retries = 3) : bool {
|
|
$this->set_ping_error_handler();
|
|
|
|
// initialize variables
|
|
$ping_ping = true;
|
|
$ping_snmp = true;
|
|
|
|
$this->ping_status = 'down';
|
|
$this->ping_response = __('Ping not performed due to setting.');
|
|
$this->ping_type = $ping_type;
|
|
$this->snmp_status = 'down';
|
|
$this->snmp_response = 'SNMP not performed due to setting or ping result.';
|
|
$this->avail_method = $avail_method;
|
|
|
|
// short circuit for availability none
|
|
if ($avail_method == AVAIL_NONE) {
|
|
$this->ping_status = '0.00';
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return true;
|
|
}
|
|
|
|
$sockets_fallback = false;
|
|
|
|
// AVAIL_NONE already returned above, so it needs no second test here
|
|
if (!function_exists('socket_create')) {
|
|
$avail_method = AVAIL_SNMP;
|
|
$sockets_fallback = true;
|
|
|
|
cacti_log('WARNING: sockets support not enabled in PHP, falling back to SNMP ping');
|
|
}
|
|
|
|
if (($retries <= 0) || ($retries > 5)) {
|
|
$this->retries = 2;
|
|
} else {
|
|
$this->retries = $retries;
|
|
}
|
|
|
|
if ($timeout <= 0) {
|
|
$this->timeout = 500;
|
|
} else {
|
|
$this->timeout = $timeout;
|
|
}
|
|
|
|
// decimal precision is 0.0000
|
|
$this->precision = 5;
|
|
|
|
// snmp pinging has been selected at a minimum
|
|
$ping_result = false;
|
|
$snmp_result = false;
|
|
|
|
// icmp/udp ping test
|
|
if (($avail_method == AVAIL_SNMP_AND_PING) ||
|
|
($avail_method == AVAIL_SNMP_OR_PING) ||
|
|
($avail_method == AVAIL_PING)) {
|
|
if ($ping_type == PING_ICMP) {
|
|
$ping_result = $this->ping_icmp();
|
|
} elseif ($ping_type == PING_UDP) {
|
|
$ping_result = $this->ping_udp();
|
|
} elseif ($ping_type == PING_TCP || $ping_type == PING_TCP_CLOSED) {
|
|
$ping_result = $this->ping_tcp();
|
|
}
|
|
}
|
|
|
|
// snmp test
|
|
if (($avail_method == AVAIL_SNMP) ||
|
|
($avail_method == AVAIL_SNMP_GET_SYSDESC) ||
|
|
($avail_method == AVAIL_SNMP_GET_NEXT) ||
|
|
($avail_method == AVAIL_SNMP_AND_PING) ||
|
|
($avail_method == AVAIL_SNMP_OR_PING)) {
|
|
// If we are in AND mode and already have a failed ping result, we don't need SNMP
|
|
if (!$ping_result && $avail_method == AVAIL_SNMP_AND_PING) {
|
|
$snmp_result = $ping_result;
|
|
} else {
|
|
/* cmd.php and other reduced-host callers can omit these keys entirely,
|
|
so a missing one counts the same as an unconfigured one. */
|
|
$have_snmp = (strlen($this->host['snmp_community'] ?? '') > 0 || ($this->host['snmp_version'] ?? 0) >= 3);
|
|
|
|
if ($have_snmp && !($avail_method == AVAIL_SNMP_OR_PING && $ping_result)) {
|
|
/* Run the real SNMP test whenever a community (or v3) is configured. In OR
|
|
mode that includes the case where the ping failed, so an unreachable
|
|
host is only reported up if SNMP actually answers. */
|
|
$snmp_result = $this->ping_snmp();
|
|
} elseif ($sockets_fallback) {
|
|
/* The fallback rewrote $avail_method to AVAIL_SNMP, so reaching here means
|
|
no community is configured and there is nothing to test against. Do not
|
|
report the device up on the strength of a check that never ran. */
|
|
$snmp_result = false;
|
|
} else {
|
|
/* Nothing left to test: either a v1/v2 host with no snmp_community, or an
|
|
OR-mode host a successful ping has already proven up. */
|
|
$snmp_result = true;
|
|
$this->snmp_status = 0.000;
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return match ($avail_method) {
|
|
AVAIL_SNMP_OR_PING => $snmp_result || $ping_result,
|
|
AVAIL_SNMP_AND_PING => $snmp_result && $ping_result,
|
|
AVAIL_SNMP => $snmp_result,
|
|
AVAIL_SNMP_GET_NEXT => $snmp_result,
|
|
AVAIL_SNMP_GET_SYSDESC => $snmp_result,
|
|
AVAIL_PING => $ping_result,
|
|
default => false,
|
|
};
|
|
} // end_ping
|
|
|
|
function is_ipaddress(string $ip_address = '') : bool {
|
|
// check for ipv4/v6
|
|
if (function_exists('filter_var')) {
|
|
if (filter_var($ip_address, FILTER_VALIDATE_IP) !== false) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} elseif (inet_pton($ip_address) !== false) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function strip_ip_address(string $ip_address) : string {
|
|
// clean up hostname if specifying snmp_transport
|
|
if (str_contains($ip_address, 'tcp6:')) {
|
|
$ip_address = str_replace('tcp6:', '', cacti_strtolower($ip_address));
|
|
|
|
if (str_contains($ip_address, '[')) {
|
|
$parts = explode(']', $ip_address);
|
|
$ip_address = trim($parts[0], '[');
|
|
}
|
|
} elseif (str_contains($ip_address, 'udp6:')) {
|
|
$ip_address = str_replace('udp6:', '', cacti_strtolower($ip_address));
|
|
|
|
if (str_contains($ip_address, '[')) {
|
|
$parts = explode(']', $ip_address);
|
|
$ip_address = trim($parts[0], '[');
|
|
}
|
|
} elseif (str_contains($ip_address, 'tcp:')) {
|
|
$ip_address = str_replace('tcp:', '', cacti_strtolower($ip_address));
|
|
} elseif (str_contains($ip_address, 'udp:')) {
|
|
$ip_address = str_replace('udp:', '', cacti_strtolower($ip_address));
|
|
}
|
|
|
|
return $ip_address;
|
|
}
|
|
}
|