diff --git a/CHANGES.rst b/CHANGES.rst index 0477c20..d825ef3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,7 @@ Changes 1.0.3 (unreleased) ------------------ -- Nothing changed yet. +- Added fdscan() method to ClamdUnixSocket for scanning file descriptors 1.0.2 (2014-08-21) diff --git a/src/clamd/__init__.py b/src/clamd/__init__.py index 92ff640..0e18304 100644 --- a/src/clamd/__init__.py +++ b/src/clamd/__init__.py @@ -16,6 +16,11 @@ import contextlib import re import base64 +# _multiprocessing.sendfd() only exists in python2 +if sys.version_info[0] < 3: + import _multiprocessing +else: + import array scan_response = re.compile(r"^(?P.*): ((?P.+) )?(?P(FOUND|OK|ERROR))$") EICAR = base64.b64decode( @@ -264,7 +269,7 @@ def _close_socket(self): def _parse_response(self, msg): """ - parses responses for SCAN, CONTSCAN, MULTISCAN and STREAM commands. + parses responses for SCAN, CONTSCAN, MULTISCAN, FILDES and STREAM commands. """ try: return scan_response.match(msg).group("path", "virus", "status") @@ -287,6 +292,25 @@ class initialisation self.unix_socket = path self.timeout = timeout + # only works for python >= 3.3 + def _send_fd_via_socket_sendmsg(self, fd): + """ + internal use only + """ + return self.clamd_socket.sendmsg([b'\0'], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array("i", [fd]))]) + + # _multiprocessing.sendfd() only exists in python2 + def _send_fd_via_mp_sendfd(self, fd): + """ + internal use only + """ + return _multiprocessing.sendfd(self.clamd_socket.fileno(), fd) + + # make _send_fd refer to one of the two _send_fd_* methods above. Either + # _send_fd_via_socket_sendmsg or _send_fd_via_mp_sendfd, depending upon the version of python + """internal use only""" + _send_fd = _send_fd_via_socket_sendmsg if sys.version_info[0] >= 3 else _send_fd_via_mp_sendfd + def _init_socket(self): """ internal use only @@ -313,3 +337,19 @@ def _error_message(self, exception): path=self.unix_socket, msg=exception.args[1] ) + + def fdscan(self, path, fd): + """ + Scan a file referenced by a file descriptor. + path (string) : path of file to use for result dictionary (otherwise unused) + fd (int) : file descriptor number (fileno) of file to scan + """ + try: + self._init_socket() + cmd = 'nFILDES\n'.encode('utf-8') + self.clamd_socket.send(cmd) + self._send_fd(fd) + _, reason, status = self._parse_response(self._recv_response_multiline()) + return {path: (status, reason)} + finally: + self._close_socket() diff --git a/src/tests/test_api.py b/src/tests/test_api.py index 42423b6..2437cac 100644 --- a/src/tests/test_api.py +++ b/src/tests/test_api.py @@ -8,6 +8,12 @@ import shutil import os import stat +import sys +try: + import _multiprocessing + have_multiprocessing_sendfd = hasattr(_multiprocessing, 'sendfd') and callable(_multiprocessing.sendfd) +except ImportError: + have_multiprocessing_sendfd = False import pytest @@ -77,6 +83,24 @@ def test_instream(self): def test_insteam_success(self): assert self.cd.instream(BytesIO(b"foo")) == {'stream': ('OK', None)} + @pytest.mark.skipif(sys.version_info[0] < 3 and not have_multiprocessing_sendfd, + reason="For Python 2.x, _multiprocessing.sendfd() is required for this test") + def test_fdscan(self): + with tempfile.NamedTemporaryFile('wb', prefix="python-clamd") as f: + f.write(clamd.EICAR) + f.flush() + expected = {f.name: ('FOUND', 'Eicar-Test-Signature')} + assert self.cd.fdscan(f.name, f.fileno()) == expected + + @pytest.mark.skipif(sys.version_info[0] < 3 and not have_multiprocessing_sendfd, + reason="For Python 2.x, _multiprocessing.sendfd() is required for this test") + def test_fdscan_success(self): + with tempfile.NamedTemporaryFile('wb', prefix="python-clamd") as f: + f.write(b"foo") + f.flush() + expected = {f.name: ('OK', None)} + assert self.cd.fdscan(f.name, f.fileno()) == expected + class TestUnixSocketTimeout(TestUnixSocket): kwargs = {"timeout": 20}