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