2
2
# -*- mode: python; coding: utf-8 -*-
 
4
4
# Mandos Monitor - Control and monitor the Mandos server
 
6
 
# Copyright © 2008-2019 Teddy Hogeborn
 
7
 
# Copyright © 2008-2019 Björn Påhlsson
 
9
 
# This file is part of Mandos.
 
11
 
# Mandos is free software: you can redistribute it and/or modify it
 
12
 
# under the terms of the GNU General Public License as published by
 
 
6
# Copyright © 2008-2012 Teddy Hogeborn
 
 
7
# Copyright © 2008-2012 Björn Påhlsson
 
 
9
# This program is free software: you can redistribute it and/or modify
 
 
10
# it under the terms of the GNU General Public License as published by
 
13
11
# the Free Software Foundation, either version 3 of the License, or
 
14
12
# (at your option) any later version.
 
16
 
#     Mandos is distributed in the hope that it will be useful, but
 
17
 
#     WITHOUT ANY WARRANTY; without even the implied warranty of
 
 
14
#     This program is distributed in the hope that it will be useful,
 
 
15
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
 
18
16
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
19
17
#     GNU General Public License for more details.
 
21
19
# You should have received a copy of the GNU General Public License
 
22
 
# along with Mandos.  If not, see <http://www.gnu.org/licenses/>.
 
 
20
# along with this program.  If not, see
 
 
21
# <http://www.gnu.org/licenses/>.
 
24
23
# Contact the authors at <mandos@recompile.se>.
 
27
26
from __future__ import (division, absolute_import, print_function,
 
31
 
    from future_builtins import *
 
 
29
from future_builtins import *
 
46
 
if sys.version_info.major == 2:
 
49
39
locale.setlocale(locale.LC_ALL, "")
 
 
66
55
    "ApprovalDelay": "Approval Delay",
 
67
56
    "ApprovalDuration": "Approval Duration",
 
68
57
    "Checker": "Checker",
 
69
 
    "ExtendedTimeout": "Extended Timeout",
 
71
 
    "LastCheckerStatus": "Last Checker Status",
 
 
58
    "ExtendedTimeout" : "Extended Timeout"
 
73
60
defaultkeywords = ("Name", "Enabled", "Timeout", "LastCheckedOK")
 
74
61
domain = "se.recompile"
 
75
62
busname = domain + ".Mandos"
 
77
64
server_interface = domain + ".Mandos"
 
78
65
client_interface = domain + ".Mandos.Client"
 
83
 
    dbus.OBJECT_MANAGER_IFACE
 
84
 
except AttributeError:
 
85
 
    dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
 
 
68
def timedelta_to_milliseconds(td):
 
 
69
    """Convert a datetime.timedelta object to milliseconds"""
 
 
70
    return ((td.days * 24 * 60 * 60 * 1000)
 
 
72
            + (td.microseconds // 1000))
 
88
74
def milliseconds_to_string(ms):
 
89
75
    td = datetime.timedelta(0, 0, 0, ms)
 
90
76
    return ("{days}{hours:02}:{minutes:02}:{seconds:02}"
 
91
 
            .format(days="{}T".format(td.days) if td.days else "",
 
92
 
                    hours=td.seconds // 3600,
 
93
 
                    minutes=(td.seconds % 3600) // 60,
 
94
 
                    seconds=td.seconds % 60))
 
97
 
def rfc3339_duration_to_delta(duration):
 
98
 
    """Parse an RFC 3339 "duration" and return a datetime.timedelta
 
100
 
    >>> rfc3339_duration_to_delta("P7D")
 
101
 
    datetime.timedelta(7)
 
102
 
    >>> rfc3339_duration_to_delta("PT60S")
 
103
 
    datetime.timedelta(0, 60)
 
104
 
    >>> rfc3339_duration_to_delta("PT60M")
 
105
 
    datetime.timedelta(0, 3600)
 
106
 
    >>> rfc3339_duration_to_delta("PT24H")
 
107
 
    datetime.timedelta(1)
 
108
 
    >>> rfc3339_duration_to_delta("P1W")
 
109
 
    datetime.timedelta(7)
 
110
 
    >>> rfc3339_duration_to_delta("PT5M30S")
 
111
 
    datetime.timedelta(0, 330)
 
112
 
    >>> rfc3339_duration_to_delta("P1DT3M20S")
 
113
 
    datetime.timedelta(1, 200)
 
116
 
    # Parsing an RFC 3339 duration with regular expressions is not
 
117
 
    # possible - there would have to be multiple places for the same
 
118
 
    # values, like seconds.  The current code, while more esoteric, is
 
119
 
    # cleaner without depending on a parsing library.  If Python had a
 
120
 
    # built-in library for parsing we would use it, but we'd like to
 
121
 
    # avoid excessive use of external libraries.
 
123
 
    # New type for defining tokens, syntax, and semantics all-in-one
 
124
 
    Token = collections.namedtuple("Token", (
 
125
 
        "regexp",  # To match token; if "value" is not None, must have
 
126
 
                   # a "group" containing digits
 
127
 
        "value",   # datetime.timedelta or None
 
128
 
        "followers"))           # Tokens valid after this token
 
129
 
    # RFC 3339 "duration" tokens, syntax, and semantics; taken from
 
130
 
    # the "duration" ABNF definition in RFC 3339, Appendix A.
 
131
 
    token_end = Token(re.compile(r"$"), None, frozenset())
 
132
 
    token_second = Token(re.compile(r"(\d+)S"),
 
133
 
                         datetime.timedelta(seconds=1),
 
134
 
                         frozenset((token_end, )))
 
135
 
    token_minute = Token(re.compile(r"(\d+)M"),
 
136
 
                         datetime.timedelta(minutes=1),
 
137
 
                         frozenset((token_second, token_end)))
 
138
 
    token_hour = Token(re.compile(r"(\d+)H"),
 
139
 
                       datetime.timedelta(hours=1),
 
140
 
                       frozenset((token_minute, token_end)))
 
141
 
    token_time = Token(re.compile(r"T"),
 
143
 
                       frozenset((token_hour, token_minute,
 
145
 
    token_day = Token(re.compile(r"(\d+)D"),
 
146
 
                      datetime.timedelta(days=1),
 
147
 
                      frozenset((token_time, token_end)))
 
148
 
    token_month = Token(re.compile(r"(\d+)M"),
 
149
 
                        datetime.timedelta(weeks=4),
 
150
 
                        frozenset((token_day, token_end)))
 
151
 
    token_year = Token(re.compile(r"(\d+)Y"),
 
152
 
                       datetime.timedelta(weeks=52),
 
153
 
                       frozenset((token_month, token_end)))
 
154
 
    token_week = Token(re.compile(r"(\d+)W"),
 
155
 
                       datetime.timedelta(weeks=1),
 
156
 
                       frozenset((token_end, )))
 
157
 
    token_duration = Token(re.compile(r"P"), None,
 
158
 
                           frozenset((token_year, token_month,
 
159
 
                                      token_day, token_time,
 
161
 
    # Define starting values:
 
163
 
    value = datetime.timedelta()
 
165
 
    # Following valid tokens
 
166
 
    followers = frozenset((token_duration, ))
 
167
 
    # String left to parse
 
169
 
    # Loop until end token is found
 
170
 
    while found_token is not token_end:
 
171
 
        # Search for any currently valid tokens
 
172
 
        for token in followers:
 
173
 
            match = token.regexp.match(s)
 
174
 
            if match is not None:
 
176
 
                if token.value is not None:
 
177
 
                    # Value found, parse digits
 
178
 
                    factor = int(match.group(1), 10)
 
179
 
                    # Add to value so far
 
180
 
                    value += factor * token.value
 
181
 
                # Strip token from string
 
182
 
                s = token.regexp.sub("", s, 1)
 
185
 
                # Set valid next tokens
 
186
 
                followers = found_token.followers
 
189
 
            # No currently valid tokens were found
 
190
 
            raise ValueError("Invalid RFC 3339 duration: {!r}"
 
 
77
            .format(days = "{0}T".format(td.days) if td.days else "",
 
 
78
                    hours = td.seconds // 3600,
 
 
79
                    minutes = (td.seconds % 3600) // 60,
 
 
80
                    seconds = td.seconds % 60,
 
196
83
def string_to_delta(interval):
 
197
84
    """Parse a string and return a datetime.timedelta
 
199
 
    >>> string_to_delta('7d')
 
 
86
    >>> string_to_delta("7d")
 
200
87
    datetime.timedelta(7)
 
201
 
    >>> string_to_delta('60s')
 
 
88
    >>> string_to_delta("60s")
 
202
89
    datetime.timedelta(0, 60)
 
203
 
    >>> string_to_delta('60m')
 
 
90
    >>> string_to_delta("60m")
 
204
91
    datetime.timedelta(0, 3600)
 
205
 
    >>> string_to_delta('24h')
 
 
92
    >>> string_to_delta("24h")
 
206
93
    datetime.timedelta(1)
 
207
 
    >>> string_to_delta('1w')
 
 
94
    >>> string_to_delta("1w")
 
208
95
    datetime.timedelta(7)
 
209
 
    >>> string_to_delta('5m 30s')
 
 
96
    >>> string_to_delta("5m 30s")
 
210
97
    datetime.timedelta(0, 330)
 
214
 
        return rfc3339_duration_to_delta(interval)
 
218
99
    value = datetime.timedelta(0)
 
219
 
    regexp = re.compile(r"(\d+)([dsmhw]?)")
 
 
100
    regexp = re.compile("(\d+)([dsmhw]?)")
 
221
102
    for num, suffix in regexp.findall(interval):
 
222
103
        if suffix == "d":
 
223
104
            value += datetime.timedelta(int(num))
 
 
323
201
    parser.add_argument("--approval-duration",
 
324
202
                        help="Set duration of one client approval")
 
325
203
    parser.add_argument("-H", "--host", help="Set host for client")
 
326
 
    parser.add_argument("-s", "--secret",
 
327
 
                        type=argparse.FileType(mode="rb"),
 
 
204
    parser.add_argument("-s", "--secret", type=file,
 
328
205
                        help="Set password blob (file) for client")
 
329
206
    parser.add_argument("-A", "--approve", action="store_true",
 
330
207
                        help="Approve any current client request")
 
331
208
    parser.add_argument("-D", "--deny", action="store_true",
 
332
209
                        help="Deny any current client request")
 
333
 
    parser.add_argument("--check", action="store_true",
 
334
 
                        help="Run self-test")
 
335
210
    parser.add_argument("client", nargs="*", help="Client name")
 
336
211
    options = parser.parse_args()
 
338
213
    if has_actions(options) and not (options.client or options.all):
 
339
214
        parser.error("Options require clients names or --all.")
 
340
215
    if options.verbose and has_actions(options):
 
341
 
        parser.error("--verbose can only be used alone.")
 
342
 
    if options.dump_json and (options.verbose
 
343
 
                              or has_actions(options)):
 
344
 
        parser.error("--dump-json can only be used alone.")
 
 
216
        parser.error("--verbose can only be used alone or with"
 
345
218
    if options.all and not has_actions(options):
 
346
219
        parser.error("--all requires an action.")
 
350
 
        fail_count, test_count = doctest.testmod()
 
351
 
        sys.exit(os.EX_OK if fail_count == 0 else 1)
 
354
222
        bus = dbus.SystemBus()
 
355
223
        mandos_dbus_objc = bus.get_object(busname, server_path)
 
356
224
    except dbus.exceptions.DBusException:
 
357
 
        print("Could not connect to Mandos server", file=sys.stderr)
 
 
225
        print("Could not connect to Mandos server",
 
360
229
    mandos_serv = dbus.Interface(mandos_dbus_objc,
 
361
 
                                 dbus_interface=server_interface)
 
362
 
    mandos_serv_object_manager = dbus.Interface(
 
363
 
        mandos_dbus_objc, dbus_interface=dbus.OBJECT_MANAGER_IFACE)
 
365
 
    # block stderr since dbus library prints to stderr
 
 
230
                                 dbus_interface = server_interface)
 
 
232
    #block stderr since dbus library prints to stderr
 
366
233
    null = os.open(os.path.devnull, os.O_RDWR)
 
367
234
    stderrcopy = os.dup(sys.stderr.fileno())
 
368
235
    os.dup2(null, sys.stderr.fileno())
 
372
 
            mandos_clients = {path: ifs_and_props[client_interface]
 
373
 
                              for path, ifs_and_props in
 
374
 
                              mandos_serv_object_manager
 
375
 
                              .GetManagedObjects().items()
 
376
 
                              if client_interface in ifs_and_props}
 
 
239
            mandos_clients = mandos_serv.GetAllClientsWithProperties()
 
379
242
            os.dup2(stderrcopy, sys.stderr.fileno())
 
380
243
            os.close(stderrcopy)
 
381
 
    except dbus.exceptions.DBusException as e:
 
382
 
        print("Access denied: "
 
383
 
              "Accessing mandos server through D-Bus: {}".format(e),
 
 
244
    except dbus.exceptions.DBusException:
 
 
245
        print("Access denied: Accessing mandos server through dbus.",
 
387
249
    # Compile dict of (clients: properties) to process
 
390
252
    if options.all or not options.client:
 
391
 
        clients = {bus.get_object(busname, path): properties
 
392
 
                   for path, properties in mandos_clients.items()}
 
 
253
        clients = dict((bus.get_object(busname, path), properties)
 
 
254
                       for path, properties in
 
 
255
                       mandos_clients.iteritems())
 
394
257
        for name in options.client:
 
395
 
            for path, client in mandos_clients.items():
 
 
258
            for path, client in mandos_clients.iteritems():
 
396
259
                if client["Name"] == name:
 
397
260
                    client_objc = bus.get_object(busname, path)
 
398
261
                    clients[client_objc] = client
 
401
 
                print("Client not found on server: {!r}"
 
 
264
                print("Client not found on server: {0!r}"
 
402
265
                      .format(name), file=sys.stderr)
 
405
268
    if not has_actions(options) and clients:
 
406
 
        if options.verbose or options.dump_json:
 
407
 
            keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK",
 
408
 
                        "Created", "Interval", "Host", "KeyID",
 
409
 
                        "Fingerprint", "CheckerRunning",
 
 
270
            keywords = ("Name", "Enabled", "Timeout",
 
 
271
                        "LastCheckedOK", "Created", "Interval",
 
 
272
                        "Host", "Fingerprint", "CheckerRunning",
 
410
273
                        "LastEnabled", "ApprovalPending",
 
411
 
                        "ApprovedByDefault", "LastApprovalRequest",
 
412
 
                        "ApprovalDelay", "ApprovalDuration",
 
413
 
                        "Checker", "ExtendedTimeout", "Expires",
 
 
275
                        "LastApprovalRequest", "ApprovalDelay",
 
 
276
                        "ApprovalDuration", "Checker",
 
416
279
            keywords = defaultkeywords
 
418
 
        if options.dump_json:
 
419
 
            json.dump({client["Name"]: {key:
 
421
 
                                        if isinstance(client[key],
 
425
 
                       for client in clients.values()},
 
426
 
                      fp=sys.stdout, indent=4,
 
427
 
                      separators=(',', ': '))
 
430
 
            print_clients(clients.values(), keywords)
 
 
281
        print_clients(clients.values(), keywords)
 
432
283
        # Process each client in the list by all selected options
 
433
284
        for client in clients:
 
435
285
            def set_client_prop(prop, value):
 
436
286
                """Set a Client D-Bus property"""
 
437
287
                client.Set(client_interface, prop, value,
 
438
288
                           dbus_interface=dbus.PROPERTIES_IFACE)
 
440
289
            def set_client_prop_ms(prop, value):
 
441
290
                """Set a Client D-Bus property, converted
 
442
291
                from a string to milliseconds."""
 
443
292
                set_client_prop(prop,
 
444
 
                                string_to_delta(value).total_seconds()
 
 
293
                                timedelta_to_milliseconds
 
 
294
                                (string_to_delta(value)))
 
447
295
            if options.remove:
 
448
296
                mandos_serv.RemoveClient(client.__dbus_object_path__)
 
449
297
            if options.enable: