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 readoptions(s): resp = receivemsg(s) opts = {} for l in resp: parts = optionsyntax.findall(l) for p in parts: p0 = force_uString(p[0]) if p0 not in opts: opts[p0] = [] opts[p0].append(force_uString(p[1])) return opts
def Dispatch(self, data): """Callback function for the milter socket server to handle a single milter command. Parses the milter command data, invokes the milter handler, and formats a suitable response for the server to send on the socket. Args: data: A (binary) string (consisting of a command code character followed by binary data for that command code). Returns: A binary string to write on the socket and return to sendmail. The string typically consists of a RESPONSE[] command character then some response-specific protocol data. Raises: PpyMilterCloseConnection: Indicating the (milter) connection should be closed. """ cmd, data = data[0], data[1:] try: if cmd not in COMMANDS: logging.warn('Unknown command code: "%s" ("%s")', force_uString(cmd), force_uString(data)) return RESPONSE['CONTINUE'] u_command = force_uString(COMMANDS[cmd]) # (unicode) parser_callback_name = '_Parse%s' % u_command handler_callback_name = 'On%s' % u_command if not hasattr(self, parser_callback_name): logging.error('No parser implemented for "%s"', u_command) return RESPONSE['CONTINUE'] if not hasattr(self.__milter, handler_callback_name): logging.warn( 'Unimplemented command in milter %s: "%s" ("%s")' % (self.__milter, u_command, data)) return RESPONSE['CONTINUE'] parser = getattr(self, parser_callback_name) callback = getattr(self.__milter, handler_callback_name) args = parser(cmd, data) return callback(*args) except PpyMilterTempFailure as e: logging.info('Temp Failure: %s', str(e)) return RESPONSE['TEMPFAIL'] except PpyMilterPermFailure as e: logging.info('Perm Failure: %s', str(e)) return RESPONSE['REJECT']
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 = int(self.config.get(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 testSMIME(self): """test if S/MIME mails still pass the signature""" # give fuglu time to start listener time.sleep(1) # send test message smtpclient = smtplib.SMTP('127.0.0.1', SMIMETestCase.FUGLU_PORT) # smtpServer.set_debuglevel(1) smtpclient.helo('test.smime') inputfile = TESTDATADIR + '/smime/signedmessage.eml' (status, output) = self.verifyOpenSSL(inputfile) self.assertTrue( status == 0, "Testdata S/MIME verification failed: \n%s" % output) msgstring = open(inputfile, 'r').read() smtpclient.sendmail( '*****@*****.**', '*****@*****.**', force_uString(msgstring)) smtpclient.quit() # verify the smtp server stored the file correctly tmpfile = self.smtp.tempfilename #self.failUnlessEqual(msgstring, tmpcontent, "SMTP Server did not store the tempfile correctly: %s"%tmpfile) (status, output) = self.verifyOpenSSL(tmpfile) self.assertTrue( status == 0, "S/MIME verification failed: \n%s\n tmpfile is:%s" % (output, tmpfile))
def lint_ping(self): """ping sa""" retries = self.config.getint(self.section, 'retries') 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(b'PING SPAMC/1.2') s.sendall(b"\r\n") s.shutdown(socket.SHUT_WR) socketfile = s.makefile("rb") line = force_uString(socketfile.readline()) line = line.strip() answer = line.split() if len(answer) != 3: print("Invalid SPAMD PONG: %s" % line) return False if answer[2] != "PONG": print("Invalid SPAMD Pong: %s" % line) return False print("Got: %s" % line) return True except socket.timeout: print('SPAMD Socket timed out.') except socket.herror as h: print('SPAMD Herror encountered : %s' % str(h)) except socket.gaierror as g: print('SPAMD gaierror encountered: %s' % str(g)) except socket.error as e: print('SPAMD socket error: %s' % str(e)) time.sleep(1) return False
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 lint_version(self): try: s = self.__init_socket__(oneshot=True) except Exception: return False s.sendall(b'VERSION') result = s.recv(20000) print("Got Version: %s" % force_uString(result)) return True
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(str(e)) time.sleep(1) return None
def handlesession(self): line = force_uString(self.socket.recv(4096)).lower().strip() if line == '': self.socket.close() return self.logger.debug('Control Socket command: %s' % line) parts = line.split() answer = self.handle_command(parts[0], parts[1:]) self.socket.sendall(force_bString(answer)) self.socket.close()
def safilter_symbols(self, messagecontent, user): """Pass content to sa, return spamflag, score, rules""" ret = self._safilter_content(messagecontent, user, 'SYMBOLS') if ret is None: return None status, score, content = ret content = force_uString(content) rules = content.split(',') return status, score, rules
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" % status) # 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 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'' data = '' completeLine = 0 while not completeLine: lump = self.socket.recv(1024) if len(lump): rawdata += lump if (len(rawdata) >= 2) and rawdata[-2:] == force_bString('\r\n'): completeLine = 1 if self.state != SMTPSession.ST_DATA: # convert data to unicode if needed data = force_uString(rawdata) rsp, keep = self.doCommand(data) else: try: #directly use raw bytes-string data 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 return True self.socket.send(force_bString(rsp + "\r\n")) if keep == 0: self.closeconn() return False else: # EOF return False
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 forwardCommand(self, command): """forward a esmtp command to outgoing postfix instance Args: command (str): command in unicode Returns (str): reply from outgoing server """ command = command.strip() if self.forwardconn is None: targethost = self.config.get('main', 'outgoinghost') if targethost == '${injecthost}': targethost = self.socket.getpeername()[0] self.forwardconn = smtplib.SMTP(force_uString(targethost), self.config.getint('main', 'outgoingport')) self.logger.debug("""SEND: "%s" """ % command) # docmd seems to have a normal string as input, so # I guess unicode will work for python 3 code, ans = self.forwardconn.docmd(command) ret = "%s %s" % (code, force_uString(ans)) if ret.find('\n'): temprv = [] parts = ret.split('\n') code = ret[:3] parts[0] = parts[0][3:] line = '' for line in parts: line = line.strip() temprv.append('%s-%s' % (code, line)) # replace - with space on last line temprv[-1] = '%s %s' % (code, line) ret = '\r\n'.join(temprv) self.logger.debug("""RECEIVE: "%s" """ % ret) return ret.strip()
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 getincomingmail(self): """return true if mail got in, false on error Session will be kept open""" self._send("220 dummy server ready \r\n") while 1: rawdata = b'' data = '' completeLine = 0 while not completeLine: lump = self.socket.recv(1024) if len(lump): rawdata += lump if (len(rawdata) >= 2) and rawdata[-2:] == '\r\n'.encode( "utf-8", "strict"): completeLine = 1 print("< %s" % data) if self.state != SMTPSession.ST_DATA: 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") return False if rsp == None: continue else: # data finished.. keep connection open though print('incoming message finished') return True self._send(rsp + "\r\n") if keep == 0: self.socket.close() return False else: # EOF return False return False
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 send_template_file(self, recipient, templatefile, suspect, values): """Send a E-Mail Bounce Message Args: recipient (str): Message recipient ([email protected]) templatefile (str): Template to use suspect (fuglu.shared.Suspect) suspect that caused the bounce values :Values to apply to the template. ensure all values are of type <str> If the suspect has the 'nobounce' tag set, the message will not be sent. The same happens if the global configuration 'disablebounces' is set. """ if not os.path.exists(templatefile): self.logger.error( 'Template file does not exist: %s' % templatefile) return with open(templatefile) as fp: filecontent = fp.read() self.send_template_string(recipient, force_uString(filecontent), suspect, values)
def _safilter_content(self, messagecontent, user, command): """pass content to sa, return body""" assert command in [ 'SYMBOLS', 'REPORT', ] 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('%s SPAMC/1.2' % command)) 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 = force_uString(socketfile.readline()) self.logger.debug(line1_info) line2_spaminfo = force_uString(socketfile.readline()) line3 = force_uString(socketfile.readline()) content = socketfile.read() content = content.strip() 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 self.logger.debug('Spamd said: %s' % line2_spaminfo) spamword, spamstatusword, colon, score, slash, required = line2_spaminfo.split( ) spstatus = False if spamstatusword == 'True': spstatus = True return spstatus, float(score), 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)) time.sleep(1) return None
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: 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 read(self, length): return force_uString(self.s.recv(length))
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__() dr = {} # Read the welcome message if not exchangeGreetings(s): raise Exception("SSSP Greeting failed") # QUERY to discover the maxclassificationsize s.send(b'SSSP/1.0 QUERY\n') if not accepted(s): raise Exception("SSSP Query rejected") options = readoptions(s) # Set the options for classification enableoptions = [ b"TnefAttachmentHandling", b"ActiveMimeHandling", b"Mime", b"ZipDecompression", b"DynamicDecompression", ] enablegroups = [ b'GrpExecutable', b'GrpArchiveUnpack', b'GrpSelfExtract', b'GrpInternet', b'GrpSuper', b'GrpMisc', ] sendbuf = "OPTIONS\nreport:all\n" for opt in enableoptions: sendbuf += "savists: %s 1\n" % force_uString(opt) for grp in enablegroups: sendbuf += "savigrp: %s 1\n" % force_uString(grp) # all sent, add aditional newline sendbuf += "\n" s.send(force_bString(sendbuf)) if not accepted(s): raise Exception("SSSP Options not accepted") resp = receivemsg(s) for l in resp: if donesyntax.match(l): parts = donesyntax.findall(l) if parts[0][0] != b'OK': raise Exception("SSSP Options failed") break # Send the SCAN request s.send(force_bString('SCANDATA ' + str(len(content)) + '\n')) if not accepted(s): raise Exception("SSSP Scan rejected") s.sendall(force_bString(content)) # and read the result events = receivemsg(s) for l in events: if virussyntax.match(l): parts = virussyntax.findall(l) virus = force_uString(parts[0][0]) filename = force_uString(parts[0][1]) dr[filename] = virus try: sayGoodbye(s) s.shutdown(socket.SHUT_RDWR) except socket.error as e: self.logger.warning('%s Error terminating connection: %s', (suspectid, str(e))) finally: s.close() if dr == {}: return None else: return dr