Files
zoneminder/web/includes/monitor_probe.php
Isaac Connor 684b0ab38b fix: escape URL in wget() to prevent command injection in camera probe (GHSA-745h-vg7c-73cg)
The wget() function in monitor_probe.php passed its URL argument directly
to exec() without shell escaping. Since probe credentials (username and
password from the camera discovery UI) are embedded in the URL by callers
like probeHikvision, probeAvigilon, and probeVivotek, an authenticated
user with Monitors Edit permission could inject shell metacharacters via
the Camera Username or Password fields to execute arbitrary commands.

Fix: use escapeshellarg() on the URL argument to exec().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 16:56:03 -04:00

1037 lines
36 KiB
PHP

<?php
//
// ZoneMinder web monitor probe view file
// Copyright (C) 2001-2008 Philip Coombes
//
// 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.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
//
ini_set('display_errors', '0');
require_once('Manufacturer.php');
// Probe Local Cameras
function probeV4L() {
$cameras = array();
$monitors = array();
foreach ( ZM\Monitor::find(['Type'=>'Local'], ['order'=>'Device, Channel']) as $monitor )
$monitors[$monitor->Device().':'.$monitor->Channel()] = $monitor;
$devices = array();
$devices_to_probe = array();
$preferredStandards = array('PAL', 'NTSC');
$preferredFormats = array('BGR3', 'RGB3', 'YUYV', 'UYVY', 'JPEG', 'MJPG', '422P', 'YU12', 'GREY');
if ( !empty($_REQUEST['device']) ) {
$devices_to_probe[] = $_REQUEST['device'];
} else {
$it = new FilesystemIterator('/dev/');
foreach ($it as $fileinfo) {
if (preg_match('/^video\d+$/', $fileinfo->getFilename())) {
$devices_to_probe[] = '/dev/'.$fileinfo->getFilename();
}
}
}
foreach ($devices_to_probe as $d) {
$command = getZmuCommand(' --query --device');
$command .= '='.escapeshellarg($d);
$result = exec(escapeshellcmd($command), $output, $status);
if ($status) {
ZM\Warning("Errors while probe local cameras using $command, status is '$status' " . implode("\n", $output));
}
foreach ( $output as $line ) {
if ( !preg_match('/^d:([^|]+).*S:([^|]*).*F:([^|]+).*I:(\d+)\|(.+)$/', $line, $deviceMatches) ) {
ZM\Error("Can't parse command output '$line'");
continue;
}
$standards = explode('/', $deviceMatches[2]);
$preferredStandard = false;
foreach ( $preferredStandards as $standard ) {
if ( in_array( $standard, $standards ) ) {
$preferredStandard = $standard;
break;
}
}
$formats = explode('/', $deviceMatches[3]);
$preferredFormat = false;
foreach ( $preferredFormats as $format ) {
if ( in_array($format, $formats) ) {
$preferredFormat = $format;
break;
}
}
$device = array(
'device' => $deviceMatches[1],
'standards' => $standard,
'preferredStandard' => $preferredStandard,
'formats' => $formats,
'preferredFormat' => $preferredFormat,
);
$inputs = array();
for ( $i = 0; $i < $deviceMatches[4]; $i++ ) {
if ( !preg_match('/i'.$i.':([^|]+)\|i'.$i.'T:([^|]+)\|/', $deviceMatches[5], $inputMatches) ) {
ZM\Error("Can't parse input '".$deviceMatches[5]."'");
continue;
}
if ( $inputMatches[2] == 'Camera' ) {
$input = array(
'index' => $i,
'id' => $deviceMatches[1].':'.$i,
'name' => $inputMatches[1],
'free' => empty($monitors[$deviceMatches[1].':'.$i]),
);
$inputMonitor = array(
'Type' => 'Local',
'Device' => $deviceMatches[1],
'Channel' => $i,
'Colours' => 3,
'Format' => $preferredStandard,
'Palette' => $preferredFormat,
);
if ( $preferredStandard == 'NTSC' ) {
$inputMonitor['Width'] = 320;
$inputMonitor['Height'] = 240;
} else {
$inputMonitor['Width'] = 384;
$inputMonitor['Height'] = 288;
}
if ( $preferredFormat == 'GREY' ) {
$inputMonitor['Colours'] = 1;
$inputMonitor['SignalCheckColour'] = '#000023';
}
$inputDesc = base64_encode(json_encode($inputMonitor));
$inputString = $deviceMatches[1].', chan '.$i.($input['free']?(' - '.translate('Available')):(' ('.$monitors[$input['id']]->Name().')'));
$inputs[] = $input;
$cameras[$inputDesc] = $inputString;
}
}
$device['inputs'] = $inputs;
$devices[] = $device;
} # end foreach output line
} # end foreach device in /dev
return $cameras;
} # end function probeV4L
function probeAxisCommunicationsAB($ip, $username, $password) {
return probeAxis($ip, $username, $password);
}
// Probe Network Cameras
//
function probeAxis($ip, $username, $password) {
if (!$username) $username = 'root';
if (!$password) $password = 'password';
$cameras = [];
$camera = array(
'ip' => $ip,
'mjpegstream' => 'http://'.$username.':'.$password.'@'.$ip.'/axis-cgi/mjpg/video.cgi?resolution=320x240',
'Manufacturer' => 'Axis',
'Model' => 'Unknown Model',
'monitor' => array(
'Path' => 'rtsp://'.$username.':'.$password.'@'.$ip.'/axis-media/media.amp',
'User' => $username,
'Pass' => $password,
'Manufacturer' => 'Axis',
),
);
$url = 'http://'.$ip.'/axis-cgi/admin/param.cgi?action=list';
$content = curl('GET', $url, $username, $password);
#ZM\Debug($content);
if ($content) {
$lines = explode("\n", $content);
foreach ( $lines as $line ) {
$line = rtrim( $line );
if ( preg_match('/^(.+)=(.+)$/', $line, $matches) ) {
if ( $matches[1] == 'root.Brand.ProdNbr' ) {
$camera['Model'] = $camera['monitor']['Model'] = $matches[2];
} else if ( $matches[1] == 'root.Image.I0.Appearance.Resolution' ) {
$resolution = explode('x', $matches[2]);
$camera['monitor']['Width'] = $resolution[0];
$camera['monitor']['Height'] = $resolution[1];
}
}
}
} else {
ZM\Debug("No content from $url");
}
$cameras[] = $camera;
return $cameras;
}
function probePanasonic($ip) {
$cameras = [];
$url = 'http://'.$ip.'/Get?Func=Model&Kind=1';
$camera = array(
'ip' => $ip,
'Model' => 'Panasonic Camera',
'monitor' => array(
'Type' => 'Remote',
'Protocol' => 'http',
'Host' => $ip,
'Port' => 80,
'Path' => '/nphMotionJpeg?Resolution=320x240&Quality=Standard',
'Colours' => 3,
'Width' => 320,
'Height' => 240,
),
);
$cameras[] = $camera;
return $cameras;
}
function probeActi($ip) {
$cameras = [];
$url = 'http://'.$ip.'/cgi-bin/system?USER=Admin&PWD=123456&SYSTEM_INFO';
$camera = array(
'ip' => $ip,
'Model' => 'Acti Camera',
'monitor' => array(
'Type' => 'Remote',
'Protocol' => 'rtsp',
'Method' => 'rtpUni',
'Host' => 'Admin:123456@'.$ip,
'Port' => 7070,
'Path' => '',
'Width' => 320,
'Height' => 240,
),
);
if ( $lines = @file($url) ) {
foreach ( $lines as $line ) {
$line = rtrim( $line );
if ( preg_match('/^(.+?)\s*=\s*(.+)$/', $line, $matches) ) {
if ( $matches[1] == 'Production ID' ) {
$camera['Model'] = 'ACTi '.substr($matches[2], 0, strpos($matches[2], '-'));
break;
}
}
}
}
$cameras[] = $camera;
return $cameras;
}
function probeAmcrestTechnologies($ip, $username='', $password='') {
return probeAmcrest($ip, $username, $password);
}
function probeAmcrest($ip, $username='', $password='') {
if (!$username) $username='admin';
if (!$password) $password='admin';
$cameras = [];
$url = 'rtsp://'.$username.':'.urlencode($password).'@'.$ip.':554/cam/realmonitor?channel=1&subtype=0&unicast=true';
$camera = array(
'mjpegstream' => 'http://'.$username.':'.urlencode($password).'@'.$ip.'/cgi-bin/snapshot.cgi',
'ip' => $ip,
'Manufacturer' => 'Amcrest',
#'Model' => 'Amcrest Camera',
'monitor' => array(
'Manufacturer' => 'Amcrest',
'Type' => 'Ffmpeg',
'Path' => $url,
'Width' => 1920,
'Height' => 1080,
),
);
$cameras[] = $camera;
return $cameras;
}
function wget($method, $url, $username, $password) {
exec('wget --keep-session-cookies -O - '.escapeshellarg($url), $output, $result_code);
return implode("\n", $output);
}
function curl($method, $url, $username, $password) {
if (function_exists('curl_version')) {
$ch = curl_init();
#curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
#curl_setopt($ch, CURLOPT_FRESH_CONNECT, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Try with SSL verification enabled first
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_COOKIESESSION, true);
$res = curl_exec($ch);
// If SSL verification failed, retry without verification
if ($res === false) {
$error = curl_error($ch);
$errno = curl_errno($ch);
// SSL certificate problem errors
// Note: CURLE_PEER_FAILED_VERIFICATION (51) may not be defined in all PHP versions
if ($errno == CURLE_SSL_CACERT || $errno == CURLE_SSL_PEER_CERTIFICATE ||
$errno == CURLE_SSL_CACERT_BADFILE || $errno == CURLE_SSL_CERTPROBLEM ||
$errno == 51 || strpos($error, 'SSL') !== false) {
ZM\Warning("SSL certificate verification failed for $url ($error), retrying without verification");
curl_close($ch);
// Retry without SSL verification
$ch = curl_init();
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_COOKIESESSION, true);
$res = curl_exec($ch);
}
}
ZM\Debug($res);
$status = curl_getinfo($ch);
ZM\Debug(print_r($status, true));
$headerSize = curl_getinfo( $ch , CURLINFO_HEADER_SIZE );
curl_close($ch);
$headerStr = substr( $res , 0 , $headerSize );
$bodyStr = substr( $res , $headerSize );
return $bodyStr;
} else {
ZM\Error("php-curl is not installed. Cannot use curl functions.");
}
return '';
}
function probeAzureWaveTechnologyInc($ip, $username, $password) {
}
function probeAvigilonCorporation($ip, $username, $password) {
return probeAvigilon($ip, $username, $password);
}
function probeAvigilon($ip, $username, $password) {
if (!$username) $username='administrator';
#if (!$password) $password='';
# Avigilon tends to want the : in auth even if no password.
$auth = $username ? $username.':'.$password.'@' : '';
$port_open = port_open($ip, 554);
$cameras = [];
$camera = [
'ip' => $ip,
'mjpegstream' => 'http://'.$auth.$ip.'/media/cam0/still.jpg',
'Manufacturer'=>'Avigilon Corporation',
'monitor' => [
'Type'=>'Ffmpeg',
'Path' => 'rtsp://'.$auth.$ip.'/rtsp/defaultPrimary?streamType=u',
'User' => $username,
'Pass' => $password,
'Manufacturer'=>'Avigilon Corporation',
]
];
# Urls: /cgi-x/get-compression?port=0
# get-ptz-capabilities?port=0
# /storage/status
# /cgi-x/positon?port=0
# /media/cam0/still.jpg
# /cgi-x/get-general
# /cgi-x/get-streamuri?port=0
#
# May have to login to onvif first, to get an auth cookie...
# http://administrator@192.168.4.60/onvif/device_service
//<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:ns1="http://www.onvif.org/ver10/device/wsdl"><s:Body><ns1:GetDeviceInformation></ns1:GetDeviceInformation></s:Body></s:Envelope>
$url = 'http://'.$auth.$ip.'/cgi-x/get-general';
$general_json = wget('GET', $url, $username, $password);
if ($general_json) {
$general = json_decode($general_json, true);
ZM\Debug(print_r($general, true));
if ($general) {
$camera['Model'] = $camera['monitor']['Model'] = (string)$general['partNumber'];
$camera['monitor']['ONVIF_URL'] = 'http://'.$username.':'.$password.'@'.$ip.'/onvif/device_service';
}
}
$url = 'http://'.$auth.$ip.'/cgi-x/get-streamuri?port=0';
$json = wget('GET', $url, $username, $password);
if ($json) {
$stream_uri = json_decode($json, true);
if ($stream_uri) {
$camera['monitor']['Path'] = $stream_uri['streamUri-unicast'];
}
}
$url = 'http://'.$auth.$ip.'/cgi-x/get-compression?port=0';
$compression_json = wget('GET', $url, $username, $password);
if ($compression_json) {
$compression = json_decode($compression_json, true);
if ($compression) {
ZM\Debug(print_r($compression, true));
$encodings = [];
foreach ($compression['optionsEncoding'] as $encoding) {
$encodings[$encoding[1]] = $encoding[0];
}
$h264options = [];
foreach ($compression['h264Options'] as $options) {
foreach ($options as $option) {
$h264options[$option[1]] = $option[0];
}
}
$h265options = [];
foreach ($compression['h265Options'] as $options) {
foreach ($options as $option) {
$h265options[$option[1]] = $option[0];
}
}
ZM\Debug(print_r($encodings, true));
$encoding = $encodings[$compression['defaultEncoding']];
$resolution = '';
if ($encoding == 'H.264') {
$resolution = $h264options[$compression['defaultResolution']];
} else if ($encoding == 'H.265') {
$resolution = $h265options[$compression['defaultResolution']];
}
if ($resolution) {
$resolution = explode('x', $resolution);
$camera['monitor']['Width'] = $resolution[0];
$camera['monitor']['Height'] = $resolution[1];
}
}
}
$cameras[] = $camera;
return $cameras;
} # End probeAvigilon
function probeGrandstreamNetworksInc($ip, $username, $password) {
if (!$username) $username='admin';
if (!$password) $password='password';
$cameras = [];
$port_open = port_open($ip, 554);
$url = 'rtsp://'.$ip.':554/0';
$camera = array(
'ip' => $ip,
'mjpegstream' => 'http://'.$username.':'.urlencode($password).'@'.$ip.'/jpg/image.jpg',
'Manufacturer' => 'Grandstream',
'Model' => ($port_open ? 'Camera' : 'Not Camera'),
'monitor' => array(
'Type' => 'Ffmpeg',
'Path' => $url,
'User' => $username,
'Pass' => $password,
'Width' => 1920,
'Height' => 1080,
'Manufacturer' => 'Grandstream',
),
);
$cameras[] = $camera;
return $cameras;
}
function probeHangzhouHikvisionDigitalTechnologyCoLtd($ip, $username, $password) {
return probeHikvision($ip, $username, $password);
}
function probeHikvision($ip, $username, $password) {
if (!$username) $username='admin';
if (!$password) $password='password';
$cameras = [];
$port_open = port_open($ip, 554);
$url = 'rtsp://'.$username.':'.$password.'@'.$ip.':554/Streaming/Channels/101?transportmode=unicast';
$camera = array(
'ip' => $ip,
'mjpegstream' => 'https://'.$username.':'.$password.'@'.$ip.'/ISAPI/streaming/channels/1/picture',
'Manufacturer' => 'Hikvision',
'Model' => ($port_open ? 'Camera' : 'Not Camera'),
'monitor' => array(
'Type' => 'Ffmpeg',
'Path' => $url,
'Width' => 1920,
'Height' => 1080,
'Manufacturer' => 'Hikvision',
),
);
$url = 'http://'.$username.':'.$password.'@'.$ip.'/ISAPI/Streaming/channels/101';
$xml_str = wget('GET', $url, $username, $password);
if ($xml_str) {
$xml = new SimpleXMLElement($xml_str);
if ($xml->Video) {
ZM\Debug(print_r($xml->Video, true));
$camera['monitor']['Width'] = (int) $xml->Video->videoResolutionWidth;
$camera['monitor']['Height'] = (int) $xml->Video->videoResolutionHeight;
$camera['monitor']['Name'] = (string) $xml->channelName;
$camera['Codec'] = (string) $xml->Video->videoCodecType;
} else {
ZM\Debug("No Video in xml".print_r($xml_str, true));
}
} else {
ZM\Debug("No xml from $url");
}
$url = 'http://'.$username.':'.$password.'@'.$ip.'/ISAPI/System/deviceInfo';
$xml_str = wget('GET', $url, $username, $password);
if ($xml_str) {
ZM\Debug($xml_str);
$xml = new SimpleXMLElement($xml_str);
$camera['Model'] = (string) $xml->model;
$camera['Name'] = (string) $xml->deviceName;
ZM\Debug(print_r($xml, true));
} else {
ZM\Debug("No xml from $url");
}
$cameras[] = $camera;
return $cameras;
}
function probeZhejiangUniviewTechnologiesCoLtd($ip, $username, $password) {
return probeUniview($ip, $username, $password);
}
function probeUniview($ip, $username, $password) {
if (!$username) $username = 'admin';
if (!$password) $password = '123456';
$cameras = [];
$port_open = port_open($ip, 554);
$url = 'rtsp://'.$username.':'.urlencode($password).'@'.$ip.':554/media/video1';
$camera = array(
'ip' => $ip,
'mjpegstream' => 'http://'.$username.':'.urlencode($password).'@'.$ip.'/LAPI/V1.0/Channels/0/Media/Video/Streams/0/Snapshot',
'Manufacturer' => 'Uniview',
'Model' => ($port_open ? 'Camera' : 'Not Camera'),
'monitor' => array(
'Type' => 'Ffmpeg',
'Path' => $url,
'Width' => 1920,
'Height' => 1080,
'Manufacturer' => 'Uniview',
),
);
$device_url = 'http://'.$ip.'/LAPI/V1.0/System/DeviceBasicInfo';
$json_str = curl('GET', $device_url, $username, $password);
if ($json_str) {
$device_info = json_decode($json_str, true);
if ($device_info && isset($device_info['Response'])) {
$data = $device_info['Response']['Data'];
if (!empty($data['DeviceModel'])) {
$camera['Model'] = $data['DeviceModel'];
$camera['monitor']['Model'] = $data['DeviceModel'];
}
if (!empty($data['DeviceName'])) {
$camera['Name'] = $data['DeviceName'];
$camera['monitor']['Name'] = $data['DeviceName'];
}
} else {
ZM\Debug("No valid JSON response from $device_url");
}
} else {
ZM\Debug("No response from $device_url");
}
$streams_url = 'http://'.$ip.'/LAPI/V1.0/Channels/0/Media/Video/Streams';
$json_str = curl('GET', $streams_url, $username, $password);
if ($json_str) {
$streams_info = json_decode($json_str, true);
if ($streams_info && isset($streams_info['Response']['Data']['StreamInfos'])) {
$stream_infos = $streams_info['Response']['Data']['StreamInfos'];
if (count($stream_infos) > 0) {
$main_stream = $stream_infos[0];
if (!empty($main_stream['Resolution']['Width'])) {
$camera['monitor']['Width'] = (int) $main_stream['Resolution']['Width'];
}
if (!empty($main_stream['Resolution']['Height'])) {
$camera['monitor']['Height'] = (int) $main_stream['Resolution']['Height'];
}
if (!empty($main_stream['Encode'])) {
$camera['Codec'] = $main_stream['Encode'];
}
}
} else {
ZM\Debug("No stream info from $streams_url");
}
} else {
ZM\Debug("No response from $streams_url");
}
$cameras[] = $camera;
return $cameras;
}
function probeUbiquitiNetworksInc($ip, $username, $password) {
return probeUbiquiti($ip, $username, $password);
}
function port_open($ip, $port) {
$fp = @fsockopen($ip, $port, $errno, $errstr, 1);
if (!$fp) {
return false;
}
fclose($fp);
return true;
}
function probeUbiquiti($ip, $username, $password) {
if (!$username) $username='ubnt';
if (!$password) $password='ubnt';
$port_open = port_open($ip, 554);
$cameras = [];
$camera = [
'ip' => $ip,
'Model' => ($port_open ? 'Ubiquiti Camera' : 'Unknown'),
'Manufacturer' => 'Ubiquiti',
'mjpegstream' => 'http://'.$username.':'.$password.'@'.$ip.'/snap.jpeg',
'monitor' => [
'Type' => 'Ffmpeg',
'Path' => 'rtsp://'.$username.':'.$password.'@'.$ip.':554/s0',
'Width' => 1920,
'Height' => 1080,
'Manufacturer' => 'Ubiquiti',
]
];
$cameras[] = $camera;
return $cameras;
}
function probeFoscam($ip, $username, $password) {
if ($username === null) $username = 'admin';
$rtsp_port = 0;
$http_port = 0;
if (port_open($ip, 554)) {
$rtsp_port = 554;
}
if (port_open($ip, 88)) {
$http_port = 88;
} else if (port_open($ip, 80)) {
$http_port = 80;
}
if (!$rtsp_port) $rtsp_port = $http_port;
$cameras = [];
$camera = array(
'ip' => $ip,
'Name' => 'Foscam Camera',
'Manufacturer' => 'Foscam',
'mjpegstream' => 'http://'.$username.':'.$password.'@'.$ip.':'.$http_port.'/videostream.cgi',
'monitor' => array(
'Manufacturer' => 'Foscam',
'Type' => 'Ffmpeg',
'Path' => ($rtsp_port == 554 ? 'rtsp' : 'http').'://'.$username.':'.$password.'@'.$ip.':'.$rtsp_port.'/videoMain',
'Host' => $ip,
'Width' => 640,
'Height' => 480,
),
);
if (!count($cameras)) {
$cameras[] = $camera;
}
return $cameras;
}
function probeDLinkInternational($ip, $username, $password) {
if ($username === null) $username = 'admin';
if ($password === null) $password = '';
$cameras = [];
$camera = array(
'ip' => $ip,
'Name' => 'D-Link Camera',
'Manufacturer' => 'D-Link',
'mjpegstream' => 'http://'.$username.':'.$password.'@'.$ip.'/image/jpeg.cgi',
'monitor' => array(
'Manufacturer' => 'D-Link',
'Type' => 'Ffmpeg',
'Path' => 'rtsp://'.$username.':'.$password.'@'.$ip.'/live1.sdp',
'User' => $username,
'Pass' => $password,
'Host' => $ip,
'Width' => 640,
'Height' => 480,
),
);
$cameras[] = $camera;
return $cameras;
}
function probeHanwhaTechwinSecurityVietnam($ip, $username, $password) {
if ($username === null) $username = 'admin';
if ($password === null) $password = '';
$cameras = [];
$camera = array(
'ip' => $ip,
'Name' => 'Hanwha Camera',
'Manufacturer' => 'Hanwha Techwin Security Vietnam',
'mjpegstream' => 'http://'.$username.':'.urlencode($password).'@'.$ip.'/stw-cgi/video.cgi?msubmenu=snapshot&action=view',
'monitor' => array(
'Manufacturer' => 'Hanwha Techwin Security Vietnam',
'Type' => 'Ffmpeg',
'Path' => 'rtsp://'.$ip.'/profile3.smp',
'User' => $username,
'Pass' => $password,
'Host' => $ip,
'Width' => 640,
'Height' => 480,
),
);
$cameras[] = $camera;
return $cameras;
}
function probeVivotek($ip, $username, $password) {
if ($username === null) $username = 'root';
if ($password === null) $password = '';
$control = ZM\Control::find_one(['Name'=>'Vivotek ePTZ']);
$cameras = [];
$camera = array(
'ip' => $ip,
'Name' => 'Vivotek Camera',
'Manufacturer' => 'Vivotek',
'mjpegstream' => 'http://'.$username.':'.urlencode($password).'@'.$ip.'/cgi-bin/viewer/video.jpg',
'monitor' => array(
'Manufacturer' => 'Vivotek',
'Type' => 'Ffmpeg',
'Path' => 'rtsp://'.$ip.'/',
'Host' => $ip,
'ControlId'=> ($control?$control->Id():''),
),
);
$settings = [];
$authority = $username.':'.$password.'@'.$ip;
$url = 'http://'.$authority.'/cgi-bin/viewer/getparam.cgi';
$content = wget('GET', $url, $username, $password);
#try {
#$content = do_request('GET', $url);
if ($content) {
ZM\Debug($content);
$lines = explode("\n", $content);
#if ($lines = @file($url)) {
foreach ($lines as $line) {
$line = rtrim($line);
if (preg_match('/^(.+?)\s*=\'(.+)\'$/', $line, $matches)) {
$settings[$matches[1]] = $matches[2];
}
}
} else {
ZM\Debug("Failed to load config from camera using $url");
}
#} catch (Exception $e) {
#ZM\Debug($e->getMessage());
#}
if (!empty($settings['system_info_modelname'])) {
$camera['Model'] = $settings['system_info_modelname'];
$camera['monitor']['Model'] = $settings['system_info_modelname'];
}
if (!empty($settings['videoin_text'])) {
$camera['Name'] = $settings['videoin_text'];
$camera['monitor']['Name'] = $settings['videoin_text'];
} else if (!empty($settings['system_hostname'])) {
$camera['Name'] = $settings['system_hostname'];
$camera['monitor']['Name'] = $settings['system_hostname'];
}
if (!empty($settings['network_rtsp_port'])) {
$camera['monitor']['Port'] = $settings['network_rtsp_port'];
}
foreach (['0', '1', '2'] as $i) {
if (!empty($settings['network_rtsp_s'.$i.'_accessname'])) {
$camera['monitor']['Path'] = 'rtsp://'.$authority.'/'.$settings['network_rtsp_s'.$i.'_accessname'];
if (!empty($settings['videoin_c0_s'.$i.'_resolution'])) {
$res = explode('x', $settings['videoin_c0_s'.$i.'_resolution']);
$camera['monitor']['Width'] = $res[0];
$camera['monitor']['Height'] = $res[1];
}
if (!empty($settings['videoin_c0_s'.$i.'_codectype'])) {
$camera['Codec'] = $settings['videoin_c0_s'.$i.'_codectype'];
if ($camera['Codec'] == 'mjpeg') {
if (!empty($settings['network_http_s'.$i.'_accessname'])) {
$camera['mjpegstream'] = 'http://'.$authority.'/'.$settings['network_http_s'.$i.'_accessname'];
}
}
}
$cameras[] = $camera;
} # has accessname
}
if (!count($cameras)) {
ZM\Debug("Failed getting streams, adding default");
$cameras[] = $camera;
}
return $cameras;
}
function probeWansview($ip) {
$cameras = [];
$camera = array(
'ip' => $ip,
'Model' => 'Wansview Camera',
'monitor' => array(
'Type' => 'Remote',
'Protocol' => 'http',
'Host' => 'admin:123456@'.$ip,
'Port' => 80,
'Path' => 'videostream.cgi',
'Width' => 640,
'Height' => 480,
'Palette' => 3
),
);
$cameras[] = $camera;
return $camera;
}
function get_arp_results() {
$results = array();
$arp_command = ZM_PATH_ARP;
$result = explode(' ', $arp_command);
if ( !is_executable($result[0]) ) {
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 .= ' -an';
}
$result = exec(escapeshellcmd($arp_command), $output, $status);
if ($status) {
ZM\Error("Unable to probe network cameras using $arp_command, status is '$status' output is ".implode("\n", $output));
return $results;
}
foreach ($output as $line) {
if ( !preg_match('/(\d+\.\d+\.\d+\.\d+).*(([0-9a-f]{2}:){5}[0-9a-fA-F]{2})/', $line, $matches) ) {
ZM\Debug("Didn't match preg $line");
continue;
} else {
ZM\Debug("Match preg got " .$matches[2] .' = '.$matches[1]);
}
$results[$matches[2]] = $matches[1]; // results[mac] = ip
}
return $results;
}
function get_arp_scan_results($network) {
ZM\Debug("arp-scanning $network");
$results = array();
$arp_scan_command = ZM_PATH_ARP_SCAN;
$result = explode(' ', $arp_scan_command);
if (!is_executable($result[0])) {
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;
$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));
return $results;
}
//ZM\Debug(print_r($output, true));
foreach ($output as $line) {
if (preg_match('/(\d+\.\d+\.\d+\.\d+)\s+([0-9a-f:]+)/', $line, $matches)) {
$results[$matches[2]] = $matches[1];
//} else {
//ZM\Debug("Didn't match preg $line");
}
}
ZM\Debug(print_r($results, true));
return $results;
} # end function get_arp_scan_results
function probeNetwork() {
$username = empty($_REQUEST['probe_username']) ? null : $_REQUEST['probe_username'];
$password = empty($_REQUEST['probe_password']) ? null : $_REQUEST['probe_password'];
$interface = isset($_REQUEST['interface']) ? $_REQUEST['interface'] : null;
$filter_ip = isset($_REQUEST['ip']) ? $_REQUEST['ip'] : null;
$filter_manufacturer = isset($_REQUEST['probe_Manufacturer']) ? $_REQUEST['probe_Manufacturer'] : null;
#$filter_manufacturer = $filter_manufacturer_id > 0 ? (new ZM\Manufacturer($filter_manufacturer_id))->Name() : '';
#ZM\Debug("$filter_manufacturer_id, $filter_manufacturer");
$cameras = array();
$results = array();
$monitors = array();
foreach (ZM\Monitor::find(['Type'=>'Remote']) as $monitor) {
if ( preg_match('/^(.+)@(.+)$/', $monitor->Host(), $matches) ) {
//echo "1: ".$matches[2]." = ".gethostbyname($matches[2])."<br/>";
$monitors[gethostbyname($matches[2])] = $monitor;
} else {
//echo "2: ".$monitor['Host']." = ".gethostbyname($monitor['Host'])."<br/>";
$monitors[gethostbyname($monitor->Host())] = $monitor;
}
}
foreach (ZM\Monitor::find(['Type'=>'Ffmpeg']) as $monitor) {
$url_parts = parse_url($monitor->Path());
if ($url_parts !== false) {
#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(print_r($monitors, true));
$macVendors = file_get_contents(ZM_PATH_DATA.'/MacVendors.json');
if (!$macVendors) {
ZM\Warning('No content from '.ZM_PATH_DATA.'/MacVendors.json');
}
$macBases = json_decode($macVendors, true);
if (defined('ZM_PATH_OUI') and ZM_PATH_OUI and file_exists(ZM_PATH_OUI)) {
$oui_txt = file_get_contents(ZM_PATH_OUI);
if (!$oui_txt) {
ZM\Warning('No content from /usr/share/arp-scan/ieee-oui.txt');
} else {
foreach (explode(PHP_EOL, $oui_txt) as $line) {
if (false === strpos($line , '#')) {
$record = explode("\t", $line);
if (count($record) < 2) continue;
$mac = strtolower($record[0]);
$type = preg_replace('/\W/', '', $record[1]);
if (!isset($macBases[$mac]))
$macBases[$mac] = [ 'vendor'=>$record[1], 'type'=>$type];
}
}
#ZM\Debug("bases: " . print_r($macBases, true));
}
} else {
ZM\Debug("No ieee-oui.txt");
}
foreach (get_arp_results() as $mac=>$ip) {
if ($filter_ip and ($ip != $filter_ip)) {
ZM\Debug("Skipping $mac $ip because of ip_filter $filter_ip");
continue;
}
$macRoot = str_replace(':', '', substr($mac, 0, 8));
if (isset($macBases[$macRoot])) {
ZM\Debug("Have match for $ip $mac $macRoot ".$macBases[$macRoot]['type']);
$macBase = $macBases[$macRoot];
if ($filter_manufacturer and ($filter_manufacturer != $macBase['type'])) {
ZM\Debug("Continuing because offilter $filter_manufacturer == ".$macBase['type']);
continue;
} else {
ZM\Debug("Not Continuing because offilter $filter_manufacturer != ".$macBase['type']);
}
if (function_exists('probe'.$macBase['type'])) {
if (!$username and isset($monitors[$ip])) {
$monitor = $monitors[$ip];
ZM\Debug("Using auth from monitor $ip ".$monitor->User().' '. $monitor->Pass());
$new_cameras = call_user_func('probe'.$macBase['type'], $ip, $monitors[$ip]->User(), $monitors[$ip]->Pass());
if (!$new_cameras) {
ZM\Warning('probe'.$macBase['type'].' returned nothing');
} else {
$cameras[$mac] = $new_cameras;
}
} else {
ZM\Debug("Not Using auth from monitor $ip $username $password");
$cameras[$mac] = call_user_func('probe'.$macBase['type'], $ip, $username, $password);
}
} else {
ZM\Debug("No probe function for {$macBase['type']}");
$cameras[$mac] = [['ip'=>$ip, 'Manufacturer'=>$macBase['vendor'],
'monitor'=>['Type'=>'Ffmpeg', 'Path'=>'rtsp://'.$ip.'/']]];
}
} else {
ZM\Debug("No match for $ip $macRoot");
$cameras[$mac] = [['ip'=>$ip, 'Manufacturer'=>'Unknown',
'monitor'=>['Type'=>'Ffmpeg', 'Path'=>'rtsp://'.$ip.'/']]];
}
if (connection_aborted()) exit();
} # end foreach output line
$interfaces = $interface ? $interface : get_networks();
foreach ($interfaces as $interface) {
foreach (get_subnets($interface) as $network) {
foreach (get_arp_scan_results($network) as $mac=>$ip) {
if ($filter_ip and ($ip != $filter_ip)) {
ZM\Debug("Skipping $mac $ip because of ip_filter $filter_ip");
continue;
}
$macRoot = str_replace(':', '', substr($mac, 0, 8));
if (!isset($cameras[$mac])) $cameras[$mac] = [];
#ZM\Debug("Got $macRoot from $mac");
if (isset($macBases[$macRoot])) {
ZM\Debug("Have match for $macRoot $ip ".$macBases[$macRoot]['type']);
$macBase = $macBases[$macRoot];
if ($filter_manufacturer and ($filter_manufacturer != $macBase['type'])) {
ZM\Debug("Continuing because offilter $filter_manufacturer == ".$macBase['type']);
continue;
}
if ($macBase['type'] != 'Unknown' and function_exists('probe'.$macBase['type'])) {
ZM\Debug("Calling ".$macBase['type']);
$found_cameras = [];
if (!$username and isset($monitors[$ip])) {
$found_cameras = call_user_func('probe'.$macBase['type'], $ip, $monitors[$ip]->User(), $monitors[$ip]->Pass());
} else {
ZM\Debug("Not Using auth from monitor $ip $username $password");
$found_cameras = call_user_func('probe'.$macBase['type'], $ip, $username, $password);
}
if (count($found_cameras)) {
$cameras[$mac] += $found_cameras;
} else {
ZM\Debug("DIdn't find any cameras");
}
} else {
$cameras[$mac] += [['ip'=>$ip, 'Manufacturer'=>$macBase['vendor'],
'monitor'=>['Type'=>'Ffmpeg', 'Path'=>'rtsp://'.$ip.'/']]];
ZM\Debug("No probe function for {$macBase['type']} {$macBase['vendor']}");
}
} else {
ZM\Debug("No match for $macRoot");
$cameras[$mac] += [['ip'=>$ip, 'Manufacturer'=>'Unknown',
'monitor'=>['Type'=>'Ffmpeg', 'Path'=>'rtsp://'.$ip.'/']]];
}
if (connection_aborted()) exit();
} # end foreach output line
} # end foreach network
} # foreach interface
$url_filter = [];
foreach ($cameras as $mac => $cams) {
foreach ($cams as $camera) {
if (isset($camera['monitor'])) {
if (isset($url_filter[$camera['monitor']['Path']])) continue;
$url_filter[$camera['monitor']['Path']] = 1;
}
if (!is_array($camera)) {
ZM\Error("What is ".print_r($camera, true));
continue;
}
$ip = $camera['ip'];
$sourceString = (isset($camera['Model']) ? ($camera['Model'].' @ '):'').$ip;
$monitor = null;
if (isset($monitors[$ip])) {
$monitor = $monitors[$ip];
$sourceString .= ' ('.$monitor->Name().')';
} else {
$sourceString .= ' - '.translate('Available');
}
$results[] = [
'mac' => $mac,
'description' => $sourceString,
'url' => (isset($camera['monitor']) ? $camera['monitor']['Path'] : ''),
'IP' => $camera['ip'],
'camera' => $camera,
'Monitor' => $monitor,
];
} # end foreach stream
} # end foreach mac
return $results;
} # end function probeNetwork()