1"""Functions that handle parsing pyzor configuration files.""" 2 3import os 4import re 5import logging 6import collections 7 8try: 9 from raven.handlers.logging import SentryHandler 10 _has_raven = True 11except ImportError: 12 _has_raven = False 13 14import pyzor.account 15 16_COMMENT_P = re.compile(r"((?<=[^\\])#.*)") 17 18 19# Configuration files for the Pyzor Server 20def load_access_file(access_fn, accounts): 21 """Load the ACL from the specified file, if it exists, and return an 22 ACL dictionary, where each key is a username and each value is a set 23 of allowed permissions (if the permission is not in the set, then it 24 is not allowed). 25 26 'accounts' is a dictionary of accounts that exist on the server - only 27 the keys are used, which must be the usernames (these are the users 28 that are granted permission when the 'all' keyword is used, as 29 described below). 30 31 Each line of the file should be in the following format: 32 operation : user : allow|deny 33 where 'operation' is a space-separated list of pyzor commands or the 34 keyword 'all' (meaning all commands), 'username' is a space-separated 35 list of usernames or the keyword 'all' (meaning all users) - the 36 anonymous user is called "anonymous", and "allow|deny" indicates whether 37 or not the specified user(s) may execute the specified operations. 38 39 The file is processed from top to bottom, with the final match for 40 user/operation being the value taken. Every file has the following 41 implicit final rule: 42 all : all : deny 43 44 If the file does not exist, then the following default is used: 45 check report ping info : anonymous : allow 46 """ 47 log = logging.getLogger("pyzord") 48 # A defaultdict is safe, because if we get a non-existant user, we get 49 # the empty set, which is the same as a deny, which is the final 50 # implicit rule. 51 acl = collections.defaultdict(set) 52 if not os.path.exists(access_fn): 53 log.info("Using default ACL: the anonymous user may use the check, " 54 "report, ping and info commands.") 55 acl[pyzor.anonymous_user] = set(("check", "report", "ping", "pong", 56 "info")) 57 return acl 58 accessf = open(access_fn) 59 for line in accessf: 60 if not line.strip() or line[0] == "#": 61 continue 62 try: 63 operations, users, allowed = [part.lower().strip() 64 for part in line.split(":")] 65 except ValueError: 66 log.warn("Invalid ACL line: %r", line) 67 continue 68 try: 69 allowed = {"allow": True, "deny": False}[allowed] 70 except KeyError: 71 log.warn("Invalid ACL line: %r", line) 72 continue 73 if operations == "all": 74 operations = ("check", "report", "ping", "pong", "info", 75 "whitelist") 76 else: 77 operations = [operation.strip() 78 for operation in operations.split()] 79 if users == "all": 80 users = accounts 81 else: 82 users = [user.strip() for user in users.split()] 83 for user in users: 84 if allowed: 85 log.debug("Granting %s to %s.", ",".join(operations), user) 86 # If these operations are already allowed, this will have 87 # no effect. 88 acl[user].update(operations) 89 else: 90 log.debug("Revoking %s from %s.", ",".join(operations), user) 91 # If these operations are not allowed yet, this will have 92 # no effect. 93 acl[user].difference_update(operations) 94 accessf.close() 95 log.info("ACL: %r", acl) 96 return acl 97 98 99def load_passwd_file(passwd_fn): 100 """Load the accounts from the specified file. 101 102 Each line of the file should be in the format: 103 username : key 104 105 If the file does not exist, then an empty dictionary is returned; 106 otherwise, a dictionary of (username, key) items is returned. 107 """ 108 log = logging.getLogger("pyzord") 109 accounts = {} 110 if not os.path.exists(passwd_fn): 111 log.info("Accounts file does not exist - only the anonymous user " 112 "will be available.") 113 return accounts 114 passwdf = open(passwd_fn) 115 for line in passwdf: 116 if not line.strip() or line[0] == "#": 117 continue 118 try: 119 user, key = line.split(":") 120 except ValueError: 121 log.warn("Invalid accounts line: %r", line) 122 continue 123 user = user.strip() 124 key = key.strip() 125 log.debug("Creating an account for %s with key %s.", user, key) 126 accounts[user] = key 127 passwdf.close() 128 # Don't log the keys at 'info' level, just ther usernames. 129 log.info("Accounts: %s", ",".join(accounts)) 130 return accounts 131 132 133# Configuration files for the Pyzor Client 134def load_accounts(filepath): 135 """Layout of file is: host : port : username : salt,key""" 136 accounts = {} 137 log = logging.getLogger("pyzor") 138 if os.path.exists(filepath): 139 accountsf = open(filepath) 140 for lineno, orig_line in enumerate(accountsf): 141 line = orig_line.strip() 142 if not line or line.startswith('#'): 143 continue 144 try: 145 host, port, username, key = [x.strip() 146 for x in line.split(":")] 147 except ValueError: 148 log.warn("account file: invalid line %d: wrong number of " 149 "parts", lineno) 150 continue 151 try: 152 port = int(port) 153 except ValueError as ex: 154 log.warn("account file: invalid line %d: %s", lineno, ex) 155 continue 156 address = (host, port) 157 try: 158 salt, key = pyzor.account.key_from_hexstr(key) 159 except ValueError as ex: 160 log.warn("account file: invalid line %d: %s", lineno, ex) 161 continue 162 if not salt and not key: 163 log.warn("account file: invalid line %d: keystuff can't be " 164 "all None's", lineno) 165 continue 166 accounts[address] = pyzor.account.Account(username, salt, key) 167 accountsf.close() 168 169 else: 170 log.warn("No accounts are setup. All commands will be executed by " 171 "the anonymous user.") 172 return accounts 173 174 175def load_servers(filepath): 176 """Load the servers file.""" 177 logger = logging.getLogger("pyzor") 178 if not os.path.exists(filepath): 179 servers = [] 180 else: 181 servers = [] 182 with open(filepath) as serverf: 183 for line in serverf: 184 line = line.strip() 185 if re.match("[^#][a-zA-Z0-9.-]+:[0-9]+", line): 186 address, port = line.rsplit(":", 1) 187 servers.append((address, int(port))) 188 189 if not servers: 190 logger.info("No servers specified, defaulting to public.pyzor.org.") 191 servers = [("public.pyzor.org", 24441)] 192 return servers 193 194 195def load_local_whitelist(filepath): 196 """Load the local digest skip file.""" 197 if not os.path.exists(filepath): 198 return set() 199 200 whitelist = set() 201 with open(filepath) as serverf: 202 for line in serverf: 203 # Remove any comments 204 line = _COMMENT_P.sub("", line).strip() 205 if line: 206 whitelist.add(line) 207 return whitelist 208 209 210# Common configurations 211def setup_logging(log_name, filepath, debug, sentry_dsn=None, 212 sentry_lvl="WARN"): 213 """Setup logging according to the specified options. Return the Logger 214 object. 215 """ 216 fmt = logging.Formatter('%(asctime)s (%(process)d) %(levelname)s ' 217 '%(message)s') 218 219 stream_handler = logging.StreamHandler() 220 221 if debug: 222 stream_log_level = logging.DEBUG 223 file_log_level = logging.DEBUG 224 else: 225 stream_log_level = logging.CRITICAL 226 file_log_level = logging.INFO 227 228 logger = logging.getLogger(log_name) 229 logger.setLevel(file_log_level) 230 231 stream_handler.setLevel(stream_log_level) 232 stream_handler.setFormatter(fmt) 233 logger.addHandler(stream_handler) 234 235 if filepath: 236 file_handler = logging.FileHandler(filepath) 237 file_handler.setLevel(file_log_level) 238 file_handler.setFormatter(fmt) 239 logger.addHandler(file_handler) 240 241 if sentry_dsn and _has_raven: 242 sentry_level = getattr(logging, sentry_lvl) 243 sentry_handler = SentryHandler(sentry_dsn) 244 sentry_handler.setLevel(sentry_level) 245 logger.addHandler(sentry_handler) 246 247 return logger 248 249 250def expand_homefiles(homefiles, category, homedir, config): 251 """Set the full file path for these configuration files.""" 252 for filename in homefiles: 253 filepath = config.get(category, filename) 254 if not filepath: 255 continue 256 filepath = os.path.expanduser(filepath) 257 if not os.path.isabs(filepath): 258 filepath = os.path.join(homedir, filepath) 259 config.set(category, filename, filepath) 260