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