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,
64
66
"ApprovalDelay": "Approval Delay",
65
67
"ApprovalDuration": "Approval Duration",
66
68
"Checker": "Checker",
67
"ExtendedTimeout": "Extended Timeout"
69
"ExtendedTimeout": "Extended Timeout",
71
"LastCheckerStatus": "Last Checker Status",
69
73
defaultkeywords = ("Name", "Enabled", "Timeout", "LastCheckedOK")
70
74
domain = "se.recompile"
80
84
except AttributeError:
81
85
dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
83
88
def milliseconds_to_string(ms):
84
89
td = datetime.timedelta(0, 0, 0, ms)
85
return ("{days}{hours:02}:{minutes:02}:{seconds:02}".format(
86
days = "{}T".format(td.days) if td.days else "",
87
hours = td.seconds // 3600,
88
minutes = (td.seconds % 3600) // 60,
89
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))
92
97
def rfc3339_duration_to_delta(duration):
93
98
"""Parse an RFC 3339 "duration" and return a datetime.timedelta
95
100
>>> rfc3339_duration_to_delta("P7D")
96
101
datetime.timedelta(7)
97
102
>>> rfc3339_duration_to_delta("PT60S")
107
112
>>> rfc3339_duration_to_delta("P1DT3M20S")
108
113
datetime.timedelta(1, 200)
111
116
# Parsing an RFC 3339 duration with regular expressions is not
112
117
# possible - there would have to be multiple places for the same
113
118
# values, like seconds. The current code, while more esoteric, is
114
119
# cleaner without depending on a parsing library. If Python had a
115
120
# built-in library for parsing we would use it, but we'd like to
116
121
# avoid excessive use of external libraries.
118
123
# New type for defining tokens, syntax, and semantics all-in-one
119
124
Token = collections.namedtuple("Token", (
120
125
"regexp", # To match token; if "value" is not None, must have
153
158
frozenset((token_year, token_month,
154
159
token_day, token_time,
156
# Define starting values
157
value = datetime.timedelta() # Value so far
161
# Define starting values:
163
value = datetime.timedelta()
158
164
found_token = None
159
followers = frozenset((token_duration, )) # Following valid tokens
160
s = duration # String left to parse
165
# Following valid tokens
166
followers = frozenset((token_duration, ))
167
# String left to parse
161
169
# Loop until end token is found
162
170
while found_token is not token_end:
163
171
# Search for any currently valid tokens
234
242
"ApprovalDuration", "ExtendedTimeout"):
235
243
return milliseconds_to_string(value)
236
244
return str(value)
238
246
# Create format string to print table rows
239
247
format_string = " ".join("{{{key}:{width}}}".format(
240
width = max(len(tablewords[key]),
241
max(len(valuetostring(client[key], key))
242
for client in clients)),
248
width=max(len(tablewords[key]),
249
max(len(valuetostring(client[key], key))
250
for client in clients)),
244
252
for key in keywords)
245
253
# Print header line
246
254
print(format_string.format(**tablewords))
247
255
for client in clients:
248
print(format_string.format(**{
249
key: valuetostring(client[key], key)
250
for key in keywords }))
257
.format(**{key: valuetostring(client[key], key)
258
for key in keywords}))
253
261
def has_actions(options):
275
283
parser = argparse.ArgumentParser()
276
284
parser.add_argument("--version", action="version",
277
version = "%(prog)s {}".format(version),
285
version="%(prog)s {}".format(version),
278
286
help="show version number and exit")
279
287
parser.add_argument("-a", "--all", action="store_true",
280
288
help="Select all clients")
281
289
parser.add_argument("-v", "--verbose", action="store_true",
282
290
help="Print all fields")
291
parser.add_argument("-j", "--dump-json", action="store_true",
292
help="Dump client data in JSON format")
283
293
parser.add_argument("-e", "--enable", action="store_true",
284
294
help="Enable client")
285
295
parser.add_argument("-d", "--disable", action="store_true",
324
334
help="Run self-test")
325
335
parser.add_argument("client", nargs="*", help="Client name")
326
336
options = parser.parse_args()
328
338
if has_actions(options) and not (options.client or options.all):
329
339
parser.error("Options require clients names or --all.")
330
340
if options.verbose and has_actions(options):
331
parser.error("--verbose can only be used alone or with"
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.")
333
345
if options.all and not has_actions(options):
334
346
parser.error("--all requires an action.")
336
348
if options.check:
337
350
fail_count, test_count = doctest.testmod()
338
351
sys.exit(os.EX_OK if fail_count == 0 else 1)
341
354
bus = dbus.SystemBus()
342
355
mandos_dbus_objc = bus.get_object(busname, server_path)
343
356
except dbus.exceptions.DBusException:
344
357
print("Could not connect to Mandos server", file=sys.stderr)
347
360
mandos_serv = dbus.Interface(mandos_dbus_objc,
348
dbus_interface = server_interface)
361
dbus_interface=server_interface)
349
362
mandos_serv_object_manager = dbus.Interface(
350
mandos_dbus_objc, dbus_interface = dbus.OBJECT_MANAGER_IFACE)
352
#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
353
366
null = os.open(os.path.devnull, os.O_RDWR)
354
367
stderrcopy = os.dup(sys.stderr.fileno())
355
368
os.dup2(null, sys.stderr.fileno())
359
mandos_clients = { path: ifs_and_props[client_interface]
360
for path, ifs_and_props in
361
mandos_serv_object_manager
362
.GetManagedObjects().items()
363
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}
366
379
os.dup2(stderrcopy, sys.stderr.fileno())
367
380
os.close(stderrcopy)
368
381
except dbus.exceptions.DBusException as e:
369
print("Access denied: Accessing mandos server through D-Bus: {}"
370
.format(e), file=sys.stderr)
382
print("Access denied: "
383
"Accessing mandos server through D-Bus: {}".format(e),
373
387
# Compile dict of (clients: properties) to process
376
390
if options.all or not options.client:
377
clients = { bus.get_object(busname, path): properties
378
for path, properties in mandos_clients.items() }
391
clients = {bus.get_object(busname, path): properties
392
for path, properties in mandos_clients.items()}
380
394
for name in options.client:
381
395
for path, client in mandos_clients.items():
387
401
print("Client not found on server: {!r}"
388
402
.format(name), file=sys.stderr)
391
405
if not has_actions(options) and clients:
406
if options.verbose or options.dump_json:
393
407
keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK",
394
"Created", "Interval", "Host", "Fingerprint",
395
"CheckerRunning", "LastEnabled",
396
"ApprovalPending", "ApprovedByDefault",
397
"LastApprovalRequest", "ApprovalDelay",
398
"ApprovalDuration", "Checker",
408
"Created", "Interval", "Host", "KeyID",
409
"Fingerprint", "CheckerRunning",
410
"LastEnabled", "ApprovalPending",
411
"ApprovedByDefault", "LastApprovalRequest",
412
"ApprovalDelay", "ApprovalDuration",
413
"Checker", "ExtendedTimeout", "Expires",
401
416
keywords = defaultkeywords
403
print_clients(clients.values(), keywords)
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)
405
432
# Process each client in the list by all selected options
406
433
for client in clients:
408
435
def set_client_prop(prop, value):
409
436
"""Set a Client D-Bus property"""
410
437
client.Set(client_interface, prop, value,
411
438
dbus_interface=dbus.PROPERTIES_IFACE)
413
440
def set_client_prop_ms(prop, value):
414
441
"""Set a Client D-Bus property, converted
415
442
from a string to milliseconds."""
416
443
set_client_prop(prop,
417
444
string_to_delta(value).total_seconds()
420
447
if options.remove:
421
448
mandos_serv.RemoveClient(client.__dbus_object_path__)
422
449
if options.enable:
430
457
if options.stop_checker:
431
458
set_client_prop("CheckerRunning", dbus.Boolean(False))
432
459
if options.is_enabled:
433
sys.exit(0 if client.Get(client_interface,
436
dbus.PROPERTIES_IFACE)
460
if client.Get(client_interface, "Enabled",
461
dbus_interface=dbus.PROPERTIES_IFACE):
438
465
if options.checker is not None:
439
466
set_client_prop("Checker", options.checker)
440
467
if options.host is not None: