Initial commit.
This commit is contained in:
518
mail/spamassassin/lib/python/pyzor/__init__.py
Normal file
518
mail/spamassassin/lib/python/pyzor/__init__.py
Normal file
@@ -0,0 +1,518 @@
|
||||
"""networked spam-signature detection"""
|
||||
|
||||
__author__ = "Frank J. Tobin, ftobin@neverending.org"
|
||||
__version__ = "0.5.0"
|
||||
__revision__ = "$Id: __init__.py,v 1.43 2002-09-17 15:12:58 ftobin Exp $"
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import re
|
||||
import sys
|
||||
import sha
|
||||
import tempfile
|
||||
import random
|
||||
import ConfigParser
|
||||
import rfc822
|
||||
import cStringIO
|
||||
import time
|
||||
|
||||
proto_name = 'pyzor'
|
||||
proto_version = 2.0
|
||||
|
||||
|
||||
class CommError(Exception):
|
||||
"""Something in general went wrong with the transaction"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class ProtocolError(CommError):
|
||||
"""Something is wrong with talking the protocol"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class TimeoutError(CommError):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class IncompleteMessageError(ProtocolError):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class UnsupportedVersionError(ProtocolError):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class SignatureError(CommError):
|
||||
"""unknown user, signature on msg invalid,
|
||||
or not within allowed time range"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class Singleton(object):
|
||||
__slots__ = []
|
||||
def __new__(cls, *args, **kwds):
|
||||
it = cls.__dict__.get('__it__')
|
||||
if it is None:
|
||||
cls.__it__ = object.__new__(cls)
|
||||
return cls.__it__
|
||||
|
||||
|
||||
|
||||
class BasicIterator(object):
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
|
||||
class Username(str):
|
||||
user_pattern = re.compile(r'^[-\.\w]+$')
|
||||
|
||||
def __init__(self, s):
|
||||
self.validate()
|
||||
|
||||
def validate(self):
|
||||
if not self.user_pattern.match(self):
|
||||
raise ValueError, "%s is an invalid username" % self
|
||||
|
||||
|
||||
|
||||
class Opname(str):
|
||||
op_pattern = re.compile(r'^[-\.\w]+$')
|
||||
|
||||
def __init__(self, s):
|
||||
self.validate()
|
||||
|
||||
def validate(self):
|
||||
if not self.op_pattern.match(self):
|
||||
raise ValueError, "%s is an invalid username" % self
|
||||
|
||||
|
||||
|
||||
class Output(Singleton):
|
||||
do_debug = False
|
||||
quiet = False
|
||||
def __init__(self, quiet=None, debug=None):
|
||||
if quiet is not None: self.quiet = quiet
|
||||
if debug is not None: self.do_debug = debug
|
||||
def data(self, msg):
|
||||
print msg
|
||||
def warn(self, msg):
|
||||
if not self.quiet: sys.stderr.write('%s\n' % msg)
|
||||
def debug(self, msg):
|
||||
if self.do_debug: sys.stderr.write('%s\n' % msg)
|
||||
|
||||
|
||||
|
||||
class DataDigest(str):
|
||||
# hex output doubles digest size
|
||||
value_size = sha.digest_size * 2
|
||||
|
||||
def __init__(self, value):
|
||||
if len(value) != self.value_size:
|
||||
raise ValueError, "invalid digest value size"
|
||||
|
||||
|
||||
|
||||
class DataDigestSpec(list):
|
||||
"""a list of tuples, (perc_offset, length)"""
|
||||
|
||||
def validate(self):
|
||||
for t in self:
|
||||
self.validate_tuple(t)
|
||||
|
||||
def validate_tuple(t):
|
||||
(perc_offset, length) = t
|
||||
if not (0 <= perc_offset < 100):
|
||||
raise ValueError, "offset percentage out of bounds"
|
||||
if not length > 0:
|
||||
raise ValueError, "piece lengths must be positive"
|
||||
|
||||
validate_tuple = staticmethod(validate_tuple)
|
||||
|
||||
def netstring(self):
|
||||
# flattened, commified
|
||||
return ','.join(map(str, reduce(lambda x, y: x + y, self, ())))
|
||||
|
||||
def from_netstring(self, s):
|
||||
new_spec = apply(self)
|
||||
|
||||
expanded_list = s.split(',')
|
||||
if len(extended_list) % 2 != 0:
|
||||
raise ValueError, "invalid list parity"
|
||||
|
||||
for i in range(0, len(expanded_list), 2):
|
||||
perc_offset = int(expanded_list[i])
|
||||
length = int(expanded_list[i+1])
|
||||
|
||||
self.validate_tuple(perc_offset, length)
|
||||
new_spec.append((perc_offset, length))
|
||||
|
||||
return new_spec
|
||||
|
||||
from_netstring = classmethod(from_netstring)
|
||||
|
||||
|
||||
|
||||
class Message(rfc822.Message, object):
|
||||
def __init__(self, fp=None):
|
||||
if fp is None:
|
||||
fp = cStringIO.StringIO()
|
||||
|
||||
super(Message, self).__init__(fp)
|
||||
self.setup()
|
||||
|
||||
|
||||
def setup(self):
|
||||
"""called after __init__, designed to be extended"""
|
||||
pass
|
||||
|
||||
|
||||
def init_for_sending(self):
|
||||
if __debug__:
|
||||
self.ensure_complete()
|
||||
|
||||
def __str__(self):
|
||||
s = ''.join(self.headers)
|
||||
s += '\n'
|
||||
self.rewindbody()
|
||||
|
||||
# okay to slurp since we're dealing with UDP
|
||||
s += self.fp.read()
|
||||
|
||||
return s
|
||||
|
||||
def __nonzero__(self):
|
||||
# just to make sure some old code doesn't try to use this
|
||||
raise NotImplementedError
|
||||
|
||||
def ensure_complete(self):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class ThreadedMessage(Message):
|
||||
def init_for_sending(self):
|
||||
if not self.has_key('Thread'):
|
||||
self.set_thread(ThreadId.generate())
|
||||
assert self.has_key('Thread')
|
||||
|
||||
self.setdefault('PV', str(proto_version))
|
||||
super(ThreadedMessage, self).init_for_sending()
|
||||
|
||||
def ensure_complete(self):
|
||||
if not (self.has_key('PV') and self.has_key('Thread')):
|
||||
raise IncompleteMessageError, \
|
||||
"doesn't have fields for a ThreadedMessage"
|
||||
super(ThreadedMessage, self).ensure_complete()
|
||||
|
||||
def get_protocol_version(self):
|
||||
return float(self['PV'])
|
||||
|
||||
def get_thread(self):
|
||||
return ThreadId(self['Thread'])
|
||||
|
||||
def set_thread(self, i):
|
||||
typecheck(i, ThreadId)
|
||||
self['Thread'] = str(i)
|
||||
|
||||
|
||||
|
||||
class MacEnvelope(Message):
|
||||
ts_diff_max = 300
|
||||
|
||||
def ensure_complete(self):
|
||||
if not (self.has_key('User')
|
||||
and self.has_key('Time')
|
||||
and self.has_key('Sig')):
|
||||
raise IncompleteMessageError, \
|
||||
"doesn't have fields for a MacEnvelope"
|
||||
super(MacEnvelope, self).ensure_complete()
|
||||
|
||||
def get_submsg(self, factory=ThreadedMessage):
|
||||
self.rewindbody()
|
||||
return apply(factory, (self.fp,))
|
||||
|
||||
def verify_sig(self, user_key):
|
||||
typecheck(user_key, long)
|
||||
|
||||
user = Username(self['User'])
|
||||
ts = int(self['Time'])
|
||||
said_sig = self['Sig']
|
||||
hashed_user_key = self.hash_key(user_key, user)
|
||||
|
||||
if abs(time.time() - ts) > self.ts_diff_max:
|
||||
raise SignatureError, "timestamp not within allowed range"
|
||||
|
||||
msg = self.get_submsg()
|
||||
|
||||
calc_sig = self.sign_msg(hashed_user_key, ts, msg)
|
||||
|
||||
if not (calc_sig == said_sig):
|
||||
raise SignatureError, "invalid signature"
|
||||
|
||||
def wrap(self, user, key, msg):
|
||||
"""This should be used to create a MacEnvelope"""
|
||||
|
||||
typecheck(user, str)
|
||||
typecheck(msg, Message)
|
||||
typecheck(key, long)
|
||||
|
||||
env = apply(self)
|
||||
ts = int(time.time())
|
||||
|
||||
env['User'] = user
|
||||
env['Time'] = str(ts)
|
||||
env['Sig'] = self.sign_msg(self.hash_key(key, user),
|
||||
ts, msg)
|
||||
|
||||
env.fp.write(str(msg))
|
||||
|
||||
return env
|
||||
|
||||
wrap = classmethod(wrap)
|
||||
|
||||
|
||||
def hash_msg(msg):
|
||||
"""returns a digest object"""
|
||||
typecheck(msg, Message)
|
||||
|
||||
return sha.new(str(msg))
|
||||
|
||||
hash_msg = staticmethod(hash_msg)
|
||||
|
||||
|
||||
def hash_key(key, user):
|
||||
"""returns lower(H(U + ':' + lower(hex(K))))"""
|
||||
typecheck(key, long)
|
||||
typecheck(user, Username)
|
||||
|
||||
return sha.new("%s:%x" % (Username, key)).hexdigest().lower()
|
||||
|
||||
hash_key = staticmethod(hash_key)
|
||||
|
||||
|
||||
def sign_msg(self, hashed_key, ts, msg):
|
||||
"""ts is timestamp for message (epoch seconds)
|
||||
|
||||
S = H(H(M) + ':' T + ':' + K)
|
||||
M is message
|
||||
T is decimal epoch timestamp
|
||||
K is hashed_key
|
||||
|
||||
returns a digest object"""
|
||||
|
||||
typecheck(ts, int)
|
||||
typecheck(msg, Message)
|
||||
typecheck(hashed_key, str)
|
||||
|
||||
h_msg = self.hash_msg(msg)
|
||||
|
||||
return sha.new("%s:%d:%s" % (h_msg.digest(), ts, hashed_key)).hexdigest().lower()
|
||||
|
||||
sign_msg = classmethod(sign_msg)
|
||||
|
||||
|
||||
|
||||
class Response(ThreadedMessage):
|
||||
ok_code = 200
|
||||
|
||||
def ensure_complete(self):
|
||||
if not(self.has_key('Code') and self.has_key('Diag')):
|
||||
raise IncompleteMessageError, \
|
||||
"doesn't have fields for a Response"
|
||||
super(Response, self).ensure_complete()
|
||||
|
||||
def is_ok(self):
|
||||
return self.get_code() == self.ok_code
|
||||
|
||||
def get_code(self):
|
||||
return int(self['Code'])
|
||||
|
||||
def get_diag(self):
|
||||
return self['Diag']
|
||||
|
||||
def head_tuple(self):
|
||||
return (self.get_code(), self.get_diag())
|
||||
|
||||
|
||||
|
||||
class Request(ThreadedMessage):
|
||||
"""this is class that should be used to read in Requests of any type.
|
||||
subclasses are responsible for setting 'Op' if they are generating
|
||||
a message"""
|
||||
|
||||
def get_op(self):
|
||||
return self['Op']
|
||||
|
||||
def ensure_complete(self):
|
||||
if not self.has_key('Op'):
|
||||
raise IncompleteMessageError, \
|
||||
"doesn't have fields for a Request"
|
||||
super(Request, self).ensure_complete()
|
||||
|
||||
|
||||
|
||||
class ClientSideRequest(Request):
|
||||
def setup(self):
|
||||
super(Request, self).setup()
|
||||
self.setdefault('Op', self.op)
|
||||
|
||||
|
||||
|
||||
class PingRequest(ClientSideRequest):
|
||||
op = Opname('ping')
|
||||
|
||||
|
||||
class ShutdownRequest(ClientSideRequest):
|
||||
op = Opname('shutdown')
|
||||
|
||||
|
||||
class SimpleDigestBasedRequest(ClientSideRequest):
|
||||
def __init__(self, digest):
|
||||
typecheck(digest, str)
|
||||
super(SimpleDigestBasedRequest, self).__init__()
|
||||
self.setdefault('Op-Digest', digest)
|
||||
|
||||
|
||||
class CheckRequest(SimpleDigestBasedRequest):
|
||||
op = Opname('check')
|
||||
|
||||
|
||||
class InfoRequest(SimpleDigestBasedRequest):
|
||||
op = Opname('info')
|
||||
|
||||
|
||||
class SimpleDigestSpecBasedRequest(SimpleDigestBasedRequest):
|
||||
def __init__(self, digest, spec):
|
||||
typecheck(digest, str)
|
||||
typecheck(spec, DataDigestSpec)
|
||||
|
||||
super(SimpleDigestSpecBasedRequest, self).__init__(digest)
|
||||
self.setdefault('Op-Spec', spec.netstring())
|
||||
|
||||
|
||||
class ReportRequest(SimpleDigestSpecBasedRequest):
|
||||
op = Opname('report')
|
||||
|
||||
|
||||
class WhitelistRequest(SimpleDigestSpecBasedRequest):
|
||||
op = Opname('whitelist')
|
||||
|
||||
|
||||
class ErrorResponse(Response):
|
||||
def __init__(self, code, s):
|
||||
typecheck(code, int)
|
||||
typecheck(s, str)
|
||||
|
||||
super(ErrorResponse, self).__init__()
|
||||
self.setdefault('Code', str(code))
|
||||
self.setdefault('Diag', s)
|
||||
|
||||
|
||||
|
||||
class ThreadId(int):
|
||||
# (0, 1024) is reserved
|
||||
full_range = (0, 2**16)
|
||||
ok_range = (1024, full_range[1])
|
||||
error_value = 0
|
||||
|
||||
def __init__(self, i):
|
||||
super(ThreadId, self).__init__(i)
|
||||
if not (self.full_range[0] <= self < self.full_range[1]):
|
||||
raise ValueError, "value outside of range"
|
||||
|
||||
def generate(self):
|
||||
return apply(self, (apply(random.randrange, self.ok_range),))
|
||||
generate = classmethod(generate)
|
||||
|
||||
def in_ok_range(self):
|
||||
return (self >= self.ok_range[0] and self < self.ok_range[1])
|
||||
|
||||
|
||||
|
||||
class Address(tuple):
|
||||
def __init__(self, *varargs, **kwargs):
|
||||
self.validate()
|
||||
|
||||
def validate(self):
|
||||
typecheck(self[0], str)
|
||||
typecheck(self[1], int)
|
||||
if len(self) != 2:
|
||||
raise ValueError, "invalid address: %s" % str(self)
|
||||
|
||||
def __str__(self):
|
||||
return (self[0] + ':' + str(self[1]))
|
||||
|
||||
def from_str(self, s):
|
||||
fields = s.split(':')
|
||||
|
||||
fields[1] = int(fields[1])
|
||||
return self(fields)
|
||||
|
||||
from_str = classmethod(from_str)
|
||||
|
||||
|
||||
|
||||
class Config(ConfigParser.ConfigParser, object):
|
||||
|
||||
def __init__(self, homedir):
|
||||
assert isinstance(homedir, str)
|
||||
self.homedir = homedir
|
||||
super(Config, self).__init__()
|
||||
|
||||
def get_filename(self, section, option):
|
||||
fn = os.path.expanduser(self.get(section, option))
|
||||
if not os.path.isabs(fn):
|
||||
fn = os.path.join(self.homedir, fn)
|
||||
return fn
|
||||
|
||||
|
||||
def get_homedir(specified):
|
||||
homedir = os.path.join('/etc', 'pyzor')
|
||||
|
||||
if specified is not None:
|
||||
homedir = specified
|
||||
else:
|
||||
userhome = os.getenv('HOME')
|
||||
if userhome is not None:
|
||||
homedir = os.path.join(userhome, '.pyzor')
|
||||
|
||||
return homedir
|
||||
|
||||
|
||||
def typecheck(inst, type_):
|
||||
if not isinstance(inst, type_):
|
||||
raise TypeError
|
||||
|
||||
|
||||
def modglobal_apply(globs, repl, obj, varargs=(), kwargs=None):
|
||||
"""temporarily modify globals during a call.
|
||||
globs is the globals to modify (e.g., the return from globals())
|
||||
repl is a dictionary of name: value replacements for the global
|
||||
dict."""
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
|
||||
saved = {}
|
||||
for (k, v) in repl.items():
|
||||
saved[k] = globs[k]
|
||||
globs[k] = v
|
||||
|
||||
try:
|
||||
r = apply(obj, varargs, kwargs)
|
||||
finally:
|
||||
globs.update(saved)
|
||||
|
||||
return r
|
||||
|
||||
|
||||
anonymous_user = Username('anonymous')
|
||||
1031
mail/spamassassin/lib/python/pyzor/client.py
Normal file
1031
mail/spamassassin/lib/python/pyzor/client.py
Normal file
File diff suppressed because it is too large
Load Diff
626
mail/spamassassin/lib/python/pyzor/server.py
Normal file
626
mail/spamassassin/lib/python/pyzor/server.py
Normal file
@@ -0,0 +1,626 @@
|
||||
"""networked spam-signature detection server"""
|
||||
|
||||
from __future__ import division
|
||||
|
||||
import os
|
||||
import sys
|
||||
import SocketServer
|
||||
import time
|
||||
import gdbm
|
||||
import cStringIO
|
||||
import traceback
|
||||
import threading
|
||||
|
||||
import pyzor
|
||||
from pyzor import *
|
||||
|
||||
__author__ = pyzor.__author__
|
||||
__version__ = pyzor.__version__
|
||||
__revision__ = "$Id: server.py,v 1.29 2002-10-09 00:45:45 ftobin Exp $"
|
||||
|
||||
|
||||
class AuthorizationError(pyzor.CommError):
|
||||
"""signature was valid, but not permitted to
|
||||
do the requested action"""
|
||||
pass
|
||||
|
||||
|
||||
class ACL(object):
|
||||
__slots__ = ['entries']
|
||||
default_allow = False
|
||||
|
||||
def __init__(self):
|
||||
self.entries = []
|
||||
|
||||
def add_entry(self, entry):
|
||||
typecheck(entry, ACLEntry)
|
||||
self.entries.append(entry)
|
||||
|
||||
def allows(self, user, op):
|
||||
typecheck(user, Username)
|
||||
typecheck(op, Opname)
|
||||
|
||||
for entry in self.entries:
|
||||
if entry.allows(user, op):
|
||||
return True
|
||||
if entry.denies(user, op):
|
||||
return False
|
||||
return self.default_allow
|
||||
|
||||
|
||||
class ACLEntry(tuple):
|
||||
all_keyword = 'all'.lower()
|
||||
|
||||
def __init__(self, v):
|
||||
(user, op, allow) = v
|
||||
typecheck(user, Username)
|
||||
typecheck(op, Opname)
|
||||
assert bool(allow) == allow
|
||||
|
||||
def user(self):
|
||||
return self[0]
|
||||
user = property(user)
|
||||
|
||||
def op(self):
|
||||
return self[1]
|
||||
op = property(op)
|
||||
|
||||
def allow(self):
|
||||
return self[2]
|
||||
allow = property(allow)
|
||||
|
||||
def allows(self, user, op):
|
||||
return self._says(user, op, True)
|
||||
|
||||
def denies(self, user, op):
|
||||
return self._says(user, op, False)
|
||||
|
||||
def _says(self, user, op, allow):
|
||||
"""If allow is True, we return true if and only if we allow user to do op.
|
||||
If allow is False, we return true if and only if we deny user to do op
|
||||
"""
|
||||
typecheck(user, Username)
|
||||
typecheck(op, Opname)
|
||||
assert bool(allow) == allow
|
||||
|
||||
return (self.allow == allow
|
||||
and (self.user == user
|
||||
or self.user.lower() == self.all_keyword)
|
||||
and (self.op == op
|
||||
or self.op.lower() == self.all_keyword))
|
||||
|
||||
|
||||
|
||||
class AccessFile(object):
|
||||
# I started doing an iterator protocol for this, but it just
|
||||
# got too complicated keeping track of everything on the line
|
||||
__slots__ = ['file', 'output', 'lineno']
|
||||
allow_keyword = 'allow'
|
||||
deny_keyword = 'deny'
|
||||
|
||||
def __init__(self, f):
|
||||
self.output = Output()
|
||||
self.file = f
|
||||
self.lineno = 0
|
||||
|
||||
def feed_into(self, acl):
|
||||
typecheck(acl, ACL)
|
||||
|
||||
for orig_line in self.file:
|
||||
self.lineno += 1
|
||||
|
||||
line = orig_line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
|
||||
parts = line.split(':')
|
||||
|
||||
if len(parts) != 3:
|
||||
self.output.warn("access file: invalid number of parts in line %d"
|
||||
% self.lineno)
|
||||
continue
|
||||
|
||||
(ops_str, users_str, allow_str) = parts
|
||||
|
||||
ops = []
|
||||
for op_str in ops_str.split():
|
||||
try:
|
||||
op = Opname(op_str)
|
||||
except ValueError, e:
|
||||
self.output.warn("access file: invalid opname %s line %d: %s"
|
||||
% (repr(op_str), self.lineno, e))
|
||||
else:
|
||||
ops.append(op)
|
||||
|
||||
users = []
|
||||
for u in users_str.split():
|
||||
try:
|
||||
user = Username(u)
|
||||
except ValueError, e:
|
||||
self.output.warn("access file: invalid username %s line %d: %s"
|
||||
% (repr(u), self.lineno, e))
|
||||
else:
|
||||
users.append(user)
|
||||
|
||||
allow_str = allow_str.strip()
|
||||
if allow_str.lower() == self.allow_keyword:
|
||||
allow = True
|
||||
elif allow_str.lower() == self.deny_keyword:
|
||||
allow = False
|
||||
else:
|
||||
self.output.warn("access file: invalid allow/deny keyword %s line %d"
|
||||
% (repr(allow_str), self.lineno))
|
||||
continue
|
||||
|
||||
for op in ops:
|
||||
for user in users:
|
||||
acl.add_entry(ACLEntry((user, op, allow)))
|
||||
|
||||
|
||||
|
||||
class Passwd(dict):
|
||||
def __setitem__(self, k, v):
|
||||
typecheck(k, pyzor.Username)
|
||||
typecheck(v, long)
|
||||
super(Passwd, self).__setitem__(k, v)
|
||||
|
||||
|
||||
|
||||
class PasswdFile(BasicIterator):
|
||||
"""Iteration gives (Username, long) objects
|
||||
|
||||
Format of file is:
|
||||
user : key
|
||||
"""
|
||||
__slots__ = ['file', 'output', 'lineno']
|
||||
|
||||
def __init__(self, f):
|
||||
self.file = f
|
||||
self.output = Output()
|
||||
self.lineno = 0
|
||||
|
||||
|
||||
def next(self):
|
||||
while True:
|
||||
orig_line = self.file.readline()
|
||||
self.lineno += 1
|
||||
|
||||
if not orig_line:
|
||||
raise StopIteration
|
||||
|
||||
line = orig_line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
fields = line.split(':')
|
||||
fields = map(lambda x: x.strip(), fields)
|
||||
|
||||
if len(fields) != 2:
|
||||
self.output.warn("passwd line %d is invalid (wrong number of parts)"
|
||||
% self.lineno)
|
||||
continue
|
||||
|
||||
try:
|
||||
return (Username(fields[0]), long(fields[1], 16))
|
||||
except ValueError, e:
|
||||
self.output.warn("invalid passwd entry line %d: %s"
|
||||
% (self.lineno, e))
|
||||
|
||||
|
||||
|
||||
class Log(object):
|
||||
__slots__ = ['fp']
|
||||
|
||||
def __init__(self, fp=None):
|
||||
self.fp = fp
|
||||
|
||||
def log(self, address, user=None, command=None, arg=None, code=None):
|
||||
# we don't use defaults because we want to be able
|
||||
# to pass in None
|
||||
if user is None: user = ''
|
||||
if command is None: command = ''
|
||||
if arg is None: arg = ''
|
||||
if code is None: code = -1
|
||||
|
||||
# We duplicate the time field merely so that
|
||||
# humans can peruse through the entries without processing
|
||||
ts = int(time.time())
|
||||
if self.fp is not None:
|
||||
self.fp.write("%s\n" %
|
||||
','.join((("%d" % ts),
|
||||
time.ctime(ts),
|
||||
user,
|
||||
address[0],
|
||||
command,
|
||||
repr(arg),
|
||||
("%d" % code)
|
||||
)))
|
||||
self.fp.flush()
|
||||
|
||||
|
||||
|
||||
class Record(object):
|
||||
"""Prefix conventions used in this class:
|
||||
r = report (spam)
|
||||
wl = whitelist
|
||||
"""
|
||||
|
||||
__slots__ = ['r_count', 'r_entered', 'r_updated',
|
||||
'wl_count', 'wl_entered', 'wl_updated',
|
||||
]
|
||||
fields = ('r_count', 'r_entered', 'r_updated',
|
||||
'wl_count', 'wl_entered', 'wl_updated',
|
||||
)
|
||||
this_version = '1'
|
||||
|
||||
# epoch seconds
|
||||
never = -1
|
||||
|
||||
def __init__(self, r_count=0, wl_count=0):
|
||||
self.r_count = r_count
|
||||
self.wl_count = wl_count
|
||||
|
||||
self.r_entered = self.never
|
||||
self.r_updated = self.never
|
||||
|
||||
self.wl_entered = self.never
|
||||
self.wl_updated = self.never
|
||||
|
||||
def wl_increment(self):
|
||||
# overflow prevention
|
||||
if self.wl_count < sys.maxint:
|
||||
self.wl_count += 1
|
||||
if self.wl_entered == self.never:
|
||||
self.wl_entered = int(time.time())
|
||||
self.wl_update()
|
||||
|
||||
def r_increment(self):
|
||||
# overflow prevention
|
||||
if self.r_count < sys.maxint:
|
||||
self.r_count += 1
|
||||
if self.r_entered == self.never:
|
||||
self.r_entered = int(time.time())
|
||||
self.r_update()
|
||||
|
||||
def r_update(self):
|
||||
self.r_updated = int(time.time())
|
||||
|
||||
def wl_update(self):
|
||||
self.wl_updated = int(time.time())
|
||||
|
||||
def __str__(self):
|
||||
return "%s,%d,%d,%d,%d,%d,%d" \
|
||||
% ((self.this_version,)
|
||||
+ tuple(map(lambda x: getattr(self, x), self.fields)))
|
||||
|
||||
|
||||
def from_str(self, s):
|
||||
parts = s.split(',')
|
||||
dispatch = None
|
||||
|
||||
version = parts[0]
|
||||
|
||||
if len(parts) == 3:
|
||||
dispatch = self.from_str_0
|
||||
elif version == '1':
|
||||
dispatch = self.from_str_1
|
||||
else:
|
||||
raise StandardError, ("don't know how to handle db value %s"
|
||||
% repr(s))
|
||||
|
||||
return apply(dispatch, (s,))
|
||||
|
||||
from_str = classmethod(from_str)
|
||||
|
||||
|
||||
def from_str_0(self, s):
|
||||
r = Record()
|
||||
parts = s.split(',')
|
||||
|
||||
fields = ('r_count', 'r_entered', 'r_updated')
|
||||
assert len(parts) == len(fields)
|
||||
|
||||
for i in range(len(parts)):
|
||||
setattr(r, fields[i], int(parts[i]))
|
||||
|
||||
return r
|
||||
|
||||
from_str_0 = classmethod(from_str_0)
|
||||
|
||||
|
||||
def from_str_1(self, s):
|
||||
r = Record()
|
||||
parts = s.split(',')[1:]
|
||||
|
||||
assert len(parts) == len(self.fields)
|
||||
|
||||
for i in range(len(parts)):
|
||||
setattr(r, self.fields[i], int(parts[i]))
|
||||
|
||||
return r
|
||||
|
||||
from_str_1 = classmethod(from_str_1)
|
||||
|
||||
|
||||
|
||||
class DBHandle(Singleton):
|
||||
__slots__ = ['output', 'initialized']
|
||||
db_lock = threading.Lock()
|
||||
max_age = 3600*24*30*4 # 3 months
|
||||
db = None
|
||||
sync_period = 60
|
||||
reorganize_period = 3600*24 # 1 day
|
||||
|
||||
def __init__(self):
|
||||
assert self.db is not None, "database was not initialized"
|
||||
|
||||
def initialize(self, fn, mode):
|
||||
self.output = Output()
|
||||
self.db = gdbm.open(fn, mode)
|
||||
self.start_reorganizing()
|
||||
self.start_syncing()
|
||||
initialize = classmethod(initialize)
|
||||
|
||||
def apply_locking_method(self, method, varargs=(), kwargs={}):
|
||||
# just so we don't carry around a mutable kwargs
|
||||
if kwargs == {}:
|
||||
kwargs = {}
|
||||
self.output.debug("acquiring lock")
|
||||
self.db_lock.acquire()
|
||||
self.output.debug("acquired lock")
|
||||
try:
|
||||
result = apply(method, varargs, kwargs)
|
||||
finally:
|
||||
self.output.debug("releasing lock")
|
||||
self.db_lock.release()
|
||||
self.output.debug("released lock")
|
||||
return result
|
||||
apply_locking_method = classmethod(apply_locking_method)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.apply_locking_method(self._really_getitem, (key,))
|
||||
|
||||
def _really_getitem(self, key):
|
||||
return self.db[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.apply_locking_method(self._really_setitem, (key, value))
|
||||
|
||||
def _really_setitem(self, key, value):
|
||||
self.db[key] = value
|
||||
|
||||
def start_syncing(self):
|
||||
self.apply_locking_method(self._really_sync)
|
||||
self.sync_timer = threading.Timer(self.sync_period,
|
||||
self.start_syncing)
|
||||
self.sync_timer.start()
|
||||
start_syncing = classmethod(start_syncing)
|
||||
|
||||
def _really_sync(self):
|
||||
self.db.sync()
|
||||
_really_sync = classmethod(_really_sync)
|
||||
|
||||
def start_reorganizing(self):
|
||||
self.apply_locking_method(self._really_reorganize)
|
||||
self.reorganize_timer = threading.Timer(self.reorganize_period,
|
||||
self.start_reorganizing)
|
||||
self.reorganize_timer.start()
|
||||
start_reorganizing = classmethod(start_reorganizing)
|
||||
|
||||
def _really_reorganize(self):
|
||||
self.output.debug("reorganizing the database")
|
||||
key = self.db.firstkey()
|
||||
breakpoint = time.time() - self.max_age
|
||||
|
||||
while key is not None:
|
||||
rec = Record.from_str(self.db[key])
|
||||
delkey = None
|
||||
if rec.r_updated < breakpoint:
|
||||
self.output.debug("deleting key %s" % key)
|
||||
delkey = key
|
||||
key = self.db.nextkey(key)
|
||||
if delkey:
|
||||
del self.db[delkey]
|
||||
self.db.reorganize()
|
||||
_really_reorganize = classmethod(_really_reorganize)
|
||||
|
||||
|
||||
class Server(SocketServer.ThreadingUDPServer, object):
|
||||
max_packet_size = 8192
|
||||
time_diff_allowance = 180
|
||||
|
||||
def __init__(self, address, log):
|
||||
typecheck(log, Log)
|
||||
self.output = Output()
|
||||
RequestHandler.output = self.output
|
||||
RequestHandler.log = log
|
||||
|
||||
self.output.debug('listening on %s' % str(address))
|
||||
super(Server, self).__init__(address, RequestHandler)
|
||||
|
||||
def serve_forever(self):
|
||||
self.pid = os.getpid()
|
||||
super(Server, self).serve_forever()
|
||||
|
||||
def replace_log(self, newlog):
|
||||
typecheck(newlog, Log)
|
||||
RequestHandler.log = newlog
|
||||
self.output.debug("changing logfile")
|
||||
|
||||
|
||||
class RequestHandler(SocketServer.DatagramRequestHandler, object):
|
||||
def setup(self):
|
||||
super(RequestHandler, self).setup()
|
||||
|
||||
# This is to work around a bug in current versions
|
||||
# of Python. The bug has been reported, and fixed
|
||||
# in Python's CVS.
|
||||
self.wfile = cStringIO.StringIO()
|
||||
|
||||
self.client_address = Address(self.client_address)
|
||||
|
||||
self.out_msg = Response()
|
||||
self.user = None
|
||||
self.op = None
|
||||
self.op_arg = None
|
||||
self.out_code = None
|
||||
self.msg_thread = None
|
||||
|
||||
|
||||
def handle(self):
|
||||
try:
|
||||
self._really_handle()
|
||||
except UnsupportedVersionError, e:
|
||||
self.handle_error(505, "Version Not Supported: %s" % e)
|
||||
except NotImplementedError, e:
|
||||
self.handle_error(501, "Not implemented: %s" % e)
|
||||
except (ProtocolError, KeyError), e:
|
||||
# We assume that KeyErrors are due to not
|
||||
# finding a key in the RFC822 message
|
||||
self.handle_error(400, "Bad request: %s" % e)
|
||||
except AuthorizationError, e:
|
||||
self.handle_error(401, "Unauthorized: %s" % e)
|
||||
except SignatureError, e:
|
||||
self.handle_error(401, "Unauthorized, Signature Error: %s" % e)
|
||||
except Exception, e:
|
||||
self.handle_error(500, "Internal Server Error: %s" % e)
|
||||
traceback.print_exc()
|
||||
|
||||
self.out_msg.setdefault('Code', str(self.out_msg.ok_code))
|
||||
self.out_msg.setdefault('Diag', 'OK')
|
||||
self.out_msg.init_for_sending()
|
||||
|
||||
self.log.log(self.client_address, self.user, self.op, self.op_arg,
|
||||
int(self.out_msg['Code']))
|
||||
|
||||
msg_str = str(self.out_msg)
|
||||
self.output.debug("sending: %s" % repr(msg_str))
|
||||
self.wfile.write(msg_str)
|
||||
|
||||
|
||||
def _really_handle(self):
|
||||
"""handle() without the exception handling"""
|
||||
|
||||
self.output.debug("received: %s" % repr(self.packet))
|
||||
|
||||
signed_msg = MacEnvelope(self.rfile)
|
||||
|
||||
self.user = Username(signed_msg['User'])
|
||||
|
||||
if self.user != pyzor.anonymous_user:
|
||||
if self.server.passwd.has_key(self.user):
|
||||
signed_msg.verify_sig(self.server.passwd[self.user])
|
||||
else:
|
||||
raise SignatureError, "unknown user"
|
||||
|
||||
self.in_msg = signed_msg.get_submsg(pyzor.Request)
|
||||
|
||||
self.msg_thread = self.in_msg.get_thread()
|
||||
|
||||
# We take the int() of the proto versions because
|
||||
# if the int()'s are the same, then they should be compatible
|
||||
if int(self.in_msg.get_protocol_version()) != int(proto_version):
|
||||
raise UnsupportedVersionError
|
||||
|
||||
self.out_msg.set_thread(self.msg_thread)
|
||||
|
||||
self.op = Opname(self.in_msg.get_op())
|
||||
if not self.server.acl.allows(self.user, self.op):
|
||||
raise AuthorizationError, "user is unauthorized to request the operation"
|
||||
|
||||
self.output.debug("got a %s command from %s" %
|
||||
(self.op, self.client_address))
|
||||
|
||||
|
||||
if not self.dispatches.has_key(self.op):
|
||||
raise NotImplementedError, "requested operation is not implemented"
|
||||
|
||||
dispatch = self.dispatches[self.op]
|
||||
if dispatch is not None:
|
||||
apply(dispatch, (self,))
|
||||
|
||||
|
||||
def handle_error(self, code, s):
|
||||
self.out_msg = ErrorResponse(code, s)
|
||||
|
||||
if self.msg_thread is None:
|
||||
self.out_msg.set_thread(ThreadId(0))
|
||||
else:
|
||||
self.out_msg.set_thread(self.msg_thread)
|
||||
|
||||
|
||||
def handle_check(self):
|
||||
digest = self.in_msg['Op-Digest']
|
||||
self.op_arg = digest
|
||||
self.output.debug("request to check digest %s" % digest)
|
||||
|
||||
db = DBHandle()
|
||||
try:
|
||||
rec = Record.from_str(db[digest])
|
||||
r_count = rec.r_count
|
||||
wl_count = rec.wl_count
|
||||
except KeyError:
|
||||
r_count = 0
|
||||
wl_count = 0
|
||||
|
||||
self.out_msg['Count'] = "%d" % r_count
|
||||
self.out_msg['WL-Count'] = "%d" % wl_count
|
||||
|
||||
|
||||
def handle_report(self):
|
||||
digest = self.in_msg['Op-Digest']
|
||||
self.op_arg = digest
|
||||
self.output.debug("request to report digest %s" % digest)
|
||||
|
||||
db = DBHandle()
|
||||
try:
|
||||
rec = Record.from_str(db[digest])
|
||||
except KeyError:
|
||||
rec = Record()
|
||||
rec.r_increment()
|
||||
db[digest] = str(rec)
|
||||
|
||||
|
||||
def handle_whitelist(self):
|
||||
digest = self.in_msg['Op-Digest']
|
||||
self.op_arg = digest
|
||||
self.output.debug("request to whitelist digest %s" % digest)
|
||||
|
||||
db = DBHandle()
|
||||
try:
|
||||
rec = Record.from_str(db[digest])
|
||||
except KeyError:
|
||||
rec = Record()
|
||||
rec.wl_increment()
|
||||
db[digest] = str(rec)
|
||||
|
||||
|
||||
def handle_info(self):
|
||||
digest = self.in_msg['Op-Digest']
|
||||
self.op_arg = digest
|
||||
self.output.debug("request to check digest %s" % digest)
|
||||
|
||||
db = DBHandle()
|
||||
try:
|
||||
record = Record.from_str(db[digest])
|
||||
except KeyError:
|
||||
record = Record()
|
||||
|
||||
r_count = record.r_count
|
||||
wl_count = record.wl_count
|
||||
|
||||
self.out_msg['Entered'] = "%d" % record.r_entered
|
||||
self.out_msg['Updated'] = "%d" % record.r_updated
|
||||
|
||||
self.out_msg['WL-Entered'] = "%d" % record.wl_entered
|
||||
self.out_msg['WL-Updated'] = "%d" % record.wl_updated
|
||||
|
||||
self.out_msg['Count'] = "%d" % r_count
|
||||
self.out_msg['WL-Count'] = "%d" % wl_count
|
||||
|
||||
|
||||
dispatches = { 'check': handle_check,
|
||||
'report': handle_report,
|
||||
'ping': None,
|
||||
'info': handle_info,
|
||||
'whitelist': handle_whitelist,
|
||||
}
|
||||
Reference in New Issue
Block a user