Commit | Line | Data |
---|---|---|
71074336 MW |
1 | #! /usr/bin/python |
2 | ### -*-python-*- | |
3 | ### | |
4 | ### Encrypted email address handling | |
5 | ### | |
6 | ### (c) 2006 Mark Wooding | |
7 | ### | |
8 | ||
9 | ###----- Licensing notice --------------------------------------------------- | |
10 | ### | |
11 | ### This program is free software; you can redistribute it and/or modify | |
12 | ### it under the terms of the GNU General Public License as published by | |
13 | ### the Free Software Foundation; either version 2 of the License, or | |
14 | ### (at your option) any later version. | |
15 | ### | |
16 | ### This program is distributed in the hope that it will be useful, | |
17 | ### but WITHOUT ANY WARRANTY; without even the implied warranty of | |
18 | ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
19 | ### GNU General Public License for more details. | |
20 | ### | |
21 | ### You should have received a copy of the GNU General Public License | |
22 | ### along with this program; if not, write to the Free Software Foundation, | |
23 | ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. | |
24 | ||
25 | ###----- External dependencies ---------------------------------------------- | |
26 | ||
27 | import catacomb as C | |
28 | import mLib as M | |
29 | from pysqlite2 import dbapi2 as sqlite | |
30 | from UserDict import DictMixin | |
31 | from getopt import getopt, GetoptError | |
32 | from getdate import getdate | |
33 | from sys import stdin, stdout, stderr, exit, argv, exc_info | |
34 | from email import Parser as EP | |
35 | import os as OS | |
36 | import time as T | |
37 | import sre as RX | |
38 | import traceback as TB | |
39 | ||
40 | ###----- Database messing --------------------------------------------------- | |
41 | ||
42 | class AttrDB (object): | |
43 | def __init__(me, dbfile): | |
44 | me.db = sqlite.connect(dbfile) | |
45 | def setup(me): | |
46 | cur = me.db.cursor() | |
47 | cur.execute('''CREATE TABLE attr | |
48 | (id INTEGER PRIMARY KEY, | |
49 | key VARCHAR(64) NOT NULL, | |
50 | value VARCHAR(256) NOT NULL)''') | |
51 | cur.execute('''CREATE TABLE attrset | |
52 | (id INTEGER NOT NULL, | |
53 | attr INTEGER NOT NULL)''') | |
54 | cur.execute('''CREATE TABLE uniq | |
55 | (id INTEGER PRIMARY KEY AUTOINCREMENT, | |
56 | dummy INTEGER NOT NULL)''') | |
57 | cur.execute('CREATE UNIQUE INDEX attr_bykv ON attr (key, value)') | |
58 | cur.execute('CREATE INDEX attrset_byid ON attrset (id)') | |
59 | cur.execute('CREATE INDEX attrset_byattr ON attrset (attr)') | |
60 | cur.execute('CREATE UNIQUE INDEX attrset_all ON attrset (id, attr)') | |
61 | def uniqueid(me): | |
62 | cur = me.db.cursor() | |
63 | cur.execute('INSERT INTO uniq (dummy) VALUES (0)') | |
64 | cur.execute('SELECT MAX(id) FROM uniq') | |
65 | id = cur.fetchone()[0] | |
66 | cur.execute('DELETE FROM uniq') | |
67 | me.commit() | |
68 | return id | |
69 | def select(me, expr, args = [], cur = None): | |
70 | if cur is None: cur = me.db.cursor() | |
71 | cur.execute(expr, args) | |
72 | while True: | |
73 | r = cur.fetchone() | |
74 | if r is None: break | |
75 | yield r | |
76 | def cleanup(me): | |
77 | cur = me.db.cursor() | |
78 | cur.execute('''DELETE FROM attr WHERE id IN | |
79 | (SELECT attr.id | |
80 | FROM attr LEFT JOIN attrset | |
81 | ON attr.id = attrset.attr | |
82 | WHERE attrset.id ISNULL)''') | |
83 | def check(me, cleanp = False): | |
84 | toclean = {} | |
85 | cur = me.db.cursor() | |
86 | for set, attr in me.select('''SELECT attrset.id, attrset.attr | |
87 | FROM attrset LEFT JOIN attr | |
88 | ON attrset.attr = attr.id | |
89 | WHERE attr.id ISNULL''', | |
90 | [], cur): | |
91 | print "attrset %d missing attr %d" % (set, attr) | |
92 | toclean[set] = True | |
93 | if cleanp: | |
94 | for set in toclean: | |
95 | cur.execute('DELETE FROM attrset WHERE id = ?', [set]) | |
96 | me.cleanup() | |
97 | def commit(me): | |
98 | me.db.commit() | |
99 | ||
100 | class AttrSet (object): | |
101 | def __init__(me, db, id = None): | |
102 | if id is None: id = db.uniqueid() | |
103 | me.id = id | |
104 | me.db = db | |
105 | def insert(me, key, value): | |
106 | cur = me.db.db.cursor() | |
107 | try: | |
108 | cur.execute('INSERT INTO attr (key, value) VALUES (?, ?)', | |
109 | [key, value]) | |
110 | except sqlite.OperationalError: | |
111 | pass | |
112 | cur.execute('SELECT id FROM attr WHERE key = ? AND value = ?', | |
113 | [key, value]) | |
114 | r = cur.fetchone() | |
115 | attr = r[0] | |
116 | try: | |
117 | cur.execute('INSERT INTO attrset VALUES (?, ?)', | |
118 | [me.id, attr]) | |
119 | except sqlite.OperationalError: | |
120 | pass | |
121 | def fetch(me): | |
122 | for r in me.db.select('''SELECT attr.key, attr.value | |
123 | FROM attr, attrset ON attr.id = attrset.attr | |
124 | WHERE attrset.id = ?''', | |
125 | [me.id]): | |
126 | yield r | |
127 | def delete(me): | |
128 | cur = me.db.db.cursor() | |
129 | cur.execute('DELETE FROM attrset WHERE id = ?', [me.id]) | |
130 | me.db.cleanup() | |
131 | ||
132 | class AttrMap (AttrSet, DictMixin): | |
133 | def __getitem__(me, key): | |
134 | it = None | |
135 | for v, in me.db.select('''SELECT attr.value | |
136 | FROM attr, attrset ON attr.id = attrset.attr | |
137 | WHERE attrset.id = ? AND attr.key = ?''', | |
138 | [me.id, key]): | |
139 | if it is None: | |
140 | it = v | |
141 | else: | |
142 | raise ValueError, 'multiple values for key %s' % key | |
143 | if it is None: | |
144 | raise KeyError, key | |
145 | return it | |
146 | def __delitem__(me, key): | |
147 | cur = me.db.db.cursor() | |
148 | cur.execute('''DELETE FROM attrset | |
149 | WHERE id = ? AND | |
150 | attr in | |
151 | (SELECT id FROM attr WHERE key = ?)''', | |
152 | [me.id, key]) | |
153 | me.db.cleanup() | |
154 | def __setitem__(me, key, value): | |
155 | me.__delitem__(key) | |
156 | me.insert(key, value) | |
157 | def __iter__(me): | |
158 | set = {} | |
159 | for k, v in me.fetch(): | |
160 | if k in set: | |
161 | continue | |
162 | set[k] = True | |
163 | yield k | |
164 | def keys(me): | |
165 | return [k for k in me] | |
166 | ||
167 | class AttrMultiMap (AttrMap): | |
168 | def __getitem__(me, key): | |
169 | them = [] | |
170 | for v, in me.db.select('''SELECT attr.value | |
171 | FROM attr, attrset ON attr.id = attrset.attr | |
172 | WHERE attrset.id = ? AND attr.key = ?''', | |
173 | [me.id, key]): | |
174 | them.append(v) | |
175 | if not them: | |
176 | raise KeyError, key | |
177 | return them | |
178 | def __setitem__(me, key, values): | |
179 | me.__delitem__(key) | |
180 | for it in values: | |
181 | me.insert(key, it) | |
182 | ||
183 | ###----- Miscellaneous utilities -------------------------------------------- | |
184 | ||
185 | def time_format(t = None): | |
186 | if t is None: | |
187 | t = T.time() | |
188 | tm = T.gmtime(t) | |
189 | return T.strftime('%Y-%m-%d %H:%M:%S', tm) | |
190 | ||
191 | def any(pred, list): | |
192 | for i in list: | |
193 | if pred(i): return True | |
194 | return False | |
195 | def every(pred, list): | |
196 | for i in list: | |
197 | if not pred(i): return False | |
198 | return True | |
199 | ||
200 | prog = RX.sub(r'^.*[/\\]', '', argv[0]) | |
201 | def moan(msg): | |
202 | print >>stderr, '%s: %s' % (prog, msg) | |
203 | def die(msg): | |
204 | moan(msg) | |
205 | exit(111) | |
206 | ||
207 | ###----- My actual database ------------------------------------------------- | |
208 | ||
209 | class CMDB (AttrDB): | |
210 | def setup(me): | |
211 | AttrDB.setup(me) | |
212 | cur = me.db.cursor() | |
213 | cur.execute('''CREATE TABLE expiry | |
214 | (attrset INTEGER PRIMARY KEY, | |
215 | time CHAR(20) NOT NULL)''') | |
216 | cur.execute('CREATE INDEX expiry_bytime ON expiry (time)') | |
217 | def cleanup(me): | |
218 | cur = me.db.cursor() | |
219 | now = time_format() | |
220 | cur.execute('''DELETE FROM attrset WHERE id IN | |
221 | (SELECT attrset FROM expiry WHERE time < ?)''', | |
222 | [now]) | |
223 | cur.execute('DELETE FROM expiry WHERE time < ?', [now]) | |
224 | AttrDB.cleanup(me) | |
225 | def expiredp(me, id): | |
226 | for t, in me.select('SELECT time FROM expiry WHERE attrset = ?', [id]): | |
227 | if t < time_format(): | |
228 | return True | |
229 | return False | |
230 | def setexpire(me, id, when): | |
231 | if when != C.KEXP_FOREVER: | |
232 | cur = me.db.cursor() | |
233 | cur.execute('INSERT INTO expiry VALUES (?, ?)', | |
234 | [id, time_format(when)]) | |
235 | ||
236 | ###----- Crypto messing about ----------------------------------------------- | |
237 | ||
238 | ## Very vague security arguments... | |
239 | ## | |
240 | ## If the block size n of the PRP is large enough (128 bits) then we encrypt | |
241 | ## id || 0^{n - 64}. Decryption checks we have the right thing. The | |
242 | ## security proofs for secrecy and integrity are trivial. | |
243 | ## | |
244 | ## If the block size is small, then we encrypt two blocks: | |
245 | ## C_0 = E_K(0^{n - 64} || id) | |
246 | ## C_1 = E_K(C_0) | |
247 | ## The proofs are a little more complicated, but essentially work like this. | |
248 | ## If no 0^{n - 64} || id is ever seen as a C_0 then an adversary can't tell | |
249 | ## the difference between this and a similar construction using independent | |
250 | ## keys. This other construction must provide secrecy (pushing a | |
251 | ## nonrepeating thing through a PRF) and integrity (PRF on noncolliding | |
252 | ## inputs). So we win, give or take a birthday term. | |
253 | class Crypto (object): | |
254 | def __init__(me, key): | |
255 | me.prp = C.gcprps[key.attr.get('prp', 'blowfish')](key.data.bin) | |
256 | def encrypt(me, id): | |
257 | blksz = type(me.prp).blksz | |
258 | p = C.MP(id).storeb(blksz) | |
259 | c = me.prp.encrypt(p) | |
260 | if blksz < 16: | |
261 | c += me.prp.encrypt(c) | |
262 | return c | |
263 | def decrypt(me, c): | |
264 | bad = False | |
265 | blksz = type(me.prp).blksz | |
266 | if blksz < 16: | |
267 | if len(c) != blksz * 2: | |
268 | return None | |
269 | c, c1 = c[:blksz], c[blksz:] | |
270 | if c1 != me.prp.encrypt(c): | |
271 | bad = True | |
272 | else: | |
273 | if len(c) != blksz: | |
274 | return None | |
275 | p = me.prp.decrypt(c) | |
276 | id = C.MP.loadb(p) | |
277 | if id >> 64: | |
278 | bad = True | |
279 | if bad: | |
280 | return None | |
281 | return long(id) | |
282 | ||
283 | ###----- Canonification ----------------------------------------------------- | |
284 | ||
285 | rx_prefix = RX.compile(r'''(?x) ^ ( | |
286 | \[ \S+ \] \s* | | |
287 | \S{,4} : \s* | | |
288 | \s+ | |
289 | ) | |
290 | ''') | |
291 | rx_suffix = RX.compile(r'''(?ix) ( | |
292 | \( \s* was \s* : .* \) \s* | | |
293 | \s+ | |
294 | ) $''') | |
295 | rx_punct = RX.compile(r'(?x) [^\w]+ ') | |
296 | ||
297 | def canon_sender(addr): | |
298 | return addr.lower() | |
299 | ||
300 | def canon_subject(subject): | |
301 | subject = subject.lower() | |
302 | while True: | |
303 | m = rx_prefix.match(subject) | |
304 | if not m: break | |
305 | subject = subject[m.end():] | |
306 | while True: | |
307 | m = rx_suffix.search(subject) | |
308 | if not m: break | |
309 | subject = subject[:m.start()] | |
310 | subject = rx_punct.sub('', subject) | |
311 | return subject | |
312 | ||
313 | ###----- Checking a message for validity ------------------------------------ | |
314 | ||
315 | class Reject (Exception): pass | |
316 | ||
317 | class MessageInfo (object): | |
318 | __slots__ = ''' | |
319 | sender msg | |
320 | '''.split() | |
321 | ||
322 | constraints = {} | |
323 | ||
324 | def check_sender(mi, vv): | |
325 | if mi.sender is None: | |
326 | raise Reject, 'no sender' | |
327 | sender = canon_sender(mi.sender) | |
328 | if not any(lambda pat: M.match(pat.lower(), sender), vv): | |
329 | raise Reject, 'unmatched sender' | |
330 | constraints['sender'] = check_sender | |
331 | ||
332 | def check_subject(mi, vv): | |
333 | if mi.msg is None: | |
334 | return | |
335 | subj = mi.msg['subject'] | |
336 | if subj is None: | |
337 | raise Reject, 'no subject' | |
338 | subj = canon_subject(subj) | |
339 | if not any(lambda pat: M.match(pat.lower(), subj), vv): | |
340 | raise Reject, 'unmatched subject' | |
341 | constraints['subject'] = check_subject | |
342 | ||
343 | def check_nothing(me, vv): | |
344 | pass | |
345 | ||
346 | def check(db, id, sender = None, msgfile = None): | |
347 | mi = MessageInfo() | |
348 | a = AttrMultiMap(db, id) | |
349 | try: | |
350 | addr = a['addr'][0] | |
351 | except KeyError: | |
352 | raise Reject, 'unknown id' | |
353 | if db.expiredp(id): | |
354 | raise Reject, 'expired' | |
355 | if msgfile is None: | |
356 | mi.msg = None | |
357 | else: | |
358 | try: | |
359 | mi.msg = EP.HeaderParser().parse(msgfile) | |
360 | except EP.Errors.HeaderParseError: | |
361 | raise Reject, 'unparseable header' | |
362 | mi.sender = sender | |
363 | for k, vv in a.iteritems(): | |
364 | constraints.get(k, check_nothing)(mi, vv) | |
365 | return a['addr'][0] | |
366 | ||
367 | ###----- Commands ----------------------------------------------------------- | |
368 | ||
369 | keyfile = 'db/keyring' | |
370 | tag = 'cryptomail' | |
371 | dbfile = 'db/cryptomail.db' | |
372 | commands = {} | |
373 | ||
374 | def timecmp(x, y): | |
375 | if x == y: | |
376 | return 0 | |
377 | elif x == C.KEXP_FOREVER or y == C.KEXP_EXPIRE: | |
378 | return +1 | |
379 | elif y == C.KEXP_FOREVER or x == C.KEXP_EXPIRE: | |
380 | return +1 | |
381 | else: | |
382 | return cmp(x, y) | |
383 | ||
384 | def cmd_generate(argv): | |
385 | try: | |
386 | opts, argv = getopt(argv, 't:c:f:', | |
387 | ['expire=', 'timeout=', 'constraint=', 'format=']) | |
388 | except GetoptError: | |
389 | return 1 | |
390 | kr = C.KeyFile(keyfile, C.KOPEN_WRITE) | |
391 | k = kr[tag] | |
392 | db = CMDB(dbfile) | |
393 | map = {} | |
394 | expwhen = C.KEXP_FOREVER | |
395 | format = '%' | |
396 | for o, a in opts: | |
397 | if o in ('-t', '--expire', '--timeout'): | |
398 | if a == 'forever': | |
399 | expwhen = C.KEXP_FOREVER | |
400 | else: | |
401 | expwhen = getdate(a) | |
402 | elif o in ('-c', '--constraint'): | |
403 | c, v = a.split('=', 1) | |
404 | if c not in constraints: | |
405 | die("unknown constraint `%s'", c) | |
406 | map.setdefault(c, []).append(v) | |
407 | elif o in ('f', '--format'): | |
408 | format = a | |
409 | else: | |
410 | raise 'Barf!' | |
411 | if timecmp(expwhen, k.deltime) > 0: | |
412 | k.deltime = expwhen | |
413 | if len(argv) != 1: | |
414 | return 1 | |
415 | addr = argv[0] | |
416 | a = AttrMultiMap(db) | |
417 | a.update(map) | |
418 | a['addr'] = [addr] | |
419 | c = Crypto(k).encrypt(a.id) | |
420 | db.setexpire(a.id, expwhen) | |
421 | print format.replace('%', M.base32_encode(Crypto(k).encrypt(a.id)). | |
422 | strip('=').lower()) | |
423 | db.commit() | |
424 | kr.save() | |
425 | commands['generate'] = \ | |
426 | (cmd_generate, '[-t TIME] [-c TYPE=VALUE] ADDR', """ | |
427 | Generate a new encrypted email address token forwarding to ADDR. | |
428 | ||
429 | Subcommand options: | |
430 | -t, --timeout=TIME Address should expire at TIME. | |
431 | -c, --constraint=TYPE=VALUE Apply constraint on the use of the address. | |
432 | -f, --format=STRING Substitute token for `%' in STRING. | |
433 | ||
434 | Constraint types: | |
435 | sender Envelope sender must match glob pattern. | |
436 | subject Message subject must match glob pattern.""") | |
437 | ||
438 | def cmd_initdb(argv): | |
439 | try: | |
440 | opts, argv = getopt(argv, '', []) | |
441 | except GetoptError: | |
442 | return 1 | |
443 | try: | |
444 | OS.unlink(dbfile) | |
445 | except OSError: | |
446 | pass | |
447 | CMDB(dbfile).setup() | |
448 | commands['initdb'] = \ | |
449 | (cmd_initdb, '', """ | |
450 | Initialize an attribute database.""") | |
451 | ||
452 | def cmd_addrcheck(argv): | |
453 | try: | |
454 | opts, argv = getopt(argv, '', []) | |
455 | except GetoptError: | |
456 | return 1 | |
457 | local, sender = (lambda addr, sender = None, *hunoz: (addr, sender))(*argv) | |
458 | k = C.KeyFile(keyfile, C.KOPEN_READ)[tag] | |
459 | db = CMDB(dbfile) | |
460 | try: | |
461 | id = Crypto(k).decrypt(M.base32_decode(local)) | |
462 | if id is None: | |
463 | raise Reject, 'decrypt failed' | |
464 | addr = check(db, id, sender) | |
465 | except Reject, msg: | |
466 | print '-%s' % msg | |
467 | return | |
468 | print '+%s' % addr | |
469 | commands['addrcheck'] = \ | |
470 | (cmd_addrcheck, 'LOCAL [SENDER [IGNORED ...]]', """ | |
471 | Check address token LOCAL, and report `-REASON' for failure or `+ADDR' for | |
472 | success.""") | |
473 | ||
474 | def cmd_fwaddr(argv): | |
475 | try: | |
476 | opts, argv = getopt(argv, '', []) | |
477 | except GetoptError: | |
478 | return 1 | |
479 | local, sender = (lambda addr, sender = None, *hunoz: (addr, sender))(*argv) | |
480 | k = C.KeyFile(keyfile, C.KOPEN_READ)[tag] | |
481 | db = CMDB(dbfile) | |
482 | try: | |
483 | id = Crypto(k).decrypt(M.base32_decode(local)) | |
484 | if id is None: | |
485 | raise Reject, 'decrypt failed' | |
486 | addr = check(db, id, sender, stdin) | |
487 | except Reject, msg: | |
488 | print >>stderr, '%s rejected message: %s' % (prog, msg) | |
489 | exit(100) | |
490 | stdin.seek(0) | |
491 | print addr | |
492 | commands['fwaddr'] = \ | |
493 | (cmd_fwaddr, 'LOCAL [SENDER [IGNORED ...]]', """ | |
494 | Check address token LOCAL. On failure, report reason to stderr and exit | |
495 | 111. On success, write forwarding address to stdout and exit 0. Expects | |
496 | the message on standard input, as a seekable file.""") | |
497 | ||
498 | def cmd_cleanup(argv): | |
499 | try: | |
500 | opts, argv = getopt(argv, '', []) | |
501 | except GetoptError: | |
502 | return 1 | |
503 | db = CMDB(dbfile) | |
504 | db.cleanup() | |
505 | cur = db.db.cursor() | |
506 | cur.execute('VACUUM') | |
507 | db.commit() | |
508 | commands['cleanup'] = \ | |
509 | (cmd_cleanup, '', """ | |
510 | Cleans up the attribute database, disposing of old records and compatifying | |
511 | the file.""") | |
512 | ||
513 | def cmd_help(argv): | |
514 | try: | |
515 | opts, argv = getopt(argv, '', []) | |
516 | except GetoptError: | |
517 | return 1 | |
518 | if len(argv) == 0: | |
519 | cmd = None | |
520 | elif len(argv) == 1: | |
521 | try: | |
522 | cmd = argv[0] | |
523 | ci = commands[cmd] | |
524 | except KeyError: | |
525 | die("unknown command `%s'" % cmd) | |
526 | else: | |
527 | return 1 | |
528 | version() | |
529 | ||
530 | if cmd: | |
531 | print 'Usage: %s [-OPTIONS] %s %s' % (prog, cmd, ci[1]) | |
532 | print ci[2] | |
533 | else: | |
534 | usage(stdout) | |
535 | print """ | |
536 | Handle encrypted email addresses. | |
537 | ||
538 | Help options: | |
539 | -h, --help Show this help text. | |
540 | -v, --version Show version number. | |
541 | -u, --usage Show a usage message. | |
542 | ||
543 | Global options: | |
544 | -d, --database=FILE Use FILE as the attribute database. | |
545 | -k, --keyring=KEYRING Use KEYRING as the keyring. | |
546 | -t, --tag=TAG Use TAG as the key tag. | |
547 | """ | |
548 | cmds = commands.keys() | |
549 | cmds.sort() | |
550 | print 'Subcommands:' | |
551 | for c in cmds: | |
552 | print ' %s %s' % (c, commands[c][1]) | |
553 | commands['help'] = \ | |
554 | (cmd_help, '[COMMAND]', """ | |
555 | Show help for subcommand COMMAND. | |
556 | """) | |
557 | ||
558 | ###----- Main program ------------------------------------------------------- | |
559 | ||
560 | def usage(file): | |
561 | print >>file, \ | |
562 | 'Usage: %s [-d FILE] [-k KEYRING] [-t TAG] COMMAND [ARGS...]' % prog | |
563 | def version(): | |
564 | print '%s version 1.0.0' % prog | |
565 | def help(): | |
566 | cmd_help() | |
567 | ||
568 | def main(): | |
569 | global argv | |
570 | try: | |
571 | opts, argv = getopt(argv[1:], | |
572 | 'hvud:k:t:', | |
573 | ['help', 'version', 'usage', | |
574 | 'database=', 'keyring=', 'tag=']) | |
575 | except GetoptError: | |
576 | usage(stderr) | |
577 | exit(111) | |
578 | for o, a in opts: | |
579 | if o in ('-h', '--help'): | |
580 | help() | |
581 | exit(0) | |
582 | elif o in ('-v', '--version'): | |
583 | version() | |
584 | exit(0) | |
585 | elif o in ('-u', '--usage'): | |
586 | usage(stdout) | |
587 | exit(0) | |
588 | elif o in ('-d', '--database'): | |
589 | dbfile = a | |
590 | elif o in ('-k', '--keyring'): | |
591 | keyfile = a | |
592 | elif o in ('-t', '--tag'): | |
593 | tag = a | |
594 | else: | |
595 | raise 'Barf!' | |
596 | if len(argv) < 1: | |
597 | usage(stderr) | |
598 | exit(111) | |
599 | ||
600 | if argv[0] in commands: | |
601 | c = argv[0] | |
602 | argv = argv[1:] | |
603 | else: | |
604 | usage(stderr) | |
605 | exit(111) | |
606 | cmd = commands[c] | |
607 | if cmd[0](argv): | |
608 | print >>stderr, 'Usage: %s %s %s' % (prog, c, cmd[1]) | |
609 | exit(111) | |
610 | ||
611 | try: | |
612 | main() | |
aeec1a4e MW |
613 | except SystemExit: |
614 | raise | |
615 | except: | |
71074336 MW |
616 | ty, exc, tb = exc_info() |
617 | moan('unhandled %s exception' % ty.__name__) | |
618 | for file, line, func, text in TB.extract_tb(tb): | |
619 | print >>stderr, \ | |
620 | ' %-35s -- %.38s' % ('%s:%d (%s)' % (file, line, func), text) | |
621 | die('%s: %s' % (ty.__name__, exc[0])) |