## $Id: tools.py 15957 2008-09-04 12:17:58Z davea $

import configxml
import os, md5, shutil, binascii

# from http://www.plope.com/software/uuidgen/view
_urandomfd = None
def urandom(n):
    """urandom(n) -> str

    Return a string of n random bytes suitable for cryptographic use.

    """
    global _urandomfd
    if _urandomfd is None:
        try:
            _urandomfd = os.open("/dev/urandom", os.O_RDONLY)
        except:
            _urandomfd = NotImplementedError
    if _urandomfd is NotImplementedError:
        raise NotImplementedError("/dev/urandom (or equivalent) not found")
    bytes = ""
    while len(bytes) < n:
        bytes += os.read(_urandomfd, n - len(bytes))
    return bytes

def make_uuid():
    return binascii.hexlify(urandom(16))

def md5_file(path):
    """Return a 16-digit MD5 hex digest of a file's contents"""
    return md5.new(open(path).read()).hexdigest()

def file_size(path):
    """Return the size of a file"""
    f = open(path)
    f.seek(0,2)
    return f.tell()

def get_kludge_open_name(filename):
    """return part before '=' (or entire filename if none)"""
    assert ('/' not in filename)
    return filename.split('=')[0]

def get_kludge_url_filename(filename):
    """return part after '=' (or entire filename if none)"""
    assert ('/' not in filename)
    if '=' in filename:
        return filename.split('=',1)[1]
    else:
        return filename

def query_sign_executable(executable_path):
    print '''\

SECURITY WARNING:
=================

You have not provided a signature file for %s.

I can generate one now, but this is highly unrecommended.  Generating code
signatures on network-connected computers is a security vulnerability, and
should not be done for publicly-accessable projects.
''' \
        % executable_path

    if not query_noyes('Continue with automatically generating a code signature?'):
        raise SystemExit

def sign_executable(executable_path, quiet=False):
    '''Returns signed text for executable'''
    config = configxml.default_config()
    if not quiet:
        query_sign_executable(executable_path)
        print 'Signing', executable_path
    code_sign_key = os.path.join(config.config.key_dir, 'code_sign_private')

    # sign_executable could be in bin/ or ../tool/, depending on
    # whether this is a test or an upgrade

    sign_executable_path = 'bin/sign_executable'
    if not os.path.exists(sign_executable_path):
        sign_executable_path = '../tools/sign_executable'
        if not os.path.exists(sign_executable_path):
            print os.getcwd()
            raise SystemExit("sign_executable not found! did you `make' it?")
    signature_text = os.popen('%s %s %s'%(sign_executable_path,
        executable_path,code_sign_key)).read()
    if not signature_text:
        raise SystemExit("Couldn't sign executable %s"%executable_path)
    return signature_text

def process_app_file(file, signature_text=None, quiet=False, executable=True):
    '''Handle a new executable (or non-executable) file to be added to the
    database.

    0. target filename is url_filename as described in process_app_version
    1. Copy file to download_dir if necessary.
    2. Return <file_info> XML.
        - if signature_text specified, use it
        - if no signature_text specified, generate md5sum.
    '''

    config = configxml.default_config()

    source_dir, source_file_base = os.path.split(file)
    target_file_base = get_kludge_url_filename(source_file_base)
    target_path = os.path.join(config.config.download_dir, target_file_base)
    target_url = os.path.join(config.config.download_url, target_file_base)
    if file != target_path:
        if not quiet:
            print "Copying %s to %s"%(source_file_base, target_path)
        shutil.copy(file, target_path)

    xml = '''<file_info>
    <name>%s</name>
    <url>%s</url>
''' %(target_file_base,target_url)
    if executable:
        xml += '    <executable/>\n'
    if signature_text:
        if signature_text.find('<signatures>') >= 0:
            xml += signature_text
        else:
            xml += '    <file_signature>\n%s    </file_signature>\n'%signature_text
    else:
        xml += '    <md5_cksum>%s</md5_cksum>\n' % md5_file(target_path)

    xml += '    <nbytes>%f</nbytes>\n</file_info>\n' % file_size(target_path)
    return xml

def process_app_version(
    app, version_num, exec_files,
    non_exec_files=[],
    signature_files={},
    file_ref_infos={},
    api_version='',
    extra_xml='',
    quiet=False
    ):
    """Return xml for application version

    app             is an instance of database.App

    version_num     is an integer such as 102 for version 1.02

    exec_files      is a list of full-path executables.
                    exec_file[0] (the first one) is the <main_program/>

    non_exec_files  is a list of full-path non-executables.

    signature_files is a dictionary of exec_file -> signature file mappings.
                    process_app_version() will generate a new signature for
                    any files that don't have one given already.

                    NOTE: using the feature of generating signature files on
                    the same machine (requiring having the private key stored
                    on this machine) is a SECURITY RISK (since this machine
                    probably has network visibility)!

    file_ref_infos  is a dictionary mapping exec_file -> extra XML strings to
                    include in <file_info>, e.g. '<copy_file/>'

    exec_files[1:] and non_exec_files should be named like
    'open_name=url_filename'.
    (url_filename is the basename of file as copied to download/)
    If there is no '=', then the entire filename is used as
    both the open_name and the filename.

    """
    assert(exec_files)
    xml_doc = ''
    for exec_file in exec_files:
        signature_file = signature_files.get(exec_file)
        if signature_file:
            if not quiet: print 'Using signature file', signature_file
            signature_text = open(signature_file).read()
        else:
            signature_text = sign_executable(exec_file, quiet=quiet)
        xml_doc += process_app_file(exec_file, signature_text, quiet=quiet)

    for non_exec_file in non_exec_files:
        signature_file = signature_files.get(non_exec_file)
        if signature_file:
            if not quiet: print 'Using signature file', signature_file
            signature_text = open(signature_file).read()
        else:
            signature_text = sign_executable(non_exec_file, quiet=quiet)
        xml_doc += process_app_file(
            non_exec_file, signature_text=signature_text,
            executable=False, quiet=quiet)

    xml_doc += ('<app_version>\n'+
                     '    <app_name>%s</app_name>\n'+
                     '    <version_num>%d</version_num>\n') %(
        app.name, version_num)

    if (len(api_version)):
        xml_doc += '    <api_version>'+api_version+'</api_version>\n'
    if (len(extra_xml)):
        xml_doc += extra_xml

    first = True
    for exec_file in exec_files + non_exec_files:
        file_base = os.path.basename(exec_file)
        open_name = get_kludge_open_name(file_base)
        url_filename = get_kludge_url_filename(file_base)
        xml_doc += '    <file_ref>\n'
        xml_doc += '       <file_name>%s</file_name>\n' % url_filename
        if first:
            xml_doc += '       <main_program/>\n'
        else:
            xml_doc += '       <open_name>%s</open_name>\n' % open_name
        extra = file_ref_infos.get(exec_file)
        if extra:
            if not extra.endswith('\n'):
                extra += '\n'
            xml_doc += extra
        xml_doc += '    </file_ref>\n'
        first = False

    xml_doc += '</app_version>\n'
    return xml_doc

def query_yesno(str):
    '''Query user; default Yes'''
    print str, "[Y/n] ",
    return not raw_input().strip().lower().startswith('n')

def query_noyes(str):
    '''Query user; default No'''
    print str, "[y/N] ",
    return raw_input().strip().lower().startswith('y')

def get_output_file_path(filename):
    """ Return the filename's path in the upload directory
        Use this if you're developing a validator/assimilator in Python
    """
    config = configxml.default_config()
    fanout = long(config.config.uldl_dir_fanout)
    s = md5.new(filename).hexdigest()[1:8]
    x = long(s, 16)
    return "%s/%x/%s" % (config.config.upload_dir, x % fanout, filename) 
