From ab17e0d198ce71257aa0cd45a6023dad93b883d8 Mon Sep 17 00:00:00 2001 From: Arinerron Date: Fri, 2 Mar 2018 20:19:01 -0800 Subject: [PATCH] Add plugin support --- onionr/api.py | 8 +- onionr/communicator.py | 26 ++++- onionr/config.py | 1 + onionr/onionr.py | 86 +++++++++++++-- onionr/onionrevents.py | 53 +++++++++ onionr/onionrplugins.py | 231 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 391 insertions(+), 14 deletions(-) create mode 100644 onionr/onionrevents.py create mode 100644 onionr/onionrplugins.py diff --git a/onionr/api.py b/onionr/api.py index 7550f1c7..229df87e 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -181,9 +181,13 @@ class API: return resp if not os.environ.get("WERKZEUG_RUN_MAIN") == "true": logger.info('Starting client on ' + self.host + ':' + str(bindPort) + '...') - #logger.debug('Client token: ' + logger.colors.underline + self.clientToken) - app.run(host=self.host, port=bindPort, debug=True, threaded=True) + try: + app.run(host=self.host, port=bindPort, debug=True, threaded=True) + except Exception as e: + logger.error(str(e)) + logger.fatal('Failed to start client on ' + self.host + ':' + str(bindPort) + ', exiting...') + exit(1) def validateHost(self, hostType): ''' diff --git a/onionr/communicator.py b/onionr/communicator.py index 56d3bbd8..802a6ab6 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -29,12 +29,19 @@ class OnionrCommunicate: This class handles communication with nodes in the Onionr network. ''' + self._core = core.Core() self._utils = onionrutils.OnionrUtils(self._core) self._crypto = onionrcrypto.OnionrCrypto(self._core) - logger.info('Starting Bitcoin Node... with Tor socks port:' + str(sys.argv[2])) - self.bitcoin = btc.OnionrBTC(torP=int(sys.argv[2])) - logger.info('Bitcoin Node started, on block: ' + self.bitcoin.node.getBlockHash(self.bitcoin.node.getLastBlockHeight())) + + try: + logger.info('Starting Bitcoin Node... with Tor socks port:' + str(sys.argv[2])) + self.bitcoin = btc.OnionrBTC(torP=int(sys.argv[2])) + logger.info('Bitcoin Node started, on block: ' + self.bitcoin.node.getBlockHash(self.bitcoin.node.getLastBlockHeight())) + except: + logger.fatal('Failed to start Bitcoin Node, exiting...') + exit(1) + blockProcessTimer = 0 blockProcessAmount = 5 heartBeatTimer = 0 @@ -48,6 +55,10 @@ class OnionrCommunicate: if os.path.exists(self._core.queueDB): self._core.clearDaemonQueue() + + # Loads in and starts the enabled plugins + plugins.reload() + while True: command = self._core.daemonQueue() # Process blocks based on a timer @@ -63,7 +74,6 @@ class OnionrCommunicate: self.lookupBlocks() self.processBlocks() blockProcessTimer = 0 - #logger.debug('Communicator daemon heartbeat') if command != False: if command[0] == 'shutdown': logger.warn('Daemon recieved exit command.') @@ -71,17 +81,19 @@ class OnionrCommunicate: time.sleep(1) return - + def getNewPeers(self): ''' Get new peers ''' + return def lookupBlocks(self): ''' Lookup blocks and merge new ones ''' + peerList = self._core.listAdders() blocks = '' for i in peerList: @@ -126,6 +138,7 @@ class OnionrCommunicate: This is meant to be called from the communicator daemon on its timer. ''' + for i in self._core.getBlockList(True).split("\n"): if i != "": logger.warn('UNSAVED BLOCK: ' + i) @@ -137,6 +150,7 @@ class OnionrCommunicate: ''' Download a block from random order of peers ''' + peerList = self._core.listAdders() blocks = '' for i in peerList: @@ -164,12 +178,14 @@ class OnionrCommunicate: ''' URL encodes the data ''' + return urllib.parse.quote_plus(data) def performGet(self, action, peer, data=None, peerType='tor'): ''' Performs a request to a peer through Tor or i2p (currently only Tor) ''' + if not peer.endswith('.onion') and not peer.endswith('.onion/'): raise PeerError('Currently only Tor .onion peers are supported. You must manually specify .onion') diff --git a/onionr/config.py b/onionr/config.py index 793d8b9b..27002d34 100644 --- a/onionr/config.py +++ b/onionr/config.py @@ -27,6 +27,7 @@ def get(key, default = None): ''' Gets the key from configuration, or returns `default` ''' + if is_set(key): return get_config()[key] return default diff --git a/onionr/onionr.py b/onionr/onionr.py index 2792b9e1..563fd777 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -21,7 +21,7 @@ along with this program. If not, see . ''' import sys, os, base64, random, getpass, shutil, subprocess, requests, time, platform -import api, core, gui, config, logger +import api, core, gui, config, logger, onionrplugins as plugins from onionrutils import OnionrUtils from netcontroller import NetController @@ -130,26 +130,44 @@ class Onionr: def getCommands(self): return { + '': self.showHelpSuggestion, 'help': self.showHelp, 'version': self.version, 'config': self.configure, 'start': self.start, 'stop': self.killDaemon, 'stats': self.showStats, + + 'enable-plugin': self.enablePlugin, + 'enplugin': self.enablePlugin, + 'enableplugin': self.enablePlugin, + 'enmod': self.enablePlugin, + 'disable-plugin': self.disablePlugin, + 'displugin': self.disablePlugin, + 'disableplugin': self.disablePlugin, + 'dismod': self.disablePlugin, + 'reload-plugin': self.reloadPlugin, + 'reloadplugin': self.reloadPlugin, + 'reload-plugins': self.reloadPlugin, + 'reloadplugins': self.reloadPlugin, + 'listpeers': self.listPeers, 'list-peers': self.listPeers, - '': self.showHelpSuggestion, + 'addmsg': self.addMessage, 'addmessage': self.addMessage, 'add-msg': self.addMessage, 'add-message': self.addMessage, 'pm': self.sendEncrypt, + 'gui': self.openGUI, + 'addpeer': self.addPeer, 'add-peer': self.addPeer, 'add-address': self.addAddress, - 'connect': self.addAddress, - 'addaddress': self.addAddress + 'addaddress': self.addAddress, + + 'connect': self.addAddress } def getHelp(self): @@ -160,10 +178,13 @@ class Onionr: 'start': 'Starts the Onionr daemon', 'stop': 'Stops the Onionr daemon', 'stats': 'Displays node statistics', - 'list-peers': 'Displays a list of peers (?)', + 'enable-plugin': 'Enables and starts a plugin', + 'disable-plugin': 'Disables and stops a plugin', + 'reload-plugin': 'Reloads a plugin', + 'list-peers': 'Displays a list of peers', 'add-peer': 'Adds a peer (?)', 'add-msg': 'Broadcasts a message to the Onionr network', - 'pm': 'Adds a private message (?)', + 'pm': 'Adds a private message to block', 'gui': 'Opens a graphical interface for Onionr' } @@ -263,7 +284,9 @@ class Onionr: else: logger.info("Adding peer: " + logger.colors.underline + newPeer) self.onionrCore.addPeer(newPeer) - + + return + def addAddress(self): '''Adds a Onionr node address''' try: @@ -277,6 +300,8 @@ class Onionr: else: logger.warn("Unable to add address") + return + def addMessage(self): ''' Broadcasts a message to the Onionr network @@ -291,6 +316,52 @@ class Onionr: self.onionrCore.addToBlockDB(addedHash, selfInsert=True) self.onionrCore.setBlockType(addedHash, 'txt') + return + + def enablePlugin(self): + ''' + Enables and starts the given plugin + ''' + + if len(sys.argv) >= 3: + plugin_name = sys.argv[2] + logger.info('Enabling plugin \"' + plugin_name + '\"...') + plugins.enable(plugin_name) + else: + logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' ') + + return + + def disablePlugin(self): + ''' + Disables and stops the given plugin + ''' + + if len(sys.argv) >= 3: + plugin_name = sys.argv[2] + logger.info('Disabling plugin \"' + plugin_name + '\"...') + plugins.disable(plugin_name) + else: + logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' ') + + return + + def reloadPlugin(self): + ''' + Reloads (stops and starts) all plugins, or the given plugin + ''' + + if len(sys.argv) >= 3: + plugin_name = sys.argv[2] + logger.info('Reloading plugin \"' + plugin_name + '\"...') + plugins.stop(plugin_name) + plugins.start(plugin_name) + else: + logger.info('Reloading all plugins...') + plugins.reload() + + return + def notFound(self): ''' Displays a "command not found" message @@ -325,6 +396,7 @@ class Onionr: ''' Starts the Onionr communication daemon ''' + if not os.environ.get("WERKZEUG_RUN_MAIN") == "true": if self._developmentMode: logger.warn('DEVELOPMENT MODE ENABLED (THIS IS LESS SECURE!)') diff --git a/onionr/onionrevents.py b/onionr/onionrevents.py new file mode 100644 index 00000000..2c148d8f --- /dev/null +++ b/onionr/onionrevents.py @@ -0,0 +1,53 @@ +''' + Onionr - P2P Microblogging Platform & Social network + + This file deals with configuration management. +''' +''' + 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 . +''' + +import config, logger, onionrplugins as plugins + +def event(event_name, data = None, onionr = None): + ''' + Calls an event on all plugins (if defined) + ''' + + for plugin in plugins.get_enabled_plugins(): + try: + call(plugins.get_plugin(plugin), event_name, data, onionr) + except: + logger.warn('Event \"' + event_name + '\" failed for plugin \"' + plugin + '\".') + +def call(plugin, event_name, data = None, onionr = None): + ''' + Calls an event on a plugin if one is defined + ''' + + if not plugin is None: + try: + attribute = 'on_' + str(event_name).lower() + + # TODO: Use multithreading perhaps? + if hasattr(plugin, attribute): + logger.debug('Calling event ' + str(event_name)) + getattr(plugin, attribute)(onionr, data) + + return True + except: + logger.warn('Failed to call event ' + str(event_name) + ' on module.') + return False + else: + return True diff --git a/onionr/onionrplugins.py b/onionr/onionrplugins.py new file mode 100644 index 00000000..d3c3aefb --- /dev/null +++ b/onionr/onionrplugins.py @@ -0,0 +1,231 @@ +''' + Onionr - P2P Microblogging Platform & Social network + + This file deals with management of modules/plugins. +''' +''' + 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 . +''' + +import os, re, importlib, config, logger +import onionrevents as events + +_pluginsfolder = 'data/plugins/' +_instances = dict() + +def reload(stop_event = True): + ''' + Reloads all the plugins + ''' + + check() + + try: + enabled_plugins = get_enabled_plugins() + + if stop_event is True: + logger.debug('Reloading all plugins...') + else: + logger.debug('Loading all plugins...') + + if stop_event is True: + for plugin in enabled_plugins: + stop(plugin) + + for plugin in enabled_plugins: + start(plugin) + + return True + except: + logger.error('Failed to reload plugins.') + + return False + + +def enable(name, start_event = True): + ''' + Enables a plugin + ''' + + check() + + if exists(name): + enabled_plugins = get_enabled_plugins() + enabled_plugins.append(name) + config_plugins = config.get('plugins') + config_plugins['enabled'] = enabled_plugins + config.set('plugins', config_plugins, True) + + events.call(get_plugin(name), 'enable') + + if start_event is True: + start(name) + + return True + else: + logger.error('Failed to enable plugin \"' + name + '\", disabling plugin.') + disable(name) + + return False + + +def disable(name, stop_event = True): + ''' + Disables a plugin + ''' + + check() + + if is_enabled(name): + enabled_plugins = get_enabled_plugins() + enabled_plugins.remove(name) + config_plugins = config.get('plugins') + config_plugins['enabled'] = enabled_plugins + config.set('plugins', config_plugins, True) + + if exists(name): + events.call(get_plugin(name), 'disable') + + if stop_event is True: + stop(name) + +def start(name): + ''' + Starts the plugin + ''' + + check() + + if exists(name): + try: + plugin = get_plugin(name) + + if plugin is None: + raise Exception('Failed to import module.') + else: + events.call(plugin, 'start') + + return plugin + except: + logger.error('Failed to start module \"' + name + '\".') + else: + logger.error('Failed to start nonexistant module \"' + name + '\".') + + return None + +def stop(name): + ''' + Stops the plugin + ''' + + check() + + if exists(name): + try: + plugin = get_plugin(name) + + if plugin is None: + raise Exception('Failed to import module.') + else: + events.call(plugin, 'stop') + + return plugin + except: + logger.error('Failed to stop module \"' + name + '\".') + else: + logger.error('Failed to stop nonexistant module \"' + name + '\".') + + return None + +def get_plugin(name): + ''' + Returns the instance of a module + ''' + + check() + + if str(name).lower() in _instances: + return _instances[str(name).lower()] + else: + _instances[str(name).lower()] = importlib.import_module(get_plugins_folder(name, False).replace('/', '.') + 'main') + return get_plugin(name) + +def get_plugins(): + ''' + Returns a list of plugins (deprecated) + ''' + + return _instances + +def exists(name): + ''' + Return value indicates whether or not the plugin exists + ''' + + check() + + return os.path.isdir(get_plugins_folder(str(name).lower())) + +def get_enabled_plugins(): + ''' + Returns a list of the enabled plugins + ''' + + check() + + config.reload() + + return config.get('plugins')['enabled'] + +def is_enabled(name): + ''' + Return value indicates whether or not the plugin is enabled + ''' + + return name in get_enabled_plugins() + +def get_plugins_folder(name = None, absolute = True): + ''' + Returns the path to the plugins folder + ''' + + path = '' + + if name is None: + path = _pluginsfolder + else: + # only allow alphanumeric characters + path = _pluginsfolder + re.sub('[^0-9a-zA-Z]+', '', str(name).lower()) + '/' + + if absolute is True: + path = os.path.abspath(path) + + return path + +def check(): + ''' + Checks to make sure files exist + ''' + + config.reload() + + if not config.is_set('plugins'): + logger.debug('Generating plugin config data...') + config.set('plugins', {'enabled': []}, True) + + if not os.path.exists(os.path.dirname(get_plugins_folder())): + logger.debug('Generating plugin data folder...') + os.path.mkdirs(os.path.dirname(get_plugins_folder())) + + return