#!/usr/bin/env python3 ''' Onionr - P2P Anonymous Storage Network Onionr is the name for both the protocol and the original/reference software. Run with 'help' for usage. ''' ''' 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 sys MIN_PY_VERSION = 6 if sys.version_info[0] == 2 or sys.version_info[1] < MIN_PY_VERSION: print('Error, Onionr requires Python 3.%s+' % (MIN_PY_VERSION,)) sys.exit(1) import os, base64, random, getpass, shutil, time, platform, datetime, re, json, getpass, sqlite3 import webbrowser, uuid, signal from threading import Thread import api, core, config, logger, onionrplugins as plugins, onionrevents as events import onionrutils import netcontroller, onionrstorage from netcontroller import NetController from onionrblockapi import Block import onionrproofs, onionrexceptions, communicator from onionrusers import onionrusers import onionrcommands as commands # Many command definitions are here try: from urllib3.contrib.socks import SOCKSProxyManager except ImportError: raise Exception("You need the PySocks module (for use with socks5 proxy to use Tor)") ONIONR_TAGLINE = 'Anonymous P2P Platform - GPLv3 - https://Onionr.net' ONIONR_VERSION = '0.0.0' # for debugging and stuff ONIONR_VERSION_TUPLE = tuple(ONIONR_VERSION.split('.')) # (MAJOR, MINOR, VERSION) API_VERSION = '0' # increments of 1; only change when something fundamental about how the API works changes. This way other nodes know how to communicate without learning too much information about you. class Onionr: def __init__(self): ''' Main Onionr class. This is for the CLI program, and does not handle much of the logic. In general, external programs and plugins should not use this class. ''' self.userRunDir = os.getcwd() # Directory user runs the program from self.killed = False if sys.argv[0] == os.path.basename(__file__): try: os.chdir(sys.path[0]) except FileNotFoundError: pass try: self.dataDir = os.environ['ONIONR_HOME'] if not self.dataDir.endswith('/'): self.dataDir += '/' except KeyError: self.dataDir = 'data/' # Load global configuration data data_exists = Onionr.setupConfig(self.dataDir, self = self) if netcontroller.torBinary() is None: logger.error('Tor is not installed') sys.exit(1) # If data folder does not exist if not data_exists: if not os.path.exists(self.dataDir + 'blocks/'): os.mkdir(self.dataDir + 'blocks/') # Copy default plugins into plugins folder if not os.path.exists(plugins.get_plugins_folder()): if os.path.exists('static-data/default-plugins/'): names = [f for f in os.listdir("static-data/default-plugins/")] shutil.copytree('static-data/default-plugins/', plugins.get_plugins_folder()) # Enable plugins for name in names: if not name in plugins.get_enabled_plugins(): plugins.enable(name, self) for name in plugins.get_enabled_plugins(): if not os.path.exists(plugins.get_plugin_data_folder(name)): try: os.mkdir(plugins.get_plugin_data_folder(name)) except: plugins.disable(name, onionr = self, stop_event = False) self.communicatorInst = None self.onionrCore = core.Core() self.onionrCore.onionrInst = self #self.deleteRunFiles() self.onionrUtils = onionrutils.OnionrUtils(self.onionrCore) self.clientAPIInst = '' # Client http api instance self.publicAPIInst = '' # Public http api instance signal.signal(signal.SIGTERM, self.exitSigterm) # Handle commands self.debug = False # Whole application debugging # Get configuration if type(config.get('client.webpassword')) is type(None): config.set('client.webpassword', base64.b16encode(os.urandom(32)).decode('utf-8'), savefile=True) if type(config.get('client.client.port')) is type(None): randomPort = netcontroller.getOpenPort() config.set('client.client.port', randomPort, savefile=True) if type(config.get('client.public.port')) is type(None): randomPort = netcontroller.getOpenPort() print(randomPort) config.set('client.public.port', randomPort, savefile=True) if type(config.get('client.participate')) is type(None): config.set('client.participate', True, savefile=True) if type(config.get('client.api_version')) is type(None): config.set('client.api_version', API_VERSION, savefile=True) self.cmds = commands.get_commands(self) self.cmdhelp = commands.cmd_help # initialize plugins events.event('init', onionr = self, threaded = False) command = '' try: command = sys.argv[1].lower() except IndexError: command = '' finally: self.execute(command) return def exitSigterm(self, signum, frame): self.killed = True ''' THIS SECTION HANDLES THE COMMANDS ''' def doExport(self, bHash): exportDir = self.dataDir + 'block-export/' if not os.path.exists(exportDir): if os.path.exists(self.dataDir): os.mkdir(exportDir) else: logger.error('Onionr Not initialized') data = onionrstorage.getData(self.onionrCore, bHash) with open('%s/%s.dat' % (exportDir, bHash), 'wb') as exportFile: exportFile.write(data) def exportBlock(self): exportDir = self.dataDir + 'block-export/' try: assert self.onionrUtils.validateHash(sys.argv[2]) except (IndexError, AssertionError): logger.error('No valid block hash specified.') sys.exit(1) else: bHash = sys.argv[2] self.doExport(bHash) def showDetails(self): commands.onionrstatistics.show_details(self) def openHome(self): commands.open_home(self) def addID(self): commands.pubkeymanager.add_ID(self) def changeID(self): commands.pubkeymanager.change_ID(self) def getCommands(self): return self.cmds def friendCmd(self): '''List, add, or remove friend(s) Changes their peer DB entry. ''' commands.pubkeymanager.friend_command(self) def deleteRunFiles(self): try: os.remove(self.onionrCore.publicApiHostFile) except FileNotFoundError: pass try: os.remove(self.onionrCore.privateApiHostFile) except FileNotFoundError: pass def deleteRunFiles(self): try: os.remove(self.onionrCore.publicApiHostFile) except FileNotFoundError: pass try: os.remove(self.onionrCore.privateApiHostFile) except FileNotFoundError: pass def banBlock(self): try: ban = sys.argv[2] except IndexError: ban = logger.readline('Enter a block hash:') if self.onionrUtils.validateHash(ban): if not self.onionrCore._blacklist.inBlacklist(ban): try: self.onionrCore._blacklist.addToDB(ban) self.onionrCore.removeBlock(ban) except Exception as error: logger.error('Could not blacklist block', error=error) else: logger.info('Block blacklisted') else: logger.warn('That block is already blacklisted') else: logger.error('Invalid block hash') return def listConn(self): randID = str(uuid.uuid4()) self.onionrCore.daemonQueueAdd('connectedPeers', responseID=randID) while True: try: time.sleep(3) peers = self.onionrCore.daemonQueueGetResponse(randID) except KeyboardInterrupt: break if not type(peers) is None: if peers not in ('', 'failure', None): if peers != False: print(peers) else: print('Daemon probably not running. Unable to list connected peers.') break def listPeers(self): logger.info('Peer transport address list:') for i in self.onionrCore.listAdders(): logger.info(i) def getWebPassword(self): return config.get('client.webpassword') def printWebPassword(self): logger.info(self.getWebPassword(), sensitive = True) def getHelp(self): return self.cmdhelp def addCommand(self, command, function): self.cmds[str(command).lower()] = function def addHelp(self, command, description): self.cmdhelp[str(command).lower()] = str(description) def delCommand(self, command): return self.cmds.pop(str(command).lower(), None) def delHelp(self, command): return self.cmdhelp.pop(str(command).lower(), None) def configure(self): ''' Displays something from the configuration file, or sets it ''' if len(sys.argv) >= 4: config.reload() config.set(sys.argv[2], sys.argv[3], True) logger.debug('Configuration file updated.') elif len(sys.argv) >= 3: config.reload() logger.info(logger.colors.bold + sys.argv[2] + ': ' + logger.colors.reset + str(config.get(sys.argv[2], logger.colors.fg.red + 'Not set.'))) else: logger.info(logger.colors.bold + 'Get a value: ' + logger.colors.reset + sys.argv[0] + ' ' + sys.argv[1] + ' ') logger.info(logger.colors.bold + 'Set a value: ' + logger.colors.reset + sys.argv[0] + ' ' + sys.argv[1] + ' ') def execute(self, argument): ''' Executes a command ''' argument = argument[argument.startswith('--') and len('--'):] # remove -- if it starts with it # define commands commands = self.getCommands() command = commands.get(argument, self.notFound) command() return ''' THIS SECTION DEFINES THE COMMANDS ''' def version(self, verbosity = 5, function = logger.info): ''' Displays the Onionr version ''' function('Onionr v%s (%s) (API v%s)' % (ONIONR_VERSION, platform.machine(), API_VERSION)) if verbosity >= 1: function(ONIONR_TAGLINE) if verbosity >= 2: function('Running on %s %s' % (platform.platform(), platform.release())) return def doPEX(self): '''make communicator do pex''' logger.info('Sending pex to command queue...') self.onionrCore.daemonQueueAdd('pex') def listKeys(self): ''' Displays a list of keys (used to be called peers) (?) ''' logger.info('%sPublic keys in database: \n%s%s' % (logger.colors.fg.lightgreen, logger.colors.fg.green, '\n'.join(self.onionrCore.listPeers()))) def addPeer(self): ''' Adds a peer (?) ''' try: newPeer = sys.argv[2] except: pass else: if self.onionrUtils.hasKey(newPeer): logger.info('We already have that key') return logger.info("Adding peer: " + logger.colors.underline + newPeer) try: if self.onionrCore.addPeer(newPeer): logger.info('Successfully added key') except AssertionError: logger.error('Failed to add key') return def addAddress(self): ''' Adds a Onionr node address ''' try: newAddress = sys.argv[2] newAddress = newAddress.replace('http:', '').replace('/', '') except: pass else: logger.info("Adding address: " + logger.colors.underline + newAddress) if self.onionrCore.addAddress(newAddress): logger.info("Successfully added address.") else: logger.warn("Unable to add address.") return def enablePlugin(self): ''' Enables and starts the given plugin ''' if len(sys.argv) >= 3: plugin_name = sys.argv[2] logger.info('Enabling plugin "%s"...' % plugin_name) plugins.enable(plugin_name, self) else: logger.info('%s %s ' % (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 "%s"...' % plugin_name) plugins.disable(plugin_name, self) else: logger.info('%s %s ' % (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 "%s"...' % plugin_name) plugins.stop(plugin_name, self) plugins.start(plugin_name, self) else: logger.info('Reloading all plugins...') plugins.reload(self) return def createPlugin(self): ''' Creates the directory structure for a plugin name ''' if len(sys.argv) >= 3: try: plugin_name = re.sub('[^0-9a-zA-Z_]+', '', str(sys.argv[2]).lower()) if not plugins.exists(plugin_name): logger.info('Creating plugin "%s"...' % plugin_name) os.makedirs(plugins.get_plugins_folder(plugin_name)) with open(plugins.get_plugins_folder(plugin_name) + '/main.py', 'a') as main: contents = '' with open('static-data/default_plugin.py', 'rb') as file: contents = file.read().decode() # TODO: Fix $user. os.getlogin() is B U G G Y main.write(contents.replace('$user', 'some random developer').replace('$date', datetime.datetime.now().strftime('%Y-%m-%d')).replace('$name', plugin_name)) with open(plugins.get_plugins_folder(plugin_name) + '/info.json', 'a') as main: main.write(json.dumps({'author' : 'anonymous', 'description' : 'the default description of the plugin', 'version' : '1.0'})) logger.info('Enabling plugin "%s"...' % plugin_name) plugins.enable(plugin_name, self) else: logger.warn('Cannot create plugin directory structure; plugin "%s" exists.' % plugin_name) except Exception as e: logger.error('Failed to create plugin directory structure.', e) else: logger.info('%s %s ' % (sys.argv[0], sys.argv[1])) return def notFound(self): ''' Displays a "command not found" message ''' logger.error('Command not found.', timestamp = False) def showHelpSuggestion(self): ''' Displays a message suggesting help ''' if __name__ == '__main__': logger.info('Do ' + logger.colors.bold + sys.argv[0] + ' --help' + logger.colors.reset + logger.colors.fg.green + ' for Onionr help.') def start(self, input = False, override = False): ''' Starts the Onionr daemon ''' if os.path.exists('.onionr-lock') and not override: logger.fatal('Cannot start. Daemon is already running, or it did not exit cleanly.\n(if you are sure that there is not a daemon running, delete .onionr-lock & try again).') else: if not self.debug and not self._developmentMode: lockFile = open('.onionr-lock', 'w') lockFile.write('') lockFile.close() self.running = True self.daemon() self.running = False if not self.debug and not self._developmentMode: try: os.remove('.onionr-lock') except FileNotFoundError: pass def setClientAPIInst(self, inst): self.clientAPIInst = inst def getClientApi(self): while self.clientAPIInst == '': time.sleep(0.5) return self.clientAPIInst def daemon(self): ''' Starts the Onionr communication daemon ''' commands.daemonlaunch.daemon(self) def killDaemon(self): ''' Shutdown the Onionr daemon ''' commands.daemonlaunch.kill_daemon(self) def showStats(self): ''' Displays statistics and exits ''' commands.onionrstatistics.show_stats(self) def showHelp(self, command = None): ''' Show help for Onionr ''' helpmenu = self.getHelp() if command is None and len(sys.argv) >= 3: for cmd in sys.argv[2:]: self.showHelp(cmd) elif not command is None: if command.lower() in helpmenu: logger.info(logger.colors.bold + command + logger.colors.reset + logger.colors.fg.blue + ' : ' + logger.colors.reset + helpmenu[command.lower()], timestamp = False) else: logger.warn(logger.colors.bold + command + logger.colors.reset + logger.colors.fg.blue + ' : ' + logger.colors.reset + 'No help menu entry was found', timestamp = False) else: self.version(0) for command, helpmessage in helpmenu.items(): self.showHelp(command) return def get_hostname(self): try: with open('./' + self.dataDir + 'hs/hostname', 'r') as hostname: return hostname.read().strip() except FileNotFoundError: return "Not Generated" except Exception: return None def getConsoleWidth(self): ''' Returns an integer, the width of the terminal/cmd window ''' columns = 80 try: columns = int(os.popen('stty size', 'r').read().split()[1]) except: # if it errors, it's probably windows, so default to 80. pass return columns def getFile(self): ''' Get a file from onionr blocks ''' try: fileName = sys.argv[2] bHash = sys.argv[3] except IndexError: logger.error("Syntax %s %s" % (sys.argv[0], '/path/to/filename ')) else: logger.info(fileName) contents = None if os.path.exists(fileName): logger.error("File already exists") return if not self.onionrUtils.validateHash(bHash): logger.error('Block hash is invalid') return with open(fileName, 'wb') as myFile: myFile.write(base64.b64decode(Block(bHash, core=self.onionrCore).bcontent)) return def addWebpage(self): ''' Add a webpage to the onionr network ''' self.addFile(singleBlock=True, blockType='html') def addFile(self, singleBlock=False, blockType='bin'): ''' Adds a file to the onionr network ''' commands.filecommands.add_file(self, singleBlock, blockType) def setupConfig(dataDir, self = None): data_exists = os.path.exists(dataDir) if not data_exists: os.mkdir(dataDir) if os.path.exists('static-data/default_config.json'): # this is the default config, it will be overwritten if a config file already exists. Else, it saves it with open('static-data/default_config.json', 'r') as configReadIn: config.set_config(json.loads(configReadIn.read())) else: # the default config file doesn't exist, try hardcoded config logger.warn('Default configuration file does not exist, switching to hardcoded fallback configuration!') config.set_config({'dev_mode': True, 'log': {'file': {'output': True, 'path': dataDir + 'output.log'}, 'console': {'output': True, 'color': True}}}) if not data_exists: config.save() config.reload() # this will read the configuration file into memory settings = 0b000 if config.get('log.console.color', True): settings = settings | logger.USE_ANSI if config.get('log.console.output', True): settings = settings | logger.OUTPUT_TO_CONSOLE if config.get('log.file.output', True): settings = settings | logger.OUTPUT_TO_FILE logger.set_settings(settings) if not self is None: if str(config.get('general.dev_mode', True)).lower() == 'true': self._developmentMode = True logger.set_level(logger.LEVEL_DEBUG) else: self._developmentMode = False logger.set_level(logger.LEVEL_INFO) verbosity = str(config.get('log.verbosity', 'default')).lower().strip() if not verbosity in ['default', 'null', 'none', 'nil']: map = { str(logger.LEVEL_DEBUG) : logger.LEVEL_DEBUG, 'verbose' : logger.LEVEL_DEBUG, 'debug' : logger.LEVEL_DEBUG, str(logger.LEVEL_INFO) : logger.LEVEL_INFO, 'info' : logger.LEVEL_INFO, 'information' : logger.LEVEL_INFO, str(logger.LEVEL_WARN) : logger.LEVEL_WARN, 'warn' : logger.LEVEL_WARN, 'warning' : logger.LEVEL_WARN, 'warnings' : logger.LEVEL_WARN, str(logger.LEVEL_ERROR) : logger.LEVEL_ERROR, 'err' : logger.LEVEL_ERROR, 'error' : logger.LEVEL_ERROR, 'errors' : logger.LEVEL_ERROR, str(logger.LEVEL_FATAL) : logger.LEVEL_FATAL, 'fatal' : logger.LEVEL_FATAL, str(logger.LEVEL_IMPORTANT) : logger.LEVEL_IMPORTANT, 'silent' : logger.LEVEL_IMPORTANT, 'quiet' : logger.LEVEL_IMPORTANT, 'important' : logger.LEVEL_IMPORTANT } if verbosity in map: logger.set_level(map[verbosity]) else: logger.warn('Verbosity level %s is not valid, using default verbosity.' % verbosity) return data_exists def header(self, message = logger.colors.fg.pink + logger.colors.bold + 'Onionr' + logger.colors.reset + logger.colors.fg.pink + ' has started.'): if os.path.exists('static-data/header.txt') and logger.get_level() <= logger.LEVEL_INFO: with open('static-data/header.txt', 'rb') as file: # only to stdout, not file or log or anything sys.stderr.write(file.read().decode().replace('P', logger.colors.fg.pink).replace('W', logger.colors.reset + logger.colors.bold).replace('G', logger.colors.fg.green).replace('\n', logger.colors.reset + '\n').replace('B', logger.colors.bold).replace('A', '%s' % API_VERSION).replace('V', ONIONR_VERSION)) logger.info(logger.colors.fg.lightgreen + '-> ' + str(message) + logger.colors.reset + logger.colors.fg.lightgreen + ' <-\n') if __name__ == "__main__": Onionr()