Esempio n. 1
0
    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
Esempio n. 2
0
    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)
Esempio n. 3
0
    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
Esempio n. 4
0
    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
Esempio n. 5
0
    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()
Esempio n. 6
0
    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))
Esempio n. 7
0
    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))
Esempio n. 8
0
    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
Esempio n. 9
0
    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()
Esempio n. 10
0
    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
Esempio n. 11
0
 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
Esempio n. 12
0
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
Esempio n. 13
0
    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)
Esempio n. 14
0
    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
Esempio n. 15
0
    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
Esempio n. 16
0
    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
Esempio n. 17
0
    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))
Esempio n. 18
0
    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
Esempio n. 19
0
    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
Esempio n. 20
0
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]]
Esempio n. 21
0
    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
Esempio n. 22
0
 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))
Esempio n. 23
0
    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
Esempio n. 24
0
    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
Esempio n. 25
0
 def write(self, st):
     return self.s.send(force_bString(st))
Esempio n. 26
0
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"
Esempio n. 27
0
    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
Esempio n. 28
0
 def _send(self, content):
     print("> %s" % content.strip())
     self.socket.send(force_bString(content))
Esempio n. 29
0
 def send(self, message):
     self.socket.sendall(force_bString(message))