[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[minion-cvs] Bugfixes for armoring, refactor to use readFile/WriteFi...



Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.mit.edu:/tmp/cvs-serv15318/lib/mixminion

Modified Files:
	ClientMain.py Common.py Crypto.py Packet.py __init__.py 
	test.py 
Log Message:
Bugfixes for armoring, refactor to use readFile/WriteFile wrappers.

Common:
- Add "tryUnlink" function to perform common unlink-this-file-if-you-can
  operation.
- Debug armorText and unarmorText.
- Document writeFile and readFile

Packet:
- Remove dead comment.

test: 
- Remove redundant readFile and writeFile
- Add tests for armorText and unarmorText.

ClientMain, Common, ServerInbox, ServerList, EventStats, HashLogs, Modules,
   ServerKeys, ServerMain, ServerQueue:
- Use writeFile, readFile, tryUnlink where possible.
- Avoid "except" without "raise" or sys.exc_info()
- Avoid "except OSError" without checking errno or displaying error.


Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.86
retrieving revision 1.87
diff -u -d -r1.86 -r1.87
--- ClientMain.py	30 May 2003 03:28:26 -0000	1.86
+++ ClientMain.py	5 Jun 2003 05:24:23 -0000	1.87
@@ -33,15 +33,15 @@
      MixError, MixFatalError, MixProtocolError, MixProtocolBadAuth, UIError, \
      UsageError, ceilDiv, createPrivateDir, isPrintingAscii, isSMTPMailbox, \
      formatDate, formatFnameTime, formatTime, Lockfile, openUnique, \
-     previousMidnight, readPickled, readPossiblyGzippedFile, secureDelete, \
-     stringContains, succeedingMidnight, writePickled
+     previousMidnight, readFile, readPickled, readPossiblyGzippedFile, \
+     secureDelete, stringContains, succeedingMidnight, tryUnlink, writeFile, \
+     writePickled
 from mixminion.Crypto import sha1, ctr_crypt, trng
 from mixminion.Config import ClientConfig, ConfigError
 from mixminion.ServerInfo import ServerInfo, ServerDirectory
 from mixminion.Packet import ParseError, parseMBOXInfo, parseReplyBlocks, \
-     parseSMTPInfo, parseTextEncodedMessages, parseTextReplyBlocks,\
-     ReplyBlock,\
-     MBOX_TYPE, SMTP_TYPE, DROP_TYPE
+     parseSMTPInfo, parseTextEncodedMessages, parseTextReplyBlocks, \
+     ReplyBlock, MBOX_TYPE, SMTP_TYPE, DROP_TYPE
 
 # FFFF This should be made configurable and adjustable.
 MIXMINION_DIRECTORY_URL = "http://mixminion.net/directory/Directory.gz";
@@ -207,11 +207,7 @@
         if fp and mixminion.Crypto.pk_fingerprint(identity) != fp:
             raise MixFatalError("Bad identity key on directory")
 
-        try:
-            os.unlink(os.path.join(self.dir, "cache"))
-        except OSError, e:
-            if e.errno != errno.ENOENT:
-                raise
+        tryUnlink(os.path.join(self.dir, "cache"))
 
         # Install the new directory
         if gz:
@@ -702,7 +698,7 @@
         for a in allowed:
             try:
                 t = mixminion.parse_version_string(a)
-            except:
+            except ValueError:
                 LOG.warn("Couldn't parse recommended version %s", a)
                 continue
             try:
@@ -987,10 +983,7 @@
     def _checkMagic(self, fn, magic):
         """Make sure that the magic string on a given key file %s starts with
            is equal to 'magic'.  Raise MixError if it isn't."""
-        f = open(fn, 'rb')
-        s = f.read()
-        f.close()
-        if not s.startswith(magic):
+        if not readFile(fn, 1).startswith(magic):
             raise MixError("Invalid versioning on key file")
 
     def _save(self, fn, data, magic, password):
@@ -1007,11 +1000,10 @@
     def _load(self, fn, magic, password):
         """Load and return the key stored in 'fn' using the magic string
            'magic' and the password 'password'.  Raise MixError on failure."""
-        f = open(fn, 'rb')
-        s = f.read()
-        f.close()
+        s = readFile(fn, 1)
         if not s.startswith(magic):
-            raise MixError("Invalid key file")
+            raise MixError("Invalid versioning on key file")
+
         s = s[len(magic):]
         if len(s) < 8:
             raise MixError("Key file too short")
@@ -1068,8 +1060,9 @@
     """Create a default, 'fail-safe' configuration in a given file"""
     LOG.warn("No configuration file found. Installing default file in %s",
                   fname)
-    f = open(os.path.expanduser(fname), 'w')
-    f.write("""\
+
+    writeFile(os.path.expanduser(fname), 
+              """\
 # This file contains your options for the mixminion client.
 [Host]
 ## Use this option to specify a 'secure remove' command.
@@ -1107,7 +1100,7 @@
 ConnectionTimeout: 20 seconds
 
 """)
-    f.close()
+
 
 class SURBLog:
     """A SURBLog manipulates a database on disk to remember which SURBs we've
@@ -1964,11 +1957,10 @@
                 if fn == '-':
                     s = sys.stdin.read()
                 else:
-                    f = open(fn, 'rb')
-                    s = f.read()
-                    f.close()
+                    s = readFile(fn, 1)
                 try:
-                    if stringContains(s, "== BEGIN TYPE III REPLY BLOCK =="):
+                    if stringContains(s,
+                                      "-----BEGIN TYPE III REPLY BLOCK-----"):
                         surbs.extend(parseTextReplyBlocks(s))
                     else:
                         surbs.extend(parseReplyBlocks(s))
@@ -2175,15 +2167,12 @@
         if inFile is None:
             inFile = "-"
 
-        if inFile == '-':
-            f = sys.stdin
-            print "Enter your message now.  Type Ctrl-D when you are done."
-        else:
-            f = open(inFile, 'r')
-
         try:
-            payload = f.read()
-            f.close()
+            if inFile == '-':
+                print "Enter your message now.  Type Ctrl-D when you are done."
+                payload = sys.stdin.read()
+            else:
+                payload = readFile(inFile)
         except KeyboardInterrupt:
             print "Interrupted.  Message not sent."
             sys.exit(1)
@@ -2423,9 +2412,7 @@
         s = sys.stdin.read()
     else:
         try:
-            f = open(inputFile, 'r')
-            s = f.read()
-            f.close()
+            s = readFile(inputFile)
         except OSError, e:
             LOG.error("Could not read file %s: %s", inputFile, e)
     try:
@@ -2578,12 +2565,10 @@
 
     try:
         for fn in args:
-            f = open(fn, 'rb')
-            s = f.read()
-            f.close()
+            s = readFile(fn, 1)
             print "==== %s"%fn
             try:
-                if stringContains(s, "== BEGIN TYPE III REPLY BLOCK =="):
+                if stringContains(s, "-----BEGIN TYPE III REPLY BLOCK-----"):
                     surbs = parseTextReplyBlocks(s)
                 else:
                     surbs = parseReplyBlocks(s)

Index: Common.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Common.py,v
retrieving revision 1.85
retrieving revision 1.86
diff -u -d -r1.85 -r1.86
--- Common.py	30 May 2003 03:11:46 -0000	1.85
+++ Common.py	5 Jun 2003 05:24:23 -0000	1.86
@@ -13,8 +13,8 @@
             'installSIGCHLDHandler', 'isSMTPMailbox', 'openUnique',
             'previousMidnight', 'readFile', 'readPickled',
             'readPossiblyGzippedFile', 'secureDelete', 'stringContains',
-            'succeedingMidnight', 'unarmorText', 'waitForChildren',
-            'writeFile', 'writePickled' ]
+            'succeedingMidnight', 'tryUnlink', 'unarmorText',
+            'waitForChildren', 'writeFile', 'writePickled' ]
 
 import binascii
 import bisect
@@ -172,7 +172,6 @@
        headers 'header'.
        
        If base64 is false, uses cleartext armor."""
-    #XXXX004 testme
     result = []
     result.append("-----BEGIN %s-----\n" %type)
     for k,v in headers:
@@ -189,7 +188,7 @@
     return "".join(result)
 
 # Matches a begin line.
-BEGIN_LINE_RE = re.compile(r'^-----BEGIN ([^-]+)-----\s*$',re.M)
+BEGIN_LINE_RE = re.compile(r'^-----BEGIN ([^-]+)-----[ \t]*$',re.M)
 
 # Matches a header line.
 ARMOR_KV_RE = re.compile(r'([^:\s]+): ([^\n]+)')
@@ -204,7 +203,6 @@
        base64fn -- if provided, called with (type, headers) to tell whether
           we do cleartext armor.
     """
-    #XXXX004 testme
     result = []
     
     while 1:
@@ -217,7 +215,7 @@
             return result
 
         tp = mBegin.group(1)
-        endPat = r"^-----END %s-----$" % tp
+        endPat = r"^-----END %s-----[ \t]*$" % tp
 
         endRE = re.compile(endPat, re.M)
         mEnd = endRE.search(s, mBegin.start())
@@ -225,15 +223,17 @@
             raise ValueError("Couldn't find end line for '%s'"%tp.lower())
 
         if tp not in findTypes:
-            idx = mEnd.end+1
+            s = s[mEnd.end()+1:]
             continue
-        
+
         idx = mBegin.end()+1
         endIdx = mEnd.start()
+
         assert s[idx-1] == s[endIdx-1] == '\n'
         while idx < endIdx:
             nl = s.index("\n", idx, endIdx)
             line = s[idx:nl]
+            idx = nl+1
             if ":" in line:
                 m = ARMOR_KV_RE.match(line)
                 if not m:
@@ -241,12 +241,10 @@
                 fields.append((m.group(1), m.group(2)))
             elif line.strip() == '':
                 break
-            idx = nl+1
 
         if base64fn:
             base64 = base64fn(tp,fields)
 
-        idx = nl+1
         if base64:
             try:
                 value = binascii.a2b_base64(s[idx:endIdx])
@@ -419,8 +417,7 @@
             LOG.error("Atomic file not closed/discarded: %s",self.tmpname)
 
 def readFile(fn, binary=0):
-    """DOCDOC"""
-    #XXXX004 use more.
+    """Return the contents of the file named <fn>."""
     f = open(fn, ['r', 'rb'][binary])
     try:
         return f.read()
@@ -428,8 +425,14 @@
         f.close()
 
 def writeFile(fn, contents, mode=0600, binary=0):
-    """DOCDOC"""
-    #XXXX004 use more.
+    """Atomically write a string <contents> into a file <file> with mode
+       <mode>.  If <binary>, binary mode will be used.
+
+       If the file exists, it will be replaced.
+
+       If two processes attempt to writeFile the same file at once,
+       the one finishing last wins.
+       """
     tmpname = fn+".tmp"
     f, tmpname = openUnique(tmpname, ['w','wb'][binary], mode)
     try:
@@ -469,6 +472,18 @@
 
     os.rename(tmpname, fn)
 
+def tryUnlink(fname):
+    """Try to remove the file named fname.  If the file is erased, return 1.
+       If the file didn't exist in the first place, return 0.  Otherwise
+       propagate an exception."""
+    try:
+        os.unlink(fname)
+        return 1
+    except OSError, e:
+        if e.errno == errno.ENOENT:
+            return 0
+        raise
+
 #----------------------------------------------------------------------
 # Secure filesystem operations.
 
@@ -621,9 +636,9 @@
             if not os.path.exists(parent):
                 createPrivateDir(parent)
             self.file = open(self.fname, 'a')
-        except OSError:
+        except OSError, e:
             self.file = None
-            raise MixError("Unable to open log file %r"%self.fname)
+            raise MixError("Unable to open log file %r: %s"%self.fname, e)
     def close(self):
         "Close the underlying file"
         self.file.close()
@@ -1250,11 +1265,7 @@
 
     def getContents(self):
         """Return the contents of the lock file."""
-        try:
-            f = open(self.filename, 'r')
-            return f.read()
-        finally:
-            f.close()
+        return readFile(self.filename)
 
     def acquire(self, contents="", blocking=0):
         """Acquire this lock.  If we're acquiring the lock for the first time,

Index: Crypto.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Crypto.py,v
retrieving revision 1.43
retrieving revision 1.44
diff -u -d -r1.43 -r1.44
--- Crypto.py	17 May 2003 00:08:42 -0000	1.43
+++ Crypto.py	5 Jun 2003 05:24:23 -0000	1.44
@@ -51,7 +51,8 @@
     except MixFatalError:
         raise
     except:
-        raise MixFatalError("Error initializing entropy source")
+        info = sys.exc_info()
+        raise MixFatalError("Error initializing entropy source: %s", info[0])
     openssl_seed(40)
 
 def sha1(s):

Index: Packet.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Packet.py,v
retrieving revision 1.44
retrieving revision 1.45
diff -u -d -r1.44 -r1.45
--- Packet.py	28 May 2003 06:37:35 -0000	1.44
+++ Packet.py	5 Jun 2003 05:24:23 -0000	1.45
@@ -361,24 +361,6 @@
        return a list containing the reply blocks.  Raise ParseError on
        failure."""
 
-##     while 1:
-##         m = RB_TEXT_RE.search(s[idx:])
-##         if m is None:
-##             # FFFF Better errors on malformatted reply blocks.
-##             break
-##         version, text = m.group(1), m.group(2)
-##         idx += m.end()
-##         if version != '0.1':
-##             LOG.warn("Skipping reply block with unrecognized version: %s",
-##                      version)
-##             continue
-##         try:
-##             val = binascii.a2b_base64(text)
-##         except (TypeError, binascii.Incomplete, binascii.Error), e:
-##             raise ParseError("Bad reply block encoding: %s"%e)
-##         blocks.append(parseReplyBlock(val))
-##     return blocks
-
     try:
         res = unarmorText(s, (RB_ARMOR_NAME,), base64=1)
     except ValueError, e:

Index: __init__.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/__init__.py,v
retrieving revision 1.36
retrieving revision 1.37
diff -u -d -r1.36 -r1.37
--- __init__.py	2 Jun 2003 17:49:51 -0000	1.36
+++ __init__.py	5 Jun 2003 05:24:23 -0000	1.37
@@ -77,7 +77,7 @@
         try:
             patch = int(patch)
         except ValueError:
-            pass
+            patch = patch
     return (int(major), int(minor), int(sub), status, patch)
 
 def cmp_versions(a,b):

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.115
retrieving revision 1.116
diff -u -d -r1.115 -r1.116
--- test.py	3 Jun 2003 17:28:11 -0000	1.115
+++ test.py	5 Jun 2003 05:24:23 -0000	1.116
@@ -107,24 +107,6 @@
     else:
         return abs(f1-f2) < .00001
 
-def readFile(fname):
-    """Return the contents of the file named 'fname'.  We could just say
-       'open(fname).read()' instead, but that isn't as clean."""
-    f = open(fname, 'r')
-    try:
-        return f.read()
-    finally:
-        f.close()
-
-def writeFile(fname, contents):
-    """Create a new file named fname, replacing any such file that exists,
-       with the contents 'contents'."""
-    f = open(fname, 'w')
-    try:
-        f.write(contents)
-    finally:
-        f.close()
-
 #----------------------------------------------------------------------
 # RSA key caching functionality
 
@@ -454,6 +436,20 @@
         t(36 not in fromSquareToSquare)
         t(100 not in fromSquareToSquare)
 
+    def _intervalEq(self, a, *others):
+        eq = self.assertEquals
+        for b in others:
+            if isinstance(b, IntervalSet):
+                eq(a,b)
+                b._checkRep()
+            elif isinstance(b, types.StringType):
+                eq(repr(a), b)
+            elif isinstance(b, types.ListType):
+                eq(a.getIntervals(), b)
+            else:
+                raise MixError()
+            a._checkRep()
+
     def test_openUnique(self):
         d = mix_mktemp()
         os.mkdir(d)
@@ -524,20 +520,94 @@
                 self.assertEquals(len(line), max)
             self.assert_(len(lines[-1]) <= max)
 
-    def _intervalEq(self, a, *others):
-        eq = self.assertEquals
-        for b in others:
-            if isinstance(b, IntervalSet):
-                eq(a,b)
-                b._checkRep()
-            elif isinstance(b, types.StringType):
-                eq(repr(a), b)
-            elif isinstance(b, types.ListType):
-                eq(a.getIntervals(), b)
-            else:
-                raise MixError()
-            a._checkRep()
+    def test_armor(self):
+        inp1 = "xyzzy111"*10
+        inp2 = "Hello\nWorld!\n--Me.\n"
+        # Simple base64
+        self.assertEquals(armorText(inp1, "FOO BAR", (), 1),
+"""-----BEGIN FOO BAR-----
+
+eHl6enkxMTF4eXp6eTExMXh5enp5MTExeHl6enkxMTF4eXp6eTExMXh5enp5MTEx
+eHl6enkxMTF4eXp6eTExMXh5enp5MTExeHl6enkxMTE=
+-----END FOO BAR-----\n""")
+        # With headers
+        self.assertEquals(armorText(inp1, "FOO BAR",
+                                    [("H1", "xyz"),("H2", "a b")], 1),
+"""-----BEGIN FOO BAR-----
+H1: xyz
+H2: a b
+
+eHl6enkxMTF4eXp6eTExMXh5enp5MTExeHl6enkxMTF4eXp6eTExMXh5enp5MTEx
+eHl6enkxMTF4eXp6eTExMXh5enp5MTExeHl6enkxMTE=
+-----END FOO BAR-----\n""")
+
+        # No base64
+        self.assertEquals(armorText(inp1, "FOO BAR", (), 0),
+"""-----BEGIN FOO BAR-----
+
+xyzzy111xyzzy111xyzzy111xyzzy111xyzzy111xyzzy111xyzzy111xyzzy111xyzzy111xyzzy111
+-----END FOO BAR-----\n""")
+
+        # No base64, hyphen escape.
+        self.assertEquals(armorText(inp2, "FOO BAR", [("H3", "text")], 0),
+"""-----BEGIN FOO BAR-----
+H3: text
+
+Hello
+World!
+- --Me.
+-----END FOO BAR-----\n""")
+
+        # Test unarmor, succeeding cases.
+        for inp in inp1, inp2:
+            for mode in 0,1:
+                for headers in [], [("H-x", "abcde fg")]:
+                    enc = armorText(inp, "THIS THAT", headers, mode)
+                    enc = "asdasdkjlasd\nHey kids!\n"+enc
+                    tp, h, b = unarmorText(enc, ["THIS THAT", "FRED"],
+                                           mode)[0]
+                    self.assertEquals(tp, "THIS THAT")
+                    self.assertEquals(h, headers)
+                    self.assertEquals(b.strip(), inp.strip())
+                    if not mode:
+                        self.assert_(b.endswith("\n"))
 
+        # Test base64fn and concatenation.
+        enc1 = armorText(inp2, "THIS THAT", [("H-64", "0")], 0)
+        enc2 = armorText(inp2, "THIS THAT", [("H-64", "1")], 1)
+        def base64fn(tp, h, assert_=self.assert_):
+            assert_(tp=="THIS THAT")
+            assert_(len(h) == 1)
+            assert_(len(h[0]) == 2)
+            assert_(h[0][0] == "H-64")
+            return int(h[0][1])
+        dec = unarmorText(enc1+enc2, ["THIS THAT"], base64fn=base64fn)
+        self.assertEquals(len(dec), 2)
+        for mode in 0, 1:
+            tp,h,b = dec[mode]
+            self.assertEquals(tp, "THIS THAT")
+            self.assertEquals(h, [("H-64", str(mode))])
+            self.assertEquals(b, inp2)
+
+        # Test skipping unwanted types
+        enc3 = armorText(inp2, "OTHER", [("H-64", "1")], 1)
+        dec2 = unarmorText(enc1+enc3+enc2, ["THIS THAT"], base64fn=base64fn)
+        self.assertEquals(dec2, dec)
+
+        # Test skipping broken armor.
+        self.assertRaises(ValueError,
+                          unarmorText,"-----BEGIN X-----\n\n", ["Y"], 1)
+        self.assertRaises(ValueError,
+                          unarmorText,
+                          "-----BEGIN X-----\n"
+                          ":B:C\n\n"
+                          "A B C\n-----END X-----\n", ["X"], 0)
+        self.assertRaises(ValueError,
+                          unarmorText,
+                          "-----BEGIN X-----\n"
+                          "A: X\n\n"
+                          "A B C\n-----END X-----\n", ["X"], 1)
+        
 #----------------------------------------------------------------------
 
 class MinionlibCryptoTests(unittest.TestCase):
@@ -5746,7 +5816,7 @@
     tc = loader.loadTestsFromTestCase
 
     if 0:
-        suite.addTest(tc(MMTPTests))
+        suite.addTest(tc(MiscTests))
         return suite
 
     suite.addTest(tc(MiscTests))