chiark / gitweb /
server/, keys/: Add bulk crypto transform based on NaCl `crypto_secretbox'.
[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
MW
241 ('kx-genalg', lambda: {'dh': 'dh',
242 'ec': 'ec'}[conf['kx']]),
243 ('kx-param-genalg', lambda: {'dh': 'dh-param',
244 'ec': 'ec-param'}[conf['kx']]),
ca3aaaeb 245 ('kx-param', lambda: {'dh': '-LS -b3072 -B256',
060ca767 246 'ec': '-Cnist-p256'}[conf['kx']]),
07bdda1f 247 ('kx-attrs', 'serialization=constlen'),
060ca767 248 ('kx-expire', 'now + 1 year'),
c2f28e4b 249 ('kx-warn-days', '28'),
39bcd193 250 ('bulk', 'iiv'),
de8edc7f
MW
251 ('cipher', lambda: conf['bulk'] == 'naclbox'
252 and 'salsa20' or 'rijndael-cbc'),
060ca767 253 ('hash', 'sha256'),
7858dfa0 254 ('master-keygen-flags', '-l'),
67bb121f 255 ('master-attrs', ''),
060ca767 256 ('mgf', '${hash}-mgf'),
de8edc7f
MW
257 ('mac', lambda: conf['bulk'] == 'naclbox'
258 and 'poly1305/128'
259 or '%s-hmac/%d' %
260 (conf['hash'],
261 C.gchashes[conf['hash']].hashsz * 4)),
060ca767 262 ('sig', lambda: {'dh': 'dsa', 'ec': 'ecdsa'}[conf['kx']]),
263 ('sig-fresh', 'always'),
264 ('sig-genalg', lambda: {'kcdsa': 'dh',
265 'dsa': 'dsa',
266 'rsapkcs1': 'rsa',
267 'rsapss': 'rsa',
268 'ecdsa': 'ec',
06a174df
MW
269 'eckcdsa': 'ec',
270 'ed25519': 'ed25519',
271 'ed448': 'ed448'}[conf['sig']]),
ca3aaaeb
MW
272 ('sig-param', lambda: {'dh': '-LS -b3072 -B256',
273 'dsa': '-b3072 -B256',
060ca767 274 'ec': '-Cnist-p256',
06a174df
MW
275 'rsa': '-b3072',
276 'ed25519': '',
277 'ed448': ''}[conf['sig-genalg']]),
060ca767 278 ('sig-hash', '${hash}'),
279 ('sig-expire', 'forever'),
280 ('fingerprint-hash', '${hash}')]:
281 try:
282 if k in conf: continue
283 if type(v) == str:
284 conf[k] = conf_subst(v)
285 else:
286 conf[k] = v()
287 except KeyError, exc:
288 if len(exc.args) == 0: raise
289 conf[k] = '<missing-var %s>' % exc.args[0]
290
fd42a1e5
MW
291###--------------------------------------------------------------------------
292### Key-management utilities.
293
294def master_keys():
295 """
296 Iterate over the master keys.
297 """
298 if not OS.path.exists('master'):
299 return
300 for k in C.KeyFile('master').itervalues():
301 if (k.type != 'tripe-keys-master' or
302 k.expiredp or
303 not k.tag.startswith('master-')):
304 continue #??
305 yield k
306
307def master_sequence(k):
308 """
309 Return the sequence number of the given master key as an integer.
310
311 No checking is done that K is really a master key.
312 """
313 return int(k.tag[7:])
314
315def max_master_sequence():
316 """
317 Find the master key with the highest sequence number and return this
318 sequence number.
319 """
320 seq = -1
321 for k in master_keys():
322 q = master_sequence(k)
323 if q > seq: seq = q
324 return seq
325
326def seqsubst(x, q):
327 """
328 Return the value of the configuration variable X, with <SEQ> replaced by
329 the value Q.
330 """
331 return rx_seq.sub(str(q), conf[x])
332
333###--------------------------------------------------------------------------
334### Commands: help [COMMAND...]
060ca767 335
336def version(fp = SYS.stdout):
337 fp.write('%s, %s version %s\n' % (quis, PACKAGE, VERSION))
338
339def usage(fp):
340 fp.write('Usage: %s SUBCOMMAND [ARGS...]\n' % quis)
341
342def cmd_help(args):
343 if len(args) == 0:
344 version(SYS.stdout)
345 print
346 usage(SYS.stdout)
347 print """
348Key management utility for TrIPE.
349
350Options supported:
351
e04c2d50
MW
352-h, --help Show this help message.
353-v, --version Show the version number.
354-u, --usage Show pointlessly short usage string.
060ca767 355
356Subcommands available:
357"""
358 args = commands.keys()
359 args.sort()
360 for c in args:
db76b51b
MW
361 try: func, min, max, help = commands[c]
362 except KeyError: die("unknown command `%s'" % c)
363 print '%s%s%s' % (c, help and ' ', help)
060ca767 364
fd42a1e5
MW
365###--------------------------------------------------------------------------
366### Commands: newmaster
c77687d5 367
368def cmd_newmaster(args):
369 seq = max_master_sequence() + 1
060ca767 370 run('''key -kmaster add
371 -a${sig-genalg} !${sig-param}
7858dfa0 372 -e${sig-expire} !${master-keygen-flags} -tmaster-%d tripe-keys-master
67bb121f 373 sig=${sig} hash=${sig-hash} !${master-attrs}''' % seq)
c77687d5 374 run('key -kmaster extract -f-secret repos/master.pub')
060ca767 375
fd42a1e5
MW
376###--------------------------------------------------------------------------
377### Commands: setup
378
c77687d5 379def cmd_setup(args):
380 OS.mkdir('repos')
060ca767 381 run('''key -krepos/param add
256bc8d0 382 -a${kx-param-genalg} !${kx-param}
fc5f4823 383 -eforever -tparam tripe-param
67bb121f 384 kx-group=${kx} mgf=${mgf} mac=${mac}
39bcd193 385 bulk=${bulk} cipher=${cipher} hash=${hash} ${kx-attrs}''')
c77687d5 386 cmd_newmaster(args)
060ca767 387
fd42a1e5
MW
388###--------------------------------------------------------------------------
389### Commands: upload
390
060ca767 391def cmd_upload(args):
392
393 ## Sanitize the repository directory
394 umask = OS.umask(0); OS.umask(umask)
395 mode = 0666 & ~umask
396 for f in OS.listdir('repos'):
397 ff = OS.path.join('repos', f)
c77687d5 398 if (f.startswith('master') or f.startswith('peer-')) \
399 and f.endswith('.old'):
060ca767 400 OS.unlink(ff)
401 continue
c77687d5 402 OS.chmod(ff, mode)
403
404 rmtree('tmp')
405 OS.mkdir('tmp')
406 OS.symlink('../repos', 'tmp/repos')
407 cwd = OS.getcwd()
408 try:
409
410 ## Build the configuration file
411 seq = max_master_sequence()
412 v = {'MASTER-SEQUENCE': str(seq),
413 'HK-MASTER': hexhyphens(fingerprint('repos/master.pub',
414 'master-%d' % seq))}
415 fin = file('tripe-keys.master')
416 fout = file('tmp/tripe-keys.conf', 'w')
417 for line in fin:
418 fout.write(subst(line, rx_atsubst, v))
419 fin.close(); fout.close()
420 SH.copyfile('tmp/tripe-keys.conf', conf_subst('${conf-file}.new'))
421 commit = [conf['repos-file'], conf['conf-file']]
422
423 ## Make and sign the repository archive
424 OS.chdir('tmp')
425 run('tar chozf ${repos-file}.new .')
426 OS.chdir(cwd)
427 for k in master_keys():
428 seq = master_sequence(k)
429 sigfile = seqsubst('sig-file', seq)
430 run('''catsign -kmaster sign -abdC -kmaster-%d
431 -o%s.new ${repos-file}.new''' % (seq, sigfile))
432 commit.append(sigfile)
433
434 ## Commit the changes
435 for base in commit:
436 new = '%s.new' % base
437 OS.rename(new, base)
838e5ce7
MW
438
439 ## Remove files in the base-dir which don't correspond to ones we just
440 ## committed
441 allow = {}
442 basedir = conf['base-dir']
443 bdl = len(basedir)
444 for base in commit:
445 if base.startswith(basedir): allow[base[bdl:]] = 1
446 for found in OS.listdir(basedir):
447 if found not in allow: OS.remove(OS.path.join(basedir, found))
c77687d5 448 finally:
449 OS.chdir(cwd)
e04c2d50 450 rmtree('tmp')
b14ccd2f 451 run('sh -c ${upload-hook}')
060ca767 452
fd42a1e5
MW
453###--------------------------------------------------------------------------
454### Commands: rebuild
455
456def cmd_rebuild(args):
457 zap('keyring.pub')
458 for i in OS.listdir('repos'):
459 if i.startswith('peer-') and i.endswith('.pub'):
460 run('key -kkeyring.pub merge %s' % OS.path.join('repos', i))
461
462###--------------------------------------------------------------------------
463### Commands: update
464
060ca767 465def cmd_update(args):
466 cwd = OS.getcwd()
467 rmtree('tmp')
468 try:
469
470 ## Fetch a new distribution
471 OS.mkdir('tmp')
472 OS.chdir('tmp')
c77687d5 473 seq = int(conf['master-sequence'])
162fcf48
MW
474 run('curl -s -o tripe-keys.tar.gz ${repos-url}')
475 run('curl -s -o tripe-keys.sig %s' % seqsubst('sig-url', seq))
060ca767 476 run('tar xfz tripe-keys.tar.gz')
477
478 ## Verify the signature
c77687d5 479 want = C.bytes(rx_nonalpha.sub('', conf['hk-master']))
480 got = fingerprint('repos/master.pub', 'master-%d' % seq)
060ca767 481 if want != got: raise VerifyError
c77687d5 482 run('''catsign -krepos/master.pub verify -avC -kmaster-%d
483 -t${sig-fresh} tripe-keys.sig tripe-keys.tar.gz''' % seq)
060ca767 484
485 ## OK: update our copy
486 OS.chdir(cwd)
487 if OS.path.exists('repos'): OS.rename('repos', 'repos.old')
488 OS.rename('tmp/repos', 'repos')
f56dbbc4 489 if not FC.cmp('tmp/tripe-keys.conf', 'tripe-keys.conf', False):
c77687d5 490 moan('configuration file changed: recommend running another update')
491 OS.rename('tmp/tripe-keys.conf', 'tripe-keys.conf')
060ca767 492 rmtree('repos.old')
493
494 finally:
495 OS.chdir(cwd)
496 rmtree('tmp')
497 cmd_rebuild(args)
498
fd42a1e5
MW
499###--------------------------------------------------------------------------
500### Commands: generate TAG
060ca767 501
502def cmd_generate(args):
503 tag, = args
504 keyring_pub = 'peer-%s.pub' % tag
505 zap('keyring'); zap(keyring_pub)
506 run('key -kkeyring merge repos/param')
256bc8d0 507 run('key -kkeyring add -a${kx-genalg} -pparam -e${kx-expire} -t%s tripe' %
c77687d5 508 tag)
ca6eb20c 509 run('key -kkeyring extract -f-secret %s %s' % (keyring_pub, tag))
060ca767 510
fd42a1e5
MW
511###--------------------------------------------------------------------------
512### Commands: clean
513
060ca767 514def cmd_clean(args):
515 rmtree('repos')
516 rmtree('tmp')
c77687d5 517 for i in OS.listdir('.'):
518 r = i
519 if r.endswith('.old'): r = r[:-4]
520 if (r == 'master' or r == 'param' or
521 r == 'keyring' or r == 'keyring.pub' or r.startswith('peer-')):
522 zap(i)
060ca767 523
c2f28e4b
MW
524###--------------------------------------------------------------------------
525### Commands: check
526
24285984 527def check_key(k):
c2f28e4b
MW
528 now = T.time()
529 thresh = int(conf['kx-warn-days']) * 86400
24285984
MW
530 if k.exptime == C.KEXP_FOREVER: return None
531 elif k.exptime == C.KEXP_EXPIRE: left = -1
532 else: left = k.exptime - now
533 if left < 0:
534 return "key `%s' HAS EXPIRED" % k.tag
535 elif left < thresh:
536 if left >= 86400: n, u, uu = left // 86400, 'day', 'days'
537 else: n, u, uu = left // 3600, 'hour', 'hours'
538 return "key `%s' EXPIRES in %d %s" % (k.tag, n, n == 1 and u or uu)
539 else:
540 return None
541
542def cmd_check(args):
543 if OS.path.exists('keyring.pub'):
544 for k in C.KeyFile('keyring.pub').itervalues():
545 whinge = check_key(k)
546 if whinge is not None: print whinge
547 if OS.path.exists('master'):
548 whinges = []
549 for k in C.KeyFile('master').itervalues():
550 whinge = check_key(k)
551 if whinge is None: break
552 whinges.append(whinge)
553 else:
554 for whinge in whinges: print whinge
c2f28e4b 555
65faf8df
MW
556###--------------------------------------------------------------------------
557### Commands: mtu
558
39bcd193
MW
559def mac_tagsz():
560 macname = conf['mac']
561 index = macname.rindex('/')
562 if index == -1: tagsz = C.gcmacs[macname].tagsz
563 else: tagsz = int(macname[index + 1:])/8
564 return tagsz
565
65faf8df
MW
566def cmd_mtu(args):
567 mtu, = (lambda mtu = '1500': (mtu,))(*args)
568 mtu = int(mtu)
569
65faf8df
MW
570 mtu -= 20 # Minimum IP header
571 mtu -= 8 # UDP header
572 mtu -= 1 # TrIPE packet type octet
39bcd193
MW
573
574 bulk = conf['bulk']
575
576 if bulk == 'v0':
577 blksz = C.gcciphers[conf['cipher']].blksz
578 mtu -= mac_tagsz() # MAC tag
579 mtu -= 4 # Sequence number
580 mtu -= blksz # Initialization vector
581
582 elif bulk == 'iiv':
583 mtu -= mac_tagsz() # MAC tag
584 mtu -= 4 # Sequence number
585
de8edc7f
MW
586 elif bulk == 'naclbox':
587 mtu -= 16 # MAC tag
588 mtu -= 4 # Sequence number
589
39bcd193
MW
590 else:
591 die("Unknown bulk transform `%s'" % bulk)
65faf8df
MW
592
593 print mtu
594
fd42a1e5
MW
595###--------------------------------------------------------------------------
596### Main driver.
060ca767 597
060ca767 598commands = {'help': (cmd_help, 0, 1, ''),
c77687d5 599 'newmaster': (cmd_newmaster, 0, 0, ''),
060ca767 600 'setup': (cmd_setup, 0, 0, ''),
601 'upload': (cmd_upload, 0, 0, ''),
602 'update': (cmd_update, 0, 0, ''),
603 'clean': (cmd_clean, 0, 0, ''),
65faf8df 604 'mtu': (cmd_mtu, 0, 1, '[PATH-MTU]'),
c2f28e4b 605 'check': (cmd_check, 0, 0, ''),
060ca767 606 'generate': (cmd_generate, 1, 1, 'TAG'),
607 'rebuild': (cmd_rebuild, 0, 0, '')}
608
609def init():
fd42a1e5
MW
610 """
611 Load the appropriate configuration file and set up the configuration
612 dictionary.
613 """
060ca767 614 for f in ['tripe-keys.master', 'tripe-keys.conf']:
615 if OS.path.exists(f):
616 conf_read(f)
617 break
618 conf_defaults()
fd42a1e5 619
060ca767 620def main(argv):
fd42a1e5
MW
621 """
622 Main program: parse options and dispatch to appropriate command handler.
623 """
060ca767 624 try:
625 opts, args = O.getopt(argv[1:], 'hvu',
626 ['help', 'version', 'usage'])
627 except O.GetoptError, exc:
628 moan(exc)
629 usage(SYS.stderr)
630 SYS.exit(1)
631 for o, v in opts:
632 if o in ('-h', '--help'):
633 cmd_help([])
634 SYS.exit(0)
635 elif o in ('-v', '--version'):
636 version(SYS.stdout)
637 SYS.exit(0)
638 elif o in ('-u', '--usage'):
639 usage(SYS.stdout)
640 SYS.exit(0)
641 if len(argv) < 2:
642 cmd_help([])
643 else:
644 c = argv[1]
db76b51b
MW
645 try: func, min, max, help = commands[c]
646 except KeyError: die("unknown command `%s'" % c)
060ca767 647 args = argv[2:]
db76b51b
MW
648 if len(args) < min or (max is not None and len(args) > max):
649 SYS.stderr.write('Usage: %s %s%s%s\n' % (quis, c, help and ' ', help))
650 SYS.exit(1)
060ca767 651 func(args)
652
fd42a1e5
MW
653###----- That's all, folks --------------------------------------------------
654
655if __name__ == '__main__':
656 init()
657 main(SYS.argv)