diff --git a/CMakeLists.txt b/CMakeLists.txt index bf0a267025f..e3007986087 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -119,6 +119,8 @@ mark_as_advanced( ZM_PATH_UNAME ZM_PATH_IP ZM_PATH_IFCONFIG + ZM_PATH_NETSTAT + ZM_PATH_PKEXEC ZM_CONFIG_DIR ZM_CONFIG_SUBDIR ZM_DETECT_SYSTEMD @@ -198,6 +200,10 @@ set(ZM_PATH_IP "" CACHE PATH "Full path to compatible ip binary. Leave empty for automatic detection.") set(ZM_PATH_IFCONFIG "" CACHE PATH "Full path to compatible ifconfig binary. Leave empty for automatic detection.") +set(ZM_PATH_NETSTAT "" CACHE PATH + "Full path to compatible netstat binary. Leave empty for automatic detection.") +set(ZM_PATH_PKEXEC "" CACHE PATH + "Full path to compatible pkexec binary. Leave empty for automatic detection.") set(ZM_CONFIG_DIR "${CMAKE_INSTALL_FULL_SYSCONFDIR}/zm" CACHE PATH "Location of ZoneMinder configuration, default system config directory") set(ZM_CONFIG_SUBDIR "${ZM_CONFIG_DIR}/conf.d" CACHE PATH @@ -820,6 +826,28 @@ if(ZM_PATH_IFCONFIG STREQUAL "") endif() endif() +# Find the path to an netstat compatible executable +if(ZM_PATH_NETSTAT STREQUAL "") + find_program(NETSTAT_EXECUTABLE netstat) + if(NETSTAT_EXECUTABLE) + set(ZM_PATH_NETSTAT "${NETSTAT_EXECUTABLE}") + mark_as_advanced(NETSTAT_EXECUTABLE) + else() + message(WARNING "Unable to find a compatible netstat binary.") + endif() +endif() + +# Find the path to an pkexec compatible executable +if(ZM_PATH_PKEXEC STREQUAL "") + find_program(PKEXEC_EXECUTABLE pkexec) + if(PKEXEC_EXECUTABLE) + set(ZM_PATH_PKEXEC "${PKEXEC_EXECUTABLE}") + mark_as_advanced(PKEXEC_EXECUTABLE) + else() + message(WARNING "Unable to find a compatible pkexec binary.") + endif() +endif() + # Some variables that zm expects set(ZM_PID "${ZM_RUNDIR}/zm.pid") set(ZM_CONFIG "${ZM_CONFIG_DIR}/zm.conf") diff --git a/conf.d/01-system-paths.conf.in b/conf.d/01-system-paths.conf.in index 3a047b00867..d3a2be1dfd8 100644 --- a/conf.d/01-system-paths.conf.in +++ b/conf.d/01-system-paths.conf.in @@ -70,6 +70,14 @@ ZM_PATH_IP="@ZM_PATH_IP@" # ZoneMinder will find the ifconfig binary automatically on most systems ZM_PATH_IFCONFIG="@ZM_PATH_IFCONFIG@" +# Full path to optional netstat binary or just netstat to use PATH +# ZoneMinder will find the netstat binary automatically on most systems +ZM_PATH_NETSTAT="@ZM_PATH_NETSTAT@" + +# Full path to optional pkexec binary or just pkexec to use PATH +# ZoneMinder will find the pkexec binary automatically on most systems +ZM_PATH_PKEXEC="@ZM_PATH_PKEXEC@" + #Full path to shutdown binary ZM_PATH_SHUTDOWN="@ZM_PATH_SHUTDOWN@" diff --git a/distros/redhat/zoneminder.spec b/distros/redhat/zoneminder.spec index ca838487996..8c268f1476d 100644 --- a/distros/redhat/zoneminder.spec +++ b/distros/redhat/zoneminder.spec @@ -230,6 +230,8 @@ mv -f CxxUrl-%{CxxUrl_version} ./dep/CxxUrl -DZM_PATH_ARP_SCAN="/usr/sbin/arp-scan" \ -DZM_PATH_IP="/usr/sbin/ip" \ -DZM_PATH_IFCONFIG="/usr/sbin/ifconfig" \ + -DZM_PATH_NETSTAT="/usr/bin/netstat" \ + -DZM_PATH_PKEXEC="/usr/bin/pkexec" \ . %cmake_build diff --git a/web/includes/functions.php b/web/includes/functions.php index 9dae2966bed..e38519d4946 100644 --- a/web/includes/functions.php +++ b/web/includes/functions.php @@ -2240,64 +2240,113 @@ function i18n() { } function get_networks() { - $interfaces = array(); + $interfaces = array(); - if (defined('ZM_PATH_IP') and ZM_PATH_IP and file_exists(ZM_PATH_IP)) { - exec(ZM_PATH_IP.' link', $output, $status); - if ( $status ) { - $html_output = implode('
', $output); - ZM\Error("Unable to list network interfaces, status is '$status'. Output was:

$html_output"); - } else { - foreach ( $output as $line ) { - if ( preg_match('/^\d+: ([[:alnum:]]+):/', $line, $matches ) ) { - if ( $matches[1] != 'lo' ) { + if (defined('ZM_PATH_IP') and ZM_PATH_IP and file_exists(ZM_PATH_IP)) { + exec(ZM_PATH_IP.' link', $output, $status); + if ( $status ) { + $html_output = implode('
', $output); + ZM\Error("Unable to list network interfaces, status is '$status'. Output was:

$html_output"); + } else { + foreach ( $output as $line ) { + if ( preg_match('/^\d+: ([[:alnum:]]+):/', $line, $matches ) ) { + if ( $matches[1] != 'lo' ) { + $interfaces[$matches[1]] = $matches[1]; + } else { + ZM\Debug("Skipping loopback interface $line"); + } + } + } + } + $output = ''; + exec(ZM_PATH_IP.' route', $output, $status); + if ($status) { + $html_output = implode('
', $output); + ZM\Error("Unable to list network interfaces, status is '$status'. Output was:

$html_output"); + } else { + foreach ($output as $line) { + if ( preg_match('/^default via [.[:digit:]]+ dev ([[:alnum:]]+)/', $line, $matches) ) { + $interfaces['default'] = $matches[1]; + } else if ( preg_match('/^([.[:digit:]]+\/[[:digit:]]+) dev ([[:alnum:]]+)/', $line, $matches) ) { + $interfaces[$matches[2]] .= ' ' . $matches[1]; + ZM\Debug("Matched $line: $matches[2] .= $matches[1]"); + } else { + ZM\Debug("Didn't match $line"); + } + } # end foreach line of output + } + } else if (defined('ZM_PATH_IFCONFIG') and ZM_PATH_IFCONFIG and file_exists(ZM_PATH_IFCONFIG)) { + exec(ZM_PATH_IFCONFIG, $output, $status); + if ($status) { + $html_output = implode("\n", $output); + ZM\Error("Unable to list network interfaces, status is '$status'. Output was:$html_output"); + } else { + array_walk($output, function($value, $key) use (&$interfaces) { + $retval = preg_match("/^([A-Za-z0-9]*):\s+flags=([0-9]*)<([A-Z,]*)>.*/ims", $value, $matches); + ZM\Debug(print_r($matches, true)); + if (($retval == 1) && (strlen($matches[3]) > 0) && + (strpos($matches[3], "LOOPBACK") === false) && (strpos($matches[3], "RUNNING") !== false)) + { $interfaces[$matches[1]] = $matches[1]; + } + }); + + if (defined('ZM_PATH_NETSTAT') and ZM_PATH_NETSTAT and file_exists(ZM_PATH_NETSTAT)) { + $output = ''; + // Get default route iface + exec(ZM_PATH_NETSTAT . " -r4 | grep '^default' | head -n 1 | awk '{print $4}'", $defaultIface, $status); + + if ($status) { + $html_output = implode("\n", $output); + ZM\Error("Unable to get default ip route, status is '$status'. Output was:$html_output"); } else { - ZM\Debug("No match for $line"); + if (isset($defaultIface) && isset($defaultIface[0])) { + $interfaces['default'] = $defaultIface[0]; + } } } - } - } - $routes = array(); - exec(ZM_PATH_IP.' route', $output, $status); - if ($status) { - $html_output = implode('
', $output); - ZM\Error("Unable to list network interfaces, status is '$status'. Output was:

$html_output"); - } else { - foreach ($output as $line) { - if ( preg_match('/^default via [.[:digit:]]+ dev ([[:alnum:]]+)/', $line, $matches) ) { - $interfaces['default'] = $matches[1]; - } else if ( preg_match('/^([.[:digit:]]+\/[[:digit:]]+) dev ([[:alnum:]]+)/', $line, $matches) ) { - $interfaces[$matches[2]] .= ' ' . $matches[1]; - ZM\Debug("Matched $line: $matches[2] .= $matches[1]"); - } else { - ZM\Debug("Didn't match $line"); - } - } # end foreach line of output - } - } else if (defined('ZM_PATH_IFCONFIG') and ZM_PATH_IFCONFIG and file_exists(ZM_PATH_IFCONFIG)) { - exec(ZM_PATH_IFCONFIG, $output, $status); - if ($status) { - $html_output = implode("\n", $output); - ZM\Error("Unable to list network interfaces, status is '$status'. Output was:$html_output"); - } else { - preg_match("/^([eth|enp][A-z0-9]*)\s+Link\s+encap:([A-z]*)\s+HWaddr\s+([A-z0-9:]*).*". - "inet addr:([0-9.]+).*Bcast:([0-9.]+).*Mask:([0-9.]+).*". - "MTU:([0-9.]+).*Metric:([0-9.]+).*". - "RX packets:([0-9.]+).*errors:([0-9.]+).*dropped:([0-9.]+).*overruns:([0-9.]+).*frame:([0-9.]+).*". - "TX packets:([0-9.]+).*errors:([0-9.]+).*dropped:([0-9.]+).*overruns:([0-9.]+).*carrier:([0-9.]+).*". - "RX bytes:([0-9.]+).*\((.*)\).*TX bytes:([0-9.]+).*\((.*)\)". - "/ims", implode("\n", $output), $regex); - - ZM\Debug(print_r( $regex,true)); - } - } - return $interfaces; -} - -# Returns an array of subnets like 192.168.1.0/24 for a given interface. -# Will ignore mdns networks. + } + } + return $interfaces; +} + +function parse_netstat_line(string $line) { + $line = trim($line); + $pattern = '/^(?:' . + '(\d{1,3}(?:\.\d{1,3}){3})\s+' . + '\d{1,3}(?:\.\d{1,3}){3}\s+' . + '\d{1,3}(?:\.\d{1,3}){3}\s+' . + '[A-Za-z]+\s+' . + '\d+\s+' . + '\d+\s+' . + '\d+\s+' . + '(\S+)' . + ')|(?:' . + '(\d{1,3}(?:\.\d{1,3}){3}\/\d{1,2})\s+' . + '\S+?\s*' . + '[A-Za-z]+\s+' . + '(\S+)' . + ')$/'; + if (!preg_match($pattern, $line, $matches)) { + return null; + } + + if (!empty($matches[1])) { + return [ + 'ip' => $matches[1], + 'iface' => $matches[2], + ]; + } + + return [ + 'ip' => $matches[3], + 'iface' => $matches[4], + ]; +} + +# Returns an array of subnets like 192.168.1.0/24 (192.168.1.0 on some platforms) for a given interface. +# Will ignore mdns networks. function get_subnets($interface) { $subnets = array(); if (defined('ZM_PATH_IP') and ZM_PATH_IP and file_exists(ZM_PATH_IP)) { @@ -2320,7 +2369,29 @@ function get_subnets($interface) { } } # end foreach line of output } + } else if (defined('ZM_PATH_NETSTAT') and ZM_PATH_NETSTAT and file_exists(ZM_PATH_NETSTAT)) { + exec(ZM_PATH_NETSTAT.' -r4', $output, $status); + if ( $status ) { + $html_output = implode('
', $output); + ZM\Error("Unable to list network interfaces, status is '$status'. Output was:

$html_output"); + } else { + foreach ($output as $line) { + $netstat_match = parse_netstat_line($line); + if (null !== $netstat_match) { + if (($netstat_match['ip'] == '169.254.0.0/16') || ($netstat_match['ip'] == '169.254.0.0')) { + # Ignore mdns + } else if ($netstat_match['iface'] == $interface) { + $subnets[] = $netstat_match['ip']; + } else { + ZM\Debug("Wrong interface " . $netstat_match['iface'] . " != " . $interface); + } + } else { + ZM\Debug("Didn't match $line"); + } + } # end foreach line of output + } } + return $subnets; } # end function get_subnets($interface) diff --git a/web/includes/monitor_probe.php b/web/includes/monitor_probe.php index 3e0610b0294..55db04d20e7 100644 --- a/web/includes/monitor_probe.php +++ b/web/includes/monitor_probe.php @@ -833,7 +833,7 @@ function get_arp_scan_results($network) { ZM\Error('arp-scan compatible binary not found or not executable by the web user account. Verify ZM_PATH_ARP_SCAN points to a valid arp-scan tool.'); return $results; } - $arp_scan_command = '/usr/bin/pkexec '.ZM_PATH_ARP_SCAN.' '.$network; + $arp_scan_command = ZM_PATH_PKEXEC.' '.ZM_PATH_ARP_SCAN.' '.$network; $result = exec(escapeshellcmd($arp_scan_command), $output, $status); if ($status) { ZM\Error("Unable to probe network cameras, command was $arp_scan_command, status is '$status' output: ".implode(PHP_EOL, $output)); diff --git a/web/skins/classic/views/monitorprobe.php b/web/skins/classic/views/monitorprobe.php index b9745fcfbde..36c719d473d 100644 --- a/web/skins/classic/views/monitorprobe.php +++ b/web/skins/classic/views/monitorprobe.php @@ -293,8 +293,8 @@ function get_arp_results() { ZM\Error('ARP compatible binary not found or not executable by the web user account. Verify ZM_PATH_ARP points to a valid arp tool.'); return $results; } - if (count($result)==1) { - $arp_command .= ' -n'; + if (count($result) == 1) { + $arp_command .= ' -na'; } $result = exec(escapeshellcmd($arp_command), $output, $status); @@ -321,7 +321,7 @@ function get_arp_scan_results($network) { ZM\Error('arp-scan compatible binary not found or not executable by the web user account. Verify ZM_PATH_ARP_SCAN points to a valid arp-scan tool.'); return $results; } - $arp_scan_command = '/usr/bin/pkexec '.ZM_PATH_ARP_SCAN.' '.$network.' 2>&1'; + $arp_scan_command = ZM_PATH_PKEXEC.' '.ZM_PATH_ARP_SCAN.' '.$network.' 2>&1'; $result = exec(escapeshellcmd($arp_scan_command), $output, $status); if ($status) { ZM\Error("Unable to probe network cameras, command was $arp_scan_command, status is '$status' output: ".implode(PHP_EOL, $output)); @@ -353,10 +353,10 @@ function probeNetwork() { foreach ( dbFetchAll("SELECT `Id`, `Name`, `Path` FROM `Monitors` WHERE `Type` = 'Ffmpeg' ORDER BY `Path`") as $monitor ) { $url_parts = parse_url($monitor['Path']); if ($url_parts !== false) { - ZM\Debug("Ffmpeg monitor ${url_parts['host']} = ${monitor['Id']} ${monitor['Name']}"); + ZM\Debug("Ffmpeg monitor " . $url_parts['host'] . " = " . $monitor['Id'] . " " . $monitor['Name']); $monitors[gethostbyname($url_parts['host'])] = $monitor; } else { - ZM\Debug("Unable to parse ${monitor['Path']}"); + ZM\Debug("Unable to parse " . $monitor['Path']); } } @@ -450,7 +450,7 @@ function probeNetwork() {