import io import ecdsa import hashlib import yaml import base64 import sys import configschema def render_path(path): if path: return "'" + ", ".join(path) + "'" else: return "the top level" import traceback class ErrorHandlingDict(dict): def __init__(self, filename, path, optionals): self._filename = filename self._path = path self._optionals = optionals dict.__init__({}) def get(self, key, default=None): path = "/".join(self._path + [key]) if path in self._optionals: return dict.get(self, key, default) print >>sys.stderr, "error: read key", path, "optionally with default", default traceback.print_stack() sys.exit(1) def __missing__(self, key): path = render_path(self._path) print >>sys.stderr, "error: could not find configuration key '%s' at %s in %s" % (key, path, self._filename) sys.exit(1) def errorhandlify(term, filename, optionals, path=[]): if isinstance(term, basestring): return term elif isinstance(term, int): return term elif isinstance(term, dict): result = ErrorHandlingDict(filename, path, optionals) for k, v in term.items(): result[k] = errorhandlify(v, filename, optionals, path + [k]) return result elif isinstance(term, list): return [errorhandlify(e, filename, optionals, path + ["item %d" % i]) for i, e in enumerate(term, start=1)] else: print "unknown type", type(term) sys.exit(1) def verify_config(rawconfig, signature, publickey_base64, filename): publickey = base64.decodestring(publickey_base64) try: vk = ecdsa.VerifyingKey.from_der(publickey) vk.verify(signature, rawconfig, hashfunc=hashlib.sha256, sigdecode=ecdsa.util.sigdecode_der) except ecdsa.keys.BadSignatureError: print >>sys.stderr, "error: configuration file %s did not have a correct signature" % (filename,) sys.exit(1) return common_read_config(io.BytesIO(rawconfig), filename, localconfig=False) def verify_and_read_config(filename, publickey_base64): rawconfig = open(filename).read() signature = open(filename + ".sig").read() return verify_config(rawconfig, signature, publickey_base64, filename) def insert_schema_path(schema, path, datatype, highleveldatatype, extra): if len(path) == 1: schema[path[0]] = (datatype, highleveldatatype, extra) else: if path[0] not in schema: schema[path[0]] = {} insert_schema_path(schema[path[0]], path[1:], datatype, highleveldatatype, extra) def transform_schema((in_schema, defaults, optionals)): defaults_dict = dict(defaults) schema = {} for (rawpath, datatype, highleveldatatype) in in_schema: path = rawpath.split("/") extra = {} extra["optional"] = rawpath in optionals extra["default"] = defaults_dict.get(rawpath) insert_schema_path(schema, path, datatype, highleveldatatype, extra) return schema def check_config_schema(config, schema): transformed_schema = transform_schema(schema) problems = check_config_schema_part(config, transformed_schema) if problems: haserrors = False for problem in problems: if problem.startswith("error:"): haserrors = True else: assert(problem.startswith("WARNING:")) print >>sys.stderr, problem if haserrors: sys.exit(1) def check_config_schema_part(term, schema, path=[]): joined_path = render_path(path) problems = [] if isinstance(term, basestring): (schema_lowlevel, schema_highlevel, extra) = schema if schema_lowlevel != "string": problems.append("error: expected %s at %s, not a string" % (schema_lowlevel, joined_path,)) elif isinstance(term, int): (schema_lowlevel, schema_highlevel, extra) = schema if schema_lowlevel != "integer": problems.append("error: expected %s at %s, not an integer" % (schema_lowlevel, joined_path,)) elif isinstance(term, dict): if not isinstance(schema, dict): problems.append("error: expected %s at %s, not a key" % (schema, joined_path,)) for k, v in term.items(): schema_part = schema.get(k) if schema_part == None and len(schema.keys()) == 1 and schema.keys()[0].startswith("*"): schema_part = schema[schema.keys()[0]] if schema_part == None: problems.append("WARNING: configuration key '%s' at %s unknown" % (k, joined_path)) del term[k] continue problems.extend(check_config_schema_part(v, schema_part, path + [k])) elif isinstance(term, list): if not isinstance(schema, dict): problems.append("error: expected %s at %s, not a list" % (schema, joined_path,)) schema_part = schema.get("[]") if schema_part == None: problems.append("error: expected dict at %s, not a list" % (joined_path,)) for i, e in enumerate(term, start=1): problems.extend(check_config_schema_part(e, schema_part, path + ["item %d" % i])) else: print >>sys.stderr, "unknown type", type(term) sys.exit(1) return problems def insert_defaults(config, configdefaults): for (rawpath, value) in configdefaults: path = rawpath.split("/") node = config for e in path[:-1]: assert(e != "[]") node = node[e] lastelem = path[-1] if lastelem not in node: node[lastelem] = value def common_read_config(f, filename, localconfig=True): if localconfig: schema = configschema.localconfigschema configdefaults = configschema.localconfigdefaults optionals = configschema.localconfigoptionals else: schema = configschema.globalconfigschema configdefaults = configschema.globalconfigdefaults optionals = configschema.globalconfigoptionals config = yaml.load(f, yaml.SafeLoader) insert_defaults(config, configdefaults) check_config_schema(config, (schema, configdefaults, optionals)) return errorhandlify(config, filename, optionals) def read_config(filename, localconfig=True): return common_read_config(open(filename), filename, localconfig=localconfig)