From 2bc6c7b99008a1c90c4457c909447bbfb1289df7 Mon Sep 17 00:00:00 2001 From: mutantsan Date: Mon, 6 Dec 2021 18:48:46 +0200 Subject: [PATCH] add tcp/ip socket support --- README.md | 16 +++++++ ckanext/clamav/adapters.py | 26 +++++++++++ ckanext/clamav/config.py | 15 +++++++ ckanext/clamav/utils.py | 90 +++++++++++++++++++++++++++----------- setup.py | 5 ++- 5 files changed, 124 insertions(+), 28 deletions(-) create mode 100644 ckanext/clamav/adapters.py create mode 100644 ckanext/clamav/config.py diff --git a/README.md b/README.md index f8306e9..61f4629 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,22 @@ To install ckanext-clamav: # (optional, default: True). ckanext.clamav.upload_unscanned = False + # ClamAV connection mechanism. There are two options: `tcp` or `unix`. + # If `tcp` selected, you must provide host:port (check next options). + # ( optional, default: unix) + ckanext.clamav.socket_type = unix + + # TCP/IP hostname + ckanext.clamav.tcp.host = your.hostname.address + + # TCP/IP port + ckanext.clamav.tcp.port = 3310 + + # ClamAV connection timeout. Either `tcp` or `unix` + # By default, there is no timeout. + # ( optional, default: 60) + ckanext.clamav.timeout = 120 + ## Developer installation diff --git a/ckanext/clamav/adapters.py b/ckanext/clamav/adapters.py new file mode 100644 index 0000000..7dbfd15 --- /dev/null +++ b/ckanext/clamav/adapters.py @@ -0,0 +1,26 @@ +import socket +import sys + +from clamd import ClamdNetworkSocket, ConnectionError + + +class CustomClamdNetworkSocket(ClamdNetworkSocket): + """Patches the default ClamdNetworkSocket adapter + with changed _init_socket method. The default implementation doesn't + respect timeout properly. + + Args: + ClamdNetworkSocket (ClamdNetworkSocket): original clamd network adapter + """ + def _init_socket(self): + """ + internal use only + """ + try: + self.clamd_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.clamd_socket.settimeout(self.timeout) + self.clamd_socket.connect((self.host, self.port)) + + except (socket.error, socket.timeout): + e = sys.exc_info()[1] + raise ConnectionError(self._error_message(e)) \ No newline at end of file diff --git a/ckanext/clamav/config.py b/ckanext/clamav/config.py new file mode 100644 index 0000000..15b14b5 --- /dev/null +++ b/ckanext/clamav/config.py @@ -0,0 +1,15 @@ +CLAMAV_STATUS_FOUND: str = "FOUND" +CLAMAV_STATUS_ERR_FILELIMIT: str = "ERR_FILELIMIT" +CLAMAV_STATUS_ERR_DISABLED: str = "ERR_DISABLED" +CLAMAV_CONF_SOCKET_PATH: str = "ckanext.clamav.socket_path" +CLAMAV_CONF_SOCKET_PATH_DF: str = "/var/run/clamav/clamd.ctl" +CLAMAV_CONF_UPLOAD_UNSCANNED: str = "ckanext.clamav.upload_unscanned" +CLAMAV_CONF_UPLOAD_UNSCANNED_DF: bool = True +CLAMAV_SOCK_UNIX: str = "unix" +CLAMAV_SOCK_TCP: str = "tcp" +CLAMAV_CONF_SOCKET_TYPE: str = "ckanext.clamav.socket_type" +CLAMAV_CONF_SOCKET_TYPE_DF: str = CLAMAV_SOCK_UNIX +CLAMAV_CONF_SOCK_TCP_HOST: str = "ckanext.clamav.tcp.host" +CLAMAV_CONF_SOCK_TCP_PORT: str = "ckanext.clamav.tcp.port" +CLAMAV_CONF_CONN_TIMEOUT: str = "ckanext.clamav.timeout" +CLAMAV_CONF_CONN_TIMEOUT_DF: int = 60 \ No newline at end of file diff --git a/ckanext/clamav/utils.py b/ckanext/clamav/utils.py index 8602e18..e59d8d3 100644 --- a/ckanext/clamav/utils.py +++ b/ckanext/clamav/utils.py @@ -1,10 +1,10 @@ from __future__ import annotations import logging -from typing import Optional, Any +from typing import Optional, Any, Union -import clamd from clamd import ( ClamdUnixSocket, + ClamdNetworkSocket, ConnectionError as ClamConnectionError, BufferTooLongError, ) @@ -12,17 +12,13 @@ import ckan.logic as logic import ckan.plugins.toolkit as tk +from ckan.exceptions import CkanConfigurationException +import ckanext.clamav.config as c +from ckanext.clamav.adapters import CustomClamdNetworkSocket -log = logging.getLogger(__name__) -CLAMAV_STATUS_FOUND: str = "FOUND" -CLAMAV_STATUS_ERR_FILELIMIT: str = "ERR_FILELIMIT" -CLAMAV_STATUS_ERR_DISABLED: str = "ERR_DISABLED" -CLAMAV_CONF_SOCKET_PATH: str = "ckanext.clamav.socket_path" -CLAMAV_CONF_SOCKET_PATH_DF: str = "/var/run/clamav/clamd.ctl" -CLAMAV_CONF_UPLOAD_UNSCANNED: str = "ckanext.clamav.upload_unscanned" -CLAMAV_CONF_UPLOAD_UNSCANNED_DF: bool = True +log = logging.getLogger(__name__) def scan_file_for_viruses(data_dict: dict[str, Any]): @@ -37,7 +33,7 @@ def scan_file_for_viruses(data_dict: dict[str, Any]): upload form """ upload_unscanned: bool = tk.asbool(tk.config.get( - CLAMAV_CONF_UPLOAD_UNSCANNED, CLAMAV_CONF_UPLOAD_UNSCANNED_DF + c.CLAMAV_CONF_UPLOAD_UNSCANNED, c.CLAMAV_CONF_UPLOAD_UNSCANNED_DF )) file: FileStorage = data_dict["upload"] @@ -45,21 +41,23 @@ def scan_file_for_viruses(data_dict: dict[str, Any]): signature: Optional[str] status, signature = _scan_filestream(file) - if status == CLAMAV_STATUS_ERR_DISABLED: - log.info("Unable to connect to clamav. Can't scan the file") + if status == c.CLAMAV_STATUS_ERR_DISABLED: + log.info("Clamd: unable to connect to clamav. Can't scan the file") if upload_unscanned: - log.info(_get_unscanned_file_message(file, data_dict['package_id'])) + log.info(_get_unscanned_file_message( + file, data_dict['package_id'])) else: raise logic.ValidationError({"Virus checker": [ "The clamav is disabled. Can't uploade the file. Contact administrator" ]}) - elif status in (CLAMAV_STATUS_ERR_FILELIMIT,): + elif status in (c.CLAMAV_STATUS_ERR_FILELIMIT,): log.warning(signature) if upload_unscanned: - log.info(_get_unscanned_file_message(file, data_dict['package_id'])) + log.info(_get_unscanned_file_message( + file, data_dict['package_id'])) else: raise logic.ValidationError({"Virus checker": [signature]}) - elif status == CLAMAV_STATUS_FOUND: + elif status == c.CLAMAV_STATUS_FOUND: error_msg: str = ( "malware has been found. " f"Filename: {file.filename}, signature: {signature}." @@ -79,30 +77,70 @@ def _scan_filestream(file: FileStorage) -> tuple[str, Optional[str]]: if status is returned error code, then instead of the signature there will be an error message. """ - socket_path: str = tk.config.get( - CLAMAV_CONF_SOCKET_PATH, CLAMAV_CONF_SOCKET_PATH_DF - ) - cd: ClamdUnixSocket = clamd.ClamdUnixSocket(socket_path) + + cd: Union[ClamdUnixSocket, ClamdNetworkSocket] = _get_conn() try: - scan_result: dict[str, tuple[str, Optional[str]]] = cd.instream(file.stream) + scan_result: dict[str, tuple[str, Optional[str]] + ] = cd.instream(file.stream) except BufferTooLongError: error_msg: str = ( "the uploaded file exceeds the filesize limit " "The file will not be scanned" ) log.error(error_msg) - return (CLAMAV_STATUS_ERR_FILELIMIT, error_msg) + return (c.CLAMAV_STATUS_ERR_FILELIMIT, error_msg) except ClamConnectionError: error_msg: str = "clamav is not accessible, check its status." log.critical(error_msg) - return (CLAMAV_STATUS_ERR_DISABLED, error_msg) + return (c.CLAMAV_STATUS_ERR_DISABLED, error_msg) return scan_result["stream"] -def _get_unscanned_file_message(file: FileStorage, pkg_id: str) -> str: +def _get_conn() -> Union[ClamdUnixSocket, CustomClamdNetworkSocket]: + """ + Simply connects to the ClamAV via TCP/IP or Unix socket and returns + the connection object + + Returns: + Union[ClamdUnixSocket, CustomClamdNetworkSocket]: a connection to ClamAV + Support two type of connection mechanism - TCP/IP or Unix socket + + Raises: + CkanConfigurationException: if the TCP/IP connection mechanism has been choosen, + the host:port must be provided, otherwise, raises an exception + + CkanConfigurationException: raises an exception, if the unsupported connection + mechanism has been choosen + """ + socket_type: str = tk.config.get( + c.CLAMAV_CONF_SOCKET_TYPE, c.CLAMAV_CONF_SOCKET_TYPE_DF + ) + conn_timeout: int = ftk.asint(tk.config.get( + c.CLAMAV_CONF_CONN_TIMEOUT, c.CLAMAV_CONF_CONN_TIMEOUT_DF + )) + + if socket_type not in (c.CLAMAV_SOCK_UNIX, c.CLAMAV_SOCK_TCP): + raise CkanConfigurationException("Clamd: unsupported connection type") + + if socket_type == c.CLAMAV_SOCK_UNIX: + socket_path: str = tk.config.get( + c.CLAMAV_CONF_SOCKET_PATH, c.CLAMAV_CONF_SOCKET_PATH_DF + ) + return ClamdUnixSocket(socket_path, conn_timeout) + + tcp_host: str = tk.config.get(c.CLAMAV_CONF_SOCK_TCP_HOST) + tcp_port: str = tk.asint(tk.config.get(c.CLAMAV_CONF_SOCK_TCP_PORT)) + + if not all((tcp_port, tcp_host)): + raise CkanConfigurationException("Clamd: please, provide TCP/IP host:port for ClamAV") + + return CustomClamdNetworkSocket(tcp_host, tcp_port, conn_timeout) + + +def _get_unscanned_file_message(file: FileStorage, pkg_id: str) -> str: return ( "The unscanned file will be uploaded because unscanned fileupload is enabled. " f"Filename: {file.filename}, package_id: {pkg_id}, name: {file.filename or None}" - ) \ No newline at end of file + ) diff --git a/setup.py b/setup.py index baa0f5c..fc47ade 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # http://packaging.python.org/en/latest/tutorial.html#version - version='0.0.1', + version='1.0.2', description='''Check upload resources for viruses''', long_description=long_description, @@ -56,13 +56,14 @@ # simple. Or you can use find_packages(). packages=find_packages(exclude=['contrib', 'docs', 'tests*']), namespace_packages=['ckanext'], - + python_requires=">=3.7", install_requires=[ # CKAN extensions should not list dependencies here, but in a separate # ``requirements.txt`` file. # # http://docs.ckan.org/en/latest/extensions/best-practices.html # add-third-party-libraries-to-requirements-txt + "clamd", ], # If there are data files included in your packages that need to be