310 lines
11 KiB
Python
Executable File
310 lines
11 KiB
Python
Executable File
#! /usr/bin/env python
|
|
|
|
"""Pyzor client."""
|
|
|
|
import os
|
|
import sys
|
|
import email
|
|
import random
|
|
import mailbox
|
|
import hashlib
|
|
import getpass
|
|
import logging
|
|
import optparse
|
|
import tempfile
|
|
import ConfigParser
|
|
|
|
import pyzor.digest
|
|
import pyzor.client
|
|
import pyzor.config
|
|
|
|
def load_configuration():
|
|
"""Load the configuration for the server.
|
|
|
|
The configuration comes from three sources: the default values, the
|
|
configuration file, and command-line options."""
|
|
# Work out the default directory for configuration files.
|
|
# If $HOME is defined, then use $HOME/.pyzor, otherwise use /etc/pyzor.
|
|
userhome = os.getenv("HOME")
|
|
if userhome:
|
|
homedir = os.path.join(userhome, '.pyzor')
|
|
else:
|
|
homedir = os.path.join("/etc", "pyzor")
|
|
|
|
# Configuration defaults. The configuration file overrides these, and
|
|
# then the command-line options override those.
|
|
defaults = {
|
|
"ServersFile" : "servers",
|
|
"AccountsFile" : "accounts",
|
|
"LogFile" : "",
|
|
"Timeout" : "5", # seconds
|
|
"Style" : "msg",
|
|
"ReportThreshold" : "0",
|
|
"WhitelistThreshold" : "0"
|
|
}
|
|
|
|
# Process any command line options.
|
|
description = ("Read data from stdin and execute the requested command "
|
|
"(one of 'check', 'report', 'ping', 'pong', 'digest', "
|
|
"'predigest', 'genkey').")
|
|
opt = optparse.OptionParser(description=description)
|
|
opt.add_option("-n", "--nice", dest="nice", type="int",
|
|
help="'nice' level", default=0)
|
|
opt.add_option("-d", "--debug", action="store_true", default=False,
|
|
dest="debug", help="enable debugging output")
|
|
opt.add_option("--homedir", action="store", default=homedir,
|
|
dest="homedir", help="configuration directory")
|
|
opt.add_option("-s", "--style", action="store",
|
|
dest="Style", default=None,
|
|
help="input style: 'msg' (individual RFC5321 message), "
|
|
"'mbox' (mbox file of messages), 'digests' (Pyzor "
|
|
"digests, one per line).")
|
|
opt.add_option("--log-file", action="store", default=None,
|
|
dest="LogFile", help="name of log file")
|
|
opt.add_option("--servers-file", action="store", default=None,
|
|
dest="ServersFile", help="name of servers file")
|
|
opt.add_option("--accounts-file", action="store", default=None,
|
|
dest="AccountsFile", help="name of accounts file")
|
|
opt.add_option("-t", "--timeout", dest="Timeout", type="int",
|
|
help="timeout (in seconds)", default=None)
|
|
opt.add_option("-r", "--report-threshold", dest="ReportThreshold",
|
|
type="int", default=None,
|
|
help="threshold for number of reports")
|
|
opt.add_option("-w", "--whitelist-threshold", dest="WhitelistThreshold",
|
|
type="int", default=None,
|
|
help="threshold for number of whitelist")
|
|
opt.add_option("-V", "--version", action="store_true", default=False,
|
|
dest="version", help="print version and exit")
|
|
options, args = opt.parse_args()
|
|
|
|
if options.version:
|
|
print "%s %s" % (sys.argv[0], pyzor.__version__)
|
|
sys.exit(0)
|
|
|
|
if not len(args):
|
|
opt.print_help()
|
|
sys.exit()
|
|
os.nice(options.nice)
|
|
|
|
# Create the configuration directory if it doesn't already exist.
|
|
if not os.path.exists(options.homedir):
|
|
os.mkdir(options.homedir)
|
|
|
|
# Load the configuration.
|
|
config = ConfigParser.ConfigParser()
|
|
# Set the defaults.
|
|
config.add_section("client")
|
|
for key, value in defaults.iteritems():
|
|
config.set("client", key, value)
|
|
# Override with the configuration.
|
|
config.read(os.path.join(options.homedir, "config"))
|
|
# Override with the command-line options.
|
|
for key in defaults:
|
|
value = getattr(options, key)
|
|
if value is not None:
|
|
config.set("client", key, str(value))
|
|
return config, options, args
|
|
|
|
def main():
|
|
"""Execute any requested actions."""
|
|
# Set umask - this restricts this process from granting any world access
|
|
# to files/directories created by this process.
|
|
os.umask(0077)
|
|
|
|
config, options, args = load_configuration()
|
|
|
|
homefiles = ["LogFile", "ServersFile", "AccountsFile"]
|
|
pyzor.config.expand_homefiles(homefiles, "client", options.homedir, config)
|
|
|
|
logger = pyzor.config.setup_logging("pyzor",
|
|
config.get("client", "LogFile"),
|
|
options.debug)
|
|
servers = pyzor.config.load_servers(config.get("client", "ServersFile"))
|
|
accounts = pyzor.config.load_accounts(config.get("client", "AccountsFile"))
|
|
|
|
# Run the specified commands.
|
|
client = pyzor.client.Client(accounts,
|
|
int(config.get("client", "Timeout")))
|
|
for command in args:
|
|
try:
|
|
dispatch = DISPATCHES[command]
|
|
except KeyError:
|
|
logger.error("Unknown command: %s", command)
|
|
else:
|
|
try:
|
|
if not dispatch(client, servers, config):
|
|
sys.exit(1)
|
|
except pyzor.TimeoutError:
|
|
# Note that most of the methods will trap their own timeout
|
|
# error.
|
|
logger.error("Timeout from server in %s", command)
|
|
|
|
def get_input_handler(style="msg", digester=pyzor.digest.DataDigester):
|
|
"""Return an object that can be iterated over to get all the digests."""
|
|
if style not in ("msg", "mbox", "digests"):
|
|
raise ValueError("Unknown input style.")
|
|
if style == "digests":
|
|
for line in sys.stdin:
|
|
yield line.strip()
|
|
return
|
|
|
|
if style == "msg":
|
|
tfile = None
|
|
msg = email.message_from_file(sys.stdin)
|
|
mbox = [msg]
|
|
elif style == 'mbox':
|
|
# We have to write the mbox to disk in order to use mailbox to work
|
|
# with it.
|
|
tfile = tempfile.NamedTemporaryFile()
|
|
tfile.write(sys.stdin.read().encode("utf8"))
|
|
tfile.seek(0)
|
|
mbox = mailbox.mbox(tfile.name)
|
|
|
|
for msg in mbox:
|
|
digested = digester(msg).value
|
|
if digested:
|
|
yield digested
|
|
if tfile:
|
|
tfile.close()
|
|
|
|
def ping(client, servers, config):
|
|
"""Check that the server is reachable."""
|
|
# pylint: disable-msg=W0613
|
|
runner = pyzor.client.ClientRunner(client.ping)
|
|
for server in servers:
|
|
runner.run(server, (server,))
|
|
return runner.all_ok
|
|
|
|
def pong(client, servers, config):
|
|
"""Used to test pyzor."""
|
|
rt = int(config.get("client", "ReportThreshold"))
|
|
wt = int(config.get("client", "WhitelistThreshold"))
|
|
style = config.get("client", "Style")
|
|
runner = pyzor.client.CheckClientRunner(client.pong, rt, wt)
|
|
for digested in get_input_handler(style):
|
|
if digested:
|
|
for server in servers:
|
|
runner.run(server, (digested, server))
|
|
return runner.all_ok and runner.found_hit and not runner.whitelisted
|
|
|
|
def info(client, servers, config):
|
|
"""Get information about each message."""
|
|
style = config.get("client", "Style")
|
|
runner = pyzor.client.InfoClientRunner(client.info)
|
|
for digested in get_input_handler(style):
|
|
if digested:
|
|
for server in servers:
|
|
runner.run(server, (digested, server))
|
|
return runner.all_ok
|
|
|
|
def check(client, servers, config):
|
|
"""Check each message against each server.
|
|
|
|
The return value is 'failure' if there is a positive spam count and
|
|
*zero* whitelisted count; otherwise 'success'.
|
|
"""
|
|
rt = int(config.get("client", "ReportThreshold"))
|
|
wt = int(config.get("client", "WhitelistThreshold"))
|
|
style = config.get("client", "Style")
|
|
runner = pyzor.client.CheckClientRunner(client.check, rt, wt)
|
|
for digested in get_input_handler(style):
|
|
if digested:
|
|
for server in servers:
|
|
runner.run(server, (digested, server))
|
|
return runner.all_ok and runner.found_hit and not runner.whitelisted
|
|
|
|
def send_digest(digested, spec, client_method, servers):
|
|
"""Send these digests to each server."""
|
|
# Digest can be None; if so, nothing is sent.
|
|
if not digested:
|
|
return
|
|
runner = pyzor.client.ClientRunner(client_method)
|
|
for server in servers:
|
|
runner.run(server, (digested, server, spec))
|
|
return runner.all_ok
|
|
|
|
def report(client, servers, config):
|
|
"""Report each message as spam."""
|
|
style = config.get("client", "Style")
|
|
all_ok = True
|
|
for digested in get_input_handler(style):
|
|
if digested and not send_digest(digested, pyzor.digest.digest_spec,
|
|
client.report, servers):
|
|
all_ok = False
|
|
return all_ok
|
|
|
|
def whitelist(client, servers, config):
|
|
"""Report each message as ham."""
|
|
style = config.get("client", "Style")
|
|
all_ok = True
|
|
for digested in get_input_handler(style):
|
|
if digested and not send_digest(digested, pyzor.digest.digest_spec,
|
|
client.whitelist, servers):
|
|
all_ok = False
|
|
return all_ok
|
|
|
|
def digest(client, servers, config):
|
|
"""Generate a digest for each message.
|
|
|
|
This method can be used to look up digests in the database when
|
|
diagnosing, or to report digests in a two-stage operation (digest,
|
|
then report with --digests)."""
|
|
style = config.get("client", "Style")
|
|
for digested in get_input_handler(style):
|
|
if digested:
|
|
print digested
|
|
return True
|
|
|
|
def predigest(client, servers, config):
|
|
"""Output the normalised version of each message, which is used to
|
|
create the digest.
|
|
|
|
This method can be used to diagnose which parts of the message are
|
|
used to determine uniqueness."""
|
|
for unused in get_input_handler(
|
|
"msg", digester=pyzor.digest.PrintingDataDigester):
|
|
pass
|
|
return True
|
|
|
|
def genkey(client, servers, config, hash_func=hashlib.sha1):
|
|
"""Generate a key to use to authenticate pyzor requests. This method
|
|
will prompt for a password (and confirmation).
|
|
|
|
A random salt is generated (which makes it extremely difficult to
|
|
reverse the generated key to get the original password) and combined
|
|
with the entered password to provide a key. This key (but not the salt)
|
|
should be provided to the pyzord administrator, along with a username.
|
|
"""
|
|
# pylint: disable-msg=W0613
|
|
password = getpass.getpass(prompt="Enter passphrase: ")
|
|
if getpass.getpass(prompt="Enter passphrase again: ") != password:
|
|
log = logging.getLogger("pyzor")
|
|
log.error("Passwords do not match.")
|
|
return False
|
|
# pylint: disable-msg=W0612
|
|
salt = "".join([chr(random.randint(0, 255))
|
|
for unused in xrange(hash_func(b"").digest_size)])
|
|
if sys.version_info >= (3, 0):
|
|
salt = salt.encode("utf8")
|
|
salt_digest = hash_func(salt)
|
|
pass_digest = hash_func(salt_digest.digest())
|
|
pass_digest.update(password.encode("utf8"))
|
|
print "salt,key:"
|
|
print "%s,%s" % (salt_digest.hexdigest(), pass_digest.hexdigest())
|
|
return True
|
|
|
|
DISPATCHES = {
|
|
"ping" : ping,
|
|
"pong" : pong,
|
|
"info" : info,
|
|
"check" : check,
|
|
"report" : report,
|
|
"whitelist" : whitelist,
|
|
"digest" : digest,
|
|
"predigest" : predigest,
|
|
"genkey" : genkey,
|
|
}
|
|
|
|
if __name__ == "__main__":
|
|
main()
|