''' Onionr - Private P2P Communication This plugin acts as a plugin manager, and allows the user to install other plugins distributed over Onionr. ''' ''' This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ''' # useful libraries import logger, config import os, sys, json, time, random, shutil, base64, getpass, datetime, re from onionrblockapi import Block from onionrutils import importnewblocks, stringvalidators plugin_name = 'pluginmanager' keys_data = {'keys' : {}, 'plugins' : [], 'repositories' : {}} # key functions def writeKeys(): ''' Serializes and writes the keystore in memory to file ''' with open(keys_file, 'w') as file: file.write(json.dumps(keys_data, indent=4, sort_keys=True)) file.close() def readKeys(): ''' Loads the keystore into memory ''' global keys_data with open(keys_file) as file: keys_data = json.loads(file.read()) return keys_data def getKey(plugin): ''' Returns the public key for a given plugin ''' global keys_data readKeys() return (keys_data['keys'][plugin] if plugin in keys_data['keys'] else None) def saveKey(plugin, key): ''' Saves the public key for a plugin to keystore ''' global keys_data readKeys() keys_data['keys'][plugin] = key writeKeys() def getPlugins(): ''' Returns a list of plugins installed by the plugin manager ''' global keys_data readKeys() return keys_data['plugins'] def addPlugin(plugin): ''' Saves the plugin name, to remember that it was installed by the pluginmanager ''' global keys_data readKeys() if not plugin in keys_data['plugins']: keys_data['plugins'].append(plugin) writeKeys() def removePlugin(plugin): ''' Removes the plugin name from the pluginmanager's records ''' global keys_data readKeys() if plugin in keys_data['plugins']: keys_data['plugins'].remove(plugin) writeKeys() def getRepositories(): ''' Returns a list of plugins installed by the plugin manager ''' global keys_data readKeys() return keys_data['repositories'] def addRepository(blockhash, data): ''' Saves the plugin name, to remember that it was installed by the pluginmanager ''' global keys_data readKeys() keys_data['repositories'][blockhash] = data writeKeys() def removeRepository(blockhash): ''' Removes the plugin name from the pluginmanager's records ''' global keys_data readKeys() if blockhash in keys_data['repositories']: del keys_data['repositories'][blockhash] writeKeys() def createRepository(plugins): contents = {'plugins' : plugins, 'author' : getpass.getuser(), 'compiled-by' : plugin_name} block = Block(core = pluginapi.get_core()) block.setType('repository') block.setContent(json.dumps(contents)) return block.save(True) def check(): ''' Checks to make sure the keystore file still exists ''' global keys_file keys_file = pluginapi.plugins.get_data_folder(plugin_name) + 'keystore.json' if not os.path.isfile(keys_file): writeKeys() # plugin management def sanitize(name): return re.sub('[^0-9a-zA-Z_]+', '', str(name).lower())[:255] def blockToPlugin(block): try: block = Block(block, core = pluginapi.get_core()) blockContent = json.loads(block.getContent()) name = sanitize(blockContent['name']) author = blockContent['author'] date = blockContent['date'] version = None if 'version' in blockContent['info']: version = blockContent['info']['version'] content = base64.b64decode(blockContent['content'].encode()) source = pluginapi.plugins.get_data_folder(plugin_name) + 'plugin.zip' destination = pluginapi.plugins.get_folder(name) with open(source, 'wb') as f: f.write(content) if os.path.exists(destination) and not os.path.isfile(destination): shutil.rmtree(destination) shutil.unpack_archive(source, destination) pluginapi.plugins.enable(name) logger.info('Installation of %s complete.' % name, terminal=True) return True except Exception as e: logger.error('Failed to install plugin.', error = e, timestamp = False, terminal=True) return False def pluginToBlock(plugin, import_block = True): try: plugin = sanitize(plugin) directory = pluginapi.get_pluginapi().get_folder(plugin) data_directory = pluginapi.get_pluginapi().get_data_folder(plugin) zipfile = pluginapi.get_pluginapi().get_data_folder(plugin_name) + 'plugin.zip' if os.path.exists(directory) and not os.path.isfile(directory): if os.path.exists(data_directory) and not os.path.isfile(data_directory): shutil.rmtree(data_directory) if os.path.exists(zipfile) and os.path.isfile(zipfile): os.remove(zipfile) if os.path.exists(directory + '__pycache__') and not os.path.isfile(directory + '__pycache__'): shutil.rmtree(directory + '__pycache__') shutil.make_archive(zipfile[:-4], 'zip', directory) data = '' with open(zipfile, 'rb') as file: data = base64.b64encode(file.read()) author = getpass.getuser() description = 'Default plugin description' info = {"name" : plugin} try: if os.path.exists(directory + 'info.json'): info = '' with open(directory + 'info.json').read() as file: info = json.loads(file.read()) if 'author' in info: author = info['author'] if 'description' in info: description = info['description'] except: pass metadata = { 'author' : author, 'date' : str(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')), 'name' : plugin, 'info' : info, 'compiled-by' : plugin_name, 'content' : data.decode('utf-8'), 'description' : description } block = Block(core = pluginapi.get_core()) block.setType('plugin') block.setContent(json.dumps(metadata)) hash = block.save(True) # hash = pluginapi.get_core().insertBlock(, header = 'plugin', sign = True) if import_block: importnewblocks.import_new_blocks(pluginapi.get_core()) return hash else: logger.error('Plugin %s does not exist.' % plugin, terminal=True) except Exception as e: logger.error('Failed to convert plugin to block.', error = e, timestamp = False, terminal=True) return False def installBlock(block): try: block = Block(block, core = pluginapi.get_core()) print(block.getContent()) blockContent = json.loads(block.getContent()) name = sanitize(blockContent['name']) author = blockContent['author'] date = blockContent['date'] version = None if 'version' in blockContent['info']: version = blockContent['info']['version'] install = False logger.info(('Will install %s' + (' v' + version if not version is None else '') + ' (%s), by %s') % (name, date, author), terminal=True) # TODO: Convert to single line if statement if os.path.exists(pluginapi.plugins.get_folder(name)): install = logger.confirm(message = 'Continue with installation (will overwrite existing plugin) %s?') else: install = logger.confirm(message = 'Continue with installation %s?') if install: blockToPlugin(block.getHash()) addPlugin(name) else: logger.info('Installation cancelled.', terminal=True) return False return True except Exception as e: logger.error('Failed to install plugin.', error = e, timestamp = False, terminal=True) return False def uninstallPlugin(plugin): try: plugin = sanitize(plugin) pluginFolder = pluginapi.plugins.get_folder(plugin) exists = (os.path.exists(pluginFolder) and not os.path.isfile(pluginFolder)) installedByPluginManager = plugin in getPlugins() remove = False if not exists: logger.warn('Plugin %s does not exist.' % plugin, timestamp = False, terminal=True) return False default = 'y' if not installedByPluginManager: logger.warn('The plugin %s was not installed by %s.' % (plugin, plugin_name), timestamp = False, terminal=True) default = 'n' remove = logger.confirm(message = 'All plugin data will be lost. Are you sure you want to proceed %s?', default = default) if remove: if installedByPluginManager: removePlugin(plugin) pluginapi.plugins.disable(plugin) shutil.rmtree(pluginFolder) logger.info('Uninstallation of %s complete.' % plugin, terminal=True) return True else: logger.info('Uninstallation cancelled.') except Exception as e: logger.error('Failed to uninstall plugin.', error = e, terminal=True) return False # command handlers def help(): logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [public key/block hash]', terminal=True) logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [public key/block hash]', terminal=True) def commandInstallPlugin(): if len(sys.argv) >= 3: check() pluginname = sys.argv[2] pkobh = None # public key or block hash version = None if ':' in pluginname: details = pluginname pluginname = sanitize(details[0]) version = details[1] sanitize(pluginname) if len(sys.argv) >= 4: # public key or block hash specified pkobh = sys.argv[3] else: # none specified, check if in config file pkobh = getKey(pluginname) if pkobh is None: # still nothing found, try searching repositories logger.info('Searching for public key in repositories...', terminal=True) try: repos = getRepositories() distributors = list() for repo, records in repos.items(): if pluginname in records: logger.debug('Found %s in repository %s for plugin %s.' % (records[pluginname], repo, pluginname), terminal=True) distributors.append(records[pluginname]) if len(distributors) != 0: distributor = None if len(distributors) == 1: logger.info('Found distributor: %s' % distributors[0], terminal=True) distributor = distributors[0] else: distributors_message = '' index = 1 for dist in distributors: distributors_message += ' ' + logger.colors.bold + str(index) + ') ' + logger.colors.reset + str(dist) + '\n' index += 1 logger.info((logger.colors.bold + 'Found distributors (%s):' + logger.colors.reset + '\n' + distributors_message) % len(distributors), terminal=True) valid = False while not valid: choice = logger.readline('Select the number of the key to use, from 1 to %s, or press Ctrl+C to cancel:' % (index - 1), terminal=True) try: choice = int(choice) if choice <= index and choice >= 1: distributor = distributors[int(choice)] valid = True except KeyboardInterrupt: logger.info('Installation cancelled.', terminal=True) return True except: pass if not distributor is None: pkobh = distributor except Exception as e: logger.warn('Failed to lookup plugin in repositories.', timestamp = False, terminal=True) return True if pkobh is None: logger.error('No key for this plugin found in keystore or repositories, please specify.', timestamp = False, terminal=True) return True valid_hash = stringvalidators.validate_hash(pkobh) real_block = False valid_key = stringvalidators.validate_pub_key(pkobh) real_key = False if valid_hash: real_block = Block.exists(pkobh) elif valid_key: real_key = pkobh in pluginapi.get_core().listPeers() blockhash = None if valid_hash and not real_block: logger.error('Block hash not found. Perhaps it has not been synced yet?', timestamp = False, terminal=True) logger.debug('Is valid hash, but does not belong to a known block.', terminal=True) return True elif valid_hash and real_block: blockhash = str(pkobh) logger.debug('Using block %s...' % blockhash, terminal=True) installBlock(blockhash) elif valid_key and not real_key: logger.error('Public key not found. Try adding the node by address manually, if possible.', timestamp = False, terminal=True) logger.debug('Is valid key, but the key is not a known one.', terminal=True) elif valid_key and real_key: publickey = str(pkobh) logger.debug('Using public key %s...' % publickey, terminal=True) saveKey(pluginname, pkobh) signedBlocks = Block.getBlocks(type = 'plugin', signed = True, signer = publickey) mostRecentTimestamp = None mostRecentVersionBlock = None for block in signedBlocks: try: blockContent = json.loads(block.getContent()) if not (('author' in blockContent) and ('info' in blockContent) and ('date' in blockContent) and ('name' in blockContent)): raise ValueError('Missing required parameter `date` in block %s.' % block.getHash()) blockDatetime = datetime.datetime.strptime(blockContent['date'], '%Y-%m-%d %H:%M:%S') if blockContent['name'] == pluginname: if ('version' in blockContent['info']) and (blockContent['info']['version'] == version) and (not version is None): mostRecentTimestamp = blockDatetime mostRecentVersionBlock = block.getHash() break elif mostRecentTimestamp is None: mostRecentTimestamp = blockDatetime mostRecentVersionBlock = block.getHash() elif blockDatetime > mostRecentTimestamp: mostRecentTimestamp = blockDatetime mostRecentVersionBlock = block.getHash() except Exception as e: pass logger.warn('Only continue the installation if you are absolutely certain that you trust the plugin distributor. Public key of plugin distributor: %s' % publickey, timestamp = False, terminal=True) logger.debug('Most recent block matching parameters is %s' % mostRecentVersionBlock, terminal=True) installBlock(mostRecentVersionBlock) else: logger.error('Unknown data "%s"; must be public key or block hash.' % str(pkobh), timestamp = False, terminal=True) return else: logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [public key/block hash]', terminal=True) return True def commandUninstallPlugin(): if len(sys.argv) >= 3: uninstallPlugin(sys.argv[2]) else: logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' ', terminal=True) return True def commandSearchPlugin(): logger.info('This feature has not been created yet. Please check back later.', terminal=True) return True def commandAddRepository(): if len(sys.argv) >= 3: check() blockhash = sys.argv[2] if stringvalidators.validate_hash(blockhash): if Block.exists(blockhash): try: blockContent = json.loads(Block(blockhash, core = pluginapi.get_core()).getContent()) pluginslist = dict() for pluginname, distributor in blockContent['plugins']: if stringvalidators.validate_pub_key(distributor): pluginslist[pluginname] = distributor logger.debug('Found %s records in repository.' % len(pluginslist), terminal=True) if len(pluginslist) != 0: addRepository(blockhash, pluginslist) logger.info('Successfully added repository.', terminal=True) else: logger.error('Repository contains no records, not importing.', timestamp = False, terminal=True) except Exception as e: logger.error('Failed to parse block.', error = e, terminal=True) else: logger.error('Block hash not found. Perhaps it has not been synced yet?', timestamp = False, terminal=True) logger.debug('Is valid hash, but does not belong to a known block.') else: logger.error('Unknown data "%s"; must be block hash.' % str(pkobh), timestamp = False, terminal=True) else: logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [block hash]', terminal=True) return True def commandRemoveRepository(): if len(sys.argv) >= 3: check() blockhash = sys.argv[2] if stringvalidators.validate_hash(blockhash): if blockhash in getRepositories(): try: removeRepository(blockhash) logger.info('Successfully removed repository.', terminal=True) except Exception as e: logger.error('Failed to parse block.', error = e, terminal=True) else: logger.error('Repository has not been imported, nothing to remove.', timestamp = False, terminal=True) else: logger.error('Unknown data "%s"; must be block hash.' % str(pkobh), terminal=True) else: logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [block hash]', terminal=True) return True def commandPublishPlugin(): if len(sys.argv) >= 3: check() pluginname = sanitize(sys.argv[2]) pluginfolder = pluginapi.plugins.get_folder(pluginname) if os.path.exists(pluginfolder) and not os.path.isfile(pluginfolder): block = pluginToBlock(pluginname) logger.info('Plugin saved in block %s.' % block, terminal=True) else: logger.error('Plugin %s does not exist.' % pluginname, timestamp = False, terminal=True) else: logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' ', terminal=True) def commandCreateRepository(): if len(sys.argv) >= 3: check() plugins = list() script = sys.argv[0] del sys.argv[:2] success = True for pluginname in sys.argv: distributor = None if ':' in pluginname: split = pluginname.split(':') pluginname = split[0] distributor = split[1] pluginname = sanitize(pluginname) if distributor is None: distributor = getKey(pluginname) if distributor is None: logger.error('No distributor key was found for the plugin %s.' % pluginname, timestamp = False, terminal=True) success = False plugins.append([pluginname, distributor]) if not success: logger.error('Please correct the above errors, then recreate the repository.', terminal=True) return True blockhash = createRepository(plugins) if not blockhash is None: logger.info('Successfully created repository. Execute the following command to add the repository:\n ' + logger.colors.underline + '%s --add-repository %s' % (script, blockhash), terminal=True) else: logger.error('Failed to create repository, an unknown error occurred.', terminal=True) else: logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [plugins...]', terminal=True) return True # event listeners def on_init(api, data = None): global pluginapi pluginapi = api check() # register some commands api.commands.register(['install-plugin', 'installplugin', 'plugin-install', 'install', 'plugininstall'], commandInstallPlugin) api.commands.register(['remove-plugin', 'removeplugin', 'plugin-remove', 'uninstall-plugin', 'uninstallplugin', 'plugin-uninstall', 'uninstall', 'remove', 'pluginremove'], commandUninstallPlugin) api.commands.register(['search', 'filter-plugins', 'search-plugins', 'searchplugins', 'search-plugin', 'searchplugin', 'findplugin', 'find-plugin', 'filterplugin', 'plugin-search', 'pluginsearch'], commandSearchPlugin) api.commands.register(['add-repo', 'add-repository', 'addrepo', 'addrepository', 'repository-add', 'repo-add', 'repoadd', 'addrepository', 'add-plugin-repository', 'add-plugin-repo', 'add-pluginrepo', 'add-pluginrepository', 'addpluginrepo', 'addpluginrepository'], commandAddRepository) api.commands.register(['remove-repo', 'remove-repository', 'removerepo', 'removerepository', 'repository-remove', 'repo-remove', 'reporemove', 'removerepository', 'remove-plugin-repository', 'remove-plugin-repo', 'remove-pluginrepo', 'remove-pluginrepository', 'removepluginrepo', 'removepluginrepository', 'rm-repo', 'rm-repository', 'rmrepo', 'rmrepository', 'repository-rm', 'repo-rm', 'reporm', 'rmrepository', 'rm-plugin-repository', 'rm-plugin-repo', 'rm-pluginrepo', 'rm-pluginrepository', 'rmpluginrepo', 'rmpluginrepository'], commandRemoveRepository) api.commands.register(['publish-plugin', 'plugin-publish', 'publishplugin', 'pluginpublish', 'publish'], commandPublishPlugin) api.commands.register(['create-repository', 'create-repo', 'createrepo', 'createrepository', 'repocreate'], commandCreateRepository) # add help menus once the features are actually implemented return