def endsession(self, code, message): self.socket.send(force_bString("%s %s\r\n" % (code, message))) rawdata = b'' while True: lump = self.socket.recv(1024) if len(lump): rawdata += lump if (len(rawdata) >= 2) and rawdata[-2:] == force_bString('\r\n'): cmd = rawdata[0:4] cmd = cmd.upper() if cmd == force_bString("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 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 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 examine(self, suspect): if self._check_too_big(suspect): return DUNNO try: content = suspect.get_message_rep().as_bytes() except AttributeError: content = force_bString(suspect.get_message_rep().as_string()) for i in range(0, self.config.getint(self.section, 'retries')): try: if self.config.getboolean(self.section, 'networkmode'): viruses = self.scan_stream(content, suspect.id) else: viruses = self.scan_file(suspect.tempfile) actioncode, message = self._virusreport(suspect, viruses) return actioncode, message except Exception as e: self.logger.warning( "%s Error encountered while contacting fpscand (try %s of %s): %s" % (suspect.id, i + 1, self.config.getint(self.section, 'retries'), str(e))) self.logger.error("fpscand failed after %s retries" % self.config.getint(self.section, 'retries')) return self._problemcode()
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 __send_response(self, response): """Send data down the milter socket. Args: response: the data to send """ self.socket.send(struct.pack('!I', len(response))) self.socket.send(force_bString(response))
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 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 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 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\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 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 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 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\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') while True: data = self.socket.recv(1024) if len(data) < 1: break self.tempfile.write(data) self.tempfile.close() self.logger.debug('Incoming message received') return True
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 = int(self.config.get(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 re_inject(self, suspect): """Send message back to postfix""" if suspect.get_tag('noreinject'): return 'message not re-injected by plugin request' if suspect.get_tag('reinjectoriginal'): self.logger.info( '%s: Injecting original message source without modifications' % suspect.id) msgcontent = suspect.get_original_source() else: msgcontent = buildmsgsource(suspect) targethost = self.config.get('main', 'outgoinghost') if targethost == '${injecthost}': targethost = self.socket.getpeername()[0] client = FUSMTPClient(targethost, self.config.getint('main', 'outgoingport')) helo = self.config.get('main', 'outgoinghelo') if helo.strip() == '': helo = socket.gethostname() client.helo(helo) # for sending, make sure the string to sent is byte string client.sendmail(suspect.from_address, suspect.recipients, force_bString(msgcontent)) # if we did not get an exception so far, we can grab the server answer using the patched client # servercode=client.lastservercode serveranswer = client.lastserveranswer try: client.quit() except Exception as e: self.logger.warning( 'Exception while quitting re-inject session: %s' % str(e)) if serveranswer is None: self.logger.warning('Re-inject: could not get server answer.') serveranswer = '' return serveranswer
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 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 = forceCharFromBytes(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 = forceCharFromBytes(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 = forceCharFromBytes(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( forceBytesFromChar(re.sub(r"\s+", "", sig['b']))) sigEncoded = forceCharFromBytes((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]]
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 unquoteData(self, data): """two leading dots at the beginning of a line must be unquoted to a single dot""" return re.sub(b'(?m)^\.\.', b'.', force_bString(data))
def examine(self, suspect): # check if someone wants to skip sa checks if suspect.get_tag('SAPlugin.skip') is True: self.logger.debug( '%s Skipping SA Plugin (requested by previous plugin)' % suspect.id) suspect.set_tag('SAPlugin.skipreason', 'requested by previous plugin') return DUNNO runtimeconfig = DBConfig(self.config, suspect) spamsize = suspect.size maxsize = self.config.getint(self.section, 'maxsize') strip_oversize = self.config.getboolean(self.section, 'strip_oversize') if spamsize > maxsize and not strip_oversize: self.logger.info('%s Size Skip, %s > %s' % (suspect.id, spamsize, maxsize)) suspect.debug('Too big for spamchecks. %s > %s' % (spamsize, maxsize)) prependheader = self.config.get('main', 'prependaddedheaders') suspect.addheader( "%sSA-SKIP" % prependheader, 'Too big for spamchecks. %s > %s' % (spamsize, maxsize)) suspect.set_tag('SAPlugin.skipreason', 'size skip') return self.check_sql_blacklist(suspect) if self.config.getboolean(self.section, 'scanoriginal'): content = suspect.get_original_source() else: content = suspect.get_source() stripped = False if spamsize > maxsize: stripped = True # keep copy of original content before stripping content_orig = content content = self._strip_attachments(content, maxsize) self.logger.info( '%s stripped attachments, body size reduced from %s to %s bytes' % (suspect.id, len(content_orig), len(content))) # stick to bytes content = force_bString(content) # prepend temporary headers set by other plugins tempheader = suspect.get_tag('SAPlugin.tempheader') if tempheader is not None: if isinstance(tempheader, list): tempheader = "\r\n".join(tempheader) tempheader = tempheader.strip() if tempheader != '': content = force_bString(tempheader + '\r\n') + content forwardoriginal = self.config.getboolean(self.section, 'forwardoriginal') if forwardoriginal: ret = self.safilter_report(content, suspect.to_address) if ret is None: suspect.debug('SA report Scan failed - please check error log') self.logger.error('%s SA report scan FAILED' % suspect.id) suspect.addheader( '%sSA-SKIP' % self.config.get('main', 'prependaddedheaders'), 'SA scan failed') suspect.set_tag('SAPlugin.skipreason', 'scan failed') return self._problemcode() isspam, spamscore, report = ret suspect.tags['SAPlugin.report'] = report else: filtered = self.safilter(content, suspect.to_address) if filtered is None: suspect.debug('SA Scan failed - please check error log') self.logger.error('%s SA scan FAILED' % suspect.id) suspect.addheader( '%sSA-SKIP' % self.config.get('main', 'prependaddedheaders'), 'SA scan failed') suspect.set_tag('SAPlugin.skipreason', 'scan failed') return self._problemcode() else: if stripped: # create msgrep of filtered msg msgrep_filtered = email.message_from_string(filtered) header_new = [] header_old = [] # create a msgrep from original msg msgrep_orig = email.message_from_string(content_orig) # read all headers from after-scan and before-scan for h, v in msgrep_filtered.items(): header_new.append(h.strip() + ': ' + v.strip()) for h, v in msgrep_orig.items(): header_old.append(h.strip() + ': ' + v.strip()) # create a list of headers added by spamd # header diff between before-scan and after-scan msg header_new = reversed(self.diff(header_new, header_old)) # add headers to msg for i in header_new: if re.match('^Received: ', i, re.I): continue # in case of stripped msg add header to original content content_orig = i + '\r\n' + content_orig content = content_orig else: content = filtered if sys.version_info > (3, ): # Python 3 and larger # the basic "str" type is unicode if isinstance(content, str): newmsgrep = email.message_from_string(content) else: newmsgrep = email.message_from_bytes(content) else: # Python 2.x newmsgrep = email.message_from_string(content) suspect.set_source(content) spamheadername = self.config.get(self.section, 'spamheader') isspam, spamscore, report = self._extract_spamstatus( newmsgrep, spamheadername, suspect) suspect.tags['SAPlugin.report'] = report self.logger.debug('suspect %s %s %s %s' % (suspect.id, isspam, spamscore, suspect.get_tag('SAPlugin.report'))) action = DUNNO message = None if isspam: self.logger.debug('%s Message is spam' % suspect.id) suspect.debug('Message is spam') configaction = string_to_actioncode( runtimeconfig.get(self.section, 'lowspamaction'), self.config) if configaction is not None: action = configaction values = dict(spamscore=spamscore) message = apply_template( self.config.get(self.section, 'rejectmessage'), suspect, values) else: self.logger.debug('%s Message is not spam' % suspect.id) suspect.debug('Message is not spam') suspect.tags['spam']['SpamAssassin'] = isspam suspect.tags['highspam']['SpamAssassin'] = False if spamscore is not None: suspect.tags['SAPlugin.spamscore'] = spamscore highspamlevel = runtimeconfig.getfloat(self.section, 'highspamlevel') if spamscore >= highspamlevel: suspect.tags['highspam']['SpamAssassin'] = True configaction = string_to_actioncode( runtimeconfig.get(self.section, 'highspamaction'), self.config) if configaction is not None: action = configaction return action, message
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 write(self, st): return self.s.send(force_bString(st))
def sign(message, selector, domain, privkey, identity=None, canonicalize=(Simple, Simple), include_headers=None, length=False, debuglog=None): """Sign an RFC822 message and return the DKIM-Signature header line. @param message: an RFC822 formatted message (with either \\n or \\r\\n line endings) @param selector: the DKIM selector value for the signature @param domain: the DKIM domain value for the signature @param privkey: a PKCS#1 private key in base64-encoded text form @param identity: the DKIM identity value for the signature (default "@"+domain) @param canonicalize: the canonicalization algorithms to use (default (Simple, Simple)) @param include_headers: a list of strings indicating which headers are to be signed (default all headers) @param length: true if the l= tag should be included to indicate body length (default False) @param debuglog: a file-like object to which debug info will be written (default None) """ (headers, body) = rfc822_parse(message) m = re.search("--\n(.*?)\n--", privkey, re.DOTALL) if m is None: raise KeyFormatError("Private key not found") try: pkdata = base64.b64decode(m.group(1)) pkdata = forceCharFromBytes(pkdata) except TypeError as e: raise KeyFormatError(str(e)) if debuglog is not None: print(" ".join("%02x" % ord(x) for x in pkdata), file=debuglog) pka = asn1_parse(ASN1_RSAPrivateKey, pkdata) pk = { 'version': pka[0][0], 'modulus': pka[0][1], 'publicExponent': pka[0][2], 'privateExponent': pka[0][3], 'prime1': pka[0][4], 'prime2': pka[0][5], 'exponent1': pka[0][6], 'exponent2': pka[0][7], 'coefficient': pka[0][8], } if identity is not None and not identity.endswith(domain): raise ParameterError("identity must end with domain") headers = canonicalize[0].canonicalize_headers(headers) if include_headers is None: include_headers = [x[0].lower() for x in headers] else: include_headers = [x.lower() for x in include_headers] sign_headers = [x for x in headers if x[0].lower() in include_headers] body = canonicalize[1].canonicalize_body(body) h = hashlib.sha256() h.update(force_bString(body)) bodyhash = base64.b64encode(h.digest()) bodyhash = forceCharFromBytes(bodyhash) sigfields = [ x for x in [ ('v', "1"), ('a', "rsa-sha256"), ('c', "%s/%s" % (canonicalize[0].name, canonicalize[1].name)), ('d', domain), ('i', identity or "@" + domain), length and ('l', len(body)), ('q', "dns/txt"), ('s', selector), ('t', str(int(time.time()))), ('h', " : ".join(x[0] for x in sign_headers)), ('bh', bodyhash), ('b', ""), ] if x ] sig = "DKIM-Signature: " + "; ".join("%s=%s" % x for x in sigfields) sig = fold(sig) if debuglog is not None: print("sign headers:", sign_headers + [("DKIM-Signature", " " + "; ".join("%s=%s" % x for x in sigfields))], file=debuglog) h = hashlib.sha256() for x in sign_headers: h.update(force_bString(x[0])) h.update(b":") h.update(force_bString(x[1])) h.update(force_bString(sig)) d = h.digest() d = forceCharFromBytes(d) if debuglog is not None: print("sign digest:", " ".join("%02x" % ord(x) for x in d), file=debuglog) dinfo = asn1_build((SEQUENCE, [ (SEQUENCE, [ (OBJECT_IDENTIFIER, HASHID_SHA256), (NULL, None), ]), (OCTET_STRING, d), ])) modlen = len(int2str(pk['modulus'])) if len(dinfo) + 3 > modlen: raise ParameterError("Hash too large for modulus") signature = "\x00\x01" + "\xff" * (modlen - len(dinfo) - 3) + "\x00" + dinfo sig2 = int2str( pow(str2int(signature), pk['privateExponent'], pk['modulus']), modlen) sigEncoded = base64.b64encode(forceBytesFromChar(''.join(sig2))) sigEncoded = forceCharFromBytes(sigEncoded) sig += sigEncoded return sig + "\r\n"
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
def _send(self, content): print("> %s" % content.strip()) self.socket.send(force_bString(content))
def send(self, message): self.socket.sendall(force_bString(message))