chiark / gitweb /
userv: Tidy up a bit. Require file descriptors to be right.
[cryptomail] / bin / cryptomail
CommitLineData
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
27import catacomb as C
28import mLib as M
29from pysqlite2 import dbapi2 as sqlite
30from UserDict import DictMixin
31from getopt import getopt, GetoptError
32from getdate import getdate
33from sys import stdin, stdout, stderr, exit, argv, exc_info
34from email import Parser as EP
35import os as OS
36import time as T
37import sre as RX
38import traceback as TB
39
40###----- Database messing ---------------------------------------------------
41
42class 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
100class 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
132class 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
167class 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
185def 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
191def any(pred, list):
192 for i in list:
193 if pred(i): return True
194 return False
195def every(pred, list):
196 for i in list:
197 if not pred(i): return False
198 return True
199
200prog = RX.sub(r'^.*[/\\]', '', argv[0])
201def moan(msg):
202 print >>stderr, '%s: %s' % (prog, msg)
203def die(msg):
204 moan(msg)
205 exit(111)
206
207###----- My actual database -------------------------------------------------
208
209class 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])
e345b51f
MW
224 cur.execute('''DELETE FROM expiry WHERE attrset IN
225 (SELECT attrset
226 FROM expiry LEFT JOIN attrset
227 ON expiry.attrset = attrset.id
228 WHERE attrset.id ISNULL)''')
71074336 229 AttrDB.cleanup(me)
e345b51f 230 def expiry(me, id):
71074336 231 for t, in me.select('SELECT time FROM expiry WHERE attrset = ?', [id]):
e345b51f
MW
232 return t
233 return None
234 def expiredp(me, id):
235 t = me.expiry(id)
236 if t is not None and t < time_format():
237 return True
238 else:
239 return False
71074336
MW
240 def setexpire(me, id, when):
241 if when != C.KEXP_FOREVER:
242 cur = me.db.cursor()
243 cur.execute('INSERT INTO expiry VALUES (?, ?)',
244 [id, time_format(when)])
245
246###----- Crypto messing about -----------------------------------------------
247
248## Very vague security arguments...
249##
250## If the block size n of the PRP is large enough (128 bits) then we encrypt
251## id || 0^{n - 64}. Decryption checks we have the right thing. The
252## security proofs for secrecy and integrity are trivial.
253##
254## If the block size is small, then we encrypt two blocks:
255## C_0 = E_K(0^{n - 64} || id)
256## C_1 = E_K(C_0)
257## The proofs are a little more complicated, but essentially work like this.
258## If no 0^{n - 64} || id is ever seen as a C_0 then an adversary can't tell
259## the difference between this and a similar construction using independent
260## keys. This other construction must provide secrecy (pushing a
261## nonrepeating thing through a PRF) and integrity (PRF on noncolliding
262## inputs). So we win, give or take a birthday term.
263class Crypto (object):
264 def __init__(me, key):
265 me.prp = C.gcprps[key.attr.get('prp', 'blowfish')](key.data.bin)
266 def encrypt(me, id):
267 blksz = type(me.prp).blksz
268 p = C.MP(id).storeb(blksz)
269 c = me.prp.encrypt(p)
270 if blksz < 16:
271 c += me.prp.encrypt(c)
272 return c
273 def decrypt(me, c):
274 bad = False
275 blksz = type(me.prp).blksz
276 if blksz < 16:
277 if len(c) != blksz * 2:
278 return None
279 c, c1 = c[:blksz], c[blksz:]
280 if c1 != me.prp.encrypt(c):
281 bad = True
282 else:
283 if len(c) != blksz:
284 return None
285 p = me.prp.decrypt(c)
286 id = C.MP.loadb(p)
287 if id >> 64:
288 bad = True
289 if bad:
290 return None
291 return long(id)
292
293###----- Canonification -----------------------------------------------------
294
295rx_prefix = RX.compile(r'''(?x) ^ (
296 \[ \S+ \] \s* |
297 \S{,4} : \s* |
298 \s+
299)
300''')
301rx_suffix = RX.compile(r'''(?ix) (
302 \( \s* was \s* : .* \) \s* |
303 \s+
304) $''')
305rx_punct = RX.compile(r'(?x) [^\w]+ ')
306
307def canon_sender(addr):
308 return addr.lower()
309
310def canon_subject(subject):
311 subject = subject.lower()
312 while True:
313 m = rx_prefix.match(subject)
314 if not m: break
315 subject = subject[m.end():]
316 while True:
317 m = rx_suffix.search(subject)
318 if not m: break
319 subject = subject[:m.start()]
320 subject = rx_punct.sub('', subject)
321 return subject
322
323###----- Checking a message for validity ------------------------------------
324
325class Reject (Exception): pass
326
327class MessageInfo (object):
328 __slots__ = '''
329 sender msg
330 '''.split()
331
332constraints = {}
333
334def check_sender(mi, vv):
335 if mi.sender is None:
336 raise Reject, 'no sender'
337 sender = canon_sender(mi.sender)
338 if not any(lambda pat: M.match(pat.lower(), sender), vv):
339 raise Reject, 'unmatched sender'
340constraints['sender'] = check_sender
341
342def check_subject(mi, vv):
343 if mi.msg is None:
344 return
345 subj = mi.msg['subject']
346 if subj is None:
347 raise Reject, 'no subject'
348 subj = canon_subject(subj)
349 if not any(lambda pat: M.match(pat.lower(), subj), vv):
350 raise Reject, 'unmatched subject'
351constraints['subject'] = check_subject
352
353def check_nothing(me, vv):
354 pass
355
356def check(db, id, sender = None, msgfile = None):
357 mi = MessageInfo()
358 a = AttrMultiMap(db, id)
359 try:
360 addr = a['addr'][0]
361 except KeyError:
362 raise Reject, 'unknown id'
363 if db.expiredp(id):
364 raise Reject, 'expired'
365 if msgfile is None:
366 mi.msg = None
367 else:
368 try:
369 mi.msg = EP.HeaderParser().parse(msgfile)
370 except EP.Errors.HeaderParseError:
371 raise Reject, 'unparseable header'
372 mi.sender = sender
373 for k, vv in a.iteritems():
374 constraints.get(k, check_nothing)(mi, vv)
375 return a['addr'][0]
376
377###----- Commands -----------------------------------------------------------
378
379keyfile = 'db/keyring'
380tag = 'cryptomail'
381dbfile = 'db/cryptomail.db'
e345b51f 382user = None
71074336
MW
383commands = {}
384
385def timecmp(x, y):
386 if x == y:
387 return 0
388 elif x == C.KEXP_FOREVER or y == C.KEXP_EXPIRE:
389 return +1
390 elif y == C.KEXP_FOREVER or x == C.KEXP_EXPIRE:
391 return +1
392 else:
393 return cmp(x, y)
394
6253ed06
MW
395def token(c, id):
396 return M.base32_encode(c.encrypt(id)).strip('=').lower()
397
71074336
MW
398def cmd_generate(argv):
399 try:
e345b51f
MW
400 opts, argv = getopt(argv, 't:c:f:i:',
401 ['expire=', 'timeout=', 'constraint=',
402 'info=', 'format='])
71074336
MW
403 except GetoptError:
404 return 1
405 kr = C.KeyFile(keyfile, C.KOPEN_WRITE)
406 k = kr[tag]
407 db = CMDB(dbfile)
408 map = {}
409 expwhen = C.KEXP_FOREVER
410 format = '%'
411 for o, a in opts:
412 if o in ('-t', '--expire', '--timeout'):
413 if a == 'forever':
414 expwhen = C.KEXP_FOREVER
415 else:
416 expwhen = getdate(a)
417 elif o in ('-c', '--constraint'):
418 c, v = a.split('=', 1)
419 if c not in constraints:
420 die("unknown constraint `%s'", c)
421 map.setdefault(c, []).append(v)
010ac9cf 422 elif o in ('-f', '--format'):
71074336 423 format = a
e345b51f
MW
424 elif o in ('-i', '--info'):
425 map['info'] = [a]
71074336
MW
426 else:
427 raise 'Barf!'
428 if timecmp(expwhen, k.deltime) > 0:
429 k.deltime = expwhen
430 if len(argv) != 1:
431 return 1
432 addr = argv[0]
433 a = AttrMultiMap(db)
434 a.update(map)
435 a['addr'] = [addr]
e345b51f
MW
436 if user is not None:
437 a['user'] = [user]
71074336 438 db.setexpire(a.id, expwhen)
6253ed06 439 print format.replace('%', token(Crypto(k), a.id))
71074336
MW
440 db.commit()
441 kr.save()
442commands['generate'] = \
443 (cmd_generate, '[-t TIME] [-c TYPE=VALUE] ADDR', """
444Generate a new encrypted email address token forwarding to ADDR.
445
446Subcommand options:
447 -t, --timeout=TIME Address should expire at TIME.
448 -c, --constraint=TYPE=VALUE Apply constraint on the use of the address.
449 -f, --format=STRING Substitute token for `%' in STRING.
450
451Constraint types:
452 sender Envelope sender must match glob pattern.
453 subject Message subject must match glob pattern.""")
454
455def cmd_initdb(argv):
456 try:
457 opts, argv = getopt(argv, '', [])
458 except GetoptError:
459 return 1
460 try:
461 OS.unlink(dbfile)
462 except OSError:
463 pass
464 CMDB(dbfile).setup()
465commands['initdb'] = \
466 (cmd_initdb, '', """
467Initialize an attribute database.""")
468
e345b51f
MW
469def getid(local):
470 k = C.KeyFile(keyfile, C.KOPEN_READ)[tag]
471 id = Crypto(k).decrypt(M.base32_decode(local))
472 if id is None:
473 raise Reject, 'decrypt failed'
474 return id
475
71074336
MW
476def cmd_addrcheck(argv):
477 try:
478 opts, argv = getopt(argv, '', [])
479 except GetoptError:
480 return 1
481 local, sender = (lambda addr, sender = None, *hunoz: (addr, sender))(*argv)
71074336
MW
482 db = CMDB(dbfile)
483 try:
e345b51f 484 id = getid(local)
71074336
MW
485 addr = check(db, id, sender)
486 except Reject, msg:
487 print '-%s' % msg
488 return
489 print '+%s' % addr
490commands['addrcheck'] = \
491 (cmd_addrcheck, 'LOCAL [SENDER [IGNORED ...]]', """
492Check address token LOCAL, and report `-REASON' for failure or `+ADDR' for
493success.""")
494
495def cmd_fwaddr(argv):
496 try:
497 opts, argv = getopt(argv, '', [])
498 except GetoptError:
499 return 1
e345b51f
MW
500 if len(argv) not in (1, 2):
501 return 1
502 local, sender = (lambda addr, sender = None: (addr, sender))(*argv)
71074336
MW
503 db = CMDB(dbfile)
504 try:
e345b51f 505 id = getid(local)
71074336
MW
506 if id is None:
507 raise Reject, 'decrypt failed'
508 addr = check(db, id, sender, stdin)
509 except Reject, msg:
510 print >>stderr, '%s rejected message: %s' % (prog, msg)
511 exit(100)
512 stdin.seek(0)
513 print addr
514commands['fwaddr'] = \
e345b51f 515 (cmd_fwaddr, 'LOCAL [SENDER]', """
71074336
MW
516Check address token LOCAL. On failure, report reason to stderr and exit
517111. On success, write forwarding address to stdout and exit 0. Expects
518the message on standard input, as a seekable file.""")
519
6253ed06
MW
520ignore = {'user': 1, 'addr': 1}
521def show(db, a):
522 keys = a.keys()
523 keys.sort()
524 for k in keys:
525 if k in ignore:
526 continue
527 for v in a[k]:
528 print '\t%s: %s' % (k, v)
529 expwhen = db.expiry(a.id)
530 if expwhen:
531 print '\texpires: %s' % expwhen
532 else:
533 print '\tno-expiry'
534
e345b51f
MW
535def cmd_info(argv):
536 try:
537 opts, argv = getopt(argv, '', [])
538 except GetoptError:
539 return 1
540 if len(argv) != 1:
541 return 1
542 local = argv[0]
543 db = CMDB(dbfile)
544 try:
545 id = getid(local)
546 a = AttrMultiMap(db, id)
547 if user is not None and user != a.get('user', [None])[0]:
548 raise Reject, 'not your token'
549 if 'addr' not in a:
550 die('unknown token (expired?)')
6253ed06
MW
551 print 'addr: %s' % a['addr'][0]
552 show(db, a)
e345b51f
MW
553 except Reject, msg:
554 die('invalid token')
555commands['info'] = \
556 (cmd_info, 'LOCAL', """
557Exaimne the address token LOCAL, and print information about it to standard
558output.""")
559
560def cmd_revoke(argv):
561 try:
562 opts, argv = getopt(argv, '', [])
563 except GetoptError:
564 return 1
565 if len(argv) != 1:
566 return 1
567 local = argv[0]
568 db = CMDB(dbfile)
569 try:
570 id = getid(local)
571 a = AttrMultiMap(db, id)
572 if user is not None and user != a.get('user', [None])[0]:
573 raise Reject, 'not your token'
574 if 'addr' not in a:
575 die('unknown token (expired?)')
576 a.clear()
577 db.cleanup()
578 db.commit()
579 except Reject, msg:
580 die('invalid token')
581commands['revoke'] = \
582 (cmd_revoke, 'LOCAL', """
583Revoke the token LOCAL.""")
584
6253ed06
MW
585def cmd_list(argv):
586 try:
587 opts, argv = getopt(argv, '', [])
588 except GetoptError:
589 return 1
590 if argv:
591 return 1
592 c = Crypto(C.KeyFile(keyfile, C.KOPEN_READ)[tag])
593 db = CMDB(dbfile)
594 if not user:
595 gen = db.select('SELECT DISTINCT id FROM attrset')
596 else:
597 gen = db.select('''SELECT DISTINCT attrset.id
598 FROM attrset, attr ON attrset.attr = attr.id
599 WHERE attr.key = 'user' AND attr.value = ?''',
600 [user])
601 for id, in gen:
602 a = AttrMultiMap(db, id)
603 print '%s %s%s' % \
604 (token(c, id),
605 a.get('addr', '<no-address>')[0],
606 (not user and ' [%s]' % a.get('user', ['<no-user>'])[0] or ''))
607 show(db, a)
608commands['list'] = \
609 (cmd_list, '', """
610List the user's tokens and information about them.""")
611
71074336
MW
612def cmd_cleanup(argv):
613 try:
614 opts, argv = getopt(argv, '', [])
615 except GetoptError:
616 return 1
617 db = CMDB(dbfile)
618 db.cleanup()
619 cur = db.db.cursor()
620 cur.execute('VACUUM')
621 db.commit()
622commands['cleanup'] = \
623 (cmd_cleanup, '', """
624Cleans up the attribute database, disposing of old records and compatifying
625the file.""")
626
627def cmd_help(argv):
628 try:
629 opts, argv = getopt(argv, '', [])
630 except GetoptError:
631 return 1
632 if len(argv) == 0:
633 cmd = None
634 elif len(argv) == 1:
635 try:
636 cmd = argv[0]
637 ci = commands[cmd]
638 except KeyError:
639 die("unknown command `%s'" % cmd)
640 else:
641 return 1
642 version()
643 print
644 if cmd:
645 print 'Usage: %s [-OPTIONS] %s %s' % (prog, cmd, ci[1])
646 print ci[2]
647 else:
648 usage(stdout)
649 print """
650Handle encrypted email addresses.
651
652Help options:
653 -h, --help Show this help text.
654 -v, --version Show version number.
655 -u, --usage Show a usage message.
656
657Global options:
658 -d, --database=FILE Use FILE as the attribute database.
659 -k, --keyring=KEYRING Use KEYRING as the keyring.
660 -t, --tag=TAG Use TAG as the key tag.
e345b51f 661 -U, --user=USER Claim to be USER.
71074336
MW
662"""
663 cmds = commands.keys()
664 cmds.sort()
665 print 'Subcommands:'
666 for c in cmds:
667 print ' %s %s' % (c, commands[c][1])
668commands['help'] = \
669 (cmd_help, '[COMMAND]', """
670Show help for subcommand COMMAND.
671""")
672
673###----- Main program -------------------------------------------------------
674
675def usage(file):
676 print >>file, \
677 'Usage: %s [-d FILE] [-k KEYRING] [-t TAG] COMMAND [ARGS...]' % prog
678def version():
679 print '%s version 1.0.0' % prog
680def help():
681 cmd_help()
682
683def main():
e345b51f 684 global argv, user, keyfile, dbfile, tag
71074336
MW
685 try:
686 opts, argv = getopt(argv[1:],
e345b51f 687 'hvud:k:t:U:',
71074336 688 ['help', 'version', 'usage',
e345b51f 689 'database=', 'keyring=', 'tag=', 'user='])
71074336
MW
690 except GetoptError:
691 usage(stderr)
692 exit(111)
693 for o, a in opts:
694 if o in ('-h', '--help'):
695 help()
696 exit(0)
697 elif o in ('-v', '--version'):
698 version()
699 exit(0)
700 elif o in ('-u', '--usage'):
701 usage(stdout)
702 exit(0)
703 elif o in ('-d', '--database'):
704 dbfile = a
705 elif o in ('-k', '--keyring'):
706 keyfile = a
707 elif o in ('-t', '--tag'):
708 tag = a
e345b51f
MW
709 elif o in ('-U', '--user'):
710 user = a
71074336
MW
711 else:
712 raise 'Barf!'
713 if len(argv) < 1:
714 usage(stderr)
715 exit(111)
716
717 if argv[0] in commands:
718 c = argv[0]
719 argv = argv[1:]
720 else:
721 usage(stderr)
722 exit(111)
723 cmd = commands[c]
724 if cmd[0](argv):
725 print >>stderr, 'Usage: %s %s %s' % (prog, c, cmd[1])
726 exit(111)
727
728try:
729 main()
aeec1a4e
MW
730except SystemExit:
731 raise
732except:
71074336
MW
733 ty, exc, tb = exc_info()
734 moan('unhandled %s exception' % ty.__name__)
735 for file, line, func, text in TB.extract_tb(tb):
736 print >>stderr, \
737 ' %-35s -- %.38s' % ('%s:%d (%s)' % (file, line, func), text)
738 die('%s: %s' % (ty.__name__, exc[0]))