diff --git a/tests/php/test_remote_control_url.php b/tests/php/test_remote_control_url.php new file mode 100644 index 000000000..d1e1f9e09 --- /dev/null +++ b/tests/php/test_remote_control_url.php @@ -0,0 +1,93 @@ +id = $id; } + public function Id() { return $this->id; } + public function UrlToIndex() { return 'http://zm2.example:8080/zm/index.php'; } +} + +class StubMonitor { + private $serverId; + private $serverKnown; + public function __construct($serverId, $serverKnown = true) { + $this->serverId = $serverId; + $this->serverKnown = $serverKnown; + } + public function ServerId() { return $this->serverId; } + public function Server() { return new StubServer($this->serverKnown ? $this->serverId : null); } +} + +$failures = 0; +$passes = 0; + +function check($name, $got, $want) { + global $failures, $passes; + if ($got === $want) { + $passes++; + echo "ok - $name\n"; + } else { + $failures++; + echo "FAIL - $name (got ".var_export($got, true).", want ".var_export($want, true).")\n"; + } +} + +$request = array( + 'view' => 'request', 'request' => 'control', 'id' => '4', 'control' => 'moveConLeft', + 'xge' => '50', 'yge' => '50', 'token' => 'jwt', 'username' => 'admin', 'password' => 'secret', +); + +check('monitor on this server is handled here', remoteControlUrl(new StubMonitor(1), $request), null); +check('monitor with no server is handled here', remoteControlUrl(new StubMonitor(0), $request), null); +check('monitor on a server ZoneMinder does not list is handled here', + remoteControlUrl(new StubMonitor(2, false), $request), null); +check('a request forwarded once is handled where it lands', + remoteControlUrl(new StubMonitor(2), $request + array('forwarded' => '1')), null); + +$url = remoteControlUrl(new StubMonitor(2), $request); +check('monitor on another server goes to that server', + strtok($url, '?'), 'http://zm2.example:8080/zm/index.php'); +parse_str(parse_url($url, PHP_URL_QUERY), $sent); +check('the control request is passed on', array( + isset($sent['view']) ? $sent['view'] : null, + isset($sent['request']) ? $sent['request'] : null, + isset($sent['id']) ? $sent['id'] : null, + isset($sent['control']) ? $sent['control'] : null, + isset($sent['xge']) ? $sent['xge'] : null, + isset($sent['forwarded']) ? $sent['forwarded'] : null, +), array('request', 'control', '4', 'moveConLeft', '50', '1')); +check('the caller\'s credentials are not passed on', array( + isset($sent['token']), isset($sent['password'])), array(false, false)); +check('the auth relay says who is asking', array( + isset($sent['auth']) ? $sent['auth'] : null, isset($sent['user']) ? $sent['user'] : null), + array('0123456789abcdef', 'admin')); + +$action = array('view' => 'request', 'request' => 'control', 'action' => 'monitorAction', 'id' => '3', 'aid' => '7'); +parse_str(parse_url(remoteControlUrl(new StubMonitor(2), $action), PHP_URL_QUERY), $sent); +check('a monitor action keeps its action and ids', array($sent['action'], $sent['id'], $sent['aid']), + array('monitorAction', '3', '7')); + +array_map('unlink', glob($stubdir.'/*')); +@rmdir($stubdir); + +echo "\n$passes passed, $failures failed\n"; +exit($failures ? 1 : 0); diff --git a/web/ajax/control.php b/web/ajax/control.php index 2777da4b8..da1e8b729 100644 --- a/web/ajax/control.php +++ b/web/ajax/control.php @@ -37,6 +37,9 @@ if ( isset($_REQUEST['action']) and ($_REQUEST['action'] == 'monitorAction') ) { if ( !canView('Control', $target->Id()) ) ajaxError('Insufficient permissions on the target monitor'); + if ( $url = remoteControlUrl($target, $_REQUEST) ) + relayControlReply($url); + $ctrlCommand = $monitorAction->controlCommand(); if ( !$ctrlCommand ) ajaxError('Action has no usable command'); @@ -52,6 +55,9 @@ if ( isset($_REQUEST['action']) and ($_REQUEST['action'] == 'monitorAction') ) { if ( canView('Control', $_REQUEST['id']) ) { $monitor = new ZM\Monitor(validCardinal($_REQUEST['id'])); + if ( $url = remoteControlUrl($monitor, $_REQUEST) ) + relayControlReply($url); + $ctrlCommand = buildControlCommand($monitor); if ( !$ctrlCommand ) { @@ -73,4 +79,15 @@ if ( canView('Control', $_REQUEST['id']) ) { } ajaxError('Unrecognised action or insufficient permissions'); + +// Hands the request to the server that records the monitor and answers with its reply. +function relayControlReply($url) { + $reply = forwardControlRequest($url); + if ( $reply === false ) + ajaxError('Failed'); + if ( function_exists('ajaxCleanup') ) + ajaxCleanup(); + header('Content-type: application/json'); + exit($reply); +} ?> diff --git a/web/includes/Monitor.php b/web/includes/Monitor.php index 6c892cc3c..d60c9881a 100644 --- a/web/includes/Monitor.php +++ b/web/includes/Monitor.php @@ -872,6 +872,12 @@ class Monitor extends ZM_Object { } socket_close($socket); } else if ($this->ServerId()) { + # The API only starts and stops zmcontrol.pl there; a camera command has to + # go to that server's web request (ajax/control.php forwards those). + if (!in_array($command, array('quit', 'start', 'stop'))) { + Error('Control command for monitor '.$this->{'Id'}.' not sent: it is recorded by server '.$this->{'ServerId'}); + return false; + } $result = $this->Server()->SendToApi('/monitors/daemonControl/'.$this->{'Id'}.'/'.$command.'/zmcontrol.pl.json'); return $result; } else { diff --git a/web/includes/control_functions.php b/web/includes/control_functions.php index 9a2e3a6e4..0e7199dc7 100644 --- a/web/includes/control_functions.php +++ b/web/includes/control_functions.php @@ -2,6 +2,47 @@ require_once('logger.php'); +// In a multi-server setup only the server that records a monitor can drive its +// camera: zmcontrol.pl runs there. When that is another server, returns the URL +// that hands it this same control request, or null to handle it here. The other +// server checks the permissions and builds the command itself. A request that +// was forwarded once is handled where it lands, so two servers that disagree +// about the monitor's server don't pass it back and forth. +function remoteControlUrl($monitor, $request) { + if ( !defined('ZM_SERVER_ID') or !ZM_SERVER_ID ) return null; + if ( !$monitor->ServerId() or ($monitor->ServerId() == ZM_SERVER_ID) ) return null; + if ( !empty($request['forwarded']) ) return null; + + $server = $monitor->Server(); + if ( !$server->Id() ) return null; + + // The other server learns who is asking from the auth relay, not from the + // credentials this request came with. + $params = array('view'=>'request', 'request'=>'control'); + foreach ( $request as $name => $value ) { + if ( !in_array($name, array('view', 'request', 'token', 'auth', 'user', 'username', 'password', 'pass', 'forwarded')) ) + $params[$name] = $value; + } + $params['forwarded'] = 1; + $url = $server->UrlToIndex().'?'.http_build_query($params); + $auth_relay = get_auth_relay(); + if ( $auth_relay ) $url .= '&'.$auth_relay; + return $url; +} + +// Sends a control request to the server that records the monitor and returns +// its JSON reply, or false when that server can't be reached. +function forwardControlRequest($url) { + $context = stream_context_create(array( + 'ssl'=>array('verify_peer'=>false, 'verify_peer_name'=>false), + )); + $reply = @file_get_contents($url, false, $context); + if ( $reply === false ) { + ZM\Error('Could not forward the control request to '.strtok($url, '?')); + } + return $reply; +} + function buildControlCommand($monitor) { $ctrlCommand = ''; $control = $monitor->Control();