#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2019, Dennis Kalbhen # Copyright (c) 2019, Carsten Rosenberg # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ### # # olefy is a little helper socket to use oletools with rspamd. (https://rspamd.com) # Please find updates and issues here: https://github.com/HeinleinSupport/olefy # ### from subprocess import Popen, PIPE import sys import os import logging import asyncio import time import magic # merge variables from /etc/olefy.conf and the defaults olefy_listen_addr = os.getenv('OLEFY_BINDADDRESS', '127.0.0.1') olefy_listen_port = int(os.getenv('OLEFY_BINDPORT', '10050')) olefy_tmp_dir = os.getenv('OLEFY_TMPDIR', '/tmp') olefy_python_path = os.getenv('OLEFY_PYTHON_PATH', '/usr/bin/python3') olefy_olevba_path = os.getenv('OLEFY_OLEVBA_PATH', '/usr/local/bin/olevba3') # 10:DEBUG, 20:INFO, 30:WARNING, 40:ERROR, 50:CRITICAL olefy_loglvl = int(os.getenv('OLEFY_LOGLVL', 20)) olefy_min_length = int(os.getenv('OLEFY_MINLENGTH', 500)) olefy_del_tmp = int(os.getenv('OLEFY_DEL_TMP', 1)) # internal used variables request_time = '0000000000.000000' olefy_protocol = 'OLEFY' olefy_protocol_sep = '\\n\\n' olefy_headers = {} # init logging logger = logging.getLogger('olefy') logging.basicConfig(stream=sys.stdout, level=olefy_loglvl, format='olefy %(levelname)s %(funcName)s %(message)s') # log runtime variables logger.info('olefy listen address: {}'.format(olefy_listen_addr)) logger.info('olefy listen port: {}'.format(olefy_listen_port)) logger.info('olefy tmp dir: {}'.format(olefy_tmp_dir)) logger.info('olefy python path: {}'.format(olefy_python_path)) logger.info('olefy olvba path: {}'.format(olefy_olevba_path)) logger.info('olefy log level: {}'.format(olefy_loglvl)) logger.info('olefy min file length: {}'.format(olefy_min_length)) logger.info('olefy delete tmp file: {}'.format(olefy_del_tmp)) if not os.path.isfile(olefy_python_path): logger.critical('python path not found: {}'.format(olefy_python_path)) exit(1) if not os.path.isfile(olefy_olevba_path): logger.critical('olevba path not found: {}'.format(olefy_olevba_path)) exit(1) # olefy protocol function def protocol_split( olefy_line ): header_lines = olefy_line.split('\\n') for line in header_lines: if line == 'OLEFY/1.0': olefy_headers['olefy'] = line elif line != '': kv = line.split(': ') if kv[0] != '' and kv[1] != '': olefy_headers[kv[0]] = kv[1] logger.debug('olefy_headers: {}'.format(olefy_headers)) # calling oletools def oletools( stream, tmp_file_name, lid ): if olefy_min_length > stream.__len__(): logger.error('{} {} bytes (Not Scanning! File smaller than {!r})'.format(lid, stream.__len__(), olefy_min_length)) out = b'[ { "error": "File too small" } ]' else: tmp_file = open(tmp_file_name, 'wb') tmp_file.write(stream) tmp_file.close() file_magic = magic.Magic(mime=True, uncompress=True) file_mime = file_magic.from_file(tmp_file_name) logger.info('{} {} (libmagic output)'.format(lid, file_mime)) # do the olefy cmd_tmp = Popen([olefy_python_path, olefy_olevba_path, '-a', '-j', tmp_file_name], stdout=PIPE, stderr=PIPE) out, err = cmd_tmp.communicate() if out.__len__() < 10: logger.error('{} olevba returned <10 chars - rc: {!r}, response: {!r}'.format(lid,cmd_tmp.returncode, out.decode('ascii'))) out = b'[ { "error": "Unhandled oletools response" } ]' if err.__len__() > 10: logger.error('{} olevba stderr >10 chars - rc: {!r}, response: {!r}'.format(lid, cmd_tmp.returncode, err.decode('ascii'))) out = b'[ { "error": "Unhandled oletools error" } ]' if cmd_tmp.returncode != 0: logger.error('{} olevba exited with code {!r}; err: {!r}'.format(lid, cmd_tmp.returncode, err.decode('ascii'))) if olefy_del_tmp == 1: logger.debug('{} {} deleting tmp file'.format(lid, tmp_file_name)) os.remove(tmp_file_name) logger.debug('{} response: {}'.format(lid, out.decode())) return out # Asyncio data handling, default AIO-Functions class AIO(asyncio.Protocol): def __init__(self): self.extra = bytearray() def connection_made(self, transport): global request_time peer = transport.get_extra_info('peername') logger.debug('{} new connection was made'.format(peer)) self.transport = transport request_time = str(time.time()) def data_received(self, request, msgid=1): peer = self.transport.get_extra_info('peername') logger.debug('{} data received from new connection'.format(peer)) self.extra.extend(request) def eof_received(self): peer = self.transport.get_extra_info('peername') olefy_protocol_err = False proto_ck = str(self.extra[0:2000]) if olefy_protocol in proto_ck: olefy_line = proto_ck[12:proto_ck.find(olefy_protocol_sep)] self.extra = bytearray(self.extra[59:len(self.extra)]) protocol_split(olefy_line) else: olefy_protocol_err = True lid = 'Rspamd-ID' in olefy_headers and '<'+olefy_headers['Rspamd-ID'][:6]+'>' or '<>' tmp_file_name = olefy_tmp_dir+'/'+request_time+'.'+str(peer[1]) logger.debug('{} {} choosen as tmp filename'.format(lid, tmp_file_name)) logger.info('{} {} bytes (stream size)'.format(lid, self.extra.__len__())) if olefy_protocol_err == True or olefy_headers['olefy'] != 'OLEFY/1.0': logger.error('Protocol ERROR: no OLEFY/1.0 found)') out = b'[ { "error": "Protocol error" } ]' elif 'Method' in olefy_headers: if olefy_headers['Method'] == 'oletools': out = oletools(self.extra, tmp_file_name, lid) else: logger.error('Protocol ERROR: Method header not found') out = b'[ { "error": "Protocol error: Method header not found" } ]' self.transport.write(out) logger.info('{} response send: {!r}'.format(peer, out)) self.transport.close() # start the listeners loop = asyncio.get_event_loop() # each client connection will create a new protocol instance coro = loop.create_server(AIO, olefy_listen_addr, olefy_listen_port) server = loop.run_until_complete(coro) logger.info('serving on {}'.format(server.sockets[0].getsockname())) # XXX serve requests until KeyboardInterrupt, not needed for production try: loop.run_forever() except KeyboardInterrupt: pass # graceful shutdown/reload server.close() loop.run_until_complete(server.wait_closed()) loop.close() logger.info('stopped serving')