config.py 15 KB
Newer Older
1
2
from configparser import (ConfigParser, ExtendedInterpolation)
import os
3
4
import logging
import logging.config
Matt Traudt's avatar
Matt Traudt committed
5
from string import Template
6
from tempfile import NamedTemporaryFile
7
from sbws.globals import PKG_DIR
8

9
10
11
12
13
_ALPHANUM = 'abcdefghijklmnopqrstuvwxyz'
_ALPHANUM += _ALPHANUM.upper()
_ALPHANUM += '0123456789'

_HEX = '0123456789ABCDEF'
14

15
log = logging.getLogger(__name__)
16

17
18

def _read_config_file(conf, fname):
19
    assert os.path.isfile(fname)
20
    log.debug('Reading config file %s', fname)
21
22
23
24
25
    with open(fname, 'rt') as fd:
        conf.read_file(fd, source=fname)
    return conf


26
def _get_default_config():
27
    conf = ConfigParser(interpolation=ExtendedInterpolation())
28
    fname = os.path.join(PKG_DIR, 'config.default.ini')
29
    assert os.path.isfile(fname)
30
    conf = _read_config_file(conf, fname)
31
32
33
    return conf


34
def _get_user_config(args, conf=None):
35
36
37
38
39
40
41
    if not conf:
        conf = ConfigParser(interpolation=ExtendedInterpolation())
    else:
        assert isinstance(conf, ConfigParser)
    fname = os.path.join(args.directory, 'config.ini')
    if not os.path.isfile(fname):
        return conf
42
43
44
45
46
47
48
49
50
51
52
53
54
    conf = _read_config_file(conf, fname)
    return conf


def _get_user_logging_config(args, conf=None):
    if not conf:
        conf = ConfigParser(interpolation=ExtendedInterpolation())
    else:
        assert isinstance(conf, ConfigParser)
    fname = os.path.join(args.directory, 'config.log.ini')
    if not os.path.isfile(fname):
        return conf
    conf = _read_config_file(conf, fname)
55
56
57
    return conf


58
59
60
61
62
def _get_default_logging_config(args, conf=None):
    if not conf:
        conf = ConfigParser(interpolation=ExtendedInterpolation())
    else:
        assert isinstance(conf, ConfigParser)
63
    fname = os.path.join(PKG_DIR, 'config.log.default.ini')
64
65
    assert os.path.isfile(fname)
    conf = _read_config_file(conf, fname)
66
67
68
    return conf


69
70
71
72
73
74
75
76
77
def get_config(args):
    conf = _get_default_config()
    conf = _get_default_logging_config(args, conf=conf)
    conf = _get_user_config(args, conf=conf)
    conf = _get_user_logging_config(args, conf=conf)
    return conf


def get_user_example_config():
78
    conf = ConfigParser(interpolation=ExtendedInterpolation())
79
    fname = os.path.join(PKG_DIR, 'config.example.ini')
80
    assert os.path.isfile(fname)
81
    conf = _read_config_file(conf, fname)
82
    return conf
Matt Traudt's avatar
Matt Traudt committed
83
84


85
86
87
88
89
90
91
92
def configure_logging(conf):
    assert isinstance(conf, ConfigParser)
    with NamedTemporaryFile('w+t') as fd:
        conf.write(fd)
        fd.seek(0, 0)
        logging.config.fileConfig(fd.name)


Matt Traudt's avatar
Matt Traudt committed
93
94
95
96
97
98
def validate_config(conf):
    ''' Checks the given conf for bad values or bad combinations of values. If
    there's something wrong, returns False and a list of error messages.
    Otherwise, return True and an empty list '''
    errors = []
    errors.extend(_validate_general(conf))
99
    errors.extend(_validate_cleanup(conf))
Matt Traudt's avatar
Matt Traudt committed
100
    errors.extend(_validate_scanner(conf))
Matt Traudt's avatar
Matt Traudt committed
101
    errors.extend(_validate_server(conf))
102
    errors.extend(_validate_server_passwords(conf))
Matt Traudt's avatar
Matt Traudt committed
103
104
    errors.extend(_validate_tor(conf))
    errors.extend(_validate_paths(conf))
105
    errors.extend(_validate_helpers(conf))
Matt Traudt's avatar
Matt Traudt committed
106
107
108
    return len(errors) < 1, errors


109
110
111
112
113
114
115
116
117
118
119
120
121
122
def _validate_cleanup(conf):
    errors = []
    sec = 'cleanup'
    err_tmpl = Template('$sec/$key ($val): $e')
    ints = {
        'stale_days': {'minimum': 1, 'maximum': None},
        'rotten_days': {'minimum': 1, 'maximum': None},
    }
    all_valid_keys = list(ints.keys())
    errors.extend(_validate_section_keys(conf, sec, all_valid_keys, err_tmpl))
    errors.extend(_validate_section_ints(conf, sec, ints, err_tmpl))
    return errors


Matt Traudt's avatar
Matt Traudt committed
123
124
125
126
127
128
129
def _validate_general(conf):
    errors = []
    sec = 'general'
    err_tmpl = Template('$sec/$key ($val): $e')
    ints = {
        'data_period': {'minimum': 1, 'maximum': None},
    }
Matt Traudt's avatar
Matt Traudt committed
130
    all_valid_keys = list(ints.keys())
Matt Traudt's avatar
Matt Traudt committed
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
    errors.extend(_validate_section_keys(conf, sec, all_valid_keys, err_tmpl))
    errors.extend(_validate_section_ints(conf, sec, ints, err_tmpl))
    return errors


def _validate_paths(conf):
    errors = []
    sec = 'paths'
    err_tmpl = Template('$sec/$key ($val): $e')
    unvalidated_keys = ['passwords', 'datadir', 'sbws_home']
    all_valid_keys = unvalidated_keys
    errors.extend(_validate_section_keys(conf, sec, all_valid_keys, err_tmpl))
    return errors


Matt Traudt's avatar
Matt Traudt committed
146
def _validate_scanner(conf):
Matt Traudt's avatar
Matt Traudt committed
147
    errors = []
Matt Traudt's avatar
Matt Traudt committed
148
    sec = 'scanner'
Matt Traudt's avatar
Matt Traudt committed
149
150
151
    err_tmpl = Template('$sec/$key ($val): $e')
    ints = {
        'max_recv_per_read': {'minimum': 1, 'maximum': None},
152
        'num_rtts': {'minimum': 1, 'maximum': 100},
153
        'num_downloads': {'minimum': 1, 'maximum': 100},
Matt Traudt's avatar
Matt Traudt committed
154
155
156
157
158
159
160
161
162
        'initial_read_request': {'minimum': 1, 'maximum': None},
        'measurement_threads': {'minimum': 1, 'maximum': None},
    }
    floats = {
        'download_toofast': {'minimum': 0.001, 'maximum': None},
        'download_min': {'minimum': 0.001, 'maximum': None},
        'download_target': {'minimum': 0.001, 'maximum': None},
        'download_max': {'minimum': 0.001, 'maximum': None},
    }
163
164
165
    bools = {
        'measure_authorities': {},
    }
Matt Traudt's avatar
Matt Traudt committed
166
    all_valid_keys = list(ints.keys()) + list(floats.keys()) + \
167
        list(bools.keys()) + ['nickname']
Matt Traudt's avatar
Matt Traudt committed
168
169
170
171
    errors.extend(_validate_section_keys(conf, sec, all_valid_keys, err_tmpl))
    errors.extend(_validate_section_ints(conf, sec, ints, err_tmpl))
    errors.extend(_validate_section_floats(conf, sec, floats, err_tmpl))
    # XXX: validate hosts func doesn't do anything currently
172
    errors.extend(_validate_section_bools(conf, sec, bools, err_tmpl))
173
174
175
176
    valid, error_msg = _validate_nickname(conf[sec], 'nickname')
    if not valid:
        errors.append(err_tmpl.substitute(
            sec=sec, key='nickname', val=conf[sec]['nickname'], e=error_msg))
Matt Traudt's avatar
Matt Traudt committed
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
    return errors


def _validate_server(conf):
    errors = []
    sec = 'server'
    err_tmpl = Template('$sec/$key ($val): $e')
    ints = {
        'max_send_per_write': {'minimum': 1, 'maximum': None},
    }
    hosts = {
        'bind_ip': {},
    }
    ports = {
        'bind_port': {},
    }
    all_valid_keys = list(ints.keys()) + list(hosts.keys()) + \
        list(ports.keys())
    errors.extend(_validate_section_keys(conf, sec, all_valid_keys, err_tmpl))
    errors.extend(_validate_section_ints(conf, sec, ints, err_tmpl))
    # XXX: validate hosts func doesn't do anything currently
    errors.extend(_validate_section_hosts(conf, sec, hosts, err_tmpl))
    errors.extend(_validate_section_ports(conf, sec, ports, err_tmpl))
    return errors


203
204
205
206
207
208
209
210
211
212
213
214
215
def _validate_server_passwords(conf):
    errors = []
    sec = 'server.passwords'
    err_tmpl = Template('$sec/$key ($val): $e')
    section = conf[sec]
    for key in section.keys():
        valid, error_msg = _validate_password(section, key)
        if not valid:
            errors.append(err_tmpl.substitute(
                sec=sec, key=key, val=section[key], e=error_msg))
    return errors


Matt Traudt's avatar
Matt Traudt committed
216
217
218
219
220
221
222
def _validate_tor(conf):
    errors = []
    sec = 'tor'
    err_tmpl = Template('$sec/$key ($val): $e')
    enums = {
        'control_type': {'valid': ['port', 'socket']},
    }
223
224
225
226
227
228
    hosts = {
        'socks_host': {},
    }
    ports = {
        'socks_port': {},
    }
Matt Traudt's avatar
Matt Traudt committed
229
    unvalidated_keys = ['control_location']
230
231
    all_valid_keys = list(enums.keys()) + list(hosts.keys()) + \
        list(ports.keys()) + unvalidated_keys
Matt Traudt's avatar
Matt Traudt committed
232
233
    errors.extend(_validate_section_keys(conf, sec, all_valid_keys, err_tmpl))
    errors.extend(_validate_section_enums(conf, sec, enums, err_tmpl))
234
235
    errors.extend(_validate_section_hosts(conf, sec, hosts, err_tmpl))
    errors.extend(_validate_section_ports(conf, sec, ports, err_tmpl))
Matt Traudt's avatar
Matt Traudt committed
236
237
238
    return errors


239
240
241
242
243
def _validate_helpers(conf):
    errors = []
    sec = 'helpers'
    section = conf[sec]
    err_tmpl = Template('$sec/$key ($val): $e')
Matt Traudt's avatar
Matt Traudt committed
244
    additional_helper_sections = []
245
246
    for key in section.keys():
        value = section[key]
247
248
249
250
251
252
253
        if key == 'reachability_test_every':
            valid, error_msg = _validate_int(section, key, minimum=1)
            if not valid:
                errors.append(err_tmpl.substitute(
                    sec=sec, key=key, val=value, e=error_msg))
            continue
        valid, error_msg = _validate_boolean(section, key)
254
255
256
257
258
259
        if not valid:
            errors.append(err_tmpl.substitute(
                sec=sec, key=key, val=value, e=error_msg))
            continue
        assert valid
        if section.getboolean(key):
Matt Traudt's avatar
Matt Traudt committed
260
            additional_helper_sections.append('{}.{}'.format(sec, key))
261
262
263
264
265
266
267
268
269
270
271
272
273
274
    fps = {
        'relay': {},
    }
    hosts = {
        'server_host': {},
    }
    ports = {
        'server_port': {},
    }
    passwords = {
        'password': {},
    }
    all_valid_keys = list(fps.keys()) + list(hosts.keys()) + \
        list(ports.keys()) + list(passwords.keys())
Matt Traudt's avatar
Matt Traudt committed
275
    for sec in additional_helper_sections:
276
277
278
279
        if sec not in conf:
            errors.append('{} is an enabled helper but is not a section in '
                          'the config'.format(sec))
            continue
280
281
282
283
284
285
286
287
288
289
        errors.extend(_validate_section_keys(conf, sec, all_valid_keys,
                                             err_tmpl))
        errors.extend(_validate_section_fingerprints(conf, sec, fps, err_tmpl))
        errors.extend(_validate_section_hosts(conf, sec, hosts, err_tmpl))
        errors.extend(_validate_section_ports(conf, sec, ports, err_tmpl))
        errors.extend(_validate_section_passwords(conf, sec, passwords,
                                                  err_tmpl))
    return errors


Matt Traudt's avatar
Matt Traudt committed
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
def _validate_section_keys(conf, sec, keys, tmpl):
    errors = []
    section = conf[sec]
    for key in section:
        if key not in keys:
            errors.append(tmpl.substitute(
                sec=sec, key=key, val=section[key], e='Unknown key'))
    return errors


def _validate_section_enums(conf, sec, enums, tmpl):
    errors = []
    section = conf[sec]
    for key in enums:
        valid = enums[key]['valid']
        if section[key] not in valid:
            errors.append(tmpl.substitute(
                sec=sec, key=key, val=section[key],
                e='Must be one of {}'.format(valid)))
    return errors


def _validate_section_ints(conf, sec, ints, tmpl):
    errors = []
    section = conf[sec]
    for key in ints:
        valid, error = _validate_int(
            section, key, minimum=ints[key]['minimum'],
            maximum=ints[key]['maximum'])
        if not valid:
            errors.append(tmpl.substitute(
                sec=sec, key=key, val=section[key], e=error))
    return errors


def _validate_section_floats(conf, sec, floats, tmpl):
    errors = []
    section = conf[sec]
    for key in floats:
        valid, error = _validate_float(
            section, key, minimum=floats[key]['minimum'],
            maximum=floats[key]['maximum'])
        if not valid:
            errors.append(tmpl.substitute(
                sec=sec, key=key, val=section[key], e=error))
    return errors


def _validate_section_hosts(conf, sec, hosts, tmpl):
    errors = []
    section = conf[sec]
    for key in hosts:
        valid, error = _validate_host(section, key)
        if not valid:
            errors.append(tmpl.substitute(
                sec=sec, key=key, val=section[key], e=error))
    return errors


def _validate_section_ports(conf, sec, ports, tmpl):
    errors = []
    section = conf[sec]
    for key in ports:
        valid, error = _validate_int(section, key, minimum=1, maximum=2**16)
        if not valid:
            errors.append(tmpl.substitute(
                sec=sec, key=key, val=section[key],
                e='Not a valid port ({})'.format(error)))
    return errors


361
362
363
364
365
366
367
368
369
370
371
372
def _validate_section_bools(conf, sec, bools, tmpl):
    errors = []
    section = conf[sec]
    for key in bools:
        valid, error = _validate_boolean(section, key)
        if not valid:
            errors.append(tmpl.substitute(
                sec=sec, key=key, val=section[key],
                e='Not a valid boolean string ({})'.format(error)))
    return errors


373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
def _validate_section_fingerprints(conf, sec, fps, tmpl):
    errors = []
    section = conf[sec]
    for key in fps:
        valid, error = _validate_fingerprint(section, key)
        if not valid:
            errors.append(tmpl.substitute(
                sec=sec, key=key, val=section[key],
                e='Not a valid fingerprint ({})'.format(error)))
    return errors


def _validate_section_passwords(conf, sec, passwords, tmpl):
    errors = []
    section = conf[sec]
    for key in passwords:
        valid, error = _validate_password(section, key)
        if not valid:
            errors.append(tmpl.substitute(
                sec=sec, key=key, val=section[key],
                e='Not a valid password ({})'.format(error)))
    return errors


Matt Traudt's avatar
Matt Traudt committed
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
def _validate_int(section, key, minimum=None, maximum=None):
    try:
        value = section.getint(key)
    except ValueError as e:
        return False, e
    if minimum is not None:
        assert isinstance(minimum, int)
        if value < minimum:
            return False, 'Cannot be less than {}'.format(minimum)
    if maximum is not None:
        assert isinstance(maximum, int)
        if value > maximum:
            return False, 'Cannot be greater than {}'.format(maximum)
    return True, ''


413
414
415
416
417
418
419
420
def _validate_boolean(section, key):
    try:
        section.getboolean(key)
    except ValueError as e:
        return False, e
    return True, ''


Matt Traudt's avatar
Matt Traudt committed
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
def _validate_float(section, key, minimum=None, maximum=None):
    try:
        value = section.getfloat(key)
    except ValueError as e:
        return False, e
    if minimum is not None:
        assert isinstance(minimum, float)
        if value < minimum:
            return False, 'Cannot be less than {}'.format(minimum)
    if maximum is not None:
        assert isinstance(maximum, float)
        if value > maximum:
            return False, 'Cannot be greater than {}'.format(maximum)
    return True, ''


def _validate_host(section, key):
    # XXX: Implement this
    return True, ''
440
441
442


def _validate_fingerprint(section, key):
443
    alphabet = _HEX
444
445
446
447
448
449
    length = 40
    return _validate_string(section, key, min_len=length, max_len=length,
                            alphabet=alphabet)


def _validate_password(section, key):
450
    alphabet = _ALPHANUM
451
452
453
454
455
    length = 64
    return _validate_string(section, key, min_len=length, max_len=length,
                            alphabet=alphabet)


456
def _validate_nickname(section, key):
457
    alphabet = _ALPHANUM
458
459
460
461
462
463
    min_len = 1
    max_len = 32
    return _validate_string(section, key, min_len=min_len, max_len=max_len,
                            alphabet=alphabet)


464
465
466
467
468
469
470
471
472
473
474
475
476
477
def _validate_string(section, key, min_len=None, max_len=None, alphabet=None):
    s = section[key]
    if min_len is not None and len(s) < min_len:
        return False, '{} is below minimum allowed length {}'.format(
            len(s), min_len)
    if max_len is not None and len(s) > max_len:
        return False, '{} is above maximum allowed length {}'.format(
            len(s), max_len)
    if alphabet is not None:
        for i, c in enumerate(s):
            if c not in alphabet:
                return False, 'Letter {} at position {} is not in allowed '\
                    'characters "{}"'.format(c, i, alphabet)
    return True, ''