=== modified file 'Makefile' --- Makefile 2012-06-17 22:26:40 +0000 +++ Makefile 2012-06-22 23:33:56 +0000 @@ -241,6 +241,7 @@ check: all ./mandos --check + ./mandos-ctl --check # Run the client with a local config and key run-client: all keydir/seckey.txt keydir/pubkey.txt === modified file 'mandos-ctl' --- mandos-ctl 2012-06-17 22:26:40 +0000 +++ mandos-ctl 2012-06-22 23:33:56 +0000 @@ -29,12 +29,15 @@ from future_builtins import * import sys -import dbus import argparse import locale import datetime import re import os +import collections +import doctest + +import dbus locale.setlocale(locale.LC_ALL, "") @@ -80,6 +83,106 @@ seconds = td.seconds % 60, )) + +def rfc3339_duration_to_delta(duration): + """Parse a RFC 3339 "duration" and return a datetime.timedelta + + >>> rfc3339_duration_to_delta("P7D") + datetime.timedelta(7) + >>> rfc3339_duration_to_delta("PT60S") + datetime.timedelta(0, 60) + >>> rfc3339_duration_to_delta("PT60M") + datetime.timedelta(0, 3600) + >>> rfc3339_duration_to_delta("PT24H") + datetime.timedelta(1) + >>> rfc3339_duration_to_delta("P1W") + datetime.timedelta(7) + >>> rfc3339_duration_to_delta("PT5M30S") + datetime.timedelta(0, 330) + >>> rfc3339_duration_to_delta("P1DT3M20S") + datetime.timedelta(1, 200) + """ + + # Parsing a RFC 3339 duration with regular expressions is not + # possible - there would have to be multiple places for the same + # values, like seconds. This, while more esoteric, is cleaner + # without depending on a parsing library. If Python had a + # built-in library for parsing we would use it, but we'd like to + # avoid excessive use of external libraries. + + # New type for defining tokens, syntax, and semantics all-in-one + Token = collections.namedtuple("Token", + ("regexp", # To match token; if + # "value" is not None, + # must have a "group" + # containing digits + "value", # datetime.timedelta or + # None + "followers")) # Tokens valid after + # this token + # RFC 3339 "duration" tokens, syntax, and semantics; taken from + # the "duration" ABNF definition in RFC 3339, Appendix A. + token_end = Token(re.compile(r"$"), None, frozenset()) + token_second = Token(re.compile(r"(\d+)S"), + datetime.timedelta(seconds=1), + frozenset((token_end,))) + token_minute = Token(re.compile(r"(\d+)M"), + datetime.timedelta(minutes=1), + frozenset((token_second, token_end))) + token_hour = Token(re.compile(r"(\d+)H"), + datetime.timedelta(hours=1), + frozenset((token_minute, token_end))) + token_time = Token(re.compile(r"T"), + None, + frozenset((token_hour, token_minute, + token_second))) + token_day = Token(re.compile(r"(\d+)D"), + datetime.timedelta(days=1), + frozenset((token_time, token_end))) + token_month = Token(re.compile(r"(\d+)M"), + datetime.timedelta(weeks=4), + frozenset((token_day, token_end))) + token_year = Token(re.compile(r"(\d+)Y"), + datetime.timedelta(weeks=52), + frozenset((token_month, token_end))) + token_week = Token(re.compile(r"(\d+)W"), + datetime.timedelta(weeks=1), + frozenset((token_end,))) + token_duration = Token(re.compile(r"P"), None, + frozenset((token_year, token_month, + token_day, token_time, + token_week))), + # Define starting values + value = datetime.timedelta() # Value so far + found_token = None + followers = frozenset(token_duration,) # Following valid tokens + s = duration # String left to parse + # Loop until end token is found + while found_token is not token_end: + # Search for any currently valid tokens + for token in followers: + match = token.regexp.match(s) + if match is not None: + # Token found + if token.value is not None: + # Value found, parse digits + factor = int(match.group(1), 10) + # Add to value so far + value += factor * token.value + # Strip token from string + s = token.regexp.sub("", s, 1) + # Go to found token + found_token = token + # Set valid next tokens + followers = found_token.followers + break + else: + # No currently valid tokens were found + raise ValueError("Invalid RFC 3339 duration") + # End token found + return value + + def string_to_delta(interval): """Parse a string and return a datetime.timedelta @@ -97,7 +200,12 @@ datetime.timedelta(0, 330) """ value = datetime.timedelta(0) - regexp = re.compile("(\d+)([dsmhw]?)") + regexp = re.compile(r"(\d+)([dsmhw]?)") + + try: + return rfc3339_duration_to_delta(interval) + except ValueError: + pass for num, suffix in regexp.findall(interval): if suffix == "d": @@ -207,6 +315,8 @@ help="Approve any current client request") parser.add_argument("-D", "--deny", action="store_true", help="Deny any current client request") + parser.add_argument("--check", action="store_true", + help="Run self-test") parser.add_argument("client", nargs="*", help="Client name") options = parser.parse_args() @@ -217,6 +327,10 @@ " --all.") if options.all and not has_actions(options): parser.error("--all requires an action.") + + if options.check: + fail_count, test_count = doctest.testmod() + sys.exit(0 if fail_count == 0 else 1) try: bus = dbus.SystemBus() === modified file 'mandos-ctl.xml' --- mandos-ctl.xml 2011-12-31 23:05:34 +0000 +++ mandos-ctl.xml 2012-06-22 23:33:56 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -195,6 +195,10 @@ + + &COMMANDNAME; + + @@ -476,6 +480,15 @@ + + + + + Run self-tests. This includes any unit tests, etc. + + + +