332 lines
12 KiB
Python
332 lines
12 KiB
Python
# zabbix_utils
|
|
#
|
|
# Copyright (C) 2001-2023 Zabbix SIA
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person
|
|
# obtaining a copy of this software and associated documentation
|
|
# files (the "Software"), to deal in the Software without restriction,
|
|
# including without limitation the rights to use, copy, modify,
|
|
# merge, publish, distribute, sublicense, and/or sell copies
|
|
# of the Software, and to permit persons to whom the Software
|
|
# is furnished to do so, subject to the following conditions:
|
|
|
|
# The above copyright notice and this permission notice shall be
|
|
# included in all copies or substantial portions of the Software.
|
|
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
# OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
import re
|
|
import json
|
|
import zlib
|
|
import struct
|
|
import asyncio
|
|
|
|
from textwrap import shorten
|
|
from logging import Logger
|
|
from socket import socket
|
|
|
|
from typing import Match, Union
|
|
|
|
|
|
class ModuleUtils():
|
|
|
|
# Hidding mask for sensitive data
|
|
HIDING_MASK = "*" * 8
|
|
|
|
# The main php-file of Zabbix API
|
|
JSONRPC_FILE = 'api_jsonrpc.php'
|
|
|
|
# Methods working without auth token
|
|
UNAUTH_METHODS = ('apiinfo.version', 'user.login', 'user.checkAuthentication')
|
|
|
|
# Methods returning files contents
|
|
FILES_METHODS = ('configuration.export',)
|
|
|
|
# List of private fields and regular expressions to hide them
|
|
PRIVATE_FIELDS = {
|
|
"token": r"^.+$",
|
|
"auth": r"^.+$",
|
|
"passwd": r"^.+$",
|
|
"sessionid": r"^.+$",
|
|
"password": r"^.+$",
|
|
"current_passwd": r"^.+$",
|
|
"result": r"^[A-Za-z0-9]{32}$", # To hide only token or sessionid in result
|
|
}
|
|
|
|
@classmethod
|
|
def check_url(cls, url: str) -> str:
|
|
"""Check url completeness
|
|
|
|
Args:
|
|
url (str): Unchecked URL of Zabbix API
|
|
|
|
Returns:
|
|
str: Checked URL of Zabbix API
|
|
"""
|
|
|
|
if not url.endswith(cls.JSONRPC_FILE):
|
|
url += cls.JSONRPC_FILE if url[-1] == '/' else '/' + cls.JSONRPC_FILE
|
|
if not url.startswith('http'):
|
|
url = 'http://' + url
|
|
|
|
return url
|
|
|
|
@classmethod
|
|
def mask_secret(cls, string: str, show_len: int = 4) -> str:
|
|
"""Replace the most part of string to hiding mask.
|
|
|
|
Args:
|
|
string (str): Raw string with without hiding.
|
|
show_len (int, optional): Number of signs shown on each side of the string. \
|
|
Defaults to 4.
|
|
|
|
Returns:
|
|
str: String with hiding part.
|
|
"""
|
|
|
|
# If show_len is 0 or the length of the string is smaller than the hiding mask length
|
|
# and show_len from both sides of the string, return only hiding mask.
|
|
if show_len == 0 or len(string) <= (len(cls.HIDING_MASK) + show_len*2):
|
|
return cls.HIDING_MASK
|
|
|
|
# Return the string with the hiding mask, surrounded by the specified number of characters
|
|
# to display on each side of the string.
|
|
return f"{string[:show_len]}{cls.HIDING_MASK}{string[-show_len:]}"
|
|
|
|
@classmethod
|
|
def hide_private(cls, input_data: dict, fields: dict = None) -> dict:
|
|
"""Hide private data Zabbix info (e.g. token, password)
|
|
|
|
Args:
|
|
input_data (dict): Input dictionary with private fields.
|
|
fields (dict): Dictionary of private fields and their filtering regexps.
|
|
|
|
Returns:
|
|
dict: Result dictionary without private data.
|
|
"""
|
|
|
|
private_fields = fields if fields is not None else cls.PRIVATE_FIELDS
|
|
|
|
if not isinstance(input_data, dict):
|
|
raise TypeError(f"Unsupported data type '{type(input_data).__name__}', \
|
|
only 'dict' is expected")
|
|
|
|
def gen_repl(match: Match):
|
|
return cls.mask_secret(match.group(0))
|
|
|
|
def hide_str(k, v):
|
|
return re.sub(private_fields[k], gen_repl, v)
|
|
|
|
def hide_dict(v):
|
|
return cls.hide_private(v, private_fields)
|
|
|
|
def hide_list(k, v):
|
|
result = []
|
|
for item in v:
|
|
if isinstance(item, dict):
|
|
result.append(hide_dict(item))
|
|
continue
|
|
if isinstance(item, list):
|
|
result.append(hide_list(k, item))
|
|
continue
|
|
if isinstance(item, str):
|
|
if k.rstrip('s') in private_fields:
|
|
result.append(hide_str(k.rstrip('s'), item))
|
|
continue
|
|
# The 'result' regex is used to hide only token or
|
|
# sessionid format for unknown values
|
|
if 'result' in private_fields:
|
|
result.append(hide_str('result', item))
|
|
continue
|
|
result.append(item)
|
|
return result
|
|
|
|
result_data = input_data.copy()
|
|
|
|
for key, value in result_data.items():
|
|
if isinstance(value, str):
|
|
if key in private_fields:
|
|
result_data[key] = hide_str(key, value)
|
|
if isinstance(value, dict):
|
|
result_data[key] = hide_dict(value)
|
|
if isinstance(value, list):
|
|
result_data[key] = hide_list(key, value)
|
|
|
|
return result_data
|
|
|
|
|
|
class ZabbixProtocol():
|
|
|
|
ZABBIX_PROTOCOL = b'ZBXD'
|
|
|
|
HEADER_SIZE = 13
|
|
|
|
@classmethod
|
|
def __prepare_request(cls, data: Union[bytes, str, list, dict]) -> bytes:
|
|
if isinstance(data, bytes):
|
|
return data
|
|
if isinstance(data, str):
|
|
return data.encode("utf-8")
|
|
if isinstance(data, list) or isinstance(data, dict):
|
|
return json.dumps(data, ensure_ascii=False).encode("utf-8")
|
|
raise TypeError("Unsupported data type, only 'bytes', 'str', 'list' or 'dict' is expected")
|
|
|
|
@classmethod
|
|
def create_packet(cls, payload: Union[bytes, str, list, dict],
|
|
log: Logger, compression: bool = False) -> bytes:
|
|
"""Create a packet for sending via the Zabbix protocol.
|
|
|
|
Args:
|
|
payload (bytes|str|list|dict): Payload of the future packet
|
|
log (Logger): Logger object
|
|
compression (bool, optional): Compression use flag. Defaults to `False`.
|
|
|
|
Returns:
|
|
bytes: Generated Zabbix protocol packet
|
|
"""
|
|
|
|
request = cls.__prepare_request(payload)
|
|
|
|
log.debug('Request data: %s', shorten(request.decode("utf-8"), 200, placeholder='...'))
|
|
|
|
# 0x01 - Zabbix communications protocol
|
|
flags = 0x01
|
|
datalen = len(request)
|
|
reserved = 0
|
|
|
|
if compression:
|
|
# 0x02 - Using packet compression mode
|
|
flags |= 0x02
|
|
reserved = datalen
|
|
request = zlib.compress(request)
|
|
datalen = len(request)
|
|
|
|
header = struct.pack('<4sBII', cls.ZABBIX_PROTOCOL, flags, datalen, reserved)
|
|
packet = header + request
|
|
|
|
log.debug('Content of the packet: %s', shorten(str(packet), 200, placeholder='...'))
|
|
|
|
return packet
|
|
|
|
@classmethod
|
|
def receive_packet(cls, conn: socket, size: int, log: Logger) -> bytes:
|
|
"""Receive a Zabbix protocol packet.
|
|
|
|
Args:
|
|
conn (socket): Opened socket connection
|
|
size (int): Expected packet size
|
|
log (Logger): Logger object
|
|
|
|
Returns:
|
|
bytes: Received packet content
|
|
"""
|
|
buf = b''
|
|
|
|
while len(buf) < size:
|
|
chunk = conn.recv(size - len(buf))
|
|
if not chunk:
|
|
log.debug("Socket connection was closed before receiving expected amount of data.")
|
|
break
|
|
buf += chunk
|
|
|
|
return buf
|
|
|
|
@classmethod
|
|
def parse_sync_packet(cls, conn: socket, log: Logger, exception) -> str:
|
|
"""Parse a received synchronously Zabbix protocol packet.
|
|
|
|
Args:
|
|
conn (socket): Opened socket connection
|
|
log (Logger): Logger object
|
|
exception: Exception type
|
|
|
|
Raises:
|
|
exception: Depends on input exception type
|
|
|
|
Returns:
|
|
str: Body of the received packet
|
|
"""
|
|
|
|
response_header = cls.receive_packet(conn, cls.HEADER_SIZE, log)
|
|
log.debug('Zabbix response header: %s', response_header)
|
|
|
|
if (not response_header.startswith(cls.ZABBIX_PROTOCOL) or
|
|
len(response_header) != cls.HEADER_SIZE):
|
|
log.debug('Unexpected response was received from Zabbix.')
|
|
raise exception('Unexpected response was received from Zabbix.')
|
|
|
|
flags, datalen, reserved = struct.unpack('<BII', response_header[4:])
|
|
|
|
# 0x01 - Zabbix communications protocol
|
|
if not flags & 0x01:
|
|
raise exception(
|
|
'Unexcepted flags were received. '
|
|
'Check debug log for more information.'
|
|
)
|
|
# 0x04 - Using large packet mode
|
|
if flags & 0x04:
|
|
raise exception(
|
|
'A large packet flag was received. '
|
|
'Current module doesn\'t support large packets.'
|
|
)
|
|
# 0x02 - Using packet compression mode
|
|
if flags & 0x02:
|
|
response_body = zlib.decompress(cls.receive_packet(conn, datalen, log))
|
|
else:
|
|
response_body = cls.receive_packet(conn, datalen, log)
|
|
|
|
return response_body.decode("utf-8")
|
|
|
|
@classmethod
|
|
async def parse_async_packet(cls, reader: asyncio.StreamReader, log: Logger, exception) -> str:
|
|
"""Parse a received asynchronously Zabbix protocol packet.
|
|
|
|
Args:
|
|
reader (StreamReader): Created asyncio.StreamReader
|
|
log (Logger): Logger object
|
|
exception: Exception type
|
|
|
|
Raises:
|
|
exception: Depends on input exception type
|
|
|
|
Returns:
|
|
str: Body of the received packet
|
|
"""
|
|
|
|
response_header = await reader.readexactly(cls.HEADER_SIZE)
|
|
log.debug('Zabbix response header: %s', response_header)
|
|
|
|
if (not response_header.startswith(cls.ZABBIX_PROTOCOL) or
|
|
len(response_header) != cls.HEADER_SIZE):
|
|
log.debug('Unexpected response was received from Zabbix.')
|
|
raise exception('Unexpected response was received from Zabbix.')
|
|
|
|
flags, datalen, reserved = struct.unpack('<BII', response_header[4:])
|
|
|
|
# 0x01 - Zabbix communications protocol
|
|
if not flags & 0x01:
|
|
raise exception(
|
|
'Unexcepted flags were received. '
|
|
'Check debug log for more information.'
|
|
)
|
|
# 0x04 - Using large packet mode
|
|
if flags & 0x04:
|
|
raise exception(
|
|
'A large packet flag was received. '
|
|
'Current module doesn\'t support large packets.'
|
|
)
|
|
# 0x02 - Using packet compression mode
|
|
if flags & 0x02:
|
|
response_body = zlib.decompress(await reader.readexactly(datalen))
|
|
else:
|
|
response_body = await reader.readexactly(datalen)
|
|
|
|
return response_body.decode("utf-8")
|