Onionr/onionr/static-data/default-plugins/pluginmanager/main.py

526 lines
19 KiB
Python
Raw Normal View History

'''
This is the future Onionr plugin manager. TODO: Add better description.
'''
# useful libraries
import logger, config
2018-05-14 04:11:31 +00:00
import os, sys, json, time, random, shutil, base64, getpass, datetime, re
2018-04-23 03:42:37 +00:00
plugin_name = 'pluginmanager'
2018-05-14 04:11:31 +00:00
keys_data = {'keys' : {}, 'plugins' : [], 'repositories' : {}}
2018-04-23 03:42:37 +00:00
# key functions
def writeKeys():
'''
Serializes and writes the keystore in memory to file
'''
file = open(keys_file, 'w')
file.write(json.dumps(keys_data, indent=4, sort_keys=True))
file.close()
def readKeys():
'''
Loads the keystore into memory
'''
global keys_data
keys_data = json.loads(open(keys_file).read())
return keys_data
def getKey(plugin):
'''
Returns the public key for a given plugin
'''
2018-05-14 04:11:31 +00:00
global keys_data
2018-04-23 03:42:37 +00:00
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
'''
2018-05-14 04:11:31 +00:00
global keys_data
readKeys()
2018-04-23 03:42:37 +00:00
keys_data['keys'][plugin] = key
writeKeys()
2018-05-14 04:11:31 +00:00
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(repositories, data):
'''
Saves the plugin name, to remember that it was installed by the pluginmanager
'''
global keys_data
readKeys()
keys_data['repositories'][repositories] = data
writeKeys()
def removeRepository(repositories):
'''
Removes the plugin name from the pluginmanager's records
'''
global keys_data
readKeys()
if plugin in keys_data['repositories']:
del keys_data['repositories'][repositories]
writeKeys()
2018-04-23 03:42:37 +00:00
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
2018-05-14 04:11:31 +00:00
def sanitize(name):
return re.sub('[^0-9a-zA-Z]+', '', str(name).lower())[:255]
def blockToPlugin(block):
try:
blockContent = pluginapi.get_core().getData(block)
blockContent = blockContent[blockContent.rfind(b'\n') + 1:].decode()
blockContent = json.loads(blockContent)
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)
return True
except Exception as e:
logger.error('Failed to install plugin.', error = e, timestamp = False)
return False
def pluginToBlock(plugin, import_block = True):
try:
2018-05-14 04:11:31 +00:00
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 = base64.b64encode(open(zipfile, 'rb').read())
2018-05-14 04:11:31 +00:00
author = getpass.getuser()
description = 'Default plugin description'
2018-05-14 04:11:31 +00:00
info = {"name" : plugin}
try:
if os.path.exists(directory + 'info.json'):
info = json.loads(open(directory + 'info.json').read())
if 'author' in info:
author = info['author']
if 'description' in info:
description = info['description']
2018-05-14 04:11:31 +00:00
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}
hash = pluginapi.get_core().insertBlock(json.dumps(metadata), header = 'plugin', sign = True)
if import_block:
pluginapi.get_utils().importNewBlocks()
return hash
else:
logger.error('Plugin %s does not exist.' % plugin)
except Exception as e:
logger.error('Failed to convert plugin to block.', error = e, timestamp = False)
return False
def parseBlock(hash, key):# deal with block metadata
blockContent = pluginapi.get_core().getData(hash)
try:
2018-05-13 06:37:47 +00:00
blockMetadata = json.loads(blockContent[:blockContent.decode().find('\n')].decode())
try:
blockMeta2 = json.loads(blockMetadata['meta'])
except KeyError:
blockMeta2 = {'type': ''}
pass
2018-05-13 06:37:47 +00:00
blockContent = blockContent[blockContent.rfind(b'\n') + 1:]
try:
blockContent = blockContent.decode()
except AttributeError:
pass
if not pluginapi.get_crypto().verifyPow(blockContent, blockMeta2):
logger.debug("(pluginmanager): %s has invalid or insufficient proof of work" % str(hash))
return False
2018-05-14 04:11:31 +00:00
if not (('sig' in blockMetadata)): # and ('id' in blockMeta2)
logger.debug('(pluginmanager): %s is missing required parameters' % hash)
return False
else:
2018-05-14 04:11:31 +00:00
if pluginapi.get_crypto().edVerify(blockMetadata['meta'] + '\n' + blockContent, key, blockMetadata['sig'], encodedData=True):
# logger.debug('(pluginmanager): %s was signed' % str(hash))
return True
else:
2018-05-14 04:11:31 +00:00
# logger.debug('(pluginmanager): %s has an invalid signature' % str(hash))
return False
except json.decoder.JSONDecodeError as e:
logger.error('(pluginmanager): Could not decode block metadata.', error = e, timestamp = False)
return False
2018-05-14 04:11:31 +00:00
def installBlock(block):
try:
blockContent = pluginapi.get_core().getData(block)
blockContent = blockContent[blockContent.rfind(b'\n') + 1:].decode()
blockContent = json.loads(blockContent)
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))
# 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)
addPlugin(name)
else:
logger.info('Installation cancelled.')
return False
return True
except Exception as e:
logger.error('Failed to install plugin.', error = e, timestamp = False)
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)
return False
default = 'y'
if not installedByPluginManager:
logger.warn('The plugin %s was not installed by %s.' % (plugin, plugin_name), timestamp = False)
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)
return True
else:
logger.info('Uninstallation cancelled.')
except Exception as e:
logger.error('Failed to uninstall plugin.', error = e)
return False
2018-04-23 03:42:37 +00:00
# command handlers
def help():
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' <plugin> [public key/block hash]')
2018-05-14 04:11:31 +00:00
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' <plugin> [public key/block hash]')
2018-04-23 03:42:37 +00:00
def commandInstallPlugin():
if len(sys.argv) >= 3:
check()
pluginname = sys.argv[2]
pkobh = None # public key or block hash
2018-05-14 04:11:31 +00:00
version = None
if ':' in pluginname:
details = pluginname
pluginname = sanitize(details[0])
version = details[1]
sanitize(pluginname)
2018-04-23 03:42:37 +00:00
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:
2018-05-14 04:11:31 +00:00
# still nothing found, try searching repositories
logger.info('Searching for public key in repositories...')
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))
distributors.append(records[pluginname])
if len(distributors) != 0:
distributor = None
if len(distributors) == 1:
logger.info('Found distributor: %s' % distributors[0])
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))
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))
try:
if int(choice) < index and int(choice) >= 1:
distributor = distributors[int(choice)]
valid = True
except KeyboardInterrupt:
logger.info('Installation cancelled.')
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)
logger.error('asdf', error = e, timestamp = False)
if pkobh is None:
logger.error('No key for this plugin found in keystore or repositories, please specify.')
2018-04-23 03:42:37 +00:00
help()
return True
valid_hash = pluginapi.get_utils().validateHash(pkobh)
real_block = False
valid_key = pluginapi.get_utils().validatePubKey(pkobh)
real_key = False
if valid_hash:
real_block = pluginapi.get_utils().hasBlock(pkobh)
elif valid_key:
real_key = pluginapi.get_utils().hasKey(pkobh)
blockhash = None
if valid_hash and not real_block:
logger.error('Block hash not found. Perhaps it has not been synced yet?')
logger.debug('Is valid hash, but does not belong to a known block.')
2018-05-14 04:11:31 +00:00
2018-04-23 03:42:37 +00:00
return True
elif valid_hash and real_block:
blockhash = str(pkobh)
2018-05-02 06:22:40 +00:00
logger.debug('Using block %s...' % blockhash)
2018-05-11 05:18:39 +00:00
installBlock(blockhash)
2018-04-23 03:42:37 +00:00
elif valid_key and not real_key:
logger.error('Public key not found. Try adding the node by address manually, if possible.')
logger.debug('Is valid key, but the key is not a known one.')
elif valid_key and real_key:
publickey = str(pkobh)
2018-05-02 06:22:40 +00:00
logger.debug('Using public key %s...' % publickey)
2018-04-23 03:42:37 +00:00
saveKey(pluginname, pkobh)
2018-05-11 05:18:39 +00:00
blocks = pluginapi.get_core().getBlocksByType('plugin')
2018-05-14 04:11:31 +00:00
signedBlocks = list()
for hash in blocks:
if parseBlock(hash, publickey):
2018-05-14 04:11:31 +00:00
signedBlocks.append(hash)
mostRecentTimestamp = None
mostRecentVersionBlock = None
for hash in signedBlocks:
try:
blockContent = pluginapi.get_core().getData(hash)
blockContent = blockContent[blockContent.rfind(b'\n') + 1:].decode()
blockContent = json.loads(blockContent)
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.' % hash)
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 = hash
break
elif mostRecentTimestamp is None:
mostRecentTimestamp = blockDatetime
mostRecentVersionBlock = hash
elif blockDatetime > mostRecentTimestamp:
mostRecentTimestamp = blockDatetime
mostRecentVersionBlock = hash
except Exception as e:
pass
logger.warn('Only continue the installation is you are absolutely certain that you trust the plugin distributor. Public key of plugin distributor: %s' % publickey, timestamp = False)
installBlock(mostRecentVersionBlock)
2018-04-23 03:42:37 +00:00
else:
2018-05-02 06:22:40 +00:00
logger.error('Unknown data "%s"; must be public key or block hash.' % str(pkobh))
2018-04-23 03:42:37 +00:00
return
else:
2018-05-14 04:11:31 +00:00
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' <plugin> [public key/block hash]')
2018-04-23 03:42:37 +00:00
return True
2018-04-23 03:42:37 +00:00
def commandUninstallPlugin():
2018-05-14 04:11:31 +00:00
if len(sys.argv) >= 3:
uninstallPlugin(sys.argv[2])
else:
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' <plugin>')
return True
2018-04-23 03:42:37 +00:00
def commandSearchPlugin():
logger.info('This feature has not been created yet. Please check back later.')
2018-05-14 04:11:31 +00:00
return True
def commandAddRepository():
logger.info('This feature has not been created yet. Please check back later.')
return True
def commandRemoveRepository():
logger.info('This feature has not been created yet. Please check back later.')
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)
else:
logger.error('Plugin %s does not exist.' % pluginname, timestamp = False)
else:
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' <plugin>')
# event listeners
def on_init(api, data = None):
global pluginapi
pluginapi = api
2018-04-23 03:42:37 +00:00
check()
# register some commands
2018-04-23 03:42:37 +00:00
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)
2018-05-14 04:11:31 +00:00
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)
# add help menus once the features are actually implemented
return