Files
cacti/lib/ping.php
TheWitnessandTheWitness 3ab4c69fb7 PHPStan Level 8 - Phase 1 Conversion (#8135)
* 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>
2026-10-02 10:08:56 -04:00

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;
}
}