221 lines
7.9 KiB
Python
221 lines
7.9 KiB
Python
"""Functions that handle parsing pyzor configuration files."""
|
|
|
|
import os
|
|
import re
|
|
import logging
|
|
import collections
|
|
|
|
import pyzor.account
|
|
|
|
# Configuration files for the Pyzor Server
|
|
|
|
def load_access_file(access_fn, accounts):
|
|
"""Load the ACL from the specified file, if it exists, and return an
|
|
ACL dictionary, where each key is a username and each value is a set
|
|
of allowed permissions (if the permission is not in the set, then it
|
|
is not allowed).
|
|
|
|
'accounts' is a dictionary of accounts that exist on the server - only
|
|
the keys are used, which must be the usernames (these are the users
|
|
that are granted permission when the 'all' keyword is used, as
|
|
described below).
|
|
|
|
Each line of the file should be in the following format:
|
|
operation : user : allow|deny
|
|
where 'operation' is a space-separated list of pyzor commands or the
|
|
keyword 'all' (meaning all commands), 'username' is a space-separated
|
|
list of usernames or the keyword 'all' (meaning all users) - the
|
|
anonymous user is called "anonymous", and "allow|deny" indicates whether
|
|
or not the specified user(s) may execute the specified operations.
|
|
|
|
The file is processed from top to bottom, with the final match for
|
|
user/operation being the value taken. Every file has the following
|
|
implicit final rule:
|
|
all : all : deny
|
|
|
|
If the file does not exist, then the following default is used:
|
|
check report ping info : anonymous : allow
|
|
"""
|
|
log = logging.getLogger("pyzord")
|
|
# A defaultdict is safe, because if we get a non-existant user, we get
|
|
# the empty set, which is the same as a deny, which is the final
|
|
# implicit rule.
|
|
acl = collections.defaultdict(set)
|
|
if not os.path.exists(access_fn):
|
|
log.info("Using default ACL: the anonymous user may use the check, "
|
|
"report, ping and info commands.")
|
|
acl[pyzor.anonymous_user] = set(("check", "report", "ping", "pong",
|
|
"info"))
|
|
return acl
|
|
for line in open(access_fn):
|
|
if not line.strip() or line[0] == "#":
|
|
continue
|
|
try:
|
|
operations, users, allowed = [part.lower().strip()
|
|
for part in line.split(":")]
|
|
except ValueError:
|
|
log.warn("Invalid ACL line: %r", line)
|
|
continue
|
|
try:
|
|
allowed = {"allow": True, "deny" : False}[allowed]
|
|
except KeyError:
|
|
log.warn("Invalid ACL line: %r", line)
|
|
continue
|
|
if operations == "all":
|
|
operations = ("check", "report", "ping", "pong", "info",
|
|
"whitelist")
|
|
else:
|
|
operations = [operation.strip()
|
|
for operation in operations.split()]
|
|
if users == "all":
|
|
users = accounts
|
|
else:
|
|
users = [user.strip() for user in users.split()]
|
|
for user in users:
|
|
if allowed:
|
|
log.debug("Granting %s to %s.", ",".join(operations), user)
|
|
# If these operations are already allowed, this will have
|
|
# no effect.
|
|
acl[user].update(operations)
|
|
else:
|
|
log.debug("Revoking %s from %s.", ",".join(operations), user)
|
|
# If these operations are not allowed yet, this will have
|
|
# no effect.
|
|
acl[user].difference_update(operations)
|
|
log.info("ACL: %r", acl)
|
|
return acl
|
|
|
|
def load_passwd_file(passwd_fn):
|
|
"""Load the accounts from the specified file.
|
|
|
|
Each line of the file should be in the format:
|
|
username : key
|
|
|
|
If the file does not exist, then an empty dictionary is returned;
|
|
otherwise, a dictionary of (username, key) items is returned.
|
|
"""
|
|
log = logging.getLogger("pyzord")
|
|
accounts = {}
|
|
if not os.path.exists(passwd_fn):
|
|
log.info("Accounts file does not exist - only the anonymous user "
|
|
"will be available.")
|
|
return accounts
|
|
for line in open(passwd_fn):
|
|
if not line.strip() or line[0] == "#":
|
|
continue
|
|
try:
|
|
user, key = line.split(":")
|
|
except ValueError:
|
|
log.warn("Invalid accounts line: %r", line)
|
|
continue
|
|
user = user.strip()
|
|
key = key.strip()
|
|
log.debug("Creating an account for %s with key %s.", user, key)
|
|
accounts[user] = key
|
|
# Don't log the keys at 'info' level, just ther usernames.
|
|
log.info("Accounts: %s", ",".join(accounts))
|
|
return accounts
|
|
|
|
# Configuration files for the Pyzor Client
|
|
|
|
def load_accounts(filepath):
|
|
"""Layout of file is: host : port : username : salt,key"""
|
|
accounts = {}
|
|
log = logging.getLogger("pyzor")
|
|
if os.path.exists(filepath):
|
|
for lineno, orig_line in enumerate(open(filepath)):
|
|
line = orig_line.strip()
|
|
if not line or line.startswith('#'):
|
|
continue
|
|
try:
|
|
host, port, username, key = [x.strip()
|
|
for x in line.split(":")]
|
|
except ValueError:
|
|
log.warn("account file: invalid line %d: wrong number of "
|
|
"parts", lineno)
|
|
continue
|
|
try:
|
|
port = int(port)
|
|
except ValueError, e:
|
|
log.warn("account file: invalid line %d: %s", lineno, e)
|
|
address = (host, port)
|
|
salt, key = pyzor.account.key_from_hexstr(key)
|
|
if not salt and not key:
|
|
log.warn("account file: invalid line %d: keystuff can't be "
|
|
"all None's", lineno)
|
|
continue
|
|
try:
|
|
accounts[address] = pyzor.account.Account(username, salt, key)
|
|
except ValueError, e:
|
|
log.warn("account file: invalid line %d: %s", lineno, e)
|
|
else:
|
|
log.warn("No accounts are setup. All commands will be executed by "
|
|
"the anonymous user.")
|
|
return accounts
|
|
|
|
|
|
def load_servers(filepath):
|
|
"""Load the servers file."""
|
|
logger = logging.getLogger("pyzor")
|
|
if not os.path.exists(filepath):
|
|
servers = []
|
|
else:
|
|
servers = []
|
|
with open(filepath) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if re.match("[^#][a-zA-Z0-9.-]+:[0-9]+", line):
|
|
address, port = line.rsplit(":", 1)
|
|
servers.append((address, int(port)))
|
|
|
|
if not servers:
|
|
logger.info("No servers specified, defaulting to public.pyzor.org.")
|
|
servers = [("public.pyzor.org", 24441)]
|
|
return servers
|
|
|
|
# Common configurations
|
|
|
|
def setup_logging(log_name, filepath, debug):
|
|
"""Setup logging according to the specified options. Return the Logger
|
|
object.
|
|
"""
|
|
fmt = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
|
|
|
|
stream_handler = logging.StreamHandler()
|
|
file_handler = None
|
|
|
|
if debug:
|
|
stream_log_level = logging.DEBUG
|
|
file_log_level = logging.DEBUG
|
|
else:
|
|
stream_log_level = logging.CRITICAL
|
|
file_log_level = logging.INFO
|
|
|
|
logger = logging.getLogger(log_name)
|
|
logger.setLevel(file_log_level)
|
|
|
|
stream_handler.setLevel(stream_log_level)
|
|
stream_handler.setFormatter(fmt)
|
|
logger.addHandler(stream_handler)
|
|
|
|
if filepath:
|
|
file_handler = logging.FileHandler(filepath)
|
|
file_handler.setLevel(file_log_level)
|
|
file_handler.setFormatter(fmt)
|
|
logger.addHandler(file_handler)
|
|
|
|
return logger
|
|
|
|
def expand_homefiles(homefiles, category, homedir, config):
|
|
"""Set the full file path for these configuration files."""
|
|
for filename in homefiles:
|
|
filepath = config.get(category, filename)
|
|
if not filepath:
|
|
continue
|
|
filepath = os.path.expanduser(filepath)
|
|
if not os.path.isabs(filepath):
|
|
filepath = os.path.join(homedir, filepath)
|
|
config.set(category, filename, filepath)
|
|
|
|
|