## $Id$ 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 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 = ''' %s %s ''' %(target_file_base,target_url) if executable: xml += ' \n' if signature_text: xml += ' \n%s \n'%signature_text else: xml += ' %s\n' % md5_file(target_path) xml += ' %f\n\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='', 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 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 , e.g. '' 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 += ('\n'+ ' %s\n'+ ' %d\n') %( app.name, version_num) if (len(api_version)): xml_doc += ' '+api_version+'\n'; 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 += ' \n' xml_doc += ' %s\n' % url_filename if first: xml_doc += ' \n' else: xml_doc += ' %s\n' % open_name extra = file_ref_infos.get(exec_file) if extra: if not extra.endswith('\n'): extra += '\n' xml_doc += extra xml_doc += ' \n' first = False xml_doc += '\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)