1<?php
2/*
3** Zabbix
4** Copyright (C) 2001-2021 Zabbix SIA
5**
6** This program is free software; you can redistribute it and/or modify
7** it under the terms of the GNU General Public License as published by
8** the Free Software Foundation; either version 2 of the License, or
9** (at your option) any later version.
10**
11** This program is distributed in the hope that it will be useful,
12** but WITHOUT ANY WARRANTY; without even the implied warranty of
13** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14** GNU General Public License for more details.
15**
16** You should have received a copy of the GNU General Public License
17** along with this program; if not, write to the Free Software
18** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19**/
20
21
22/**
23 * A class for interacting with the Zabbix server.
24 *
25 * Class CZabbixServer
26 */
27class CZabbixServer {
28
29	/**
30	 * Return item queue overview.
31	 */
32	const QUEUE_OVERVIEW = 'overview';
33
34	/**
35	 * Return item queue overview by proxy.
36	 */
37	const QUEUE_OVERVIEW_BY_PROXY = 'overview by proxy';
38
39	/**
40	 * Return a detailed item queue.
41	 */
42	const QUEUE_DETAILS = 'details';
43
44	/**
45	 * Response value if the request has been executed successfully.
46	 */
47	const RESPONSE_SUCCESS = 'success';
48
49	/**
50	 * Response value if an error occurred.
51	 */
52	const RESPONSE_FAILED = 'failed';
53
54	/**
55	 * Auxiliary constants for request() method.
56	 */
57	const ZBX_TCP_EXPECT_HEADER = 1;
58	const ZBX_TCP_EXPECT_DATA = 2;
59
60	/**
61	 * Max number of bytes to read from the response for each each iteration.
62	 */
63	const READ_BYTES_LIMIT = 8192;
64
65	/**
66	 * Zabbix server host name.
67	 *
68	 * @var string
69	 */
70	protected $host;
71
72	/**
73	 * Zabbix server port number.
74	 *
75	 * @var string
76	 */
77	protected $port;
78
79	/**
80	 * Request timeout.
81	 *
82	 * @var int
83	 */
84	protected $timeout;
85
86	/**
87	 * Maximum response size. If the size of the response exceeds this value, an error will be triggered.
88	 *
89	 * @var int
90	 */
91	protected $totalBytesLimit;
92
93	/**
94	 * Zabbix server socket resource.
95	 *
96	 * @var resource
97	 */
98	protected $socket;
99
100	/**
101	 * Error message.
102	 *
103	 * @var string
104	 */
105	protected $error;
106
107	/**
108	 * Total result count (if any).
109	 *
110	 * @var int
111	 */
112	protected $total;
113
114	/**
115	 * @var array $debug  Section 'debug' data from server response.
116	 */
117	protected $debug = [];
118
119	/**
120	 * Class constructor.
121	 *
122	 * @param string $host
123	 * @param int $port
124	 * @param int $timeout
125	 * @param int $totalBytesLimit
126	 */
127	public function __construct($host, $port, $timeout, $totalBytesLimit) {
128		$this->host = $host;
129		$this->port = $port;
130		$this->timeout = $timeout;
131		$this->totalBytesLimit = $totalBytesLimit;
132	}
133
134	/**
135	 * Executes a script on the given host and returns the result.
136	 *
137	 * @param $scriptId
138	 * @param $hostId
139	 * @param $sid
140	 *
141	 * @return bool|array
142	 */
143	public function executeScript($scriptId, $hostId, $sid) {
144		return $this->request([
145			'request' => 'command',
146			'scriptid' => $scriptId,
147			'hostid' => $hostId,
148			'sid' => $sid,
149			'clientip' => CWebUser::getIp()
150		]);
151	}
152
153	/**
154	 * Request server to test item preprocessing steps.
155	 *
156	 * @param array  $data                                     Array of preprocessing steps test.
157	 * @param string $data['value']                            Value to use for preprocessing step testing.
158	 * @param int    $data['value_type']                       Item value type.
159	 * @param array  $data['history']                          Previous value object.
160	 * @param string $data['history']['value']                 Previous value.
161	 * @param string $data['history']['timestamp']             Previous value time.
162	 * @param array  $data['steps']                            Preprocessing step object.
163	 * @param int    $data['steps'][]['type']                  Type of preprocessing step.
164	 * @param string $data['steps'][]['params']                Parameters of preprocessing step.
165	 * @param int    $data['steps'][]['error_handler']         Error handler selected as "custom on fail".
166	 * @param string $data['steps'][]['error_handler_params']  Parameters configured for selected error handler.
167	 * @param string $sid                                      User session ID.
168	 *
169	 * @return array
170	 */
171	public function testPreprocessingSteps(array $data, $sid) {
172		return $this->request([
173			'request' => 'preprocessing.test',
174			'data' => $data,
175			'sid' => $sid
176		]);
177	}
178
179	/**
180	 * Request server to test item.
181	 *
182	 * @param array  $data    Array of item properties to test.
183	 * @param string $sid     User session ID.
184	 *
185	 * @return array
186	 */
187	public function testItem(array $data, $sid) {
188		/*
189		 * Timeout for 'item.test' request is increased because since message can be forwarded from server to proxy and
190		 * later to agent, it might take more time due network latency.
191		 */
192		$this->timeout = 60;
193
194		return $this->request([
195			'request' => 'item.test',
196			'data' => $data,
197			'sid' => $sid
198		]);
199	}
200
201	/**
202	 * Retrieve item queue information.
203	 *
204	 * Possible $type values:
205	 * - self::QUEUE_OVERVIEW
206	 * - self::QUEUE_OVERVIEW_BY_PROXY
207	 * - self::QUEUE_DETAILS
208	 *
209	 * @param string $type
210	 * @param string $sid   user session ID
211	 * @param int    $limit item count for details type
212	 *
213	 * @return bool|array
214	 */
215	public function getQueue($type, $sid, $limit = 0) {
216		$request = [
217			'request' => 'queue.get',
218			'sid' => $sid,
219			'type' => $type
220		];
221
222		if ($type == self::QUEUE_DETAILS) {
223			$request['limit'] = $limit;
224		}
225
226		return $this->request($request);
227	}
228
229	/**
230	 * Request server to test media type.
231	 *
232	 * @param array  $data                 Array of media type test data to send.
233	 * @param string $data['mediatypeid']  Media type ID.
234	 * @param string $data['sendto']       Message destination.
235	 * @param string $data['subject']      Message subject.
236	 * @param string $data['message']      Message body.
237	 * @param string $data['params']       Custom parameters for media type webhook.
238	 * @param string $sid                  User session ID.
239	 *
240	 * @return bool|array
241	 */
242	public function testMediaType(array $data, $sid) {
243		return $this->request([
244			'request' => 'alert.send',
245			'sid' => $sid,
246			'data' => $data
247		]);
248	}
249
250	/**
251	 * Retrieve System information.
252	 *
253	 * @param $sid
254	 *
255	 * @return bool|array
256	 */
257	public function getStatus($sid) {
258		$response = $this->request([
259			'request' => 'status.get',
260			'type' => 'full',
261			'sid' => $sid
262		]);
263
264		if ($response === false) {
265			return false;
266		}
267
268		$api_input_rules = ['type' => API_OBJECT, 'fields' => [
269			'template stats' =>			['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'fields' => [
270				'count' =>					['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => '0:'.ZBX_MAX_INT32]
271			]],
272			'host stats' =>				['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'fields' => [
273				'attributes' =>				['type' => API_OBJECT, 'flags' => API_REQUIRED, 'fields' => [
274					'proxyid' =>				['type' => API_ID, 'flags' => API_REQUIRED],
275					'status' =>					['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [HOST_STATUS_MONITORED, HOST_STATUS_NOT_MONITORED])]
276				]],
277				'count' =>					['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => '0:'.ZBX_MAX_INT32]
278			]],
279			'item stats' =>				['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'fields' => [
280				'attributes' =>				['type' => API_OBJECT, 'flags' => API_REQUIRED, 'fields' => [
281					'proxyid' =>				['type' => API_ID, 'flags' => API_REQUIRED],
282					'status' =>					['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [ITEM_STATUS_ACTIVE, ITEM_STATUS_DISABLED])],
283					'state' =>					['type' => API_INT32, 'in' => implode(',', [ITEM_STATE_NORMAL, ITEM_STATE_NOTSUPPORTED])]
284				]],
285				'count' =>					['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => '0:'.ZBX_MAX_INT32]
286			]],
287			'trigger stats' =>			['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'fields' => [
288				'attributes' =>				['type' => API_OBJECT, 'flags' => API_REQUIRED, 'fields' => [
289					'status' =>					['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [TRIGGER_STATUS_ENABLED, TRIGGER_STATUS_DISABLED])],
290					'value' =>					['type' => API_INT32, 'in' => implode(',', [TRIGGER_VALUE_FALSE, TRIGGER_VALUE_TRUE])]
291				]],
292				'count' =>					['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => '0:'.ZBX_MAX_INT32]
293			]],
294			'user stats' =>				['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'fields' => [
295				'attributes' =>				['type' => API_OBJECT, 'flags' => API_REQUIRED, 'fields' => [
296					'status' =>					['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [ZBX_SESSION_ACTIVE, ZBX_SESSION_PASSIVE])]
297				]],
298				'count' =>					['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => '0:'.ZBX_MAX_INT32]
299			]],
300			// only for super-admins 'required performance' is available
301			'required performance' =>	['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY, 'fields' => [
302				'attributes' =>				['type' => API_OBJECT, 'flags' => API_REQUIRED, 'fields' => [
303					'proxyid' =>				['type' => API_ID, 'flags' => API_REQUIRED]
304				]],
305				'count' =>					['type' => API_STRING_UTF8, 'flags' => API_REQUIRED]	// API_FLOAT 0-n
306			]]
307		]];
308
309		if (!CApiInputValidator::validate($api_input_rules, $response, '/', $this->error)) {
310			return false;
311		}
312
313		return $response;
314	}
315
316	/**
317	 * Returns true if the Zabbix server is running and false otherwise.
318	 *
319	 * @param $sid
320	 *
321	 * @return bool
322	 */
323	public function isRunning($sid) {
324		$response = $this->request([
325			'request' => 'status.get',
326			'type' => 'ping',
327			'sid' => $sid
328		]);
329
330		if ($response === false) {
331			return false;
332		}
333
334		$api_input_rules = ['type' => API_OBJECT, 'fields' => []];
335		return CApiInputValidator::validate($api_input_rules, $response, '/', $this->error);
336	}
337
338	/**
339	 * Evaluate trigger expressions.
340	 *
341	 * @param array  $data
342	 * @param string $sid
343	 *
344	 * @return bool|array
345	 */
346	public function expressionsEvaluate(array $data, string $sid) {
347		$response = $this->request([
348			'request' => 'expressions.evaluate',
349			'sid' => $sid,
350			'data' => $data
351		]);
352
353		if ($response === false) {
354			return false;
355		}
356
357		$api_input_rules = ['type' => API_OBJECTS, 'fields' => [
358			'expression' =>	['type' => API_STRING_UTF8, 'flags' => API_REQUIRED],
359			'value' =>		['type' => API_INT32, 'in' => '0,1'],
360			'error' =>		['type' => API_STRING_UTF8]
361		]];
362
363		if (!CApiInputValidator::validate($api_input_rules, $response, '/', $this->error)) {
364			return false;
365		}
366
367		return $response;
368	}
369
370	/**
371	 * Returns the error message.
372	 *
373	 * @return string
374	 */
375	public function getError() {
376		return $this->error;
377	}
378
379	/**
380	 * Returns the total result count.
381	 *
382	 * @return int|null
383	 */
384	public function getTotalCount() {
385		return $this->total;
386	}
387
388	/**
389	 * Returns debug section from server response.
390	 *
391	 * @return array
392	 */
393	public function getDebug() {
394		return $this->debug;
395	}
396
397	/**
398	 * Executes a given JSON request and returns the result. Returns false if an error has occurred.
399	 *
400	 * @param array $params
401	 *
402	 * @return mixed    the output of the script if it has been executed successfully or false otherwise
403	 */
404	protected function request(array $params) {
405		// Reset object state.
406		$this->error = null;
407		$this->total = null;
408		$this->debug = [];
409
410		// Connect to the server.
411		if (!$this->connect()) {
412			return false;
413		}
414
415		// Set timeout.
416		stream_set_timeout($this->socket, $this->timeout);
417
418		// Send the command.
419		$json = json_encode($params);
420		if (fwrite($this->socket, ZBX_TCP_HEADER.pack('V', strlen($json))."\x00\x00\x00\x00".$json) === false) {
421			$this->error = _s('Cannot send command, check connection with Zabbix server "%1$s".', $this->host);
422			return false;
423		}
424
425		$expect = self::ZBX_TCP_EXPECT_HEADER;
426		$response = '';
427		$response_len = 0;
428		$expected_len = null;
429		$now = time();
430
431		while (true) {
432			if ((time() - $now) >= $this->timeout) {
433				$this->error = _s(
434					'Connection timeout of %1$s seconds exceeded when connecting to Zabbix server "%2$s".',
435					$this->timeout, $this->host
436				);
437				return false;
438			}
439
440			if (!feof($this->socket) && ($buffer = fread($this->socket, self::READ_BYTES_LIMIT)) !== false) {
441				$response_len += strlen($buffer);
442				$response .= $buffer;
443
444				if ($expect == self::ZBX_TCP_EXPECT_HEADER) {
445					if (strncmp($response, ZBX_TCP_HEADER, min($response_len, ZBX_TCP_HEADER_LEN)) != 0) {
446						$this->error = _s('Incorrect response received from Zabbix server "%1$s".', $this->host);
447						return false;
448					}
449
450					if ($response_len < ZBX_TCP_HEADER_LEN) {
451						continue;
452					}
453
454					$expect = self::ZBX_TCP_EXPECT_DATA;
455				}
456
457				if ($response_len < ZBX_TCP_HEADER_LEN + ZBX_TCP_DATALEN_LEN) {
458					continue;
459				}
460
461				if ($expected_len === null) {
462					$expected_len = unpack('Vlen', substr($response, ZBX_TCP_HEADER_LEN, 4))['len'];
463					$expected_len += ZBX_TCP_HEADER_LEN + ZBX_TCP_DATALEN_LEN;
464
465					if ($this->totalBytesLimit != 0 && $expected_len >= $this->totalBytesLimit) {
466						$this->error = _s(
467							'Size of the response received from Zabbix server "%1$s" exceeds the allowed size of %2$s bytes. This value can be increased in the ZBX_SOCKET_BYTES_LIMIT constant in include/defines.inc.php.',
468							$this->host, $this->totalBytesLimit
469						);
470						return false;
471					}
472				}
473
474				if ($response_len >= $expected_len) {
475					break;
476				}
477			}
478			else {
479				$this->error =
480					_s('Cannot read the response, check connection with the Zabbix server "%1$s".', $this->host);
481				return false;
482			}
483		}
484
485		fclose($this->socket);
486
487		if ($expected_len > $response_len || $response_len > $expected_len) {
488			$this->error = _s('Incorrect response received from Zabbix server "%1$s".', $this->host);
489			return false;
490		}
491
492		$response = json_decode(substr($response, ZBX_TCP_HEADER_LEN + ZBX_TCP_DATALEN_LEN), true);
493
494		if (!$response || !$this->normalizeResponse($response)) {
495			$this->error = _s('Incorrect response received from Zabbix server "%1$s".', $this->host);
496
497			return false;
498		}
499
500		if (array_key_exists('debug', $response)) {
501			$this->debug = $response['debug'];
502		}
503
504		// Request executed successfully.
505		if ($response['response'] == self::RESPONSE_SUCCESS) {
506			// saves total count
507			$this->total = array_key_exists('total', $response) ? $response['total'] : null;
508
509			return array_key_exists('data', $response) ? $response['data'] : true;
510		}
511
512		// An error on the server side occurred.
513		$this->error = $response['info'];
514
515		return false;
516	}
517
518	/**
519	 * Opens a socket to the Zabbix server. Returns the socket resource if the connection has been established or
520	 * false otherwise.
521	 *
522	 * @return bool|resource
523	 */
524	protected function connect() {
525		if (!$this->socket) {
526			if (!$this->host || !$this->port) {
527				return false;
528			}
529
530			if (!$socket = @fsockopen($this->host, $this->port, $errorCode, $errorMsg, ZBX_CONNECT_TIMEOUT)) {
531				switch ($errorMsg) {
532					case 'Connection refused':
533						$dErrorMsg = _s("Connection to Zabbix server \"%1\$s\" refused. Possible reasons:\n1. Incorrect server IP/DNS in the \"zabbix.conf.php\";\n2. Security environment (for example, SELinux) is blocking the connection;\n3. Zabbix server daemon not running;\n4. Firewall is blocking TCP connection.\n", $this->host);
534						break;
535
536					case 'No route to host':
537						$dErrorMsg = _s("Zabbix server \"%1\$s\" can not be reached. Possible reasons:\n1. Incorrect server IP/DNS in the \"zabbix.conf.php\";\n2. Incorrect network configuration.\n", $this->host);
538						break;
539
540					case 'Connection timed out':
541						$dErrorMsg = _s("Connection to Zabbix server \"%1\$s\" timed out. Possible reasons:\n1. Incorrect server IP/DNS in the \"zabbix.conf.php\";\n2. Firewall is blocking TCP connection.\n", $this->host);
542						break;
543
544					default:
545						$dErrorMsg = _s("Connection to Zabbix server \"%1\$s\" failed. Possible reasons:\n1. Incorrect server IP/DNS in the \"zabbix.conf.php\";\n2. Incorrect DNS server configuration.\n", $this->host);
546				}
547
548				$this->error = $dErrorMsg.$errorMsg;
549			}
550
551			$this->socket = $socket;
552		}
553
554		return $this->socket;
555	}
556
557	/**
558	 * Returns true if the response received from the Zabbix server is valid.
559	 *
560	 * @param array $response
561	 *
562	 * @return bool
563	 */
564	protected function normalizeResponse(array &$response) {
565		return (array_key_exists('response', $response) && ($response['response'] == self::RESPONSE_SUCCESS
566				|| $response['response'] == self::RESPONSE_FAILED && array_key_exists('info', $response))
567		);
568	}
569}
570