def endsession(self, code, message): """End session with incoming postfix""" self.socket.send(force_bString("%s %s\r\n" % (code, message))) rawdata = b'' data = '' while True: lump = self.socket.recv(1024) if len(lump): rawdata += lump if (len(rawdata) >= 2) and rawdata[-2:] == b'\r\n': cmd = data[0:4] cmd = cmd.upper() if cmd == b"QUIT": self.socket.send( force_bString("%s %s\r\n" % (220, "BYE"))) self.closeconn() return self.socket.send( force_bString("%s %s\r\n" % (421, "Cannot accept further commands"))) self.closeconn() return else: self.closeconn() return
def scan_stream(self, content, suspectid='(NA)'): """ Scan a buffer content (string) : buffer to scan return either : - (dict) : {filename1: "virusname"} - None if no virus found """ s = self.__init_socket__() content = force_bString(content) buflen = len(content) s.sendall(force_bString('SCAN %s STREAM fu_stream SIZE %s' % (self.config.get(self.section, 'scanoptions'), buflen))) s.sendall(b'\n') self.logger.debug('%s Sending buffer (length=%s) to fpscand...' % (suspectid, buflen)) s.sendall(content) self.logger.debug('%s Sent %s bytes to fpscand, waiting for scan result' % (suspectid, buflen)) result = force_uString(s.recv(20000)) if len(result) < 1: self.logger.error('Got no reply from fpscand') s.close() return self._parse_result(result)
def examine(self, suspect): if not DKIMPY_AVAILABLE: suspect.debug("dkimpy not available, can not check") self.logger.error("DKIM signing skipped - missing dkimpy library") return DUNNO message = suspect.get_source() domain = extract_from_domain(suspect) addvalues = dict(header_from_domain=domain) selector = apply_template(self.config.get(self.section, 'selector'), suspect, addvalues) if domain is None: self.logger.error( "%s: Failed to extract From-header domain for DKIM signing" % suspect.id) return DUNNO privkeyfile = apply_template( self.config.get(self.section, 'privatekeyfile'), suspect, addvalues) if not os.path.isfile(privkeyfile): self.logger.debug( "%s: DKIM signing failed for domain %s, private key not found: %s" % (suspect.id, domain, privkeyfile)) return DUNNO with open(privkeyfile, 'br') as f: privkeycontent = f.read() canH = Simple canB = Simple if self.config.get(self.section, 'canonicalizeheaders').lower() == 'relaxed': canH = Relaxed if self.config.get(self.section, 'canonicalizebody').lower() == 'relaxed': canB = Relaxed canon = (canH, canB) headerconfig = self.config.get(self.section, 'signheaders') if headerconfig is None or headerconfig.strip() == '': inc_headers = None else: inc_headers = headerconfig.strip().split(',') blength = self.config.getboolean(self.section, 'signbodylength') dkimhdr = sign(message, force_bString(selector), force_bString(domain), privkeycontent, canonicalize=canon, include_headers=inc_headers, length=blength, logger=suspect.get_tag('debugfile')) if dkimhdr.startswith(b'DKIM-Signature: '): dkimhdr = dkimhdr[16:] suspect.addheader('DKIM-Signature', dkimhdr, immediate=True)
def test_bounce(self): """Test bounce message, especially the encoding""" suspect = Suspect('*****@*****.**', '*****@*****.**', '/dev/null') # include non-ascii charset unicode characters to make sure the encoding/decoding # works correctly displayname = u"((testing placeholder for displayname -> äää))" asciirep = u"((testing placeholder for asciirep -> üüü))" description = u"((testing placeholder for description -> ööö))" blockinfo = ("%s %s: %s" % (displayname, asciirep, description)).strip() blockedfiletemplate = os.path.join( *[CONFDIR, "templates", "blockedfile.tmpl.dist"]) bounce = Bounce(self.config) bounce.send_template_file(suspect.from_address, blockedfiletemplate, suspect, dict(blockinfo=blockinfo)) # might be needed to wait for a bit to make sure answer is available counter = 0 while self.smtp.suspect is None and counter < 20: counter = counter + 1 time.sleep(0.05) # sleep is needed to gotback = self.smtp.suspect self.assertFalse(gotback == None, "Did not get message from dummy smtp server") # get message received by dummy smtp server msg = gotback.get_message_rep() receivedMsg = msg.get_payload(decode='utf-8') # Build the message according to what Bounce is doing so it can be compared # to what was received from DummySMTPServer with open(blockedfiletemplate) as fp: templatecontent = fp.read() blockinfo = ("%s %s: %s" % (displayname, asciirep, description)).strip() message = apply_template(templatecontent, suspect, dict(blockinfo=blockinfo)) messageB = force_bString(message) # modify received message to add header parts from template messageToCompare = force_bString("To: " + msg['To'] + "\nSubject: " + msg['Subject'] + "\n\n") + force_bString(receivedMsg) # make sure comparison will not fail because of newlines # For example, Python 2.6 has only one "\n" at the end of the received message, whereas Python 2.7 and 3 have to messageToCompare = messageToCompare.replace(b"\r", b"\n").replace( b"\n\n", b"\n") messageB = messageB.replace(b"\r", b"\n").replace(b"\n\n", b"\n") self.assertEqual(messageB, messageToCompare)
def test_encode2bytes(self): """Test if strings are correctly encoded""" self.assertEqual(bytes, type(force_bString("bla")), "After byte conversion, type has to be bytes") self.assertEqual(bytes, type(force_bString(u"bla")), "After byte conversion, type has to be bytes") self.assertEqual(bytes, type(force_bString(b"bla")), "After byte conversion, type has to be bytes") mixedlist = ["bla", u"bla", b"bla"] for item in force_bString(mixedlist): self.assertEqual(bytes, type(item), "After byte conversion, type has to be bytes") self.assertEqual(b"bla", item, "String has to match the test string b\"bla\"")
def test_SMTPUTF8_E2E(self): """test if a UTF-8 message runs through""" # give fuglu time to start listener time.sleep(1) root = logging.getLogger() root.setLevel(logging.DEBUG) ch = logging.StreamHandler(sys.stdout) ch.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') ch.setFormatter(formatter) root.addHandler(ch) # send test message smtpclient = smtplib.SMTP('127.0.0.1', EndtoEndBaseTestCase.FUGLU_PORT) # smtpServer.set_debuglevel(1) (code, msg) = smtpclient.ehlo('test.e2e') msg = force_uString(msg.split()) self.assertEqual(250, code) print("%s"%msg) self.assertIn("SMTPUTF8", msg) testunicodemessage = u"""Hello Wörld!\r Don't där yü tschänsch äny of mai baits or iwen remüv ön!""" # TODO: this test fails if we don't put in the \r in there... (eg, # fuglu adds it) - is this a bug or wrong test? msg = MIMEText(testunicodemessage, _charset='utf-8') msg["Subject"] = "End to End Test" msgstring = msg.as_string() inbytes = len(msg.get_payload(decode=True)) # envelope sender/recipients env_sender = u'sä[email protected]' env_recipients = [u'rö[email protected]', u'récipiè[email protected]'] smtpclient.sendmail(force_uString(env_sender), force_uString(env_recipients), force_bString(msgstring), mail_options=["SMTPUTF8"]) smtpclient.quit() # get answer (wait to give time to create suspect) time.sleep(0.1) gotback = self.smtp.suspect self.assertFalse(gotback == None, "Did not get message from dummy smtp server") # check a few things on the received message msgrep = gotback.get_message_rep() self.assertTrue('X-Fuglutest-Spamstatus' in msgrep, "Fuglu SPAM Header not found in message") payload = msgrep.get_payload(decode=True) outbytes = len(payload) self.assertEqual(inbytes, outbytes,"Message size change: bytes in: %u, bytes out %u" % (inbytes, outbytes)) self.assertEqual(testunicodemessage, force_uString(payload), "Message body has been altered. In: %u bytes, Out: %u bytes, teststring=->%s<- result=->%s<-" % (inbytes, outbytes, testunicodemessage, force_uString(payload))) # check sender/recipients self.assertEqual(env_sender, gotback.from_address) self.assertEqual(env_recipients, gotback.recipients)
def handlesession(self): line = force_uString(self.socket.recv(4096)).strip() if line == '': self.socket.close() return self.logger.debug('Control Socket command: %s' % line) answer = None try: if line.startswith("objgraph"): # special handling for objgraph # -> argument is a dict in json format # -> check attributes for commands, don't use list parts = line.split(maxsplit=1) if len(parts) == 2: argsdict = ControlSession.json_string_to_obj( parts[1], ForcedType=dict) else: argsdict = {} self.logger.debug('objgraph_growth: args dict: %s' % argsdict) answer = self.handle_command(parts[0], argsdict, checkattr=True) else: # default handling line = line.lower() parts = line.split() answer = self.handle_command(parts[0], parts[1:]) except Exception as e: if not answer: answer = force_uString(e) else: answer += force_uString(e) self.socket.sendall(force_bString(answer)) self.socket.close()
def doData(self, data): """Store data in temporary file Args: data (str or bytes): data as byte-string """ data = self.unquoteData(data) # store the last few bytes in memory to keep track when the msg is # finished self.dataAccum = self.dataAccum + data if len(self.dataAccum) > 4: self.dataAccum = self.dataAccum[-5:] if len(self.dataAccum) > 4 and self.dataAccum[-5:] == force_bString( '\r\n.\r\n'): # check if there is more data to write to the file if len(data) > 4: self.tempfile.write(data[0:-5]) self._close_tempfile() self.state = SMTPSession.ST_HELO return "250 OK - Data and terminator. found" else: self.tempfile.write(data) return None
def process(self, suspect, decision): recipient = force_uString( suspect.to_domain) # work with unicode string if self.config.get(self.section, 'level') == 'email': recipient = suspect.to_address recipient = recipient.replace('.', '-') recipient = recipient.replace('@', '--') host = self.config.get(self.section, 'host') port = self.config.getint(self.section, 'port') buffer = "" if self.sock is None: addr_f = socket.getaddrinfo(host, 0)[0][0] self.sock = socket.socket(addr_f, socket.SOCK_DGRAM) if suspect.is_virus(): buffer = "%s%s.fuglu.recipient.%s.virus:1|c\n" % ( buffer, self.nodename, recipient) elif suspect.is_highspam(): buffer = "%s%s.fuglu.recipient.%s.highspam:1|c\n" % ( buffer, self.nodename, recipient) elif suspect.is_spam(): buffer = "%s%s.fuglu.recipient.%s.spam:1|c\n" % ( buffer, self.nodename, recipient) else: buffer = "%s%s.fuglu.recipient.%s.clean:1|c\n" % ( buffer, self.nodename, recipient) self.sock.sendto(force_bString(buffer), (host, port))
def changeheader(self, key, value): """ Change header in message sending corresponding command to MTA using protocol stored in self.sess Args: key (string(encoded)): header key value (string(encoded)): header value """ if not self.sess.has_option(lm.SMFIF_CHGHDRS): self.logger.error( 'Change header called without the proper opts set, ' 'availability -> fuglu: %s, mta: %s' % (self.sess.has_option(lm.SMFIF_CHGHDRS, client="fuglu"), self.sess.has_option(lm.SMFIF_CHGHDRS, client="mta"))) return self.sess.chgHeader(force_bString(key), force_bString(value))
def test_int2bytes(self): """Test if an integer can be converted to string->bytes""" myint = 550 converted = force_bString(myint) self.assertEqual( b"550", converted, "Integer should be converted to a string and then to bytes") pass
def getincomingmail(self): """return true if mail got in, false on error Session will be kept open""" self.socket.send(force_bString("220 fuglu scanner ready \r\n")) while True: rawdata = b'' completeLine = 0 while not completeLine: lump = self.socket.recv(1024) if len(lump): rawdata += lump if (len(rawdata) >= 2) and rawdata[-2:] == b'\r\n': completeLine = 1 if self.state != ESMTPPassthroughSession.ST_DATA: # decode message (except data) from binary to unicode data = force_uString(rawdata) rsp, keep = self.doCommand(data) else: try: rsp = self.doData(rawdata) except IOError: self.endsession( 421, "Could not write to temp file") self._close_tempfile() return False if rsp is None: continue else: # data finished.. keep connection open though self.logger.debug('incoming message finished') return True self.socket.send(force_bString(rsp + "\r\n")) if keep == 0: self.closeconn() self.finish_outgoing_connection() return False else: # EOF return False
def safilter(self, messagecontent, user): """pass content to sa, return sa-processed mail""" retries = self.config.getint(self.section, 'retries') peruserconfig = self.config.getboolean(self.section, 'peruserconfig') spamsize = len(messagecontent) for i in range(0, retries): try: self.logger.debug('Contacting spamd (Try %s of %s)' % (i + 1, retries)) s = self.__init_socket() s.sendall(force_bString('PROCESS SPAMC/1.2')) s.sendall(force_bString("\r\n")) s.sendall(force_bString("Content-length: %s" % spamsize)) s.sendall(force_bString("\r\n")) if peruserconfig: s.sendall(force_bString("User: %s" % user)) s.sendall(force_bString("\r\n")) s.sendall(force_bString("\r\n")) s.sendall(force_bString(messagecontent)) self.logger.debug('Sent %s bytes to spamd' % spamsize) s.shutdown(socket.SHUT_WR) socketfile = s.makefile("rb") line1_info = socketfile.readline() line1_info = force_uString(line1_info) # convert to unicode string self.logger.debug(line1_info) line2_contentlength = socketfile.readline() line3_empty = socketfile.readline() content = socketfile.read() self.logger.debug('Got %s message bytes from back from spamd' % len(content)) answer = line1_info.strip().split() if len(answer) != 3: self.logger.error("Got invalid status line from spamd: %s" % line1_info) continue version, number, status = answer if status != 'EX_OK': self.logger.error("Got bad status from spamd: %s" % status) continue return content except socket.timeout: self.logger.error('SPAMD Socket timed out.') except socket.herror as h: self.logger.error('SPAMD Herror encountered : %s' % str(h)) except socket.gaierror as g: self.logger.error('SPAMD gaierror encountered: %s' % str(g)) except socket.error as e: self.logger.error('SPAMD socket error: %s' % str(e)) except Exception as e: self.logger.error('SPAMD communication error: %s' % str(e)) time.sleep(1) return None
def buildmsgsource(suspect): """Build the message source with fuglu headers prepended""" # we must prepend headers manually as we can't set a header order in email # objects # -> the original message source is bytes origmsgtxt = suspect.get_source() newheaders = "" for key in suspect.addheaders: # is ignore the right thing to do here? val = suspect.addheaders[key] #self.logger.debug('Adding header %s : %s'%(key,val)) hdr = Header(val, header_name=key, continuation_ws=' ') newheaders += "%s: %s\r\n" % (key, hdr.encode()) # the original message should be in bytes, make sure the header added # is an encoded string as well modifiedtext = force_bString(newheaders) + force_bString(origmsgtxt) return modifiedtext
def lint_ping(self): try: s = self.__init_socket__(oneshot=True) except Exception as e: print("Could not contact clamd: %s" % (str(e))) return False s.sendall(force_bString('PING')) result = s.recv(20000) print("Got Pong: %s" % force_uString(result)) if result.strip() != b'PONG': print("Invalid PONG: %s" % force_uString(result)) return True
def scan_file(self, filename): filename = os.path.abspath(filename) s = self.__init_socket__() s.sendall(force_bString('SCAN %s FILE %s' % (self.config.get(self.section, 'scanoptions'), filename))) s.sendall(b'\n') result = s.recv(20000) if len(result) < 1: self.logger.error('Got no reply from fpscand') s.close() return self._parse_result(result)
def lint_file(self): import tempfile (handle, tempfilename) = tempfile.mkstemp(prefix='fuglu', dir=self.config.get('main', 'tempdir')) tempfilename = tempfilename stream = """Date: Mon, 08 Sep 2008 17:33:54 +0200 To: [email protected] From: [email protected] Subject: test eicar attachment X-Mailer: swaks v20061116.0 jetmore.org/john/code/#swaks MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="----=_MIME_BOUNDARY_000_12140" ------=_MIME_BOUNDARY_000_12140 Content-Type: text/plain Eicar test ------=_MIME_BOUNDARY_000_12140 Content-Type: application/octet-stream Content-Transfer-Encoding: BASE64 Content-Disposition: attachment UEsDBAoAAAAAAGQ7WyUjS4psRgAAAEYAAAAJAAAAZWljYXIuY29tWDVPIVAlQEFQWzRcUFpYNTQo UF4pN0NDKTd9JEVJQ0FSLVNUQU5EQVJELUFOVElWSVJVUy1URVNULUZJTEUhJEgrSCoNClBLAQIU AAoAAAAAAGQ7WyUjS4psRgAAAEYAAAAJAAAAAAAAAAEAIAD/gQAAAABlaWNhci5jb21QSwUGAAAA AAEAAQA3AAAAbQAAAAAA ------=_MIME_BOUNDARY_000_12140--""" with os.fdopen(handle, 'w+b') as fd: fd.write(force_bString(stream)) try: viruses = self.scan_file(tempfilename) except Exception as e: print(e) return False try: os.remove(tempfilename) except Exception: pass try: for fname, virus in iter(viruses.items()): print("F-Prot AV (file mode): Found virus: %s in %s" % (virus, fname)) if "EICAR" in virus: return True except Exception as e: print(e) return False print("Couldn't find EICAR in tmp file: %s" % fname) return False
def test_reinject_error(self): """test if a reinject error is passed""" # give fuglu time to start listener time.sleep(1) import logging import sys root = logging.getLogger() root.setLevel(logging.DEBUG) ch = logging.StreamHandler(sys.stdout) ch.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') ch.setFormatter(formatter) root.addHandler(ch) # send test message smtpclient = smtplib.SMTP('127.0.0.1', ReinjectErrorTestCase.FUGLU_PORT) # smtpServer.set_debuglevel(1) (code, msg) = smtpclient.helo('test.e2e') self.assertEqual(250, code) testmessage = u"""Hello World!""" # TODO: this test fails if we don't put in the \r in there... (eg, # fuglu adds it) - is this a bug or wrong test? msg = MIMEText(testmessage) msg["Subject"] = "End to End Test" msgstring = msg.as_string() inbytes = len(msg.get_payload(decode=True)) # envelope sender/recipients env_sender = u'*****@*****.**' env_recipients = [u'*****@*****.**'] self.smtp.response_code = 554 # python 3 returnes bytes self.smtp.response_message = '5.4.0 Error: too many hops' try: smtpclient.sendmail(force_uString(env_sender), force_uString(env_recipients), force_bString(msgstring)) except smtplib.SMTPDataError as e: self.assertEqual(self.smtp.response_code, e.smtp_code) self.assertEqual(self.smtp.response_message, force_uString(e.smtp_error)) pass
def add_rcpt(self, rcpt): """ Add a new envelope recipient Args: rcpt (str, unicode): new recipient mail address, with <> qualification """ if not self.sess.has_option(lm.SMFIF_ADDRCPT_PAR): self.logger.error( 'Add rcpt called without the proper opts set, ' 'availability -> fuglu: %s, mta: %s' % (self.sess.has_option(lm.SMFIF_ADDRCPT_PAR, client="fuglu"), self.sess.has_option(lm.SMFIF_ADDRCPT_PAR, client="mta"))) return self.sess.addRcpt(force_bString(rcpt))
def _create_hash(self, value): hashtype = self.config.get(self.section, 'hash').lower() if hasattr(hashlib, 'algorithms_guaranteed'): algorithms = hashlib.algorithms_guaranteed else: algorithms = ['md5', 'sha1'] #python 2.6 if hashtype in algorithms: hasher = getattr(hashlib, hashtype) myhash = hasher(force_bString(value)).hexdigest() else: myhash = '' return myhash
def change_from(self, from_address): """ Change envelope from mail address. Args: from_address (unicode,str): new from mail address """ if not self.sess.has_option(lm.SMFIF_CHGFROM): self.logger.error( 'Change from called without the proper opts set, ' 'availability -> fuglu: %s, mta: %s' % (self.sess.has_option(lm.SMFIF_CHGFROM, client="fuglu"), self.sess.has_option(lm.SMFIF_CHGFROM, client="mta"))) return self.sess.chgFrom(force_bString(from_address))
def re_inject(self, suspect): """Send message back to postfix""" if suspect.get_tag('noreinject'): # in esmtp sessions we don't want to provide info to the connecting # client return 250, 'OK' if suspect.get_tag('reinjectoriginal'): self.logger.info( 'Injecting original message source without modifications') msgcontent = suspect.get_original_source() else: msgcontent = buildmsgsource(suspect) code, answer = self.sess.forwardconn.data(force_bString(msgcontent)) answer = force_uString(answer) return code, answer
def replacebody(self, newbody): """ Replace message body sending corresponding command to MTA using protocol stored in self.sess Args: newbody (string(encoded)): new message body """ # check if option is available if not self.sess.has_option(lm.SMFIF_CHGBODY): self.logger.error( 'Change body called without the proper opts set, ' 'availability -> fuglu: %s, mta: %s' % (self.sess.has_option(lm.SMFIF_CHGBODY, client="fuglu"), self.sess.has_option(lm.SMFIF_CHGBODY, client="mta"))) return self.sess.replBody(force_bString(newbody))
def scan_shell(self, content): clamscan = self.config.get(self.section, 'clamscan') timeout = self.config.getint(self.section, 'clamscantimeout') if not os.path.exists(clamscan): raise Exception('could not find clamscan executable in %s' % clamscan) try: process = subprocess.Popen( [clamscan, u'-'], stdin=subprocess.PIPE, stdout=subprocess.PIPE) # file data by pipe kill_proc = lambda p: p.kill() timer = threading.Timer(timeout, kill_proc, [process]) timer.start() stdout = process.communicate(force_bString(content))[0] process.stdin.close() exitcode = process.wait() timer.cancel() except Exception: exitcode = -1 stdout = '' if exitcode > 1: # 0: no virus, 1: virus, >1: error, -1 subprocess error raise Exception('clamscan error') elif exitcode < 0: raise Exception('clamscan timeout after %ss' % timeout) dr = {} for line in stdout.splitlines(): line = line.strip() if line.endswith(b'FOUND'): filename, virusname, found = line.rsplit(None, 2) filename = force_uString(filename.rstrip(b':')) dr[filename] = virusname if dr == {}: return None else: return dr
def process(self, suspect, decision): buffer = "%s.fuglu.decision.%s:1|c\n" % ( self.nodename, actioncode_to_string(decision)) host = self.config.get(self.section, 'host') port = self.config.getint(self.section, 'port') if self.sock is None: addr_f = socket.getaddrinfo(host, 0)[0][0] self.sock = socket.socket(addr_f, socket.SOCK_DGRAM) if suspect.is_virus(): buffer = "%s%s.fuglu.message.virus:1|c\n" % (buffer, self.nodename) elif suspect.is_highspam(): buffer = "%s%s.fuglu.message.highspam:1|c\n" % (buffer, self.nodename) elif suspect.is_spam(): buffer = "%s%s.fuglu.message.spam:1|c\n" % (buffer, self.nodename) else: buffer = "%s%s.fuglu.message.clean:1|c\n" % (buffer, self.nodename) self.sock.sendto(force_bString(buffer), (host, port))
def _parse_result(self, result): dr = {} result = force_uString(result) for line in result.strip().split('\n'): m = self.pattern.match(force_bString(line)) if m is None: self.logger.error('Could not parse line from f-prot: %s' % line) raise Exception('f-prot: Unparseable answer: %s' % result) status = force_uString(m.group(1)) text = force_uString(m.group(2)) details = force_uString(m.group(3)) status = int(status) self.logger.debug("f-prot scan status: %s" % status) self.logger.debug("f-prot scan text: %s" % text) if status == 0: continue if status > 3: self.logger.warning("f-prot: got unusual status %s (result: %s)" % (status, result)) # http://www.f-prot.com/support/helpfiles/unix/appendix_c.html if status & 1 == 1 or status & 2 == 2: # we have a infection if text[0:10] == "infected: ": text = text[10:] elif text[0:27] == "contains infected objects: ": text = text[27:] else: self.logger.warn("Unexpected reply from f-prot: %s" % text) continue dr[details] = text if len(dr) == 0: return None else: return dr
def _add_required_headers(self, recipient, messagecontent): """add headers required for sending automated mail""" msgrep = email.message_from_bytes(force_bString(messagecontent)) msgrep.set_charset( "utf-8") # define unicode because the messagecontent is unicode if not 'to' in msgrep: msgrep['To'] = Header("<%s>" % recipient).encode() if not 'From' in msgrep: msgrep['from'] = Header("<MAILER-DAEMON@%s>" % socket.gethostname()).encode() if not 'auto-submitted' in msgrep: msgrep['auto-submitted'] = Header('auto-generated').encode() if not 'date' in msgrep: msgrep['Date'] = formatdate(localtime=True) if not 'Message-id' in msgrep: msgrep['Message-ID'] = make_msgid() return msgrep.as_string()
def getincomingmail(self): """return true if mail got in, false on error Session will be kept open""" self.socket.send( force_bString( "fuglu scanner ready - please pipe your message, " "(optional) include env sender/recipient in the beginning, " "see documentation\r\n")) try: (handle, tempfilename) = tempfile.mkstemp(prefix='fuglu', dir=self.config.get( 'main', 'tempdir')) self.tempfilename = tempfilename self.tempfile = os.fdopen(handle, 'w+b') except Exception as e: self.endsession('could not write to tempfile') collect_lumps = [] while True: data = self.socket.recv(1024) if len(data) < 1: break else: collect_lumps.append(data) data = b"".join(collect_lumps) data = self.parse_remove_env_data(data) self.tempfile.write(data) self.tempfile.close() if not data: self.logger.debug('Problem receiving or parsing message') return False else: self.logger.debug('Incoming message received') return True
def scan_stream(self, content, suspectid="(NA)"): """ Scan byte buffer return either : - (dict) : {filename1: "virusname"} - None if no virus found - raises Exception if something went wrong """ pipelining = self.config.getboolean(self.section, 'pipelining') s = self.__init_socket__(oneshot=not pipelining) s.sendall(b'zINSTREAM\0') default_chunk_size = 2048 remainingbytes = force_bString(content) numChunksToSend = math.ceil(len(remainingbytes) / default_chunk_size) iChunk = 0 chunklength = 0 self.logger.debug('%s: sending message in %u chunks of size %u bytes' % (suspectid, numChunksToSend, default_chunk_size)) while len(remainingbytes) > 0: iChunk = iChunk + 1 chunklength = min(default_chunk_size, len(remainingbytes)) #self.logger.debug('sending chunk %u/%u' % (iChunk,numChunksToSend)) #self.logger.debug('sending %s byte chunk' % chunklength) chunkdata = remainingbytes[:chunklength] remainingbytes = remainingbytes[chunklength:] s.sendall(struct.pack(b'!L', chunklength)) s.sendall(chunkdata) self.logger.debug( '%s: sent chunk %u/%u, last number of bytes sent was %u' % (suspectid, iChunk, numChunksToSend, chunklength)) self.logger.debug( '%s: All chunks send, send 0 - size to tell ClamAV the whole message has been sent' % suspectid) s.sendall(struct.pack(b'!L', 0)) dr = {} result = force_uString(self._read_until_delimiter(s, suspectid)).strip() if result.startswith('INSTREAM size limit exceeded'): raise Exception( "%s: Clamd size limit exeeded. Make sure fuglu's clamd maxsize config is not larger than clamd's StreamMaxLength" % suspectid) if result.startswith('UNKNOWN'): raise Exception( "%s: Clamd doesn't understand INSTREAM command. very old version?" % suspectid) if pipelining: try: ans_id, filename, virusinfo = result.split(':', 2) filename = force_uString( filename.strip()) # use unicode for filename virusinfo = force_uString( virusinfo.strip()) # lets use unicode for the info except Exception: raise Exception( "%s: Protocol error, could not parse result: %s" % (suspectid, result)) threadLocal.expectedID += 1 if threadLocal.expectedID != int(ans_id): raise Exception( "Commands out of sync - expected ID %s - got %s" % (threadLocal.expectedID, ans_id)) if virusinfo[-5:] == 'ERROR': raise Exception(virusinfo) elif virusinfo != 'OK': dr[filename] = virusinfo.replace(" FOUND", '') if threadLocal.expectedID >= MAX_SCANS_PER_SOCKET: try: s.sendall(b'zEND\0') s.close() finally: self.__invalidate_socket() else: filename, virusinfo = result.split(':', 1) filename = force_uString( filename.strip()) # use unicode for filename virusinfo = force_uString( virusinfo.strip()) # use unicode for virus info if virusinfo[-5:] == 'ERROR': raise Exception(virusinfo) elif virusinfo != 'OK': dr[filename] = virusinfo.replace(" FOUND", '') s.close() if dr == {}: return None else: return dr
def verify(message, debuglog=None): """Verify a DKIM signature on an RFC822 formatted message. @param message: an RFC822 formatted message (with either \\n or \\r\\n line endings) @param debuglog: a file-like object to which debug info will be written (default None) """ (headers, body) = rfc822_parse(message) sigheaders = [x for x in headers if x[0].lower() == "dkim-signature"] if len(sigheaders) < 1: return False # Currently, we only validate the first DKIM-Signature line found. a = re.split(r"\s*;\s*", sigheaders[0][1].strip()) if debuglog is not None: print("a:", a, file=debuglog) sig = {} for x in a: if x: m = re.match(r"(\w+)\s*=\s*(.*)", x, re.DOTALL) if m is None: if debuglog is not None: print("invalid format of signature part: %s" % x, file=debuglog) return False sig[m.group(1)] = m.group(2) if debuglog is not None: print("sig:", sig, file=debuglog) if 'v' not in sig: if debuglog is not None: print("signature missing v=", file=debuglog) return False if sig['v'] != "1": if debuglog is not None: print("v= value is not 1 (%s)" % sig['v'], file=debuglog) return False if 'a' not in sig: if debuglog is not None: print("signature missing a=", file=debuglog) return False if 'b' not in sig: if debuglog is not None: print("signature missing b=", file=debuglog) return False if re.match(r"[\s0-9A-Za-z+/]+=*$", sig['b']) is None: if debuglog is not None: print("b= value is not valid base64 (%s)" % sig['b'], file=debuglog) return False if 'bh' not in sig: if debuglog is not None: print("signature missing bh=", file=debuglog) return False if re.match(r"[\s0-9A-Za-z+/]+=*$", sig['bh']) is None: if debuglog is not None: print("bh= value is not valid base64 (%s)" % sig['bh'], file=debuglog) return False if 'd' not in sig: if debuglog is not None: print("signature missing d=", file=debuglog) return False if 'h' not in sig: if debuglog is not None: print("signature missing h=", file=debuglog) return False if 'i' in sig and (not sig['i'].endswith(sig['d']) or sig['i'][-len(sig['d']) - 1] not in "@."): if debuglog is not None: print("i= domain is not a subdomain of d= (i=%s d=%d)" % (sig['i'], sig['d']), file=debuglog) return False if 'l' in sig and re.match(r"\d{,76}$", sig['l']) is None: if debuglog is not None: print("l= value is not a decimal integer (%s)" % sig['l'], file=debuglog) return False if 'q' in sig and sig['q'] != "dns/txt": if debuglog is not None: print("q= value is not dns/txt (%s)" % sig['q'], file=debuglog) return False if 's' not in sig: if debuglog is not None: print("signature missing s=", file=debuglog) return False if 't' in sig and re.match(r"\d+$", sig['t']) is None: if debuglog is not None: print("t= value is not a decimal integer (%s)" % sig['t'], file=debuglog) return False if 'x' in sig: if re.match(r"\d+$", sig['x']) is None: if debuglog is not None: print("x= value is not a decimal integer (%s)" % sig['x'], file=debuglog) return False if int(sig['x']) < int(sig['t']): if debuglog is not None: print("x= value is less than t= value (x=%s t=%s)" % (sig['x'], sig['t']), file=debuglog) return False m = re.match("(\w+)(?:/(\w+))?$", sig['c']) if m is None: if debuglog is not None: print("c= value is not in format method/method (%s)" % sig['c'], file=debuglog) return False can_headers = m.group(1) if m.group(2) is not None: can_body = m.group(2) else: can_body = "simple" if can_headers == "simple": canonicalize_headers = Simple elif can_headers == "relaxed": canonicalize_headers = Relaxed else: if debuglog is not None: print("Unknown header canonicalization (%s)" % can_headers, file=debuglog) return False headers = canonicalize_headers.canonicalize_headers(headers) if can_body == "simple": body = Simple.canonicalize_body(body) elif can_body == "relaxed": body = Relaxed.canonicalize_body(body) else: if debuglog is not None: print("Unknown body canonicalization (%s)" % can_body, file=debuglog) return False if sig['a'] == "rsa-sha1": hasher = hashlib.sha1 hashid = HASHID_SHA1 elif sig['a'] == "rsa-sha256": hasher = hashlib.sha256 hashid = HASHID_SHA256 else: if debuglog is not None: print("Unknown signature algorithm (%s)" % sig['a'], file=debuglog) return False if 'l' in sig: body = body[:int(sig['l'])] h = hasher() h.update(force_bString(body)) bodyhash = h.digest() if debuglog is not None: print("bh:", base64.b64encode(bodyhash), file=debuglog) if bodyhash != base64.b64decode(re.sub(r"\s+", "", sig['bh'])): if debuglog is not None: print("body hash mismatch (got %s, expected %s)" % (base64.b64encode(bodyhash), sig['bh']), file=debuglog) return False s = dnstxt(sig['s'] + "._domainkey." + sig['d'] + ".") if not s: return False a = re.split(r"\s*;\s*", s) pub = {} for f in a: m = re.match(r"(\w+)=(.*)", f) if m is not None: pub[m.group(1)] = m.group(2) else: if debuglog is not None: print("invalid format in _domainkey txt record", file=debuglog) return False pkey = base64.b64decode(pub['p']) pkey = force_cfromb(pkey) x = asn1_parse(ASN1_Object, pkey) # Not sure why the [1:] is necessary to skip a byte. pkd = asn1_parse(ASN1_RSAPublicKey, x[0][1][1:]) pk = { 'modulus': pkd[0][0], 'publicExponent': pkd[0][1], } modlen = len(int2str(pk['modulus'])) if debuglog is not None: print("modlen:", modlen, file=debuglog) include_headers = re.split(r"\s*:\s*", sig['h']) if debuglog is not None: print("include_headers:", include_headers, file=debuglog) sign_headers = [] lastindex = {} for h in include_headers: i = lastindex.get(h, len(headers)) while i > 0: i -= 1 if h.lower() == headers[i][0].lower(): sign_headers.append(headers[i]) break lastindex[h] = i # The call to _remove() assumes that the signature b= only appears once in # the signature header sign_headers += [ (x[0], x[1].rstrip()) for x in canonicalize_headers.canonicalize_headers([( sigheaders[0][0], _remove(sigheaders[0][1], sig['b']))]) ] if debuglog is not None: print("verify headers:", sign_headers, file=debuglog) h = hasher() for x in sign_headers: h.update(force_bString(x[0])) h.update(force_bString(":")) h.update(force_bString(x[1])) d = h.digest() d = force_cfromb(d) if debuglog is not None: print("verify digest:", " ".join("%02x" % ord(x) for x in d), file=debuglog) dinfo = asn1_build((SEQUENCE, [ (SEQUENCE, [ (OBJECT_IDENTIFIER, hashid), (NULL, None), ]), (OCTET_STRING, d), ])) if debuglog is not None: print("dinfo:", " ".join("%02x" % ord(x) for x in dinfo), file=debuglog) if len(dinfo) + 3 > modlen: if debuglog is not None: print("Hash too large for modulus", file=debuglog) return False sig2 = "\x00\x01" + "\xff" * (modlen - len(dinfo) - 3) + "\x00" + dinfo sig2 = force_cfromb(sig2) if debuglog is not None: print("sig2:", " ".join("%02x" % ord(x) for x in sig2), file=debuglog) print(sig['b'], file=debuglog) print(re.sub(r"\s+", "", sig['b']), file=debuglog) sigEncoded = base64.b64decode(force_bfromc(re.sub(r"\s+", "", sig['b']))) sigEncoded = force_cfromb((sigEncoded)) v = int2str(pow(str2int(sigEncoded), pk['publicExponent'], pk['modulus']), modlen) if debuglog is not None: print("v:", " ".join("%02x" % ord(x) for x in v), file=debuglog) assert len(v) == len(sig2) # Byte-by-byte compare of signatures return not [1 for x in zip(v, sig2) if x[0] != x[1]]