olefy.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # Copyright (c) 2019, Dennis Kalbhen <d.kalbhen@heinlein-support.de>
  4. # Copyright (c) 2019, Carsten Rosenberg <c.rosenberg@heinlein-support.de>
  5. #
  6. # Licensed under the Apache License, Version 2.0 (the "License");
  7. # you may not use this file except in compliance with the License.
  8. # You may obtain a copy of the License at
  9. #
  10. # http://www.apache.org/licenses/LICENSE-2.0
  11. #
  12. # Unless required by applicable law or agreed to in writing, software
  13. # distributed under the License is distributed on an "AS IS" BASIS,
  14. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. # See the License for the specific language governing permissions and
  16. # limitations under the License.
  17. ###
  18. #
  19. # olefy is a little helper socket to use oletools with rspamd. (https://rspamd.com)
  20. # Please find updates and issues here: https://github.com/HeinleinSupport/olefy
  21. #
  22. ###
  23. from subprocess import Popen, PIPE
  24. import sys
  25. import os
  26. import logging
  27. import asyncio
  28. import time
  29. import magic
  30. # merge variables from /etc/olefy.conf and the defaults
  31. olefy_listen_addr = os.getenv('OLEFY_BINDADDRESS', '127.0.0.1')
  32. olefy_listen_port = int(os.getenv('OLEFY_BINDPORT', '10050'))
  33. olefy_tmp_dir = os.getenv('OLEFY_TMPDIR', '/tmp')
  34. olefy_python_path = os.getenv('OLEFY_PYTHON_PATH', '/usr/bin/python3')
  35. olefy_olevba_path = os.getenv('OLEFY_OLEVBA_PATH', '/usr/local/bin/olevba3')
  36. # 10:DEBUG, 20:INFO, 30:WARNING, 40:ERROR, 50:CRITICAL
  37. olefy_loglvl = int(os.getenv('OLEFY_LOGLVL', 20))
  38. olefy_min_length = int(os.getenv('OLEFY_MINLENGTH', 500))
  39. olefy_del_tmp = int(os.getenv('OLEFY_DEL_TMP', 1))
  40. # internal used variables
  41. request_time = '0000000000.000000'
  42. olefy_protocol = 'OLEFY'
  43. olefy_protocol_sep = '\\n\\n'
  44. olefy_headers = {}
  45. # init logging
  46. logger = logging.getLogger('olefy')
  47. logging.basicConfig(stream=sys.stdout, level=olefy_loglvl, format='olefy %(levelname)s %(funcName)s %(message)s')
  48. # log runtime variables
  49. logger.info('olefy listen address: {}'.format(olefy_listen_addr))
  50. logger.info('olefy listen port: {}'.format(olefy_listen_port))
  51. logger.info('olefy tmp dir: {}'.format(olefy_tmp_dir))
  52. logger.info('olefy python path: {}'.format(olefy_python_path))
  53. logger.info('olefy olvba path: {}'.format(olefy_olevba_path))
  54. logger.info('olefy log level: {}'.format(olefy_loglvl))
  55. logger.info('olefy min file length: {}'.format(olefy_min_length))
  56. logger.info('olefy delete tmp file: {}'.format(olefy_del_tmp))
  57. if not os.path.isfile(olefy_python_path):
  58. logger.critical('python path not found: {}'.format(olefy_python_path))
  59. exit(1)
  60. if not os.path.isfile(olefy_olevba_path):
  61. logger.critical('olevba path not found: {}'.format(olefy_olevba_path))
  62. exit(1)
  63. # olefy protocol function
  64. def protocol_split( olefy_line ):
  65. header_lines = olefy_line.split('\\n')
  66. for line in header_lines:
  67. if line == 'OLEFY/1.0':
  68. olefy_headers['olefy'] = line
  69. elif line != '':
  70. kv = line.split(': ')
  71. if kv[0] != '' and kv[1] != '':
  72. olefy_headers[kv[0]] = kv[1]
  73. logger.debug('olefy_headers: {}'.format(olefy_headers))
  74. # calling oletools
  75. def oletools( stream, tmp_file_name, lid ):
  76. if olefy_min_length > stream.__len__():
  77. logger.error('{} {} bytes (Not Scanning! File smaller than {!r})'.format(lid, stream.__len__(), olefy_min_length))
  78. out = b'[ { "error": "File too small" } ]'
  79. else:
  80. tmp_file = open(tmp_file_name, 'wb')
  81. tmp_file.write(stream)
  82. tmp_file.close()
  83. file_magic = magic.Magic(mime=True, uncompress=True)
  84. file_mime = file_magic.from_file(tmp_file_name)
  85. logger.info('{} {} (libmagic output)'.format(lid, file_mime))
  86. # do the olefy
  87. cmd_tmp = Popen([olefy_python_path, olefy_olevba_path, '-a', '-j', tmp_file_name], stdout=PIPE, stderr=PIPE)
  88. out, err = cmd_tmp.communicate()
  89. if out.__len__() < 10:
  90. logger.error('{} olevba returned <10 chars - rc: {!r}, response: {!r}'.format(lid,cmd_tmp.returncode, out.decode('ascii')))
  91. out = b'[ { "error": "Unhandled oletools response" } ]'
  92. if err.__len__() > 10:
  93. logger.error('{} olevba stderr >10 chars - rc: {!r}, response: {!r}'.format(lid, cmd_tmp.returncode, err.decode('ascii')))
  94. out = b'[ { "error": "Unhandled oletools error" } ]'
  95. if cmd_tmp.returncode != 0:
  96. logger.error('{} olevba exited with code {!r}; err: {!r}'.format(lid, cmd_tmp.returncode, err.decode('ascii')))
  97. if olefy_del_tmp == 1:
  98. logger.debug('{} {} deleting tmp file'.format(lid, tmp_file_name))
  99. os.remove(tmp_file_name)
  100. logger.debug('{} response: {}'.format(lid, out.decode()))
  101. return out
  102. # Asyncio data handling, default AIO-Functions
  103. class AIO(asyncio.Protocol):
  104. def __init__(self):
  105. self.extra = bytearray()
  106. def connection_made(self, transport):
  107. global request_time
  108. peer = transport.get_extra_info('peername')
  109. logger.debug('{} new connection was made'.format(peer))
  110. self.transport = transport
  111. request_time = str(time.time())
  112. def data_received(self, request, msgid=1):
  113. peer = self.transport.get_extra_info('peername')
  114. logger.debug('{} data received from new connection'.format(peer))
  115. self.extra.extend(request)
  116. def eof_received(self):
  117. peer = self.transport.get_extra_info('peername')
  118. olefy_protocol_err = False
  119. proto_ck = str(self.extra[0:2000])
  120. if olefy_protocol in proto_ck:
  121. olefy_line = proto_ck[12:proto_ck.find(olefy_protocol_sep)]
  122. self.extra = bytearray(self.extra[59:len(self.extra)])
  123. protocol_split(olefy_line)
  124. else:
  125. olefy_protocol_err = True
  126. lid = 'Rspamd-ID' in olefy_headers and '<'+olefy_headers['Rspamd-ID'][:6]+'>' or '<>'
  127. tmp_file_name = olefy_tmp_dir+'/'+request_time+'.'+str(peer[1])
  128. logger.debug('{} {} choosen as tmp filename'.format(lid, tmp_file_name))
  129. logger.info('{} {} bytes (stream size)'.format(lid, self.extra.__len__()))
  130. if olefy_protocol_err == True or olefy_headers['olefy'] != 'OLEFY/1.0':
  131. logger.error('Protocol ERROR: no OLEFY/1.0 found)')
  132. out = b'[ { "error": "Protocol error" } ]'
  133. elif 'Method' in olefy_headers:
  134. if olefy_headers['Method'] == 'oletools':
  135. out = oletools(self.extra, tmp_file_name, lid)
  136. else:
  137. logger.error('Protocol ERROR: Method header not found')
  138. out = b'[ { "error": "Protocol error: Method header not found" } ]'
  139. self.transport.write(out)
  140. logger.info('{} response send: {!r}'.format(peer, out))
  141. self.transport.close()
  142. # start the listeners
  143. loop = asyncio.get_event_loop()
  144. # each client connection will create a new protocol instance
  145. coro = loop.create_server(AIO, olefy_listen_addr, olefy_listen_port)
  146. server = loop.run_until_complete(coro)
  147. logger.info('serving on {}'.format(server.sockets[0].getsockname()))
  148. # XXX serve requests until KeyboardInterrupt, not needed for production
  149. try:
  150. loop.run_forever()
  151. except KeyboardInterrupt:
  152. pass
  153. # graceful shutdown/reload
  154. server.close()
  155. loop.run_until_complete(server.wait_closed())
  156. loop.close()
  157. logger.info('stopped serving')