diff --git a/warden3/warden_server/warden_server.py b/warden3/warden_server/warden_server.py index 24cafc34dd8cc83edcafc1a050db598fd46e41b3..3b48297b96b9f37d45ba96f908d08faa5a255d44 100755 --- a/warden3/warden_server/warden_server.py +++ b/warden3/warden_server/warden_server.py @@ -14,6 +14,8 @@ import M2Crypto.X509 import json import MySQLdb as my import MySQLdb.cursors as mycursors +import re +import email.utils from collections import namedtuple from uuid import uuid4 from time import time, gmtime, sleep @@ -210,17 +212,17 @@ def SysLogger(req, socket="/dev/log", facility=logging.handlers.SysLogHandler.LO class Client(namedtuple("ClientTuple", ["id", "registered", "requestor", "hostname", "service", "note", - "identity", "secret", "read", "debug", "write", "test"])): + "valid", "identity", "secret", "read", "debug", "write", "test"])): def __str__(self): return ( "%s(id=%i, registered=%s, requestor=\"%s\", hostname=\"%s\", " "service=\"%s\", note=\"%s\", identity=\"%s\", secret=%s, " - "read=%i, debug=%i, write=%i, test=%i)") % ( + "valid=%i read=%i, debug=%i, write=%i, test=%i)") % ( type(self).__name__, self.id, self.registered, self.requestor, self.hostname, self.service, self.note, self.identity, "..." if self.secret is not None else "None", - self.read, self.debug, self.write, self.test) + self.valid, self.read, self.debug, self.write, self.test) @@ -514,7 +516,7 @@ class MySQL(ObjectReq): def get_client_by_name(self, cert_names, identity=None, secret=None): - query = ["SELECT id, registered, requestor, hostname, service, note, identity, secret, `read`, debug, `write`, test FROM clients WHERE valid = 1"] + query = ["SELECT id, registered, requestor, hostname, service, note, valid, identity, secret, `read`, debug, `write`, test FROM clients WHERE valid = 1"] params = [] if identity: query.append(" AND identity = %s") @@ -534,6 +536,42 @@ class MySQL(ObjectReq): return Client(**rows[0]) if rows else None + def get_clients(self, id=None): + query = ["SELECT id, registered, requestor, hostname, service, note, valid, identity, secret, `read`, debug, `write`, test FROM clients"] + params = [] + if id: + query.append("WHERE id = %s") + params.append(id) + query.append("ORDER BY id") + rows = self.query(" ".join(query), params) + return [Client(**row) for row in rows] + + + def add_modify_client(self, id=None, **kwargs): + query = [] + params = [] + uquery = [] + if id is None: + query.append("INSERT INTO clients SET") + uquery.append("registered = now()") + else: + query.append("UPDATE clients SET") + for attr in ["identity", "hostname", "requestor", "secret", "note", + "valid", "read", "write", "debug", "test"]: + val = kwargs.get(attr, None) + if val is not None: + uquery.append("`%s` = %%s" % attr) + params.append(val) + if not uquery: + return id + query.append(", ".join(uquery)) + if id is not None: + query.append("WHERE id = %s") + params.append(id) + self.query(" ".join(query), params) + return self.crs.lastrowid if id is None else id + + def get_debug(self): rows = self.query("SELECT VERSION() AS VER") tablestat = self.query("SHOW TABLE STATUS") @@ -653,11 +691,13 @@ class MySQL(ObjectReq): self.query("INSERT INTO last_events(client_id, event_id, timestamp) VALUES(%s, %s, NOW())", (client.id, id), dml=True) self.con.commit() + def getLastEventId(self): row = self.query("SELECT MAX(id) as id FROM events")[0] return row['id'] or 0 + def getLastReceivedId(self, client): row = self.query("SELECT MAX(event_id) as id FROM last_events WHERE client_id = %s", client.id)[0] @@ -1163,11 +1203,191 @@ def build_server(conf): logging.debug("", exc_info=sys.exc_info()) return fallback_wsgi - logging.info("Ready to serve") + logging.info("Server ready") return objects["server"] + +# Command line utilities + +def check_config(): + # If we got so far, server object got set up fine + print >>sys.stderr, "Looks clear." + return 0 + + +def list_clients(id=None): + clients = server.handler.db.get_clients(id) + order = ["id", "registered", "requestor", "hostname", "service", "identity", + "secret", "valid", "read", "debug", "write", "test", "note"] + lines = [[str(getattr(client, col)) for col in order] for client in clients] + col_width = [max(len(val) for val in col) for col in zip(*(lines+[order]))] + divider = ["-" * l for l in col_width] + for line in [order, divider] + lines: + print " ".join([val.ljust(width) for val, width in zip(line, col_width)]) + + +def register_client(name, hostname, requestor, secret, note, valid, read, write, debug, test): + # argparse does _always_ return something, so we cannot rely on missing arguments + if valid is None: valid = 1 + if read is None: read = 1 + if write is None: write = 0 + if debug is None: debug = 0 + if test is None: test = 1 + modify_client(id=None, + name=name, hostname=hostname, requestor=requestor, secret=secret, + note=note, valid=valid, read=read, write=write, debug=debug, test=test) + + +def modify_client(id, name, hostname, requestor, secret, note, valid, read, write, debug, test): + + def isValidHostname(hostname): + if len(hostname) > 255: + return False + if hostname.endswith("."): # A single trailing dot is legal + hostname = hostname[:-1] # strip exactly one dot from the right, if present + disallowed = re.compile("[^A-Z\d-]", re.IGNORECASE) + return all( # Split by labels and verify individually + (label and len(label) <= 63 # length is within proper range + and not label.startswith("-") and not label.endswith("-") # no bordering hyphens + and not disallowed.search(label)) # contains only legal characters + for label in hostname.split(".")) + + def isValidNSID(nsid): + allowed = re.compile("^(?:[a-zA-Z_][a-zA-Z0-9_]*\\.)*[a-zA-Z_][a-zA-Z0-9_]*$") + return allowed.match(nsid) + + def isValidEmail(mail): + split = email.utils.parseaddr(mail) + allowed = re.compile("^[a-zA-Z0-9_.%!+-]+@[a-zA-Z0-9-.]+$") # just basic check + return allowed.match(split[1]) + + def isValidID(id): + client = server.handler.db.get_clients(id) + return client and True or False + + + if name is not None and not isValidNSID(name): + print >>sys.stderr, "Invalid client name \"%s\"." % name + + if hostname is not None and not isValidHostname(hostname): + print >>sys.stderr, "Invalid hostname \"%s\"." % hostname + return 254 + + if requestor is not None and not isValidEmail(requestor): + print >>sys.stderr, "Invalid requestor email \"%s\"." % requestor + return 254 + + if id is not None and not isValidID(id): + print >>sys.stderr, "Invalid id \"%s\"." % id + return 254 + + existing_clients = server.handler.db.get_client_by_name([hostname], identity=name, secret=secret) + if existing_clients: + print >>sys.stderr, "Clash with existing hostname/identity/secret: %s" % str(existing_clients) + return 254 + + newid = server.handler.db.add_modify_client( + id=id, identity=name, hostname=hostname, + requestor=requestor, secret=secret, note=note, valid=valid, + read=read, write=write, debug=debug, test=test) + + list_clients(id=newid) + + +def add_client_args(subargp, mod=False): + subargp.add_argument("--help", action="help", help="show this help message and exit") + if mod: + subargp.add_argument("-i", "--id", required=True, type=int, + help="client id") + subargp.add_argument("-n", "--name", required=not mod, + help="client name (in dotted reverse path notation)") + subargp.add_argument("-h", "--hostname", required=not mod, + help="client FQDN hostname") + subargp.add_argument("-r", "--requestor", required=not mod, + help="requestor email") + subargp.add_argument("-s", "--secret", + help="authentication token") + subargp.add_argument("--note", + help="client freetext description") + + reg_valid = subargp.add_mutually_exclusive_group(required=False) + reg_valid.add_argument("--valid", action="store_const", const=1, default=None, + help="valid client (default)") + reg_valid.add_argument("--novalid", action="store_const", const=0, dest="valid", default=None) + + reg_read = subargp.add_mutually_exclusive_group(required=False) + reg_read.add_argument("--read", action="store_const", const=1, default=None, + help="client is allowed to read (default)") + reg_read.add_argument("--noread", action="store_const", const=0, dest="read", default=None) + + reg_write = subargp.add_mutually_exclusive_group(required=False) + reg_write.add_argument("--nowrite", action="store_const", const=0, dest="write", default=None, + help="client is allowed to send (default - no)") + reg_write.add_argument("--write", action="store_const", const=1, default=None) + + reg_debug = subargp.add_mutually_exclusive_group(required=False) + reg_debug.add_argument("--nodebug", action="store_const", const=0, dest="debug", default=None, + help="client is allowed receive debug output (default - no)") + reg_debug.add_argument("--debug", action="store_const", const=1, default=None) + + reg_test = subargp.add_mutually_exclusive_group(required=False) + reg_test.add_argument("--test", action="store_const", const=1, default=None, + help="client is yet in testing phase (default - yes)") + reg_test.add_argument("--notest", action="store_const", const=0, dest="test", default=None) + + +def get_args(): + import argparse + argp = argparse.ArgumentParser( + description="Warden server " + VERSION, add_help=False) + argp.add_argument("--help", action="help", + help="show this help message and exit") + argp.add_argument("-c", "--config", + help="show this help message and exit") + subargp = argp.add_subparsers(title="commands") + + subargp_check = subargp.add_parser("check", add_help=False, + description="Try to setup server based on configuration file.", + help="check configuration") + subargp_check.set_defaults(command=check_config) + subargp_check.add_argument("--help", action="help", + help="show this help message and exit") + + subargp_reg = subargp.add_parser("register", add_help=False, + description="Add new client registration entry.", + help="register new client") + subargp_reg.set_defaults(command=register_client) + add_client_args(subargp_reg) + + subargp_mod = subargp.add_parser("modify", add_help=False, + description="Modify details of client registration entry.", + help="modify client registration") + subargp_mod.set_defaults(command=modify_client) + add_client_args(subargp_mod, mod=True) + + subargp_list = subargp.add_parser("list", add_help=False, + description="List details of client registration entries.", + help="list registered clients") + subargp_list.set_defaults(command=list_clients) + subargp_list.add_argument("--help", action="help", + help="show this help message and exit") + subargp_list.add_argument("--id", action="store", type=int, + help="client id", default=None) + + return argp.parse_args() + + if __name__=="__main__": - # FIXME: just development stuff - srv = build_server(read_ini("warden3.cfg.wheezy-warden3")) + args = get_args() + config = path.join(path.dirname(__file__), args.config or "warden_server.cfg") + server = build_server(read_cfg(config)) + command = args.command + subargs = vars(args) + del subargs["command"] + del subargs["config"] + if not server or server is fallback_wsgi: + print >>sys.stderr, "Failed initialization, check configured log targets for reasons." + sys.exit(255) + sys.exit(command(**subargs))