# Copyright 2002-2004 Nick Mathewson.  See LICENSE for licensing information.
# $Id: testSupport.py,v 1.26 2004/12/13 06:01:58 nickm Exp $

"""mixminion.testSupport

   Shared support code for unit tests, benchmark tests, and integration tests.
   """

import base64
import cStringIO
import os
import stat
import sys

import mixminion.Crypto
import mixminion.Common
from mixminion.Common import waitForChildren, ceilDiv, createPrivateDir, LOG
from mixminion.Config import _parseBoolean, _parseIntervalList, ConfigError

from mixminion.server.Modules import DELIVER_FAIL_NORETRY, DELIVER_FAIL_RETRY,\
     DELIVER_OK, DeliveryModule, ImmediateDeliveryQueue, \
     SimpleModuleDeliveryQueue, _escapeMessageForEmail

#----------------------------------------------------------------------
# DirectoryStoreModule

class DirectoryStoreModule(DeliveryModule):
    """Delivery module for testing: puts messages in files in a given
       directory.  Can be configured to use a delivery queue or not.

       When this module delivers a message:
       If the routing info is 'FAIL!', the message is treated as undeliverable.
       If the routing info is 'fail', the message is treated as temporarily
         undeliverable (and so will eventually time out).
       Otherwise, creates a file in the specified directory, containing
          the routing info, a newline, and the message contents.
    """
    def __init__(self):
        DeliveryModule.__init__(self)

    ## Fields:
    # loc -- The directory to store files in.  All filenames are numbers;
    #    we always put new messages in the smallest number greater than
    #    all existing numbers.
    # next -- the number of the next file.
    def getConfigSyntax(self):
        return { 'Testing/DirectoryDump':
                 { 'Location' : ('REQUIRE', None, None),
                   'UseQueue': ('REQUIRE', "boolean", None),
                   'Retry' : ('ALLOW', "intervalList",
                              "every 1 min for 10 min") } }

    def validateConfig(self, config, lines, contents):
        # loc = sections['Testing/DirectoryDump'].get('Location')
        pass

    def getRetrySchedule(self):
        return self.retry

    def configure(self, config, manager):
        self.loc = config['Testing/DirectoryDump'].get('Location')
        if not self.loc:
            return
        self.useQueue = config['Testing/DirectoryDump']['UseQueue']

        if not os.path.exists(self.loc):
            createPrivateDir(self.loc)

        self.next = 1 + max([-1]+[int(f) for f in os.listdir(self.loc)])

        self.retry = config['Testing/DirectoryDump']['Retry']
        manager.enableModule(self)

    def getServerInfoBlock(self):
        return ""

    def getName(self):
        return "Testing_DirectoryDump"

    def getExitTypes(self):
        return [ 0xFFFE ]

    def createDeliveryQueue(self, queueDir):
        if self.useQueue:
            return SimpleModuleDeliveryQueue(self, queueDir,
                                             retrySchedule=self.retry)
        else:
            return ImmediateDeliveryQueue(self)

    def processMessage(self, packet):
        assert packet.getExitType() == 0xFFFE
        exitInfo = packet.getAddress()

        if exitInfo == 'fail':
            return DELIVER_FAIL_RETRY
        elif exitInfo == 'FAIL!':
            return DELIVER_FAIL_NORETRY

        LOG.debug("Delivering test message")

        m = _escapeMessageForEmail(packet)
        if m is None:
            # Ordinarily, we'd drop corrupt messages, but this module is
            # meant for debugging.
            m = """\
==========CORRUPT OR UNDECODABLE MESSAGE
Decoding handle: %s%s==========MESSAGE ENDS""" % (
                      base64.encodestring(packet.getTag()),
                      base64.encodestring(packet.getContents()))

        f = open(os.path.join(self.loc, str(self.next)), 'w')
        self.next += 1
        f.write(m)
        f.close()
        return DELIVER_OK

#----------------------------------------------------------------------
# mix_mktemp: A secure, paranoid mktemp replacement.  (May be overkill
# for testing, but better safe than sorry.)

# Name of our temporary directory: all temporary files go under this
# directory.  If None, it hasn't been created yet.  If it exists,
# it must be owned by us, mode 700.

_CHECK_MODE = 1
_CHECK_UID = 1
if sys.platform in ('cygwin', 'win32') or os.environ.get("MM_NO_FILE_PARANOIA"):
    _CHECK_MODE = _CHECK_UID = 0

_MM_TESTING_TEMPDIR = None
# How many temporary files have we created so far?
_MM_TESTING_TEMPDIR_COUNTER = 0
# Do we nuke the contents of _MM_TESTING_TEMPDIR on exit?
_MM_TESTING_TEMPDIR_REMOVE_ON_EXIT = 1
def mix_mktemp(extra=""):
    '''mktemp wrapper. puts all files under a securely mktemped
       directory.'''
    global _MM_TESTING_TEMPDIR
    global _MM_TESTING_TEMPDIR_COUNTER
    if _MM_TESTING_TEMPDIR is None:
        # We haven't configured our temporary directory yet.
        import tempfile

        # If tempfile.mkdtemp exists, use it.  This avoids warnings, and
        # is harder for people to exploit.
        if hasattr(tempfile, 'mkdtemp'):
            try:
                temp = tempfile.mkdtemp()
            except OSError, e:
                print "mkdtemp failure: %s" % e
                sys.exit(1)
        else:
        # Otherwise, pick a dirname, make sure it doesn't exist, and try to
        # create it.
            temp = tempfile.mktemp()
            if os.path.exists(temp):
                print "I think somebody's trying to exploit mktemp."
                sys.exit(1)
            try:
                os.mkdir(temp, 0700)
            except OSError, e:
                print "Something's up with mktemp: %s" % e
                sys.exit(1)

        # The directory must exist....
        if not os.path.exists(temp):
            print "Couldn't create temp dir %r" %temp
            sys.exit(1)
        st = os.stat(temp)

        # And be writeable only by us...
        if _CHECK_MODE and st[stat.ST_MODE] & 077:
            print "Couldn't make temp dir %r with secure permissions" %temp
            sys.exit(1)
        # And be owned by us...
        if _CHECK_UID and st[stat.ST_UID] != os.getuid():
            print "The wrong user owns temp dir %r"%temp
            sys.exit(1)

        _MM_TESTING_TEMPDIR = temp
        if _MM_TESTING_TEMPDIR_REMOVE_ON_EXIT:
            import atexit
            atexit.register(deltree, temp)

    # So now we have a temporary directory; return the name of a new
    # file there.
    _MM_TESTING_TEMPDIR_COUNTER += 1
    return os.path.join(_MM_TESTING_TEMPDIR,
                        "tmp%05d%s" % (_MM_TESTING_TEMPDIR_COUNTER,extra))

_WAIT_FOR_KIDS = 1
def deltree(*dirs):
    """Delete each one of a list of directories, along with all of its
       contents."""
    global _WAIT_FOR_KIDS
    #print "deltree(%r)"%dirs
    if _WAIT_FOR_KIDS:
        print "Waiting for shred processes to finish."
        waitForChildren()
        _WAIT_FOR_KIDS = 0
    for d in dirs:
        #print "Considering",d
        if os.path.isdir(d):
            #print "deleting from %s: %s" % (d, os.listdir(d))
            for fn in os.listdir(d):
                loc = os.path.join(d,fn)
                if os.path.isdir(loc):
                    #print "deleting (I)",loc
                    deltree(loc)
                else:
                    #print "unlinking (I)",loc
                    os.unlink(loc)
            #ld = os.listdir(d)
            #if ld: print "remaining in %s: %s" % (d, ld)
            if os.listdir(d):
                print "os.listdir(%r)==(%r)"%(d,os.listdir(d))
            os.rmdir(d)
        elif os.path.exists(d):
            #print "Unlinking", d
            os.unlink(d)
        else:
            pass #print "DNE", d

#----------------------------------------------------------------------
# suspendLog

def suspendLog(severity=None):
    """Temporarily suppress logging output"""
    log = LOG
    if hasattr(log, '_storedHandlers'):
        resumeLog()
    buf = cStringIO.StringIO()
    h = mixminion.Common._ConsoleLogHandler(buf)
    log._storedHandlers = log.handlers
    log._storedSeverity = log.severity
    log._testBuf = buf
    log.handlers = []
    if severity is not None:
        log.setMinSeverity(severity)
    log.addHandler(h)

def resumeLog():
    """Resume logging output.  Return all new log messages since the last
       suspend."""
    log = LOG
    if not hasattr(log, '_storedHandlers'):
        return None
    buf = log._testBuf
    del log._testBuf
    log.handlers = log._storedHandlers
    del log._storedHandlers
    log.setMinSeverity(log._storedSeverity)
    del log._storedSeverity
    return buf.getvalue()

#----------------------------------------------------------------------
# Facilities to temporarily replace attributes and functions for testing

# List of object, attribute, old-value for all replaced attributes.
_REPLACED_OBJECT_STACK = []

def replaceAttribute(object, attribute, value):
    """Temporarily replace <object.attribute> with value.  When
       undoReplacedAttributes() is called, the old value is restored."""
    if hasattr(object, attribute):
        tup = (object, attribute, getattr(object, attribute))
    else:
        tup = (object, attribute)
    _REPLACED_OBJECT_STACK.append(tup)
    setattr(object, attribute, value)

# List of (fnname, args, kwargs) for all the replaced functions that
# have been called.
_CALL_LOG = []

class _ReplacementFunc:
    """Helper object: callable stub that logs its invocations to _CALL_LOG
       and delegates to an internal function."""
    def __init__(self, name, fn=None):
        self.name = name
        self.fn = fn
    def __call__(self, *args, **kwargs):
        _CALL_LOG.append((self.name, args, kwargs))
        if self.fn:
            return self.fn(*args, **kwargs)
        else:
            return None

def replaceFunction(object, attribute, fn=None):
    """Temporarily replace the function or method <object.attribute>.
       If <fn> is provided, replace it with fn; otherwise, the new
       function will just return None.  All invocations of the new
       function will logged, and retrievable by getReplacedFunctionCallLog()"""
    replaceAttribute(object, attribute, _ReplacementFunc(attribute, fn))

def getReplacedFunctionCallLog():
    """Return a list of (functionname, args, kwargs)"""
    return _CALL_LOG

def clearReplacedFunctionCallLog():
    """Clear all entries from the replaced function call log"""
    del _CALL_LOG[:]

def undoReplacedAttributes():
    """Undo all replaceAttribute and replaceFunction calls in effect."""
    # Remember to traverse _REPLACED_OBJECT_STACK in reverse, so that
    # "replace(a,b,c1); replace(a,b,c2)" is properly undone.
    r = _REPLACED_OBJECT_STACK[:]
    r.reverse()
    del _REPLACED_OBJECT_STACK[:]
    for item in r:
        if len(item) == 2:
            o,a = item
            delattr(o,a)
        else:
            o,a,v = item
            setattr(o,a,v)

#----------------------------------------------------------------------
# Test vectors.

class CyclicRNG(mixminion.Crypto.RNG):
    def __init__(self):
        mixminion.Crypto.RNG.__init__(self,4096)
        self.idx = 0
        self.pattern = "".join(map(chr,range(256)))
    def _prng(self,n):
        reps = ceilDiv(n+self.idx,256)
        r = (self.pattern*reps)[self.idx:self.idx+n]
        self.idx = (self.idx+n) % 256
        assert len(r) == n
        return r

def unHexStr(s):
    assert s[0] == '['
    assert s[-1] == ']'
    r = []
    for i in xrange(1,len(s)-1,3):
        r.append(chr(int(s[i:i+2],16)))
        assert s[i+2] in ' ]'
    return "".join(r)

def unHexNum(s):
    assert s[0] == '['
    assert s[-1] == ']'
    r = [  ]
    for i in xrange(1,len(s)-1,3):
        r.append(s[i:i+2])
        assert s[i+2] in ' ]'
    return long("".join(r), 16)

def hexStr(s):
    r = []
    for c in s:
        r.append("%02X"%ord(c))
    return "[%s]"%(" ".join(r))

def hexNum(n):
    hn = "%X"%n
    if len(hn)%2 == 1:
        hn = "0"+hn
    r = []
    for i in xrange(0,len(hn),2):
        r.append(hn[i:i+2])
    return "[%s]"%(" ".join(r))

def tvRSA():
    print "======================================== RSA"
    pk1 = TEST_KEYS_2048[0]
    print "Example 2048-bit Key K"
    n,e = pk1.get_public_key()
    n2,e2,d,p,q = pk1.get_private_key()
    print "   exponent =",hexNum(e)
    print "   modulus =",hexNum(n)
    print "   Private key (P)=",hexNum(p)
    print "   Private key (Q)=",hexNum(q)
    print "   Private key (D)=",hexNum(d)

    print "   PK_Encode(K) =",hexStr(pk1.encode_key(1))
    print "   Fingerprint =",mixminion.Crypto.pk_fingerprint(pk1)

    print
    ms = CyclicRNG().getBytes(20)
    print "OAEP Padding/PKCS encoding example: (Using MGF SEED %s)"%hexStr(ms)
    s = "Hello world"
    print "   original string M:",hexStr(s)
    assert pk1.get_modulus_bytes() == 256
    enc = mixminion.Crypto._add_oaep_padding(s,
                mixminion.Crypto.OAEP_PARAMETER,256,CyclicRNG())
    print "   Padded string (2048 bits):",hexStr(enc)
    pkenc = pk1.crypt(enc,1,1)

    print
    print "   PK_Encrypt(K,M):",hexStr(pkenc)
    assert mixminion.Crypto.pk_decrypt(pkenc,pk1) == s

def tvAES():
    import mixminion._minionlib as _ml
    print "======================================== AES"
    print "Single block encryption"
    k = unHexStr("[00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF]")
    b = "MixminionTypeIII"
    print "   Key:",hexStr(k)
    print "   Plaintext block:",hexStr(b)
    eb = _ml.aes128_block_crypt(_ml.aes_key(k),b,1)
    db = _ml.aes128_block_crypt(_ml.aes_key(k),b,0)
    print "   Encrypted block:",hexStr(eb)
    print "   Decrypted block:",hexStr(db)

    print
    print "Counter mode encryption:"
    k = unHexStr("[02 13 24 35 46 57 68 79 8A 9B AC BD CE DF E0 F1]")
    print "   Key:",hexStr(k)
    print "   Keystream[0x00000...0x0003F]:",hexStr(mixminion.Crypto.prng(k,64))
    print "   Keystream[0x002C0...0x002FF]:",hexStr(mixminion.Crypto.prng(k,64,0x2c0))
    print "   Keystream[0xF0000...0xF003F]:",hexStr(mixminion.Crypto.prng(k,64,0xF0000))

    txt = "Hello world!"
    print "   Example text M:",hexStr(txt)
    print "   Encrypt(K,M):",hexStr(mixminion.Crypto.ctr_crypt(txt,k))

def tvLIONESS():
    print "======================================== LIONESS"
    print "SPRP_Encrypt:"
    ks = mixminion.Crypto.Keyset("basic key")
    k1,k2,k3,k4=ks.getLionessKeys("A")
    print " Base key K:",hexStr(k1)
    print "         K2:",hexStr(k2)
    print "         K3:",hexStr(k3)
    print "         K4:",hexStr(k4)
    txt = "I never believe in code until it's running, and I never believe in the next release until it's out."
    print
    print "      Example text M:",hexStr(txt)
    print "   SPRP_Encrypt(K,M):",hexStr(mixminion.Crypto.lioness_encrypt(
        txt,(k1,k2,k3,k4)))
    print "   SPRP_Decrypt(K,M):",hexStr(mixminion.Crypto.lioness_decrypt(
        txt,(k1,k2,k3,k4)))

def testVectors(name,args):
    assert hexStr("ABCDEFGHI") == "[41 42 43 44 45 46 47 48 49]"
    assert hexNum(10000) == '[27 10]'
    assert hexNum(100000) == '[01 86 A0]'
    assert hexNum(1000000000L) == '[3B 9A CA 00]'

    assert unHexStr(hexStr("ABCDEFGHI")) == "ABCDEFGHI"
    assert unHexNum(hexNum(10000))  in (10000, 10000L)
    assert unHexNum(hexNum(100000)) in (100000,100000L)
    assert unHexNum(hexNum(1000000000L)) == 1000000000L

    tvRSA()
    tvAES()
    tvLIONESS()

#----------------------------------------------------------------------
# Long keypairs: stored here to avoid regenerating them every time we need
# to run tests.  (We can't use 1024-bit keys, since they're not long enough
# to use as identity keys.)

TEST_KEYS_2048 = [
"""\
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""",
]

TEST_KEYS_2048 = [
    mixminion.Crypto.pk_decode_private_key(base64.decodestring(s))
    for s in TEST_KEYS_2048 ]
del s
