2
2
# -*- mode: python; coding: utf-8 -*-
4
4
# Mandos Monitor - Control and monitor the Mandos server
6
# Copyright © 2008-2016 Teddy Hogeborn
7
# Copyright © 2008-2016 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
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
11
13
# the Free Software Foundation, either version 3 of the License, or
12
14
# (at your option) any later version.
14
# This program is distributed in the hope that it will be useful,
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16
# Mandos is distributed in the hope that it will be useful, but
17
# WITHOUT ANY WARRANTY; without even the implied warranty of
16
18
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
19
# GNU General Public License for more details.
19
21
# You should have received a copy of the GNU General Public License
20
# along with this program. If not, see
21
# <http://www.gnu.org/licenses/>.
22
# along with Mandos. If not, see <http://www.gnu.org/licenses/>.
23
24
# Contact the authors at <mandos@recompile.se>.
26
27
from __future__ import (division, absolute_import, print_function,
65
66
"ApprovalDelay": "Approval Delay",
66
67
"ApprovalDuration": "Approval Duration",
67
68
"Checker": "Checker",
68
"ExtendedTimeout": "Extended Timeout"
69
"ExtendedTimeout": "Extended Timeout",
71
"LastCheckerStatus": "Last Checker Status",
70
73
defaultkeywords = ("Name", "Enabled", "Timeout", "LastCheckedOK")
71
74
domain = "se.recompile"
81
84
except AttributeError:
82
85
dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
84
88
def milliseconds_to_string(ms):
85
89
td = datetime.timedelta(0, 0, 0, ms)
86
return ("{days}{hours:02}:{minutes:02}:{seconds:02}".format(
87
days = "{}T".format(td.days) if td.days else "",
88
hours = td.seconds // 3600,
89
minutes = (td.seconds % 3600) // 60,
90
seconds = td.seconds % 60))
90
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))
93
97
def rfc3339_duration_to_delta(duration):
94
98
"""Parse an RFC 3339 "duration" and return a datetime.timedelta
96
100
>>> rfc3339_duration_to_delta("P7D")
97
101
datetime.timedelta(7)
98
102
>>> rfc3339_duration_to_delta("PT60S")
108
112
>>> rfc3339_duration_to_delta("P1DT3M20S")
109
113
datetime.timedelta(1, 200)
112
116
# Parsing an RFC 3339 duration with regular expressions is not
113
117
# possible - there would have to be multiple places for the same
114
118
# values, like seconds. The current code, while more esoteric, is
115
119
# cleaner without depending on a parsing library. If Python had a
116
120
# built-in library for parsing we would use it, but we'd like to
117
121
# avoid excessive use of external libraries.
119
123
# New type for defining tokens, syntax, and semantics all-in-one
120
124
Token = collections.namedtuple("Token", (
121
125
"regexp", # To match token; if "value" is not None, must have
154
158
frozenset((token_year, token_month,
155
159
token_day, token_time,
157
# Define starting values
158
value = datetime.timedelta() # Value so far
161
# Define starting values:
163
value = datetime.timedelta()
159
164
found_token = None
160
followers = frozenset((token_duration, )) # Following valid tokens
161
s = duration # String left to parse
165
# Following valid tokens
166
followers = frozenset((token_duration, ))
167
# String left to parse
162
169
# Loop until end token is found
163
170
while found_token is not token_end:
164
171
# Search for any currently valid tokens
235
242
"ApprovalDuration", "ExtendedTimeout"):
236
243
return milliseconds_to_string(value)
237
244
return str(value)
239
246
# Create format string to print table rows
240
247
format_string = " ".join("{{{key}:{width}}}".format(
241
width = max(len(tablewords[key]),
242
max(len(valuetostring(client[key], key))
243
for client in clients)),
248
width=max(len(tablewords[key]),
249
max(len(valuetostring(client[key], key))
250
for client in clients)),
245
252
for key in keywords)
246
253
# Print header line
247
254
print(format_string.format(**tablewords))
248
255
for client in clients:
249
print(format_string.format(**{
250
key: valuetostring(client[key], key)
251
for key in keywords }))
257
.format(**{key: valuetostring(client[key], key)
258
for key in keywords}))
254
261
def has_actions(options):
276
283
parser = argparse.ArgumentParser()
277
284
parser.add_argument("--version", action="version",
278
version = "%(prog)s {}".format(version),
285
version="%(prog)s {}".format(version),
279
286
help="show version number and exit")
280
287
parser.add_argument("-a", "--all", action="store_true",
281
288
help="Select all clients")
327
334
help="Run self-test")
328
335
parser.add_argument("client", nargs="*", help="Client name")
329
336
options = parser.parse_args()
331
338
if has_actions(options) and not (options.client or options.all):
332
339
parser.error("Options require clients names or --all.")
333
340
if options.verbose and has_actions(options):
334
341
parser.error("--verbose can only be used alone.")
335
if options.dump_json and (options.verbose or has_actions(options)):
342
if options.dump_json and (options.verbose
343
or has_actions(options)):
336
344
parser.error("--dump-json can only be used alone.")
337
345
if options.all and not has_actions(options):
338
346
parser.error("--all requires an action.")
340
348
if options.check:
341
350
fail_count, test_count = doctest.testmod()
342
351
sys.exit(os.EX_OK if fail_count == 0 else 1)
345
354
bus = dbus.SystemBus()
346
355
mandos_dbus_objc = bus.get_object(busname, server_path)
347
356
except dbus.exceptions.DBusException:
348
357
print("Could not connect to Mandos server", file=sys.stderr)
351
360
mandos_serv = dbus.Interface(mandos_dbus_objc,
352
dbus_interface = server_interface)
361
dbus_interface=server_interface)
353
362
mandos_serv_object_manager = dbus.Interface(
354
mandos_dbus_objc, dbus_interface = dbus.OBJECT_MANAGER_IFACE)
356
#block stderr since dbus library prints to stderr
363
mandos_dbus_objc, dbus_interface=dbus.OBJECT_MANAGER_IFACE)
365
# block stderr since dbus library prints to stderr
357
366
null = os.open(os.path.devnull, os.O_RDWR)
358
367
stderrcopy = os.dup(sys.stderr.fileno())
359
368
os.dup2(null, sys.stderr.fileno())
363
mandos_clients = { path: ifs_and_props[client_interface]
364
for path, ifs_and_props in
365
mandos_serv_object_manager
366
.GetManagedObjects().items()
367
if client_interface in ifs_and_props }
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}
370
379
os.dup2(stderrcopy, sys.stderr.fileno())
371
380
os.close(stderrcopy)
372
381
except dbus.exceptions.DBusException as e:
373
print("Access denied: Accessing mandos server through D-Bus: {}"
374
.format(e), file=sys.stderr)
382
print("Access denied: "
383
"Accessing mandos server through D-Bus: {}".format(e),
377
387
# Compile dict of (clients: properties) to process
380
390
if options.all or not options.client:
381
clients = { bus.get_object(busname, path): properties
382
for path, properties in mandos_clients.items() }
391
clients = {bus.get_object(busname, path): properties
392
for path, properties in mandos_clients.items()}
384
394
for name in options.client:
385
395
for path, client in mandos_clients.items():
391
401
print("Client not found on server: {!r}"
392
402
.format(name), file=sys.stderr)
395
405
if not has_actions(options) and clients:
396
406
if options.verbose or options.dump_json:
397
407
keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK",
398
"Created", "Interval", "Host", "Fingerprint",
399
"CheckerRunning", "LastEnabled",
400
"ApprovalPending", "ApprovedByDefault",
401
"LastApprovalRequest", "ApprovalDelay",
402
"ApprovalDuration", "Checker",
408
"Created", "Interval", "Host", "KeyID",
409
"Fingerprint", "CheckerRunning",
410
"LastEnabled", "ApprovalPending",
411
"ApprovedByDefault", "LastApprovalRequest",
412
"ApprovalDelay", "ApprovalDuration",
413
"Checker", "ExtendedTimeout", "Expires",
405
416
keywords = defaultkeywords
407
418
if options.dump_json:
408
json.dump({client["Name"]: {key: client[key]
409
for key in keywords }
410
for client in clients.values() },
411
fp = sys.stdout, indent = 4,
412
separators = (',', ': '))
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=(',', ': '))
415
430
print_clients(clients.values(), keywords)
417
432
# Process each client in the list by all selected options
418
433
for client in clients:
420
435
def set_client_prop(prop, value):
421
436
"""Set a Client D-Bus property"""
422
437
client.Set(client_interface, prop, value,
423
438
dbus_interface=dbus.PROPERTIES_IFACE)
425
440
def set_client_prop_ms(prop, value):
426
441
"""Set a Client D-Bus property, converted
427
442
from a string to milliseconds."""
428
443
set_client_prop(prop,
429
444
string_to_delta(value).total_seconds()
432
447
if options.remove:
433
448
mandos_serv.RemoveClient(client.__dbus_object_path__)
434
449
if options.enable:
442
457
if options.stop_checker:
443
458
set_client_prop("CheckerRunning", dbus.Boolean(False))
444
459
if options.is_enabled:
445
sys.exit(0 if client.Get(client_interface,
448
dbus.PROPERTIES_IFACE)
460
if client.Get(client_interface, "Enabled",
461
dbus_interface=dbus.PROPERTIES_IFACE):
450
465
if options.checker is not None:
451
466
set_client_prop("Checker", options.checker)
452
467
if options.host is not None: