chiark / gitweb /
(Python): Use more modern `raise' syntax.
[tripe] / keys / tripe-keys.in
CommitLineData
060ca767 1#! @PYTHON@
fd42a1e5
MW
2### -*-python-*-
3###
4### Key management and distribution
5###
6### (c) 2006 Straylight/Edgeware
7###
8
9###----- Licensing notice ---------------------------------------------------
10###
11### This file is part of Trivial IP Encryption (TrIPE).
12###
11ad66c2
MW
13### TrIPE is free software: you can redistribute it and/or modify it under
14### the terms of the GNU General Public License as published by the Free
15### Software Foundation; either version 3 of the License, or (at your
16### option) any later version.
fd42a1e5 17###
11ad66c2
MW
18### TrIPE is distributed in the hope that it will be useful, but WITHOUT
19### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
20### FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
21### for more details.
fd42a1e5
MW
22###
23### You should have received a copy of the GNU General Public License
11ad66c2 24### along with TrIPE. If not, see <https://www.gnu.org/licenses/>.
fd42a1e5
MW
25
26###--------------------------------------------------------------------------
27### External dependencies.
060ca767 28
29import catacomb as C
30import os as OS
31import sys as SYS
c55f0b7c 32import re as RX
060ca767 33import getopt as O
c77687d5 34import shutil as SH
c2f28e4b 35import time as T
c77687d5 36import filecmp as FC
060ca767 37from cStringIO import StringIO
38from errno import *
39from stat import *
40
fd42a1e5 41###--------------------------------------------------------------------------
060ca767 42### Useful regular expressions
43
fd42a1e5 44## Match a comment or blank line.
c77687d5 45rx_comment = RX.compile(r'^\s*(#|$)')
fd42a1e5
MW
46
47## Match a KEY = VALUE assignment.
c77687d5 48rx_keyval = RX.compile(r'^\s*([-\w]+)(?:\s+(?!=)|\s*=\s*)(|\S|\S.*\S)\s*$')
fd42a1e5
MW
49
50## Match a ${KEY} substitution.
c77687d5 51rx_dollarsubst = RX.compile(r'\$\{([-\w]+)\}')
fd42a1e5
MW
52
53## Match a @TAG@ substitution.
c77687d5 54rx_atsubst = RX.compile(r'@([-\w]+)@')
fd42a1e5
MW
55
56## Match a single non-alphanumeric character.
c77687d5 57rx_nonalpha = RX.compile(r'\W')
fd42a1e5
MW
58
59## Match the literal string "<SEQ>".
c77687d5 60rx_seq = RX.compile(r'\<SEQ\>')
060ca767 61
6005ef9b
MW
62## Match a shell metacharacter.
63rx_shmeta = RX.compile('[\\s`!"#$&*()\\[\\];\'|<>?\\\\]')
64
65## Match a character which needs escaping in a shell double-quoted string.
66rx_shquote = RX.compile(r'["`$\\]')
67
fd42a1e5
MW
68###--------------------------------------------------------------------------
69### Utility functions.
060ca767 70
fd42a1e5 71## Exceptions.
060ca767 72class SubprocessError (Exception): pass
73class VerifyError (Exception): pass
74
fd42a1e5 75## Program name and identification.
060ca767 76quis = OS.path.basename(SYS.argv[0])
77PACKAGE = "@PACKAGE@"
78VERSION = "@VERSION@"
79
80def moan(msg):
fd42a1e5 81 """Report MSG to standard error."""
060ca767 82 SYS.stderr.write('%s: %s\n' % (quis, msg))
83
84def die(msg, rc = 1):
fd42a1e5 85 """Report MSG to standard error, and exit with code RC."""
060ca767 86 moan(msg)
87 SYS.exit(rc)
88
89def subst(s, rx, map):
fd42a1e5
MW
90 """
91 Substitute values into a string.
92
93 Repeatedly match RX (a compiled regular expression) against the string S.
94 For each match, extract group 1, and use it as a key to index the MAP;
95 replace the match by the result. Finally, return the fully-substituted
96 string.
97 """
060ca767 98 out = StringIO()
99 i = 0
100 for m in rx.finditer(s):
101 out.write(s[i:m.start()] + map[m.group(1)])
102 i = m.end()
103 out.write(s[i:])
104 return out.getvalue()
105
6005ef9b
MW
106def shell_quotify(arg):
107 """
108 Quotify ARG to keep the shell happy.
109
110 This isn't actually used for invoking commands, just for presentation
111 purposes; but correctness is still nice.
112 """
113 if not rx_shmeta.search(arg):
114 return arg
115 elif arg.find("'") == -1:
116 return "'%s'" % arg
117 else:
118 return '"%s"' % rx_shquote.sub(lambda m: '\\' + m.group(0), arg)
119
060ca767 120def rmtree(path):
fd42a1e5 121 """Delete the directory tree given by PATH."""
060ca767 122 try:
c77687d5 123 st = OS.lstat(path)
060ca767 124 except OSError, err:
125 if err.errno == ENOENT:
126 return
127 raise
128 if not S_ISDIR(st.st_mode):
129 OS.unlink(path)
130 else:
131 cwd = OS.getcwd()
132 try:
133 OS.chdir(path)
134 for i in OS.listdir('.'):
135 rmtree(i)
136 finally:
137 OS.chdir(cwd)
138 OS.rmdir(path)
139
140def zap(file):
fd42a1e5 141 """Delete the named FILE if it exists; otherwise do nothing."""
060ca767 142 try:
143 OS.unlink(file)
144 except OSError, err:
145 if err.errno == ENOENT: return
146 raise
147
148def run(args):
fd42a1e5
MW
149 """
150 Run a subprocess whose arguments are given by the string ARGS.
151
152 The ARGS are split at word boundaries, and then subjected to configuration
153 variable substitution (see conf_subst). Individual argument elements
154 beginning with `!' are split again into multiple arguments at word
155 boundaries.
156 """
060ca767 157 args = map(conf_subst, args.split())
158 nargs = []
159 for a in args:
160 if len(a) > 0 and a[0] != '!':
161 nargs += [a]
162 else:
163 nargs += a[1:].split()
164 args = nargs
6005ef9b 165 print '+ %s' % ' '.join([shell_quotify(arg) for arg in args])
8cae2567 166 SYS.stdout.flush()
060ca767 167 rc = OS.spawnvp(OS.P_WAIT, args[0], args)
168 if rc != 0:
171206b5 169 raise SubprocessError(rc)
060ca767 170
171def hexhyphens(bytes):
fd42a1e5
MW
172 """
173 Convert a byte string BYTES into hex, with hyphens at each 4-byte boundary.
174 """
060ca767 175 out = StringIO()
176 for i in xrange(0, len(bytes)):
177 if i > 0 and i % 4 == 0: out.write('-')
178 out.write('%02x' % ord(bytes[i]))
179 return out.getvalue()
180
181def fingerprint(kf, ktag):
fd42a1e5
MW
182 """
183 Compute the fingerprint of a key, using the user's selected hash.
184
185 KF is the name of a keyfile; KTAG is the tag of the key.
186 """
060ca767 187 h = C.gchashes[conf['fingerprint-hash']]()
188 k = C.KeyFile(kf)[ktag].fingerprint(h, '-secret')
189 return h.done()
190
fd42a1e5
MW
191###--------------------------------------------------------------------------
192### The configuration file.
060ca767 193
fd42a1e5 194## Exceptions.
060ca767 195class ConfigFileError (Exception): pass
fd42a1e5
MW
196
197## The configuration dictionary.
060ca767 198conf = {}
199
fd42a1e5
MW
200def conf_subst(s):
201 """
202 Apply configuration substitutions to S.
203
204 That is, for each ${KEY} in S, replace it with the current value of the
205 configuration variable KEY.
206 """
207 return subst(s, rx_dollarsubst, conf)
060ca767 208
060ca767 209def conf_read(f):
fd42a1e5
MW
210 """
211 Read the file F and insert assignments into the configuration dictionary.
212 """
060ca767 213 lno = 0
214 for line in file(f):
215 lno += 1
c77687d5 216 if rx_comment.match(line): continue
060ca767 217 if line[-1] == '\n': line = line[:-1]
c77687d5 218 match = rx_keyval.match(line)
060ca767 219 if not match:
171206b5 220 raise ConfigFileError("%s:%d: bad line `%s'" % (f, lno, line))
060ca767 221 k, v = match.groups()
222 conf[k] = conf_subst(v)
223
060ca767 224def conf_defaults():
fd42a1e5
MW
225 """
226 Apply defaults to the configuration dictionary.
227
228 Fill in all the interesting configuration variables based on the existing
229 contents, as described in the manual.
230 """
c77687d5 231 for k, v in [('repos-base', 'tripe-keys.tar.gz'),
232 ('sig-base', 'tripe-keys.sig-<SEQ>'),
233 ('repos-url', '${base-url}${repos-base}'),
234 ('sig-url', '${base-url}${sig-base}'),
235 ('sig-file', '${base-dir}${sig-base}'),
236 ('repos-file', '${base-dir}${repos-base}'),
060ca767 237 ('conf-file', '${base-dir}tripe-keys.conf'),
b14ccd2f 238 ('upload-hook', ': run upload hook'),
060ca767 239 ('kx', 'dh'),
256bc8d0 240 ('kx-genalg', lambda: {'dh': 'dh',
26936c83
MW
241 'ec': 'ec',
242 'x25519': 'x25519',
243 'x448': 'x448'}[conf['kx']]),
256bc8d0 244 ('kx-param-genalg', lambda: {'dh': 'dh-param',
26936c83
MW
245 'ec': 'ec-param',
246 'x25519': 'empty',
247 'x448': 'empty'}[conf['kx']]),
ca3aaaeb 248 ('kx-param', lambda: {'dh': '-LS -b3072 -B256',
26936c83
MW
249 'ec': '-Cnist-p256',
250 'x25519': '',
251 'x448': ''}[conf['kx']]),
252 ('kx-attrs', lambda: {'dh': 'serialization=constlen',
253 'ec': 'serialization=constlen',
254 'x25519': '',
255 'x448': ''}[conf['kx']]),
060ca767 256 ('kx-expire', 'now + 1 year'),
c2f28e4b 257 ('kx-warn-days', '28'),
39bcd193 258 ('bulk', 'iiv'),
de8edc7f
MW
259 ('cipher', lambda: conf['bulk'] == 'naclbox'
260 and 'salsa20' or 'rijndael-cbc'),
060ca767 261 ('hash', 'sha256'),
7858dfa0 262 ('master-keygen-flags', '-l'),
67bb121f 263 ('master-attrs', ''),
060ca767 264 ('mgf', '${hash}-mgf'),
de8edc7f
MW
265 ('mac', lambda: conf['bulk'] == 'naclbox'
266 and 'poly1305/128'
267 or '%s-hmac/%d' %
268 (conf['hash'],
269 C.gchashes[conf['hash']].hashsz * 4)),
26936c83
MW
270 ('sig', lambda: {'dh': 'dsa',
271 'ec': 'ecdsa',
272 'x25519': 'ed25519',
273 'x448': 'ed448'}[conf['kx']]),
060ca767 274 ('sig-fresh', 'always'),
275 ('sig-genalg', lambda: {'kcdsa': 'dh',
276 'dsa': 'dsa',
277 'rsapkcs1': 'rsa',
278 'rsapss': 'rsa',
279 'ecdsa': 'ec',
06a174df
MW
280 'eckcdsa': 'ec',
281 'ed25519': 'ed25519',
282 'ed448': 'ed448'}[conf['sig']]),
ca3aaaeb
MW
283 ('sig-param', lambda: {'dh': '-LS -b3072 -B256',
284 'dsa': '-b3072 -B256',
060ca767 285 'ec': '-Cnist-p256',
06a174df
MW
286 'rsa': '-b3072',
287 'ed25519': '',
288 'ed448': ''}[conf['sig-genalg']]),
060ca767 289 ('sig-hash', '${hash}'),
290 ('sig-expire', 'forever'),
291 ('fingerprint-hash', '${hash}')]:
292 try:
293 if k in conf: continue
294 if type(v) == str:
295 conf[k] = conf_subst(v)
296 else:
297 conf[k] = v()
298 except KeyError, exc:
299 if len(exc.args) == 0: raise
300 conf[k] = '<missing-var %s>' % exc.args[0]
301
fd42a1e5
MW
302###--------------------------------------------------------------------------
303### Key-management utilities.
304
305def master_keys():
306 """
307 Iterate over the master keys.
308 """
309 if not OS.path.exists('master'):
310 return
311 for k in C.KeyFile('master').itervalues():
312 if (k.type != 'tripe-keys-master' or
313 k.expiredp or
314 not k.tag.startswith('master-')):
315 continue #??
316 yield k
317
318def master_sequence(k):
319 """
320 Return the sequence number of the given master key as an integer.
321
322 No checking is done that K is really a master key.
323 """
324 return int(k.tag[7:])
325
326def max_master_sequence():
327 """
328 Find the master key with the highest sequence number and return this
329 sequence number.
330 """
331 seq = -1
332 for k in master_keys():
333 q = master_sequence(k)
334 if q > seq: seq = q
335 return seq
336
337def seqsubst(x, q):
338 """
339 Return the value of the configuration variable X, with <SEQ> replaced by
340 the value Q.
341 """
342 return rx_seq.sub(str(q), conf[x])
343
344###--------------------------------------------------------------------------
345### Commands: help [COMMAND...]
060ca767 346
347def version(fp = SYS.stdout):
348 fp.write('%s, %s version %s\n' % (quis, PACKAGE, VERSION))
349
350def usage(fp):
351 fp.write('Usage: %s SUBCOMMAND [ARGS...]\n' % quis)
352
353def cmd_help(args):
354 if len(args) == 0:
355 version(SYS.stdout)
356 print
357 usage(SYS.stdout)
358 print """
359Key management utility for TrIPE.
360
361Options supported:
362
e04c2d50
MW
363-h, --help Show this help message.
364-v, --version Show the version number.
365-u, --usage Show pointlessly short usage string.
060ca767 366
367Subcommands available:
368"""
369 args = commands.keys()
370 args.sort()
371 for c in args:
db76b51b
MW
372 try: func, min, max, help = commands[c]
373 except KeyError: die("unknown command `%s'" % c)
374 print '%s%s%s' % (c, help and ' ', help)
060ca767 375
fd42a1e5
MW
376###--------------------------------------------------------------------------
377### Commands: newmaster
c77687d5 378
379def cmd_newmaster(args):
380 seq = max_master_sequence() + 1
060ca767 381 run('''key -kmaster add
382 -a${sig-genalg} !${sig-param}
7858dfa0 383 -e${sig-expire} !${master-keygen-flags} -tmaster-%d tripe-keys-master
67bb121f 384 sig=${sig} hash=${sig-hash} !${master-attrs}''' % seq)
c77687d5 385 run('key -kmaster extract -f-secret repos/master.pub')
060ca767 386
fd42a1e5
MW
387###--------------------------------------------------------------------------
388### Commands: setup
389
c77687d5 390def cmd_setup(args):
391 OS.mkdir('repos')
060ca767 392 run('''key -krepos/param add
256bc8d0 393 -a${kx-param-genalg} !${kx-param}
fc5f4823 394 -eforever -tparam tripe-param
67bb121f 395 kx-group=${kx} mgf=${mgf} mac=${mac}
39bcd193 396 bulk=${bulk} cipher=${cipher} hash=${hash} ${kx-attrs}''')
c77687d5 397 cmd_newmaster(args)
060ca767 398
fd42a1e5
MW
399###--------------------------------------------------------------------------
400### Commands: upload
401
060ca767 402def cmd_upload(args):
403
404 ## Sanitize the repository directory
405 umask = OS.umask(0); OS.umask(umask)
406 mode = 0666 & ~umask
407 for f in OS.listdir('repos'):
408 ff = OS.path.join('repos', f)
c77687d5 409 if (f.startswith('master') or f.startswith('peer-')) \
410 and f.endswith('.old'):
060ca767 411 OS.unlink(ff)
412 continue
c77687d5 413 OS.chmod(ff, mode)
414
415 rmtree('tmp')
416 OS.mkdir('tmp')
417 OS.symlink('../repos', 'tmp/repos')
418 cwd = OS.getcwd()
419 try:
420
421 ## Build the configuration file
422 seq = max_master_sequence()
423 v = {'MASTER-SEQUENCE': str(seq),
424 'HK-MASTER': hexhyphens(fingerprint('repos/master.pub',
425 'master-%d' % seq))}
426 fin = file('tripe-keys.master')
427 fout = file('tmp/tripe-keys.conf', 'w')
428 for line in fin:
429 fout.write(subst(line, rx_atsubst, v))
430 fin.close(); fout.close()
431 SH.copyfile('tmp/tripe-keys.conf', conf_subst('${conf-file}.new'))
432 commit = [conf['repos-file'], conf['conf-file']]
433
434 ## Make and sign the repository archive
435 OS.chdir('tmp')
436 run('tar chozf ${repos-file}.new .')
437 OS.chdir(cwd)
438 for k in master_keys():
439 seq = master_sequence(k)
440 sigfile = seqsubst('sig-file', seq)
441 run('''catsign -kmaster sign -abdC -kmaster-%d
442 -o%s.new ${repos-file}.new''' % (seq, sigfile))
443 commit.append(sigfile)
444
445 ## Commit the changes
446 for base in commit:
447 new = '%s.new' % base
448 OS.rename(new, base)
838e5ce7
MW
449
450 ## Remove files in the base-dir which don't correspond to ones we just
451 ## committed
452 allow = {}
453 basedir = conf['base-dir']
454 bdl = len(basedir)
455 for base in commit:
456 if base.startswith(basedir): allow[base[bdl:]] = 1
457 for found in OS.listdir(basedir):
458 if found not in allow: OS.remove(OS.path.join(basedir, found))
c77687d5 459 finally:
460 OS.chdir(cwd)
e04c2d50 461 rmtree('tmp')
b14ccd2f 462 run('sh -c ${upload-hook}')
060ca767 463
fd42a1e5
MW
464###--------------------------------------------------------------------------
465### Commands: rebuild
466
467def cmd_rebuild(args):
468 zap('keyring.pub')
469 for i in OS.listdir('repos'):
470 if i.startswith('peer-') and i.endswith('.pub'):
471 run('key -kkeyring.pub merge %s' % OS.path.join('repos', i))
472
473###--------------------------------------------------------------------------
474### Commands: update
475
060ca767 476def cmd_update(args):
477 cwd = OS.getcwd()
478 rmtree('tmp')
479 try:
480
481 ## Fetch a new distribution
482 OS.mkdir('tmp')
483 OS.chdir('tmp')
c77687d5 484 seq = int(conf['master-sequence'])
5d044380
MW
485 run('curl -sL -o tripe-keys.tar.gz ${repos-url}')
486 run('curl -sL -o tripe-keys.sig %s' % seqsubst('sig-url', seq))
060ca767 487 run('tar xfz tripe-keys.tar.gz')
488
489 ## Verify the signature
c77687d5 490 want = C.bytes(rx_nonalpha.sub('', conf['hk-master']))
491 got = fingerprint('repos/master.pub', 'master-%d' % seq)
171206b5 492 if want != got: raise VerifyError()
c77687d5 493 run('''catsign -krepos/master.pub verify -avC -kmaster-%d
494 -t${sig-fresh} tripe-keys.sig tripe-keys.tar.gz''' % seq)
060ca767 495
496 ## OK: update our copy
497 OS.chdir(cwd)
498 if OS.path.exists('repos'): OS.rename('repos', 'repos.old')
499 OS.rename('tmp/repos', 'repos')
f56dbbc4 500 if not FC.cmp('tmp/tripe-keys.conf', 'tripe-keys.conf', False):
c77687d5 501 moan('configuration file changed: recommend running another update')
502 OS.rename('tmp/tripe-keys.conf', 'tripe-keys.conf')
060ca767 503 rmtree('repos.old')
504
505 finally:
506 OS.chdir(cwd)
507 rmtree('tmp')
508 cmd_rebuild(args)
509
fd42a1e5
MW
510###--------------------------------------------------------------------------
511### Commands: generate TAG
060ca767 512
513def cmd_generate(args):
514 tag, = args
515 keyring_pub = 'peer-%s.pub' % tag
516 zap('keyring'); zap(keyring_pub)
517 run('key -kkeyring merge repos/param')
256bc8d0 518 run('key -kkeyring add -a${kx-genalg} -pparam -e${kx-expire} -t%s tripe' %
c77687d5 519 tag)
ca6eb20c 520 run('key -kkeyring extract -f-secret %s %s' % (keyring_pub, tag))
060ca767 521
fd42a1e5
MW
522###--------------------------------------------------------------------------
523### Commands: clean
524
060ca767 525def cmd_clean(args):
526 rmtree('repos')
527 rmtree('tmp')
c77687d5 528 for i in OS.listdir('.'):
529 r = i
530 if r.endswith('.old'): r = r[:-4]
531 if (r == 'master' or r == 'param' or
532 r == 'keyring' or r == 'keyring.pub' or r.startswith('peer-')):
533 zap(i)
060ca767 534
c2f28e4b
MW
535###--------------------------------------------------------------------------
536### Commands: check
537
24285984 538def check_key(k):
c2f28e4b
MW
539 now = T.time()
540 thresh = int(conf['kx-warn-days']) * 86400
24285984
MW
541 if k.exptime == C.KEXP_FOREVER: return None
542 elif k.exptime == C.KEXP_EXPIRE: left = -1
543 else: left = k.exptime - now
544 if left < 0:
545 return "key `%s' HAS EXPIRED" % k.tag
546 elif left < thresh:
547 if left >= 86400: n, u, uu = left // 86400, 'day', 'days'
548 else: n, u, uu = left // 3600, 'hour', 'hours'
549 return "key `%s' EXPIRES in %d %s" % (k.tag, n, n == 1 and u or uu)
550 else:
551 return None
552
553def cmd_check(args):
554 if OS.path.exists('keyring.pub'):
555 for k in C.KeyFile('keyring.pub').itervalues():
556 whinge = check_key(k)
557 if whinge is not None: print whinge
558 if OS.path.exists('master'):
559 whinges = []
560 for k in C.KeyFile('master').itervalues():
561 whinge = check_key(k)
562 if whinge is None: break
563 whinges.append(whinge)
564 else:
565 for whinge in whinges: print whinge
c2f28e4b 566
65faf8df
MW
567###--------------------------------------------------------------------------
568### Commands: mtu
569
39bcd193
MW
570def mac_tagsz():
571 macname = conf['mac']
572 index = macname.rindex('/')
573 if index == -1: tagsz = C.gcmacs[macname].tagsz
574 else: tagsz = int(macname[index + 1:])/8
575 return tagsz
576
65faf8df
MW
577def cmd_mtu(args):
578 mtu, = (lambda mtu = '1500': (mtu,))(*args)
579 mtu = int(mtu)
580
65faf8df
MW
581 mtu -= 20 # Minimum IP header
582 mtu -= 8 # UDP header
583 mtu -= 1 # TrIPE packet type octet
39bcd193
MW
584
585 bulk = conf['bulk']
586
587 if bulk == 'v0':
588 blksz = C.gcciphers[conf['cipher']].blksz
589 mtu -= mac_tagsz() # MAC tag
590 mtu -= 4 # Sequence number
591 mtu -= blksz # Initialization vector
592
593 elif bulk == 'iiv':
594 mtu -= mac_tagsz() # MAC tag
595 mtu -= 4 # Sequence number
596
de8edc7f
MW
597 elif bulk == 'naclbox':
598 mtu -= 16 # MAC tag
599 mtu -= 4 # Sequence number
600
39bcd193
MW
601 else:
602 die("Unknown bulk transform `%s'" % bulk)
65faf8df
MW
603
604 print mtu
605
fd42a1e5
MW
606###--------------------------------------------------------------------------
607### Main driver.
060ca767 608
060ca767 609commands = {'help': (cmd_help, 0, 1, ''),
c77687d5 610 'newmaster': (cmd_newmaster, 0, 0, ''),
060ca767 611 'setup': (cmd_setup, 0, 0, ''),
612 'upload': (cmd_upload, 0, 0, ''),
613 'update': (cmd_update, 0, 0, ''),
614 'clean': (cmd_clean, 0, 0, ''),
65faf8df 615 'mtu': (cmd_mtu, 0, 1, '[PATH-MTU]'),
c2f28e4b 616 'check': (cmd_check, 0, 0, ''),
060ca767 617 'generate': (cmd_generate, 1, 1, 'TAG'),
618 'rebuild': (cmd_rebuild, 0, 0, '')}
619
620def init():
fd42a1e5
MW
621 """
622 Load the appropriate configuration file and set up the configuration
623 dictionary.
624 """
060ca767 625 for f in ['tripe-keys.master', 'tripe-keys.conf']:
626 if OS.path.exists(f):
627 conf_read(f)
628 break
629 conf_defaults()
fd42a1e5 630
060ca767 631def main(argv):
fd42a1e5
MW
632 """
633 Main program: parse options and dispatch to appropriate command handler.
634 """
060ca767 635 try:
636 opts, args = O.getopt(argv[1:], 'hvu',
637 ['help', 'version', 'usage'])
638 except O.GetoptError, exc:
639 moan(exc)
640 usage(SYS.stderr)
641 SYS.exit(1)
642 for o, v in opts:
643 if o in ('-h', '--help'):
644 cmd_help([])
645 SYS.exit(0)
646 elif o in ('-v', '--version'):
647 version(SYS.stdout)
648 SYS.exit(0)
649 elif o in ('-u', '--usage'):
650 usage(SYS.stdout)
651 SYS.exit(0)
652 if len(argv) < 2:
653 cmd_help([])
654 else:
655 c = argv[1]
db76b51b
MW
656 try: func, min, max, help = commands[c]
657 except KeyError: die("unknown command `%s'" % c)
060ca767 658 args = argv[2:]
db76b51b
MW
659 if len(args) < min or (max is not None and len(args) > max):
660 SYS.stderr.write('Usage: %s %s%s%s\n' % (quis, c, help and ' ', help))
661 SYS.exit(1)
060ca767 662 func(args)
663
fd42a1e5
MW
664###----- That's all, folks --------------------------------------------------
665
666if __name__ == '__main__':
667 init()
668 main(SYS.argv)