From a87d1bf1e8466c71d4270df347862a721904cb60 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 18 Mar 2019 00:22:31 -0500 Subject: [PATCH 001/173] bumped stem version and started work on new dir conn system --- onionr/netcontroller.py | 14 +---- onionr/onionrservices/__init__.py | 18 ++++++ onionr/onionrservices/bootstrapservice.py | 67 +++++++++++++++++++++++ onionr/onionrservices/connectionserver.py | 4 ++ onionr/onionrutils.py | 6 +- requirements.in | 2 +- requirements.txt | 4 +- 7 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 onionr/onionrservices/__init__.py create mode 100644 onionr/onionrservices/bootstrapservice.py create mode 100644 onionr/onionrservices/connectionserver.py diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py index 2898256c..fcba14c2 100755 --- a/onionr/netcontroller.py +++ b/onionr/netcontroller.py @@ -17,12 +17,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' - -import subprocess, os, random, sys, logger, time, signal, config, base64, socket -from stem.control import Controller +import subprocess, os, random, sys, time, signal, base64, socket +from shutil import which +import logger, config from onionrblockapi import Block from dependencies import secrets -from shutil import which def getOpenPort(): # taken from (but modified) https://stackoverflow.com/a/2838309 @@ -69,13 +68,6 @@ class NetController: self.torBinary = 'tor' config.reload() - ''' - if os.path.exists(self.torConfigLocation): - torrc = open(self.torConfigLocation, 'r') - if not str(self.hsPort) in torrc.read(): - os.remove(self.torConfigLocation) - torrc.close() - ''' return diff --git a/onionr/onionrservices/__init__.py b/onionr/onionrservices/__init__.py new file mode 100644 index 00000000..68612cf7 --- /dev/null +++ b/onionr/onionrservices/__init__.py @@ -0,0 +1,18 @@ +import stem +import core +from . import connectionserver, connectionclient, bootstrapservice +class OnionrServices: + def __init__(self, onionr_core): + assert isinstance(onionr_core, core.Core) + self._core = onionr_core + self.servers = {} + self.clients = {} + return + + def create_server(self): + return + + def create_client(self, peer): + # Create ephemeral onion service to bootstrap connection + address = bootstrapservice.bootstrap_client_service(peer) + return address \ No newline at end of file diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py new file mode 100644 index 00000000..7a2c3823 --- /dev/null +++ b/onionr/onionrservices/bootstrapservice.py @@ -0,0 +1,67 @@ +''' + Onionr - P2P Anonymous Storage Network + + Bootstrap onion direct connections for the clients +''' +''' + 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 . +''' +from gevent.pywsgi import WSGIServer, WSGIHandler +from stem.control import Controller +from flask import Flask +import core +from netcontroller import getOpenPort + +def bootstrap_client_service(peer, core_inst=None): + ''' + Bootstrap client services + ''' + if core_inst is None: + core_inst = core.Core() + + if not core_inst._utils.validatePubKey(peer): + raise ValueError('Peer must be valid base32 ed25519 public key') + + http_server = WSGIServer(('127.0.0.1', bootstrap_port), bootstrap_app, log=None) + bootstrap_port = getOpenPort() + bootstrap_app = flask.Flask(__name__) + + bootstrap_address = '' + + @bootstrap_app.route('/ping') + def get_ping(): + return "pong!" + + @bootstrap_app.route('/bs/
', methods=['POST']) + def get_bootstrap(address): + if core_inst._utils.validateID(address): + # Set the bootstrap address then close the server + bootstrap_address = address + http_server.stop() + + with Controller.from_port() as controller: + # Connect to the Tor process for Onionr + controller.authenticate() + # Create the v3 onion service + response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, await_publication = True, key_type='ED25519-V3') + + core_inst.insertBlock(response.hostname, header='con', sign=True, encryptType='asym', + asymPeer=peer, disableForward=True) + + # Run the bootstrap server + http_server.serve_forever() + # This line reached when server is shutdown by being bootstrapped + + # Now that the bootstrap server has received a server, return the address + return bootstrap_address diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py new file mode 100644 index 00000000..d602d8ce --- /dev/null +++ b/onionr/onionrservices/connectionserver.py @@ -0,0 +1,4 @@ +class ConnectionServer: + def __init__(self): + return + \ No newline at end of file diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index f8fd13af..082b14fb 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -329,7 +329,8 @@ class OnionrUtils: retVal = True return retVal - def validateID(self, id): + @staticmethod + def validateID(id): ''' Validate if an address is a valid tor or i2p hidden service ''' @@ -381,7 +382,8 @@ class OnionrUtils: except: return False - def isIntegerString(self, data): + @staticmethod + def isIntegerString(data): '''Check if a string is a valid base10 integer (also returns true if already an int)''' try: int(data) diff --git a/requirements.in b/requirements.in index 6b1cb663..7bf8ff02 100755 --- a/requirements.in +++ b/requirements.in @@ -5,5 +5,5 @@ gevent==1.3.6 defusedxml==0.5.0 Flask==1.0.2 PySocks==1.6.8 -stem==1.6.0 +stem==1.7.1 deadsimplekv==0.0.1 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b2c93d7c..9bb58ea1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -185,8 +185,8 @@ six==1.12.0 \ --hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \ --hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73 \ # via pynacl -stem==1.6.0 \ - --hash=sha256:d7fe1fb13ed5a94d610b5ad77e9f1b3404db0ca0586ded7a34afd323e3b849ed +stem==1.7.1 \ + --hash=sha256:c9eaf3116cb60c15995cbd3dec3a5cbc50e9bb6e062c4d6d42201e566f498ca2 urllib3==1.23 \ --hash=sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf \ --hash=sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5 From 2da5e0479a9dec52751d3d6a0bb2453d0a4ab563 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 19 Mar 2019 00:09:53 -0500 Subject: [PATCH 002/173] work on direct connections --- onionr/communicator.py | 21 +++----------- onionr/core.py | 1 - onionr/onionrservices/__init__.py | 20 ++++++++++--- onionr/onionrservices/bootstrapservice.py | 18 ++++++------ onionr/onionrservices/connectionserver.py | 29 +++++++++++++++++-- .../default-plugins/metadataprocessor/main.py | 26 +++++------------ 6 files changed, 62 insertions(+), 53 deletions(-) diff --git a/onionr/communicator.py b/onionr/communicator.py index 12fbabbc..37c0cbdb 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -24,7 +24,7 @@ from dependencies import secrets from utils import networkmerger import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block from communicatorutils import onionrdaemontools -import onionrsockets, onionr, onionrproofs +import onionrservices, onionr, onionrproofs from communicatorutils import onionrcommunicatortimers, proxypicker OnionrCommunicatorTimers = onionrcommunicatortimers.OnionrCommunicatorTimers @@ -126,9 +126,9 @@ class OnionrCommunicatorDaemon: #forwardSecrecyTimer.count = (forwardSecrecyTimer.frequency - 990) if config.get('general.socket_servers'): - self.socketServer = threading.Thread(target=onionrsockets.OnionrSocketServer, args=(self._core,)) - self.socketServer.start() - self.socketClient = onionrsockets.OnionrSocketClient(self._core) + self.services = onionrservices.OnionrServices(self._core) + else: + self.services = None # Main daemon loop, mainly for calling timers, don't do any complex operations here to avoid locking try: @@ -565,19 +565,6 @@ class OnionrCommunicatorDaemon: i.count = (i.frequency - 1) elif cmd[0] == 'uploadBlock': self.blocksToUpload.append(cmd[1]) - elif cmd[0] == 'startSocket': - # Create our own socket server - socketInfo = json.loads(cmd[1]) - socketInfo['id'] = uuid.uuid4() - self._core.startSocket = socketInfo - elif cmd[0] == 'addSocket': - # Socket server was created for us - socketInfo = json.loads(cmd[1]) - peer = socketInfo['peer'] - reason = socketInfo['reason'] - threading.Thread(target=self.socketClient.startSocket, args=(peer, reason)).start() - else: - logger.info('Recieved daemonQueue command:' + cmd[0]) if cmd[0] not in ('', None): if response != '': diff --git a/onionr/core.py b/onionr/core.py index f178fefd..0c8e1527 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -355,7 +355,6 @@ class Core: conn = sqlite3.connect(self.queueDB, timeout=30) c = conn.cursor() t = (command, data, date, responseID) - try: c.execute('INSERT INTO commands (command, data, date, responseID) VALUES(?, ?, ?, ?)', t) conn.commit() diff --git a/onionr/onionrservices/__init__.py b/onionr/onionrservices/__init__.py index 68612cf7..5ea0ef3e 100644 --- a/onionr/onionrservices/__init__.py +++ b/onionr/onionrservices/__init__.py @@ -1,17 +1,29 @@ +import time import stem import core -from . import connectionserver, connectionclient, bootstrapservice +from . import connectionserver, bootstrapservice class OnionrServices: def __init__(self, onionr_core): assert isinstance(onionr_core, core.Core) self._core = onionr_core self.servers = {} self.clients = {} + self.shutdown = False return - def create_server(self): - return - + def create_server(self, peer, address): + assert self._core._utils.validateID(address) + BOOTSTRAP_TRIES = 10 + TRY_WAIT = 3 + for x in range(BOOTSTRAP_TRIES): + if self._core._utils.doGetRequest('http://' + address + '/ping') == 'pong!': + connectionserver.ConnectionServer(peer, address, core_inst=self._core) + return True + else: + time.sleep(TRY_WAIT) + else: + return False + def create_client(self, peer): # Create ephemeral onion service to bootstrap connection address = bootstrapservice.bootstrap_client_service(peer) diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py index 7a2c3823..b0b2fb7d 100644 --- a/onionr/onionrservices/bootstrapservice.py +++ b/onionr/onionrservices/bootstrapservice.py @@ -23,7 +23,7 @@ from flask import Flask import core from netcontroller import getOpenPort -def bootstrap_client_service(peer, core_inst=None): +def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=120): ''' Bootstrap client services ''' @@ -32,10 +32,10 @@ def bootstrap_client_service(peer, core_inst=None): if not core_inst._utils.validatePubKey(peer): raise ValueError('Peer must be valid base32 ed25519 public key') - - http_server = WSGIServer(('127.0.0.1', bootstrap_port), bootstrap_app, log=None) + bootstrap_port = getOpenPort() - bootstrap_app = flask.Flask(__name__) + bootstrap_app = Flask(__name__) + http_server = WSGIServer(('127.0.0.1', bootstrap_port), bootstrap_app, log=None) bootstrap_address = '' @@ -50,14 +50,14 @@ def bootstrap_client_service(peer, core_inst=None): bootstrap_address = address http_server.stop() - with Controller.from_port() as controller: + with Controller.from_port(port=core_inst.config.get('tor.controlPort')) as controller: # Connect to the Tor process for Onionr - controller.authenticate() + controller.authenticate(core_inst.config.get('tor.controlpassword')) # Create the v3 onion service - response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, await_publication = True, key_type='ED25519-V3') + response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, await_publication = True, key_content = 'ED25519-V3') - core_inst.insertBlock(response.hostname, header='con', sign=True, encryptType='asym', - asymPeer=peer, disableForward=True) + core_inst.insertBlock(response.service_id, header='con', sign=True, encryptType='asym', + asymPeer=peer, disableForward=True, expire=(core_inst._utils.getEpoch() + bootstrap_timeout)) # Run the bootstrap server http_server.serve_forever() diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index d602d8ce..54ff03c3 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -1,4 +1,27 @@ +import stem, flask +import core class ConnectionServer: - def __init__(self): - return - \ No newline at end of file + def __init__(self, peer, address, core_inst=None): + if core_inst is None: + self.core_inst = core.Core() + else: + self.core_inst = core_inst + + if not core_inst._utils.validatePubKey(peer): + raise ValueError('Peer must be valid base32 ed25519 public key') + + service_app = flask.Flask(__name__) + service_port = getOpenPort() + http_server = WSGIServer(('127.0.0.1', service_port), service_app, log=None) + + @service_app.route('/ping') + def get_ping(): + return "pong!" + + with Controller.from_port(port=core_inst.config.get('tor.controlPort')) as controller: + # Connect to the Tor process for Onionr + controller.authenticate(core_inst.config.get('tor.controlpassword')) + # Create the v3 onion service + response = controller.create_ephemeral_hidden_service({80: service_port}, await_publication = True, key_type='ED25519-V3') + logger.info('hosting on ' + response.service_id) + http_server.serve_forever() \ No newline at end of file diff --git a/onionr/static-data/default-plugins/metadataprocessor/main.py b/onionr/static-data/default-plugins/metadataprocessor/main.py index e7277c7d..54f7dc18 100755 --- a/onionr/static-data/default-plugins/metadataprocessor/main.py +++ b/onionr/static-data/default-plugins/metadataprocessor/main.py @@ -46,6 +46,8 @@ def on_processblocks(api, data=None): myBlock = api.data['block'] blockType = api.data['type'] logger.info('blockType is ' + blockType) + utils = api.get_utils() + core = api.get_core() # Process specific block types @@ -54,26 +56,12 @@ def on_processblocks(api, data=None): if api.data['validSig'] == True: _processForwardKey(api, myBlock) # socket blocks - elif blockType == 'socket': + elif blockType == 'con': if api.data['validSig'] == True and myBlock.decrypted: # we check if it is decrypted as a way of seeing if it was for us - logger.info('Detected socket advertised to us...') - try: - address = myBlock.getMetadata('address') - except KeyError: - raise onionrexceptions.MissingAddress("Missing address for new socket") - try: - port = myBlock.getMetadata('port') - except KeyError: - raise ValueError("Missing port for new socket") - try: - reason = myBlock.getMetadata('reason') - except KeyError: - raise ValueError("Missing socket reason") - - socketInfo = json.dumps({'peer': api.data['signer'], 'address': address, 'port': port, 'create': False, 'reason': reason}) - api.get_core().daemonQueueAdd('addSocket', socketInfo) - else: - logger.warn("socket is not for us or is invalid") + myBlock.bcontent = utils.bytesToStr(myBlock.bcontent) + if utils.validateID('%s.onion' % (myBlock.bcontent,)): + logger.info('Detected socket advertised to us...') + core.keyStore.put('con', (myBlock.content, myBlock.signer)) def on_init(api, data = None): From 85adef16606c5c0e0c98ee418ade1c3bcb938296 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 22 Mar 2019 21:58:09 -0500 Subject: [PATCH 003/173] bootstrap blocks now inserted and detected --- onionr/communicator.py | 2 ++ onionr/communicatorutils/onionrcommunicatortimers.py | 5 +++-- onionr/onionrservices/__init__.py | 1 + onionr/onionrservices/bootstrapservice.py | 5 +++-- onionr/onionrservices/connectionserver.py | 2 +- onionr/onionrutils.py | 2 ++ .../default-plugins/metadataprocessor/main.py | 9 +-------- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/onionr/communicator.py b/onionr/communicator.py index 37c0cbdb..e4a513d6 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -24,6 +24,7 @@ from dependencies import secrets from utils import networkmerger import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block from communicatorutils import onionrdaemontools +from communicatorutils import servicecreator import onionrservices, onionr, onionrproofs from communicatorutils import onionrcommunicatortimers, proxypicker @@ -107,6 +108,7 @@ class OnionrCommunicatorDaemon: OnionrCommunicatorTimers(self, self.uploadBlock, 10, requiresPeer=True, maxThreads=1) OnionrCommunicatorTimers(self, self.daemonCommands, 6, maxThreads=1) OnionrCommunicatorTimers(self, self.detectAPICrash, 30, maxThreads=1) + OnionrCommunicatorTimers(self, servicecreator.service_creator, 5, maxThreads=10, myArgs=(self,)) deniableBlockTimer = OnionrCommunicatorTimers(self, self.daemonTools.insertDeniableBlock, 180, requiresPeer=True, maxThreads=1) netCheckTimer = OnionrCommunicatorTimers(self, self.daemonTools.netCheck, 600) diff --git a/onionr/communicatorutils/onionrcommunicatortimers.py b/onionr/communicatorutils/onionrcommunicatortimers.py index c15f20fc..a94a7821 100755 --- a/onionr/communicatorutils/onionrcommunicatortimers.py +++ b/onionr/communicatorutils/onionrcommunicatortimers.py @@ -20,7 +20,7 @@ ''' import threading, onionrexceptions, logger class OnionrCommunicatorTimers: - def __init__(self, daemonInstance, timerFunction, frequency, makeThread=True, threadAmount=1, maxThreads=5, requiresPeer=False): + def __init__(self, daemonInstance, timerFunction, frequency, makeThread=True, threadAmount=1, maxThreads=5, requiresPeer=False, myArgs=[]): self.timerFunction = timerFunction self.frequency = frequency self.threadAmount = threadAmount @@ -29,6 +29,7 @@ class OnionrCommunicatorTimers: self.daemonInstance = daemonInstance self.maxThreads = maxThreads self._core = self.daemonInstance._core + self.args = myArgs self.daemonInstance.timers.append(self) self.count = 0 @@ -55,7 +56,7 @@ class OnionrCommunicatorTimers: logger.debug('%s is currently using the maximum number of threads, not starting another.' % self.timerFunction.__name__) else: self.daemonInstance.threadCounts[self.timerFunction.__name__] += 1 - newThread = threading.Thread(target=self.timerFunction) + newThread = threading.Thread(target=self.timerFunction, args=self.args) newThread.start() else: self.timerFunction() diff --git a/onionr/onionrservices/__init__.py b/onionr/onionrservices/__init__.py index 5ea0ef3e..7194ea22 100644 --- a/onionr/onionrservices/__init__.py +++ b/onionr/onionrservices/__init__.py @@ -2,6 +2,7 @@ import time import stem import core from . import connectionserver, bootstrapservice + class OnionrServices: def __init__(self, onionr_core): assert isinstance(onionr_core, core.Core) diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py index b0b2fb7d..5dafe0f3 100644 --- a/onionr/onionrservices/bootstrapservice.py +++ b/onionr/onionrservices/bootstrapservice.py @@ -23,7 +23,7 @@ from flask import Flask import core from netcontroller import getOpenPort -def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=120): +def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): ''' Bootstrap client services ''' @@ -54,7 +54,8 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=120): # Connect to the Tor process for Onionr controller.authenticate(core_inst.config.get('tor.controlpassword')) # Create the v3 onion service - response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, await_publication = True, key_content = 'ED25519-V3') + #response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, await_publication = True, key_content = 'ED25519-V3') + response = controller.create_ephemeral_hidden_service(80, key_type = 'NEW', await_publication = True) core_inst.insertBlock(response.service_id, header='con', sign=True, encryptType='asym', asymPeer=peer, disableForward=True, expire=(core_inst._utils.getEpoch() + bootstrap_timeout)) diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index 54ff03c3..32d2640a 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -22,6 +22,6 @@ class ConnectionServer: # Connect to the Tor process for Onionr controller.authenticate(core_inst.config.get('tor.controlpassword')) # Create the v3 onion service - response = controller.create_ephemeral_hidden_service({80: service_port}, await_publication = True, key_type='ED25519-V3') + response = controller.create_ephemeral_hidden_service({80: service_port}, await_publication = True, key_type='NEW') logger.info('hosting on ' + response.service_id) http_server.serve_forever() \ No newline at end of file diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 082b14fb..b9ae61c4 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -182,6 +182,8 @@ class OnionrUtils: pass else: self._core.updateBlockInfo(blockHash, 'expire', expireTime) + if not blockType is None: + self._core.updateBlockInfo(blockHash, 'dataType', blockType) else: pass #logger.debug('Not processing metadata on encrypted block we cannot decrypt.') diff --git a/onionr/static-data/default-plugins/metadataprocessor/main.py b/onionr/static-data/default-plugins/metadataprocessor/main.py index 54f7dc18..787b0174 100755 --- a/onionr/static-data/default-plugins/metadataprocessor/main.py +++ b/onionr/static-data/default-plugins/metadataprocessor/main.py @@ -55,14 +55,7 @@ def on_processblocks(api, data=None): if blockType == 'forwardKey': if api.data['validSig'] == True: _processForwardKey(api, myBlock) - # socket blocks - elif blockType == 'con': - if api.data['validSig'] == True and myBlock.decrypted: # we check if it is decrypted as a way of seeing if it was for us - myBlock.bcontent = utils.bytesToStr(myBlock.bcontent) - if utils.validateID('%s.onion' % (myBlock.bcontent,)): - logger.info('Detected socket advertised to us...') - core.keyStore.put('con', (myBlock.content, myBlock.signer)) - + def on_init(api, data = None): pluginapi = api From b9beef3cce6f642869824ae8752bd085e71ff8ce Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 23 Mar 2019 21:56:46 -0500 Subject: [PATCH 004/173] direct connections closer to working --- onionr/communicator.py | 12 ++++++------ onionr/communicatorutils/servicecreator.py | 17 +++++++++++++++++ onionr/onionrservices/__init__.py | 3 +-- onionr/onionrservices/bootstrapservice.py | 3 +-- onionr/static-data/default_config.json | 2 +- 5 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 onionr/communicatorutils/servicecreator.py diff --git a/onionr/communicator.py b/onionr/communicator.py index e4a513d6..670e08fa 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -108,7 +108,12 @@ class OnionrCommunicatorDaemon: OnionrCommunicatorTimers(self, self.uploadBlock, 10, requiresPeer=True, maxThreads=1) OnionrCommunicatorTimers(self, self.daemonCommands, 6, maxThreads=1) OnionrCommunicatorTimers(self, self.detectAPICrash, 30, maxThreads=1) - OnionrCommunicatorTimers(self, servicecreator.service_creator, 5, maxThreads=10, myArgs=(self,)) + if config.get('general.socket_servers', False): + self.services = onionrservices.OnionrServices(self._core) + self.active_services = [] + OnionrCommunicatorTimers(self, servicecreator.service_creator, 5, maxThreads=10, myArgs=(self,)) + else: + self.services = None deniableBlockTimer = OnionrCommunicatorTimers(self, self.daemonTools.insertDeniableBlock, 180, requiresPeer=True, maxThreads=1) netCheckTimer = OnionrCommunicatorTimers(self, self.daemonTools.netCheck, 600) @@ -127,11 +132,6 @@ class OnionrCommunicatorDaemon: blockCleanupTimer.count = (blockCleanupTimer.frequency - 5) #forwardSecrecyTimer.count = (forwardSecrecyTimer.frequency - 990) - if config.get('general.socket_servers'): - self.services = onionrservices.OnionrServices(self._core) - else: - self.services = None - # Main daemon loop, mainly for calling timers, don't do any complex operations here to avoid locking try: while not self.shutdown: diff --git a/onionr/communicatorutils/servicecreator.py b/onionr/communicatorutils/servicecreator.py new file mode 100644 index 00000000..4914547a --- /dev/null +++ b/onionr/communicatorutils/servicecreator.py @@ -0,0 +1,17 @@ +import communicator, onionrblockapi +def service_creator(daemon): + assert isinstance(daemon, communicator.OnionrCommunicatorDaemon) + core = daemon._core + utils = core._utils + + # Find socket connection blocks + con_blocks = core.getBlocksByType('con') + for b in con_blocks: + if not b in daemon.active_services: + bl = onionrblockapi.Block(b, core=core, decrypt=True) + bs = utils.bytesToStr(bl.bcontent) + '.onion' + if utils.validatePubKey(bl.signer) and utils.validateID(bs): + daemon.active_services.append(b) + daemon.services.create_server(bl.signer, bs) + + daemon.decrementThreadCount('service_creator') \ No newline at end of file diff --git a/onionr/onionrservices/__init__.py b/onionr/onionrservices/__init__.py index 7194ea22..3f0e81e5 100644 --- a/onionr/onionrservices/__init__.py +++ b/onionr/onionrservices/__init__.py @@ -17,9 +17,8 @@ class OnionrServices: BOOTSTRAP_TRIES = 10 TRY_WAIT = 3 for x in range(BOOTSTRAP_TRIES): - if self._core._utils.doGetRequest('http://' + address + '/ping') == 'pong!': + if self._core._utils.doGetRequest('http://' + address + '/ping', port=self._core.config.get('tor.socksport')) == 'pong!': connectionserver.ConnectionServer(peer, address, core_inst=self._core) - return True else: time.sleep(TRY_WAIT) else: diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py index 5dafe0f3..33c4e8b4 100644 --- a/onionr/onionrservices/bootstrapservice.py +++ b/onionr/onionrservices/bootstrapservice.py @@ -55,8 +55,7 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): controller.authenticate(core_inst.config.get('tor.controlpassword')) # Create the v3 onion service #response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, await_publication = True, key_content = 'ED25519-V3') - response = controller.create_ephemeral_hidden_service(80, key_type = 'NEW', await_publication = True) - + response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, key_type = 'NEW', await_publication = True) core_inst.insertBlock(response.service_id, header='con', sign=True, encryptType='asym', asymPeer=peer, disableForward=True, expire=(core_inst._utils.getEpoch() + bootstrap_timeout)) diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index 86261502..2b9d47df 100755 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -4,7 +4,7 @@ "display_header" : false, "minimum_block_pow": 4, "minimum_send_pow": 4, - "socket_servers": false, + "socket_servers": true, "security_level": 0, "max_block_age": 2678400, "bypass_tor_check": false, From dfbb0a512ca95d587e8791f3cc0af56e405da4d8 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 24 Mar 2019 22:32:17 -0500 Subject: [PATCH 005/173] cleanup, work, and bugfixes for direct connections --- onionr/api.py | 7 ++-- onionr/onionrblockapi.py | 5 ++- onionr/onionrservices/__init__.py | 4 ++- onionr/onionrservices/bootstrapservice.py | 12 ++++--- onionr/onionrservices/connectionserver.py | 42 ++++++++++++++++++++--- 5 files changed, 57 insertions(+), 13 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 79694238..4f7276aa 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -39,7 +39,7 @@ class FDSafeHandler(WSGIHandler): except Timeout as ex: raise -def setBindIP(filePath): +def setBindIP(filePath, writeOut=True): '''Set a random localhost IP to a specified file (intended for private or public API localhost IPs)''' if config.get('general.random_bind_ip', True): hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))] @@ -55,8 +55,9 @@ def setBindIP(filePath): s.close() else: data = '127.0.0.1' - with open(filePath, 'w') as bindFile: - bindFile.write(data) + if writeOut: + with open(filePath, 'w') as bindFile: + bindFile.write(data) return data class PublicAPI: diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py index e7a4395b..1f890bcc 100755 --- a/onionr/onionrblockapi.py +++ b/onionr/onionrblockapi.py @@ -215,7 +215,10 @@ class Block: ''' if self.exists(): - os.remove(self.getBlockFile()) + try: + os.remove(self.getBlockFile()) + except TypeError: + pass self.getCore().removeBlock(self.getHash()) return True return False diff --git a/onionr/onionrservices/__init__.py b/onionr/onionrservices/__init__.py index 3f0e81e5..21ded6ec 100644 --- a/onionr/onionrservices/__init__.py +++ b/onionr/onionrservices/__init__.py @@ -16,8 +16,10 @@ class OnionrServices: assert self._core._utils.validateID(address) BOOTSTRAP_TRIES = 10 TRY_WAIT = 3 + base_url = 'http://%s/' % (address,) + socks = self._core.config.get('tor.socksport') for x in range(BOOTSTRAP_TRIES): - if self._core._utils.doGetRequest('http://' + address + '/ping', port=self._core.config.get('tor.socksport')) == 'pong!': + if self._core._utils.doGetRequest(base_url + 'ping', port=socks, ignoreAPI=True) == 'pong!': connectionserver.ConnectionServer(peer, address, core_inst=self._core) else: time.sleep(TRY_WAIT) diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py index 33c4e8b4..f1e0b35c 100644 --- a/onionr/onionrservices/bootstrapservice.py +++ b/onionr/onionrservices/bootstrapservice.py @@ -17,11 +17,12 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' +import time from gevent.pywsgi import WSGIServer, WSGIHandler from stem.control import Controller from flask import Flask import core -from netcontroller import getOpenPort +from netcontroller import getOpenPort def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): ''' @@ -38,6 +39,7 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): http_server = WSGIServer(('127.0.0.1', bootstrap_port), bootstrap_app, log=None) bootstrap_address = '' + shutdown = False @bootstrap_app.route('/ping') def get_ping(): @@ -48,20 +50,22 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): if core_inst._utils.validateID(address): # Set the bootstrap address then close the server bootstrap_address = address - http_server.stop() + shutdown = True + return "success" with Controller.from_port(port=core_inst.config.get('tor.controlPort')) as controller: # Connect to the Tor process for Onionr controller.authenticate(core_inst.config.get('tor.controlpassword')) # Create the v3 onion service - #response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, await_publication = True, key_content = 'ED25519-V3') response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, key_type = 'NEW', await_publication = True) core_inst.insertBlock(response.service_id, header='con', sign=True, encryptType='asym', asymPeer=peer, disableForward=True, expire=(core_inst._utils.getEpoch() + bootstrap_timeout)) # Run the bootstrap server - http_server.serve_forever() + threading.Thread(target=http_server.serve_forever).start() # This line reached when server is shutdown by being bootstrapped + while not shutdown and not core_inst.killSockets: + time.sleep(1) # Now that the bootstrap server has received a server, return the address return bootstrap_address diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index 32d2640a..60aff03e 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -1,5 +1,30 @@ -import stem, flask -import core +''' + Onionr - P2P Anonymous Storage Network + + This module does the second part of the bootstrap block handshake and creates the API server +''' +''' + 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 threading, time +from gevent.pywsgi import WSGIServer, WSGIHandler +from stem.control import Controller +from flask import Flask +import core, logger +from netcontroller import getOpenPort +from api import setBindIP + class ConnectionServer: def __init__(self, peer, address, core_inst=None): if core_inst is None: @@ -10,10 +35,15 @@ class ConnectionServer: if not core_inst._utils.validatePubKey(peer): raise ValueError('Peer must be valid base32 ed25519 public key') - service_app = flask.Flask(__name__) + socks = core_inst.config.get('tor.socksport') # Load config for Tor socks port for proxy + service_app = Flask(__name__) # Setup Flask app for server. service_port = getOpenPort() + service_ip = setBindIP() http_server = WSGIServer(('127.0.0.1', service_port), service_app, log=None) + + # TODO define basic endpoints useful for direct connections like stats + # TODO load endpoints from plugins @service_app.route('/ping') def get_ping(): return "pong!" @@ -23,5 +53,9 @@ class ConnectionServer: controller.authenticate(core_inst.config.get('tor.controlpassword')) # Create the v3 onion service response = controller.create_ephemeral_hidden_service({80: service_port}, await_publication = True, key_type='NEW') + self.core_inst._utils.doPostRequest('http://' + address + '/bs/' + response.service_id, port=socks) logger.info('hosting on ' + response.service_id) - http_server.serve_forever() \ No newline at end of file + threading.Thread(target=http_server.serve_forever).start() + while not self.core_inst.killSockets: + time.sleep(1) + http_server.stop() \ No newline at end of file From d3f57fe3e78c9f43841b40c1b7abe4877e30dfc6 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 25 Mar 2019 18:46:25 -0500 Subject: [PATCH 006/173] bug fixes for direct connnections --- onionr/api.py | 4 ++-- onionr/communicator.py | 2 +- onionr/onionrservices/bootstrapservice.py | 28 +++++++++++++---------- onionr/onionrservices/connectionserver.py | 11 ++++----- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 4f7276aa..e542bd35 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -39,7 +39,7 @@ class FDSafeHandler(WSGIHandler): except Timeout as ex: raise -def setBindIP(filePath, writeOut=True): +def setBindIP(filePath=''): '''Set a random localhost IP to a specified file (intended for private or public API localhost IPs)''' if config.get('general.random_bind_ip', True): hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))] @@ -55,7 +55,7 @@ def setBindIP(filePath, writeOut=True): s.close() else: data = '127.0.0.1' - if writeOut: + if filePath != '': with open(filePath, 'w') as bindFile: bindFile.write(data) return data diff --git a/onionr/communicator.py b/onionr/communicator.py index 670e08fa..bb56ca19 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -111,7 +111,7 @@ class OnionrCommunicatorDaemon: if config.get('general.socket_servers', False): self.services = onionrservices.OnionrServices(self._core) self.active_services = [] - OnionrCommunicatorTimers(self, servicecreator.service_creator, 5, maxThreads=10, myArgs=(self,)) + OnionrCommunicatorTimers(self, servicecreator.service_creator, 5, maxThreads=50, myArgs=(self,)) else: self.services = None deniableBlockTimer = OnionrCommunicatorTimers(self, self.daemonTools.insertDeniableBlock, 180, requiresPeer=True, maxThreads=1) diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py index f1e0b35c..95ae0158 100644 --- a/onionr/onionrservices/bootstrapservice.py +++ b/onionr/onionrservices/bootstrapservice.py @@ -17,10 +17,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import time +import time, threading, uuid from gevent.pywsgi import WSGIServer, WSGIHandler from stem.control import Controller -from flask import Flask +from flask import Flask, Response import core from netcontroller import getOpenPort @@ -40,6 +40,7 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): bootstrap_address = '' shutdown = False + bs_id = str(uuid.uuid4()) @bootstrap_app.route('/ping') def get_ping(): @@ -47,25 +48,28 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): @bootstrap_app.route('/bs/
', methods=['POST']) def get_bootstrap(address): - if core_inst._utils.validateID(address): + if core_inst._utils.validateID(address + '.onion'): # Set the bootstrap address then close the server - bootstrap_address = address - shutdown = True - return "success" + bootstrap_address = address + '.onion' + core_inst.keyStore.put(bs_id, bootstrap_address) + http_server.stop() + return Response("success") + else: + return Response("") with Controller.from_port(port=core_inst.config.get('tor.controlPort')) as controller: # Connect to the Tor process for Onionr controller.authenticate(core_inst.config.get('tor.controlpassword')) # Create the v3 onion service - response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, key_type = 'NEW', await_publication = True) + response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, key_type = 'NEW', key_content = 'ED25519-V3', await_publication = True) core_inst.insertBlock(response.service_id, header='con', sign=True, encryptType='asym', asymPeer=peer, disableForward=True, expire=(core_inst._utils.getEpoch() + bootstrap_timeout)) - # Run the bootstrap server - threading.Thread(target=http_server.serve_forever).start() + try: + http_server.serve_forever() + except TypeError: + pass # This line reached when server is shutdown by being bootstrapped - while not shutdown and not core_inst.killSockets: - time.sleep(1) # Now that the bootstrap server has received a server, return the address - return bootstrap_address + return core_inst.keyStore.get(bs_id) diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index 60aff03e..d8a5f773 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -23,7 +23,7 @@ from stem.control import Controller from flask import Flask import core, logger from netcontroller import getOpenPort -from api import setBindIP +import api class ConnectionServer: def __init__(self, peer, address, core_inst=None): @@ -38,9 +38,8 @@ class ConnectionServer: socks = core_inst.config.get('tor.socksport') # Load config for Tor socks port for proxy service_app = Flask(__name__) # Setup Flask app for server. service_port = getOpenPort() - service_ip = setBindIP() + service_ip = api.setBindIP() http_server = WSGIServer(('127.0.0.1', service_port), service_app, log=None) - # TODO define basic endpoints useful for direct connections like stats # TODO load endpoints from plugins @@ -52,10 +51,8 @@ class ConnectionServer: # Connect to the Tor process for Onionr controller.authenticate(core_inst.config.get('tor.controlpassword')) # Create the v3 onion service - response = controller.create_ephemeral_hidden_service({80: service_port}, await_publication = True, key_type='NEW') + response = controller.create_ephemeral_hidden_service({80: service_port}, await_publication = True, key_type='NEW', key_content = 'ED25519-V3') self.core_inst._utils.doPostRequest('http://' + address + '/bs/' + response.service_id, port=socks) logger.info('hosting on ' + response.service_id) - threading.Thread(target=http_server.serve_forever).start() - while not self.core_inst.killSockets: - time.sleep(1) + http_server.serve_forever() http_server.stop() \ No newline at end of file From c855a2ea6bc72115c88058487399e47e212d2e53 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 25 Mar 2019 23:25:46 -0500 Subject: [PATCH 007/173] more work on direct connections --- onionr/api.py | 17 +++-------------- onionr/communicator.py | 4 +++- onionr/onionrservices/bootstrapservice.py | 6 ++++++ onionr/onionrservices/connectionserver.py | 3 ++- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index e542bd35..7e552355 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -27,6 +27,7 @@ from onionrblockapi import Block import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config import httpapi from httpapi import friendsapi, simplecache +from onionrservices import httpheaders import onionr class FDSafeHandler(WSGIHandler): @@ -92,17 +93,9 @@ class PublicAPI: @app.after_request def sendHeaders(resp): '''Send api, access control headers''' - resp.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch, since we can't fully remove the header. - # CSP to prevent XSS. Mainly for client side attacks (if hostname protection could somehow be bypassed) - resp.headers["Content-Security-Policy"] = "default-src 'none'; script-src 'none'; object-src 'none'; style-src data: 'unsafe-inline'; img-src data:; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'none'" - # Prevent click jacking - resp.headers['X-Frame-Options'] = 'deny' - # No sniff is possibly not needed - resp.headers['X-Content-Type-Options'] = "nosniff" + resp = httpheaders.set_default_onionr_http_headers(resp) # Network API version resp.headers['X-API'] = onionr.API_VERSION - # Close connections to limit FD use - resp.headers['Connection'] = "close" self.lastRequest = clientAPI._core._utils.getRoundedEpoch(roundS=5) return resp @@ -300,15 +293,11 @@ class API: @app.after_request def afterReq(resp): # Security headers + resp = httpheaders.set_default_onionr_http_headers(resp) if request.endpoint == 'site': resp.headers['Content-Security-Policy'] = "default-src 'none'; style-src data: 'unsafe-inline'; img-src data:" else: resp.headers['Content-Security-Policy'] = "default-src 'none'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'self'" - resp.headers['X-Frame-Options'] = 'deny' - resp.headers['X-Content-Type-Options'] = "nosniff" - resp.headers['Server'] = '' - resp.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch. - resp.headers['Connection'] = "close" return resp @app.route('/board/', endpoint='board') diff --git a/onionr/communicator.py b/onionr/communicator.py index bb56ca19..47f8e5d6 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -111,6 +111,7 @@ class OnionrCommunicatorDaemon: if config.get('general.socket_servers', False): self.services = onionrservices.OnionrServices(self._core) self.active_services = [] + self.service_greenlets = [] OnionrCommunicatorTimers(self, servicecreator.service_creator, 5, maxThreads=50, myArgs=(self,)) else: self.services = None @@ -148,7 +149,8 @@ class OnionrCommunicatorDaemon: pass logger.info('Goodbye.') - self._core.killSockets = True + for server in self.service_greenlets: + server.stop() self._core._utils.localCommand('shutdown') # shutdown the api time.sleep(0.5) diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py index 95ae0158..292df4ed 100644 --- a/onionr/onionrservices/bootstrapservice.py +++ b/onionr/onionrservices/bootstrapservice.py @@ -37,6 +37,12 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): bootstrap_port = getOpenPort() bootstrap_app = Flask(__name__) http_server = WSGIServer(('127.0.0.1', bootstrap_port), bootstrap_app, log=None) + try: + core_inst.onionrInst.communicatorInst + except AttributeError: + pass + else: + core_inst.onionrInst.communicatorInst.service_greenlets.append(http_server) bootstrap_address = '' shutdown = False diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index d8a5f773..6b04645c 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -40,6 +40,7 @@ class ConnectionServer: service_port = getOpenPort() service_ip = api.setBindIP() http_server = WSGIServer(('127.0.0.1', service_port), service_app, log=None) + core_inst.onionrInst.communicatorInst.service_greenlets.append(http_server) # TODO define basic endpoints useful for direct connections like stats # TODO load endpoints from plugins @@ -53,6 +54,6 @@ class ConnectionServer: # Create the v3 onion service response = controller.create_ephemeral_hidden_service({80: service_port}, await_publication = True, key_type='NEW', key_content = 'ED25519-V3') self.core_inst._utils.doPostRequest('http://' + address + '/bs/' + response.service_id, port=socks) - logger.info('hosting on ' + response.service_id) + logger.info('hosting on %s with %s' % (response.service_id, peer)) http_server.serve_forever() http_server.stop() \ No newline at end of file From 15e321feeee2d2672c3fa6198e5b4008d4c643a0 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 26 Mar 2019 10:23:31 -0500 Subject: [PATCH 008/173] added httpheaders.py --- onionr/onionrservices/httpheaders.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 onionr/onionrservices/httpheaders.py diff --git a/onionr/onionrservices/httpheaders.py b/onionr/onionrservices/httpheaders.py new file mode 100644 index 00000000..2b2c1770 --- /dev/null +++ b/onionr/onionrservices/httpheaders.py @@ -0,0 +1,28 @@ +''' + Onionr - P2P Anonymous Storage Network + + Set default onionr http headers +''' +''' + 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 . +''' +def set_default_onionr_http_headers(flask_response): + '''Response headers''' + flask_response.headers['Content-Security-Policy'] = "default-src 'none'; style-src data: 'unsafe-inline'; img-src data:" + flask_response.headers['X-Frame-Options'] = 'deny' + flask_response.headers['X-Content-Type-Options'] = "nosniff" + flask_response.headers['Server'] = '' + flask_response.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch. + flask_response.headers['Connection'] = "close" + return flask_response \ No newline at end of file From fb32201df3c374021b2614dabc9105979386a035 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 27 Mar 2019 12:38:46 -0500 Subject: [PATCH 009/173] added clandestine --- onionr/onionrservices/bootstrapservice.py | 7 + onionr/onionrservices/connectionserver.py | 7 + onionr/onionrsockets.py | 172 ------------------ .../default-plugins/clandestine/controlapi.py | 26 +++ .../default-plugins/clandestine/info.json | 5 + .../default-plugins/clandestine/main.py | 46 +++++ 6 files changed, 91 insertions(+), 172 deletions(-) delete mode 100755 onionr/onionrsockets.py create mode 100644 onionr/static-data/default-plugins/clandestine/controlapi.py create mode 100755 onionr/static-data/default-plugins/clandestine/info.json create mode 100755 onionr/static-data/default-plugins/clandestine/main.py diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py index 292df4ed..a2952563 100644 --- a/onionr/onionrservices/bootstrapservice.py +++ b/onionr/onionrservices/bootstrapservice.py @@ -23,6 +23,7 @@ from stem.control import Controller from flask import Flask, Response import core from netcontroller import getOpenPort +from . import httpheaders def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): ''' @@ -52,6 +53,12 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): def get_ping(): return "pong!" + @bootstrap_app.after_request + def afterReq(resp): + # Security headers + resp = httpheaders.set_default_onionr_http_headers(resp) + return resp + @bootstrap_app.route('/bs/
', methods=['POST']) def get_bootstrap(address): if core_inst._utils.validateID(address + '.onion'): diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index 6b04645c..ee586e2b 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -24,6 +24,7 @@ from flask import Flask import core, logger from netcontroller import getOpenPort import api +from . import httpheaders class ConnectionServer: def __init__(self, peer, address, core_inst=None): @@ -48,6 +49,12 @@ class ConnectionServer: def get_ping(): return "pong!" + @service_app.after_request + def afterReq(resp): + # Security headers + resp = httpheaders.set_default_onionr_http_headers(resp) + return resp + with Controller.from_port(port=core_inst.config.get('tor.controlPort')) as controller: # Connect to the Tor process for Onionr controller.authenticate(core_inst.config.get('tor.controlpassword')) diff --git a/onionr/onionrsockets.py b/onionr/onionrsockets.py deleted file mode 100755 index aa02c3db..00000000 --- a/onionr/onionrsockets.py +++ /dev/null @@ -1,172 +0,0 @@ -''' - Onionr - P2P Anonymous Storage Network - - Onionr Socket interface -''' -''' - 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 stem.control -import threading -import socks, config, uuid -import onionrexceptions, time, requests, onionrblockapi, logger -from dependencies import secrets -from gevent.pywsgi import WSGIServer -from flask import request, Response, abort -import flask -class OnionrSocketServer: - def __init__(self, coreInst): - self._core = coreInst - app = flask.Flask(__name__) - self._core.socketServerConnData = {} - self.bindPort = 0 - - self.sockets = {} - - while self.bindPort < 1024: - self.bindPort = secrets.randbelow(65535) - - self.responseData = {} - - threading.Thread(target=self.detectShutdown).start() - threading.Thread(target=self.socketStarter).start() - app = flask.Flask(__name__) - self.http_server = WSGIServer(('127.0.0.1', self.bindPort), app) - self.http_server.serve_forever() - - @app.route('/dc/', methods=['POST']) - def acceptConn(self): - data = request.form['data'] - data = self._core._utils.bytesTorStr(data) - data = {'date': self._core._utils.getEpoch(), 'data': data} - myPeer = '' - retData = '' - for peer in self.sockets: - if self.sockets[peer] == request.host: - myPeer = peer - break - else: - return "" - - if request.host in self.sockets: - self._core.socketServerConnData[myPeer].append(data) - else: - self._core.socketServerConnData[myPeer] = [data] - - try: - retData = self._core.socketServerResponseData[myPeer] - except KeyError: - pass - else: - self._core.socketServerResponseData[myPeer] = '' - - return retData - - def socketStarter(self): - while not self._core.killSockets: - try: - self.addSocket(self._core.startSocket['peer'], reason=self._core.startSocket['reason']) - except KeyError: - pass - else: - logger.info('%s socket started with %s' % (self._core.startSocket['reason'], self._core.startSocket['peer'])) - self._core.startSocket = {} - time.sleep(1) - - def detectShutdown(self): - while not self._core.killSockets: - time.sleep(5) - - logger.debug('Killing socket server...') - self.http_server.stop() - - def addSocket(self, peer, reason=''): - bindPort = 1337 - - assert len(reason) <= 12 - - with stem.control.Controller.from_port(port=config.get('tor.controlPort')) as controller: - controller.authenticate(config.get('tor.controlpassword')) - - socket = controller.create_ephemeral_hidden_service({80: bindPort}, await_publication = True) - self.sockets[peer] = socket.service_id + '.onion' - - self.responseData[socket.service_id + '.onion'] = '' - - self._core.insertBlock(str(uuid.uuid4()), header='socket', sign=True, encryptType='asym', asymPeer=peer, meta={'reason': reason, 'address': socket.service_id + '.onion'}) - self._core.socketReasons[peer] = reason - return - -class OnionrSocketClient: - def __init__(self, coreInst): - self.sockets = {} # pubkey: tor address - self.connPool = {} - self.sendData = {} - self._core = coreInst - self.response = '' - self.request = '' - self.connected = False - self.killSocket = False - - def startSocket(self, peer, reason): - address = '' - logger.info('Trying to find socket server for %s' % (peer,)) - # Find the newest open socket for a given peer - for block in self._core.getBlocksByType('socket'): - block = onionrblockapi.Block(block, core=self._core) - if block.decrypt(): - theSigner = block.signer - try: - theSigner = theSigner.decode() - except AttributeError: - pass - if block.verifySig() and theSigner == peer: - address = block.getMetadata('address') - if self._core._utils.validateID(address): - # If we got their address, it is valid, and verified, we can break out - if block.getMetadata('reason') == reason: - break - else: - logger.error('The socket the peer opened is not for %s' % (reason,)) - else: - logger.error('Peer transport id is invalid for socket: %s' % (address,)) - address = '' - else: - logger.warn('Block has invalid sig or id, was for %s' % (theSigner,)) - if address != '': - logger.info('%s socket client started with %s' % (reason, peer)) - self.sockets[peer] = address - data = 'hey' - while not self.killSocket: - try: - data = self.sendData[peer] - logger.info('Sending %s to %s' % (data, peer)) - except KeyError: - pass - else: - self.sendData[peer] = '' - postData = {'data': data} - self.connPool[peer] = {'date': self._core._utils.getEpoch(), 'data': self._core._utils.doPostRequest('http://' + address + '/dc/', data=postData)} - time.sleep(2) - - def getResponse(self, peer): - retData = '' - try: - retData = self.connPool[peer] - except KeyError: - pass - return - - def sendData(self, peer, data): - self.sendData[peer] = data diff --git a/onionr/static-data/default-plugins/clandestine/controlapi.py b/onionr/static-data/default-plugins/clandestine/controlapi.py new file mode 100644 index 00000000..ce341f82 --- /dev/null +++ b/onionr/static-data/default-plugins/clandestine/controlapi.py @@ -0,0 +1,26 @@ +''' + Onionr - P2P Anonymous Storage Network + + HTTP endpoints for controlling IMs +''' +''' + 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 . +''' +from flask import Response, request, redirect, Blueprint, abort + +flask_blueprint = Blueprint('clandenstine', __name__) + +@flask_blueprint.route('/mail/ping') +def ping(): + return 'pong!' diff --git a/onionr/static-data/default-plugins/clandestine/info.json b/onionr/static-data/default-plugins/clandestine/info.json new file mode 100755 index 00000000..7c5a143c --- /dev/null +++ b/onionr/static-data/default-plugins/clandestine/info.json @@ -0,0 +1,5 @@ +{ + "name" : "clandestine", + "version" : "1.0", + "author" : "onionr" +} diff --git a/onionr/static-data/default-plugins/clandestine/main.py b/onionr/static-data/default-plugins/clandestine/main.py new file mode 100755 index 00000000..bc7ca112 --- /dev/null +++ b/onionr/static-data/default-plugins/clandestine/main.py @@ -0,0 +1,46 @@ +''' + Onionr - P2P Anonymous Storage Network + + Instant message conversations with Onionr peers +''' +''' + 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 . +''' + +# Imports some useful libraries +import locale + +locale.setlocale(locale.LC_ALL, '') + +plugin_name = 'clandenstine' +PLUGIN_VERSION = '0.0.0' + +sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) +from . import controlapi +flask_blueprint = controlapi.flask_blueprint + +class Clandenstine: + def __init__(self, pluginapi): + self.myCore = pluginapi.get_core() + +def on_init(api, data = None): + ''' + This event is called after Onionr is initialized, but before the command + inputted is executed. Could be called when daemon is starting or when + just the client is running. + ''' + + pluginapi = api + chat = Clandenstine(pluginapi) + return From e1c62fee5bc342e467f6e46ef0748f61189214d9 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 27 Mar 2019 13:55:43 -0500 Subject: [PATCH 010/173] improved expiry --- onionr/communicator.py | 9 +++++++-- onionr/etc/onionrvalues.py | 3 ++- onionr/onionrutils.py | 10 +++++----- onionr/static-data/bootstrap-nodes.txt | 1 + onionr/static-data/default-plugins/clandestine/main.py | 2 +- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/onionr/communicator.py b/onionr/communicator.py index 47f8e5d6..ef94ae14 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -149,8 +149,13 @@ class OnionrCommunicatorDaemon: pass logger.info('Goodbye.') - for server in self.service_greenlets: - server.stop() + try: + self.service_greenlets + except AttributeError: + pass + else: + for server in self.service_greenlets: + server.stop() self._core._utils.localCommand('shutdown') # shutdown the api time.sleep(0.5) diff --git a/onionr/etc/onionrvalues.py b/onionr/etc/onionrvalues.py index 84203d88..338dc0d3 100755 --- a/onionr/etc/onionrvalues.py +++ b/onionr/etc/onionrvalues.py @@ -21,4 +21,5 @@ class OnionrValues: def __init__(self): self.passwordLength = 20 - self.blockMetadataLengths = {'meta': 1000, 'sig': 200, 'signer': 200, 'time': 10, 'powRandomToken': 1000, 'encryptType': 4, 'expire': 14} #TODO properly refine values to minimum needed \ No newline at end of file + self.blockMetadataLengths = {'meta': 1000, 'sig': 200, 'signer': 200, 'time': 10, 'powRandomToken': 1000, 'encryptType': 4, 'expire': 14} #TODO properly refine values to minimum needed + self.default_expire = 2678400 \ No newline at end of file diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index b9ae61c4..ea56b593 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -25,7 +25,7 @@ import onionrexceptions from onionr import API_VERSION import onionrevents import storagecounter -from etc import pgpwords +from etc import pgpwords, onionrvalues from onionrusers import onionrusers if sys.version_info < (3, 6): try: @@ -179,8 +179,8 @@ class OnionrUtils: expireTime = myBlock.getHeader('expire') assert len(str(int(expireTime))) < 20 # test that expire time is an integer of sane length (for epoch) except (AssertionError, ValueError, TypeError) as e: - pass - else: + expireTime = onionrvalues.OnionrValues().default_expire + finally: self._core.updateBlockInfo(blockHash, 'expire', expireTime) if not blockType is None: self._core.updateBlockInfo(blockHash, 'dataType', blockType) @@ -253,7 +253,7 @@ class OnionrUtils: pass # Validate metadata dict for invalid keys to sizes that are too large - maxAge = config.get("general.max_block_age", 2678400) + maxAge = config.get("general.max_block_age", onionrvalues.OnionrValues().default_expire) if type(metadata) is dict: for i in metadata: try: @@ -285,7 +285,7 @@ class OnionrUtils: try: assert int(metadata[i]) > self.getEpoch() except AssertionError: - logger.warn('Block is expired: %s greater than %s' % (metadata[i], self.getEpoch())) + logger.warn('Block is expired: %s less than %s' % (metadata[i], self.getEpoch())) break elif i == 'encryptType': try: diff --git a/onionr/static-data/bootstrap-nodes.txt b/onionr/static-data/bootstrap-nodes.txt index e69de29b..374ea7b4 100755 --- a/onionr/static-data/bootstrap-nodes.txt +++ b/onionr/static-data/bootstrap-nodes.txt @@ -0,0 +1 @@ +huei7fugyuesltkf3yzj27an36skz2f2u55g7ganyaq6yjhauwicwfyd.onion diff --git a/onionr/static-data/default-plugins/clandestine/main.py b/onionr/static-data/default-plugins/clandestine/main.py index bc7ca112..93bfd2d3 100755 --- a/onionr/static-data/default-plugins/clandestine/main.py +++ b/onionr/static-data/default-plugins/clandestine/main.py @@ -19,7 +19,7 @@ ''' # Imports some useful libraries -import locale +import locale, sys, os locale.setlocale(locale.LC_ALL, '') From 815f1b1d4ed43cb01bb2a19633473f6a8dd9c9a7 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 29 Mar 2019 12:37:51 -0500 Subject: [PATCH 011/173] work on clandestine --- onionr/communicatorutils/onionrdaemontools.py | 2 +- onionr/communicatorutils/servicecreator.py | 1 + onionr/core.py | 6 +-- onionr/etc/onionrvalues.py | 2 +- onionr/httpapi/__init__.py | 4 +- onionr/onionrcommands/pubkeymanager.py | 4 +- onionr/onionrservices/connectionserver.py | 8 +++- .../default-plugins/clandestine/controlapi.py | 2 +- .../default-plugins/clandestine/main.py | 9 ++-- .../default-plugins/clandestine/peerserver.py | 42 +++++++++++++++++++ 10 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 onionr/static-data/default-plugins/clandestine/peerserver.py diff --git a/onionr/communicatorutils/onionrdaemontools.py b/onionr/communicatorutils/onionrdaemontools.py index b68c693d..1c4e0220 100755 --- a/onionr/communicatorutils/onionrdaemontools.py +++ b/onionr/communicatorutils/onionrdaemontools.py @@ -98,7 +98,7 @@ class DaemonTools: if len(self.daemon.onlinePeers) == 0: if not netutils.checkNetwork(self.daemon._core._utils, torPort=self.daemon.proxyPort): if not self.daemon.shutdown: - logger.warn('Network check failed, are you connected to the internet?') + logger.warn('Network check failed, are you connected to the Internet, and is Tor working?') self.daemon.isOnline = False else: self.daemon.isOnline = True diff --git a/onionr/communicatorutils/servicecreator.py b/onionr/communicatorutils/servicecreator.py index 4914547a..ecd1543c 100644 --- a/onionr/communicatorutils/servicecreator.py +++ b/onionr/communicatorutils/servicecreator.py @@ -12,6 +12,7 @@ def service_creator(daemon): bs = utils.bytesToStr(bl.bcontent) + '.onion' if utils.validatePubKey(bl.signer) and utils.validateID(bs): daemon.active_services.append(b) + daemon.active_services.append(bl.signer) daemon.services.create_server(bl.signer, bs) daemon.decrementThreadCount('service_creator') \ No newline at end of file diff --git a/onionr/core.py b/onionr/core.py index 0c8e1527..09b589f0 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -796,11 +796,11 @@ class Core: retData = False else: # Tell the api server through localCommand to wait for the daemon to upload this block to make stastical analysis more difficult - self._utils.localCommand('/waitforshare/' + retData, post=True) + if self._utils.localCommand('/ping', maxWait=10) == 'pong!': + self._utils.localCommand('/waitforshare/' + retData, post=True, maxWait=5) + self.daemonQueueAdd('uploadBlock', retData) self.addToBlockDB(retData, selfInsert=True, dataSaved=True) - #self.setBlockType(retData, meta['type']) self._utils.processBlockMetadata(retData) - self.daemonQueueAdd('uploadBlock', retData) if retData != False: if plaintextPeer == 'OVPCZLOXD6DC5JHX4EQ3PSOGAZ3T24F75HQLIUZSDSMYPEOXCPFA====': diff --git a/onionr/etc/onionrvalues.py b/onionr/etc/onionrvalues.py index 338dc0d3..8627d246 100755 --- a/onionr/etc/onionrvalues.py +++ b/onionr/etc/onionrvalues.py @@ -22,4 +22,4 @@ class OnionrValues: def __init__(self): self.passwordLength = 20 self.blockMetadataLengths = {'meta': 1000, 'sig': 200, 'signer': 200, 'time': 10, 'powRandomToken': 1000, 'encryptType': 4, 'expire': 14} #TODO properly refine values to minimum needed - self.default_expire = 2678400 \ No newline at end of file + self.default_expire = 2592000 \ No newline at end of file diff --git a/onionr/httpapi/__init__.py b/onionr/httpapi/__init__.py index 018d3abb..f38d80bd 100755 --- a/onionr/httpapi/__init__.py +++ b/onionr/httpapi/__init__.py @@ -19,11 +19,11 @@ ''' import onionrplugins -def load_plugin_blueprints(flaskapp): +def load_plugin_blueprints(flaskapp, blueprint='flask_blueprint'): '''Iterate enabled plugins and load any http endpoints they have''' for plugin in onionrplugins.get_enabled_plugins(): plugin = onionrplugins.get_plugin(plugin) try: - flaskapp.register_blueprint(getattr(plugin, 'flask_blueprint')) + flaskapp.register_blueprint(getattr(plugin, blueprint)) except AttributeError: pass \ No newline at end of file diff --git a/onionr/onionrcommands/pubkeymanager.py b/onionr/onionrcommands/pubkeymanager.py index ea55a3fe..00d17175 100644 --- a/onionr/onionrcommands/pubkeymanager.py +++ b/onionr/onionrcommands/pubkeymanager.py @@ -19,7 +19,7 @@ ''' import sys, getpass -import logger +import logger, onionrexceptions from onionrusers import onionrusers def add_ID(o_inst): try: @@ -38,7 +38,7 @@ def add_ID(o_inst): logger.info('Generating deterministic key. This can take a while.') newID, privKey = o_inst.onionrCore._crypto.generateDeterministic(pass1) except onionrexceptions.PasswordStrengthError: - logger.error('Must use at least 25 characters.') + logger.error('Passphrase must use at least %s characters.' % (o_inst.onionrCore._crypto.deterministicRequirement,)) sys.exit(1) else: logger.error('Passwords do not match.') diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index ee586e2b..ad34cae1 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -21,7 +21,7 @@ import threading, time from gevent.pywsgi import WSGIServer, WSGIHandler from stem.control import Controller from flask import Flask -import core, logger +import core, logger, httpapi from netcontroller import getOpenPort import api from . import httpheaders @@ -44,7 +44,9 @@ class ConnectionServer: core_inst.onionrInst.communicatorInst.service_greenlets.append(http_server) # TODO define basic endpoints useful for direct connections like stats - # TODO load endpoints from plugins + + httpapi.load_plugin_blueprints(service_app, blueprint='direct_blueprint') + @service_app.route('/ping') def get_ping(): return "pong!" @@ -60,7 +62,9 @@ class ConnectionServer: controller.authenticate(core_inst.config.get('tor.controlpassword')) # Create the v3 onion service response = controller.create_ephemeral_hidden_service({80: service_port}, await_publication = True, key_type='NEW', key_content = 'ED25519-V3') + self.core_inst.keyStore.put('dc-' + response.service_id, self.core_inst._utils.bytesToStr(peer)) self.core_inst._utils.doPostRequest('http://' + address + '/bs/' + response.service_id, port=socks) logger.info('hosting on %s with %s' % (response.service_id, peer)) http_server.serve_forever() + self.core_inst.keyStore.delete('dc-' + response.service_id) http_server.stop() \ No newline at end of file diff --git a/onionr/static-data/default-plugins/clandestine/controlapi.py b/onionr/static-data/default-plugins/clandestine/controlapi.py index ce341f82..5631a2a4 100644 --- a/onionr/static-data/default-plugins/clandestine/controlapi.py +++ b/onionr/static-data/default-plugins/clandestine/controlapi.py @@ -21,6 +21,6 @@ from flask import Response, request, redirect, Blueprint, abort flask_blueprint = Blueprint('clandenstine', __name__) -@flask_blueprint.route('/mail/ping') +@flask_blueprint.route('/clandenstine/ping') def ping(): return 'pong!' diff --git a/onionr/static-data/default-plugins/clandestine/main.py b/onionr/static-data/default-plugins/clandestine/main.py index 93bfd2d3..9a1e75a2 100755 --- a/onionr/static-data/default-plugins/clandestine/main.py +++ b/onionr/static-data/default-plugins/clandestine/main.py @@ -23,14 +23,15 @@ import locale, sys, os locale.setlocale(locale.LC_ALL, '') -plugin_name = 'clandenstine' +plugin_name = 'clandestine' PLUGIN_VERSION = '0.0.0' sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) -from . import controlapi +from . import controlapi, peerserver flask_blueprint = controlapi.flask_blueprint +direct_blueprint = peerserver.direct_blueprint -class Clandenstine: +class Clandestine: def __init__(self, pluginapi): self.myCore = pluginapi.get_core() @@ -42,5 +43,5 @@ def on_init(api, data = None): ''' pluginapi = api - chat = Clandenstine(pluginapi) + chat = Clandestine(pluginapi) return diff --git a/onionr/static-data/default-plugins/clandestine/peerserver.py b/onionr/static-data/default-plugins/clandestine/peerserver.py new file mode 100644 index 00000000..e10fa016 --- /dev/null +++ b/onionr/static-data/default-plugins/clandestine/peerserver.py @@ -0,0 +1,42 @@ +''' + Onionr - P2P Anonymous Storage Network + + HTTP endpoints for communicating with peers +''' +''' + 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 core +from flask import Response, request, redirect, Blueprint, abort, g + +direct_blueprint = Blueprint('clandestine', __name__) +core_inst = core.Core() + +storage_dir = core_inst.dataDir + +@direct_blueprint.before_request +def request_setup(): + core_inst.keyStore.refresh() + host = request.host + host = host.strip('.b32.i2p') + host = host.strip('.onion') + g.host = host + g.peer = core_inst.keyStore.get('dc-' + g.host) + +@direct_blueprint.route('/clandestine/ping') +def pingdirect(): + return 'pong!' + g.peer + +@direct_blueprint.route('/clandestine/send') +def poll_chat \ No newline at end of file From 063330cfee16554a872a7dd9cf51c043b46aad31 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 30 Mar 2019 23:50:54 -0500 Subject: [PATCH 012/173] basic cs messages supported --- .../default-plugins/clandestine/controlapi.py | 51 +++++++++++++++++-- .../default-plugins/clandestine/peerserver.py | 21 ++++++-- 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/onionr/static-data/default-plugins/clandestine/controlapi.py b/onionr/static-data/default-plugins/clandestine/controlapi.py index 5631a2a4..f203c0af 100644 --- a/onionr/static-data/default-plugins/clandestine/controlapi.py +++ b/onionr/static-data/default-plugins/clandestine/controlapi.py @@ -17,10 +17,55 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -from flask import Response, request, redirect, Blueprint, abort +import json +from flask import Response, request, redirect, Blueprint, g +import core -flask_blueprint = Blueprint('clandenstine', __name__) +core_inst = core.Core() +flask_blueprint = Blueprint('clandestine_control', __name__) -@flask_blueprint.route('/clandenstine/ping') +@flask_blueprint.route('/clandestine/ping') def ping(): return 'pong!' + +@flask_blueprint.route('/clandestine/send/', methods=['POST']) +def send_message(peer): + data = request.get_json(force=True) + core_inst.keyStore.refresh() + existing = core_inst.keyStore.get('s' + peer) + if existing is None: + existing = [] + existing.append(data) + core_inst.keyStore.put('s' + peer, existing) + core_inst.keyStore.flush() + return Response('success') + +@flask_blueprint.route('/clandestine/gets/') +def get_sent(peer): + sent = core_inst.keyStore.get('s' + peer) + if sent is None: + sent = [] + return Response(json.dumps(sent)) + +@flask_blueprint.route('/clandestine/addrec/', methods=['POST']) +def add_rec(peer): + data = request.get_json(force=True) + core_inst.keyStore.refresh() + existing = core_inst.keyStore.get('r' + peer) + if existing is None: + existing = [] + existing.append(data) + core_inst.keyStore.put('r' + peer, existing) + core_inst.keyStore.flush() + return Response('success') + +@flask_blueprint.route('/clandestine/getrec/') +def get_messages(peer): + core_inst.keyStore.refresh() + existing = core_inst.keyStore.get('r' + peer) + if existing is None: + existing = [] + else: + existing = list(existing) + core_inst.keyStore.delete('r' + peer) + return Response(json.dumps(existing)) \ No newline at end of file diff --git a/onionr/static-data/default-plugins/clandestine/peerserver.py b/onionr/static-data/default-plugins/clandestine/peerserver.py index e10fa016..b5fb3913 100644 --- a/onionr/static-data/default-plugins/clandestine/peerserver.py +++ b/onionr/static-data/default-plugins/clandestine/peerserver.py @@ -17,9 +17,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' +import sys, os, json import core from flask import Response, request, redirect, Blueprint, abort, g - +sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) direct_blueprint = Blueprint('clandestine', __name__) core_inst = core.Core() @@ -36,7 +37,19 @@ def request_setup(): @direct_blueprint.route('/clandestine/ping') def pingdirect(): - return 'pong!' + g.peer + return 'pong!' -@direct_blueprint.route('/clandestine/send') -def poll_chat \ No newline at end of file +@direct_blueprint.route('/clandestine/sendto', methods=['POST', 'GET']) +def sendto(): + try: + msg = request.get_json(force=True) + except: + msg = '' + if msg == None or msg == '': + msg = json.dumps({'m': 'hello world', 't': core_inst._utils.getEpoch()}) + core_inst._utils.localCommand('/clandestine/addrec/%s' % (g.peer,), post=True, postData=msg) + return Response('success') + +@direct_blueprint.route('/clandestine/poll') +def poll_chat(): + return Response(core_inst._utils.localCommand('/clandestine/gets/%s' % (g.peer,))) \ No newline at end of file From faf42071eb38f67420697e856c2eb16333dc00b7 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 31 Mar 2019 12:16:09 -0500 Subject: [PATCH 013/173] plugin example of connecting to dir conn --- onionr/onionrservices/bootstrapservice.py | 4 ++-- onionr/onionrservices/connectionserver.py | 6 +++++ .../default-plugins/clandestine/main.py | 24 ++++++++++++++++++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py index a2952563..160b716d 100644 --- a/onionr/onionrservices/bootstrapservice.py +++ b/onionr/onionrservices/bootstrapservice.py @@ -39,8 +39,8 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): bootstrap_app = Flask(__name__) http_server = WSGIServer(('127.0.0.1', bootstrap_port), bootstrap_app, log=None) try: - core_inst.onionrInst.communicatorInst - except AttributeError: + assert core_inst.onionrInst.communicatorInst is not None + except (AttributeError, AssertionError) as e: pass else: core_inst.onionrInst.communicatorInst.service_greenlets.append(http_server) diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index ad34cae1..dea9f393 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -50,6 +50,12 @@ class ConnectionServer: @service_app.route('/ping') def get_ping(): return "pong!" + + @service_app.route('/shutdown') + def shutdown_server(): + core_inst.onionrInst.communicatorInst.service_greenlets.remove(http_server) + http_server.stop() + return Response('goodbye') @service_app.after_request def afterReq(resp): diff --git a/onionr/static-data/default-plugins/clandestine/main.py b/onionr/static-data/default-plugins/clandestine/main.py index 9a1e75a2..3fab2dd0 100755 --- a/onionr/static-data/default-plugins/clandestine/main.py +++ b/onionr/static-data/default-plugins/clandestine/main.py @@ -20,8 +20,9 @@ # Imports some useful libraries import locale, sys, os - locale.setlocale(locale.LC_ALL, '') +import onionrservices, logger +from onionrservices import bootstrapservice plugin_name = 'clandestine' PLUGIN_VERSION = '0.0.0' @@ -31,9 +32,29 @@ from . import controlapi, peerserver flask_blueprint = controlapi.flask_blueprint direct_blueprint = peerserver.direct_blueprint +def exit_with_error(text=''): + if text != '': + logger.error(text) + sys.exit(1) + class Clandestine: def __init__(self, pluginapi): self.myCore = pluginapi.get_core() + + def create(self): + try: + peer = sys.argv[2] + if not self.myCore._utils.validatePubKey(peer): + exit_with_error('Invalid public key specified') + except IndexError: + exit_with_error('You must specify a peer public key') + + # Ask peer for transport address by creating block for them + peer_transport_address = bootstrapservice.bootstrap_client_service(peer, self.myCore) + + print(peer_transport_address) + if self.myCore._utils.doGetRequest('http://%s/ping' % (peer_transport_address,), ignoreAPI=True, port=self.myCore.config.get('tor.socksport')) == 'pong!': + print('connected', peer_transport_address) def on_init(api, data = None): ''' @@ -44,4 +65,5 @@ def on_init(api, data = None): pluginapi = api chat = Clandestine(pluginapi) + api.commands.register(['clandestine'], chat.create) return From 7c57d95d1d07cd28ac69264043e2b567d78e9f20 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 2 Apr 2019 11:50:09 -0500 Subject: [PATCH 014/173] work on direct connections and clandestine --- onionr/api.py | 10 ++++++ onionr/communicatorutils/servicecreator.py | 5 +-- onionr/onionrservices/connectionserver.py | 2 +- .../default-plugins/clandestine/main.py | 34 ++++++++++++++++--- .../default-plugins/clandestine/peerserver.py | 2 ++ 5 files changed, 46 insertions(+), 7 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 7e552355..3df4741b 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -318,6 +318,16 @@ class API: @app.route('/friends/', endpoint='friendsindex') def loadContacts(): return send_from_directory('static-data/www/friends/', 'index.html') + + @app.route('/serviceactive/') + def serviceActive(pubkey): + try: + if pubkey in self._core.onionrInst.communicatorInst.active_services: + return Response('true') + except AttributeError as e: + print('attribute error', str(e)) + pass + return Response('false') @app.route('/board/', endpoint='boardContent') def boardContent(path): diff --git a/onionr/communicatorutils/servicecreator.py b/onionr/communicatorutils/servicecreator.py index ecd1543c..fbb4c5b9 100644 --- a/onionr/communicatorutils/servicecreator.py +++ b/onionr/communicatorutils/servicecreator.py @@ -11,8 +11,9 @@ def service_creator(daemon): bl = onionrblockapi.Block(b, core=core, decrypt=True) bs = utils.bytesToStr(bl.bcontent) + '.onion' if utils.validatePubKey(bl.signer) and utils.validateID(bs): + signer = utils.bytesToStr(bl.signer) daemon.active_services.append(b) - daemon.active_services.append(bl.signer) - daemon.services.create_server(bl.signer, bs) + daemon.active_services.append(signer) + daemon.services.create_server(signer, bs) daemon.decrementThreadCount('service_creator') \ No newline at end of file diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index dea9f393..27a0b55f 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -70,7 +70,7 @@ class ConnectionServer: response = controller.create_ephemeral_hidden_service({80: service_port}, await_publication = True, key_type='NEW', key_content = 'ED25519-V3') self.core_inst.keyStore.put('dc-' + response.service_id, self.core_inst._utils.bytesToStr(peer)) self.core_inst._utils.doPostRequest('http://' + address + '/bs/' + response.service_id, port=socks) - logger.info('hosting on %s with %s' % (response.service_id, peer)) + logger.info('hosting on %s with %s' % (response.service_id, peer)) http_server.serve_forever() self.core_inst.keyStore.delete('dc-' + response.service_id) http_server.stop() \ No newline at end of file diff --git a/onionr/static-data/default-plugins/clandestine/main.py b/onionr/static-data/default-plugins/clandestine/main.py index 3fab2dd0..8f202dac 100755 --- a/onionr/static-data/default-plugins/clandestine/main.py +++ b/onionr/static-data/default-plugins/clandestine/main.py @@ -19,7 +19,7 @@ ''' # Imports some useful libraries -import locale, sys, os +import locale, sys, os, threading, json locale.setlocale(locale.LC_ALL, '') import onionrservices, logger from onionrservices import bootstrapservice @@ -40,7 +40,30 @@ def exit_with_error(text=''): class Clandestine: def __init__(self, pluginapi): self.myCore = pluginapi.get_core() + self.peer = None + self.transport = None + self.shutdown = False + def _sender_loop(self): + print('Enter a message to send, with ctrl-d or -s on a new line.') + print('-c on a new line or ctrl-c stops') + message = '' + while not self.shutdown: + try: + message += input() + if message == '-s': + raise EOFError + elif message == '-c': + raise KeyboardInterrupt + else: + message += '\n' + except EOFError: + message = json.dumps({'m': message, 't': self.myCore._utils.getEpoch()}) + print(self.myCore._utils.doPostRequest('http://%s/clandestine/sendto' % (self.transport,), port=self.socks, data=message)) + message = '' + except KeyboardInterrupt: + self.shutdown = True + def create(self): try: peer = sys.argv[2] @@ -48,13 +71,16 @@ class Clandestine: exit_with_error('Invalid public key specified') except IndexError: exit_with_error('You must specify a peer public key') - + self.peer = peer # Ask peer for transport address by creating block for them peer_transport_address = bootstrapservice.bootstrap_client_service(peer, self.myCore) + self.transport = peer_transport_address + self.socks = self.myCore.config.get('tor.socksport') - print(peer_transport_address) - if self.myCore._utils.doGetRequest('http://%s/ping' % (peer_transport_address,), ignoreAPI=True, port=self.myCore.config.get('tor.socksport')) == 'pong!': + print('connected with', peer, 'on', peer_transport_address) + if self.myCore._utils.doGetRequest('http://%s/ping' % (peer_transport_address,), ignoreAPI=True, port=self.socks) == 'pong!': print('connected', peer_transport_address) + threading.Thread(target=self._sender_loop).start() def on_init(api, data = None): ''' diff --git a/onionr/static-data/default-plugins/clandestine/peerserver.py b/onionr/static-data/default-plugins/clandestine/peerserver.py index b5fb3913..64e4eaeb 100644 --- a/onionr/static-data/default-plugins/clandestine/peerserver.py +++ b/onionr/static-data/default-plugins/clandestine/peerserver.py @@ -47,6 +47,8 @@ def sendto(): msg = '' if msg == None or msg == '': msg = json.dumps({'m': 'hello world', 't': core_inst._utils.getEpoch()}) + else: + msg = json.dumps(msg) core_inst._utils.localCommand('/clandestine/addrec/%s' % (g.peer,), post=True, postData=msg) return Response('success') From 93b3f998998c1ace7856542d36ffbc46cd56d70e Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 4 Apr 2019 16:56:18 -0500 Subject: [PATCH 015/173] added first block spec draft --- docs/block-spec.md | 64 ++++++++++++++++++++++++++++++++++++++ onionr/api.py | 1 - onionr/etc/onionrvalues.py | 2 +- onionr/onionrproofs.py | 2 +- onionr/subprocesspow.py | 2 +- 5 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 docs/block-spec.md diff --git a/docs/block-spec.md b/docs/block-spec.md new file mode 100644 index 00000000..0a5f1491 --- /dev/null +++ b/docs/block-spec.md @@ -0,0 +1,64 @@ +# Onionr Block Spec v1.0.0 + +# Block Description + +Onionr 'Blocks' are the primary means of sharing information in Onionr. Blocks are identified by a single hash value of their entire contents. + +They contain a JSON metadata section followed by a line break, with the main data following. + +In the future, the spec will be updated to use flags and MessagePack instead of JSON with english keys. + + +# Encryption and Signatures + +Onionr blocks may be encrypted or signed. In the reference client, this is done with libsodium, for both asymmetric and symmetric encryption. + +Unlike many similar projects, blocks may completely be in plaintext, making Onionr suitable for sharing information publicly. + +# Metadata Section + +The metadata section has the following fields. If a block contains any other field, it must be considered invalid. All metadata fields are technically optional, but many are useful and essentially necessary for most use cases. + +## meta + +Max byte size: 1000 + +Meta is a string field which can contain arbitrary sub fields. It is intended for applications and plugins to use it for arbitrary metadata information. In the reference client, if the data section is encrypted or signed, the meta section also is. + +Common meta fields, such as 'type' are used by the reference Onionr client to describe the type of a block. + +## sig + +Max byte size: 200 + +Sig is a field for storing public key signatures of the block, typically ed25519. In the reference client, this field is a base64 encoded signature of the meta field combined with the block data. (**Therefore, information outside of the meta and data fields cannot be trusted to be placed there by the signer, although it can still be assured that the particular block has not been modified.**) + +Note: the max field size is larger than a EdDSA signature (which is what is typically used) in order to allow other primitives for signing in alternative implementations or future versions. + +## signer + +Max byte size: 200 + +Signer is a field for specifying the public key which signed the block. In the reference client this is a base64 encoded ed25519 public key. + +## time + +Max byte size: 10 + +Time is an integer field for specifying the time of which a block was created. The trustworthiness of this field is based on one's trust of the block creator, however blocks with a time field set in the future (past a reasonable clock skew) are thrown out by the reference client. + +## expire + +Max byte size: 10 + +Expire is an integer field for specifying the time of which the block creator has indicated that the block should be deleted. The purpose of this is for voluntarily freeing the burden of unwanted blocks on the Onionr network, rather than security/privacy (since blocks could be trivially kept past expiration). Regardless, the reference client deletes blocks after a preset time if the expire field is either not set or longer than the preset time. + +## pow + +Max byte size: 1000 + +Pow is a field for placing the nonce found to make a block meet a target proof of work. In theory, a block could meet a target without a random token in this field. + +## encryptType + +encryptType is a field to specify the mode of encryption for a block. The values supported by Onionr are 'asym' and 'sym'. diff --git a/onionr/api.py b/onionr/api.py index 3df4741b..bba548c5 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -325,7 +325,6 @@ class API: if pubkey in self._core.onionrInst.communicatorInst.active_services: return Response('true') except AttributeError as e: - print('attribute error', str(e)) pass return Response('false') diff --git a/onionr/etc/onionrvalues.py b/onionr/etc/onionrvalues.py index 8627d246..9f636147 100755 --- a/onionr/etc/onionrvalues.py +++ b/onionr/etc/onionrvalues.py @@ -21,5 +21,5 @@ class OnionrValues: def __init__(self): self.passwordLength = 20 - self.blockMetadataLengths = {'meta': 1000, 'sig': 200, 'signer': 200, 'time': 10, 'powRandomToken': 1000, 'encryptType': 4, 'expire': 14} #TODO properly refine values to minimum needed + self.blockMetadataLengths = {'meta': 1000, 'sig': 200, 'signer': 200, 'time': 10, 'pow': 1000, 'encryptType': 4, 'expire': 14} #TODO properly refine values to minimum needed self.default_expire = 2592000 \ No newline at end of file diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index 62dc215c..53c6d881 100755 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -247,7 +247,7 @@ class POW: startNonce = nonce while self.hashing: #token = nacl.hash.blake2b(rand + self.data).decode() - self.metadata['powRandomToken'] = nonce + self.metadata['pow'] = nonce payload = json.dumps(self.metadata).encode() + b'\n' + self.data token = myCore._crypto.sha3Hash(payload) try: diff --git a/onionr/subprocesspow.py b/onionr/subprocesspow.py index a2fee7c6..fee2f2dd 100755 --- a/onionr/subprocesspow.py +++ b/onionr/subprocesspow.py @@ -75,7 +75,7 @@ class SubprocessPOW: difficulty = self.difficulty mcore = core.Core() while True: - metadata['powRandomToken'] = nonce + metadata['pow'] = nonce payload = json.dumps(metadata).encode() + b'\n' + data token = mcore._crypto.sha3Hash(payload) try: From 81cfc64658e2ae361647f15cd276ab45417e3ceb Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 9 Apr 2019 12:30:54 -0500 Subject: [PATCH 016/173] fixes for mail, work on specs, and added files for profile viewer --- README.md | 2 + docs/{api.md => http-api.md} | 0 docs/{ => specs}/block-spec.md | 3 +- onionr/api.py | 14 +++++- onionr/httpapi/profilesapi/__init__.py | 28 ++++++++++++ onionr/httpapi/profilesapi/profiles.py | 2 + onionr/static-data/bootstrap-nodes.txt | 2 +- .../default-plugins/pms/mailapi.py | 25 ++++++++--- onionr/static-data/default_config.json | 4 +- onionr/static-data/www/mail/index.html | 5 ++- onionr/static-data/www/mail/mail.css | 2 +- onionr/static-data/www/mail/mail.js | 44 +++++++++++-------- onionr/static-data/www/profiles/index.html | 20 +++++++++ onionr/static-data/www/profiles/profiles.js | 0 14 files changed, 119 insertions(+), 32 deletions(-) rename docs/{api.md => http-api.md} (100%) rename docs/{ => specs}/block-spec.md (96%) create mode 100644 onionr/httpapi/profilesapi/__init__.py create mode 100644 onionr/httpapi/profilesapi/profiles.py create mode 100644 onionr/static-data/www/profiles/index.html create mode 100644 onionr/static-data/www/profiles/profiles.js diff --git a/README.md b/README.md index 8b0de5c2..ebae2e10 100755 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ Users are identified by ed25519 public keys, which can be used to sign blocks or Onionr can be used for mail, as a social network, instant messenger, file sharing software, or for encrypted group discussion. +The whitepaper (subject to change prior to first alpha release) is available [here](docs/whitepaper.md). + ![Tor stinks slide image](docs/tor-stinks-02.png) ## Main Features diff --git a/docs/api.md b/docs/http-api.md similarity index 100% rename from docs/api.md rename to docs/http-api.md diff --git a/docs/block-spec.md b/docs/specs/block-spec.md similarity index 96% rename from docs/block-spec.md rename to docs/specs/block-spec.md index 0a5f1491..f9cd3509 100644 --- a/docs/block-spec.md +++ b/docs/specs/block-spec.md @@ -6,8 +6,7 @@ Onionr 'Blocks' are the primary means of sharing information in Onionr. Blocks a They contain a JSON metadata section followed by a line break, with the main data following. -In the future, the spec will be updated to use flags and MessagePack instead of JSON with english keys. - +In the future, the specification will likely be updated to use flags and MessagePack instead of JSON with english keys. # Encryption and Signatures diff --git a/onionr/api.py b/onionr/api.py index bba548c5..415707e5 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -26,7 +26,7 @@ import core from onionrblockapi import Block import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config import httpapi -from httpapi import friendsapi, simplecache +from httpapi import friendsapi, simplecache, profilesapi from onionrservices import httpheaders import onionr @@ -256,7 +256,8 @@ class API: self.bindPort = bindPort # Be extremely mindful of this. These are endpoints available without a password - self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'board', 'boardContent', 'sharedContent', 'mail', 'mailindex', 'friends', 'friendsindex') + self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'board', 'profiles', 'profilesindex', + 'boardContent', 'sharedContent', 'mail', 'mailindex', 'friends', 'friendsindex') self.clientToken = config.get('client.webpassword') self.timeBypassToken = base64.b16encode(os.urandom(32)).decode() @@ -272,6 +273,7 @@ class API: onionrInst.setClientAPIInst(self) app.register_blueprint(friendsapi.friends) app.register_blueprint(simplecache.simplecache) + app.register_blueprint(profilesapi.profile_BP) httpapi.load_plugin_blueprints(app) @app.before_request @@ -318,6 +320,14 @@ class API: @app.route('/friends/', endpoint='friendsindex') def loadContacts(): return send_from_directory('static-data/www/friends/', 'index.html') + + @app.route('/profiles/', endpoint='profiles') + def loadContacts(path): + return send_from_directory('static-data/www/profiles/', path) + + @app.route('/profiles/', endpoint='profilesindex') + def loadContacts(): + return send_from_directory('static-data/www/profiles/', 'index.html') @app.route('/serviceactive/') def serviceActive(pubkey): diff --git a/onionr/httpapi/profilesapi/__init__.py b/onionr/httpapi/profilesapi/__init__.py new file mode 100644 index 00000000..f3c3ba2f --- /dev/null +++ b/onionr/httpapi/profilesapi/__init__.py @@ -0,0 +1,28 @@ +''' + Onionr - P2P Anonymous Storage Network + + This file creates http endpoints for user profile pages +''' +''' + 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 core +from flask import Blueprint, Response, request, abort +from . import profiles + +profile_BP = Blueprint('profile_BP', __name__) + +@profile_BP.route('/profile/get/', endpoint='profiles') +def get_profile_page(pubkey): + return Response(pubkey) \ No newline at end of file diff --git a/onionr/httpapi/profilesapi/profiles.py b/onionr/httpapi/profilesapi/profiles.py new file mode 100644 index 00000000..11a73b13 --- /dev/null +++ b/onionr/httpapi/profilesapi/profiles.py @@ -0,0 +1,2 @@ +def get_latest_user_profile(pubkey): + return '' \ No newline at end of file diff --git a/onionr/static-data/bootstrap-nodes.txt b/onionr/static-data/bootstrap-nodes.txt index 374ea7b4..52622b12 100755 --- a/onionr/static-data/bootstrap-nodes.txt +++ b/onionr/static-data/bootstrap-nodes.txt @@ -1 +1 @@ -huei7fugyuesltkf3yzj27an36skz2f2u55g7ganyaq6yjhauwicwfyd.onion +ecaufnpxx67xmvzwcriohapmmnqbj665h3ynemvwrr2juxl5oa5g7cad.onion diff --git a/onionr/static-data/default-plugins/pms/mailapi.py b/onionr/static-data/default-plugins/pms/mailapi.py index 87cdb678..0a60e67c 100644 --- a/onionr/static-data/default-plugins/pms/mailapi.py +++ b/onionr/static-data/default-plugins/pms/mailapi.py @@ -42,6 +42,7 @@ def mail_delete(block): if block not in existing: existing.append(block) kv.put('deleted_mail', existing) + kv.flush() return 'success' @flask_blueprint.route('/mail/getinbox') @@ -50,16 +51,30 @@ def list_inbox(): @flask_blueprint.route('/mail/getsentbox') def list_sentbox(): + kv.refresh() sentbox_list = sentboxdb.SentBox(c).listSent() - sentbox_list_copy = list(sentbox_list) + list_copy = list(sentbox_list) deleted = kv.get('deleted_mail') if deleted is None: deleted = [] - for x in range(len(sentbox_list_copy) - 1): + for x in list_copy: + if x['hash'] in deleted: + sentbox_list.remove(x) + return json.dumps(sentbox_list) +''' +@flask_blueprint.route('/mail/getsentbox') +def list_sentbox(): + sentbox_list = sentboxdb.SentBox(c).listSent() + sentbox_list_copy = list(sentbox_list) + kv.refresh() + deleted = kv.get('deleted_mail') + if deleted is None: + deleted = [] + for x in range(len(sentbox_list_copy)): if sentbox_list_copy[x]['hash'] in deleted: - x -= 1 - sentbox_list.pop(x) + sentbox_list.remove(sentbox_list_copy[x]['hash']) else: sentbox_list[x]['name'] = contactmanager.ContactManager(c, sentbox_list_copy[x]['peer'], saveUser=False).get_info('name') - return json.dumps(sentbox_list) \ No newline at end of file + return json.dumps(sentbox_list) +''' \ No newline at end of file diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index 2b9d47df..988e91f0 100755 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -4,12 +4,12 @@ "display_header" : false, "minimum_block_pow": 4, "minimum_send_pow": 4, - "socket_servers": true, + "socket_servers": false, "security_level": 0, "max_block_age": 2678400, "bypass_tor_check": false, "public_key": "", - "random_bind_ip": true + "random_bind_ip": false }, "www" : { diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index 8af6f035..f3f337bd 100644 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -36,8 +36,11 @@
From: Signature:
+
+ Subject: +
- +
diff --git a/onionr/static-data/www/mail/mail.css b/onionr/static-data/www/mail/mail.css index af80deb1..88afc0b9 100644 --- a/onionr/static-data/www/mail/mail.css +++ b/onionr/static-data/www/mail/mail.css @@ -92,7 +92,7 @@ input{ color: black; } -#replyBtn{ +.break-up{ margin-top: 1em; } diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index 302c84f6..cfbb7e89 100644 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -26,7 +26,7 @@ threadContent = {} myPub = httpGet('/getActivePubkey') replyBtn = document.getElementById('replyBtn') -function openReply(bHash){ +function openReply(bHash, quote, subject){ var inbox = document.getElementsByClassName('threadEntry') var entry = '' var friendName = '' @@ -41,13 +41,23 @@ function openReply(bHash){ } key = entry.getAttribute('data-pubkey') document.getElementById('draftID').value = key + document.getElementById('draftSubject').value = 'RE: ' + subject + + // Add quoted reply + var splitQuotes = quote.split('\n') + for (var x = 0; x < splitQuotes.length; x++){ + splitQuotes[x] = '>' + splitQuotes[x] + } + quote = splitQuotes.join('\n') + document.getElementById('draftText').value = quote setActiveTab('send message') } -function openThread(bHash, sender, date, sigBool, pubkey){ +function openThread(bHash, sender, date, sigBool, pubkey, subjectLine){ var messageDisplay = document.getElementById('threadDisplay') var blockContent = httpGet('/getblockbody/' + bHash) document.getElementById('fromUser').value = sender + document.getElementById('subjectView').innerText = subjectLine messageDisplay.innerText = blockContent var sigEl = document.getElementById('sigValid') var sigMsg = 'signature' @@ -64,7 +74,7 @@ function openThread(bHash, sender, date, sigBool, pubkey){ sigEl.innerText = sigMsg overlay('messageDisplay') replyBtn.onclick = function(){ - openReply(bHash) + openReply(bHash, messageDisplay.innerText, subjectLine) } } @@ -174,7 +184,7 @@ function loadInboxEntries(bHash){ if (event.target.classList.contains('deleteBtn')){ return } - openThread(entry.getAttribute('data-hash'), senderInput.value, dateStr.innerText, resp['meta']['validSig'], entry.getAttribute('data-pubkey')) + openThread(entry.getAttribute('data-hash'), senderInput.value, dateStr.innerText, resp['meta']['validSig'], entry.getAttribute('data-pubkey'), subjectLine.innerText) } deleteBtn.onclick = function(){ @@ -216,7 +226,7 @@ function getSentbox(){ if (keys.length == 0){ threadPart.innerHTML = "nothing to show here yet." } - for (var i = 0; i < keys.length; i++){ + for (var i = 0; i < keys.length; i++) (function(i, resp){ var entry = document.createElement('div') var obj = resp[i] var toLabel = document.createElement('span') @@ -245,19 +255,18 @@ function getSentbox(){ entry.appendChild(toEl) entry.appendChild(preview) entry.appendChild(sentDate) - entry.onclick = (function(tree, el, msg) {return function() { - console.log(resp) - if (! entry.classList.contains('deleteBtn')){ - showSentboxWindow(el.value, msg) - } - };})(entry, toEl, message); - - deleteBtn.onclick = function(){ - entry.parentNode.removeChild(entry); - deleteMessage(entry.getAttribute('data-hash')) - } + threadPart.appendChild(entry) - } + + entry.onclick = function(e){ + if (e.target.classList.contains('deleteBtn')){ + deleteMessage(e.target.parentNode.getAttribute('data-hash')) + e.target.parentNode.parentNode.removeChild(e.target.parentNode) + return + } + showSentboxWindow() + } + })(i, resp) threadPart.appendChild(entry) }.bind(threadPart)) } @@ -304,7 +313,6 @@ for (var i = 0; i < document.getElementsByClassName('refresh').length; i++){ document.getElementsByClassName('refresh')[i].style.float = 'right' } - fetch('/friends/list', { headers: { "token": webpass diff --git a/onionr/static-data/www/profiles/index.html b/onionr/static-data/www/profiles/index.html new file mode 100644 index 00000000..a4dad9b7 --- /dev/null +++ b/onionr/static-data/www/profiles/index.html @@ -0,0 +1,20 @@ + + + + + + Onionr Profiles + + + + + + +
+

+
+ + + + + \ No newline at end of file diff --git a/onionr/static-data/www/profiles/profiles.js b/onionr/static-data/www/profiles/profiles.js new file mode 100644 index 00000000..e69de29b From fb15061ff7c172883884486e2e55d54e290983ad Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 9 Apr 2019 12:35:40 -0500 Subject: [PATCH 017/173] fixes for mail, work on specs, and added files for profile viewer --- onionr/communicator.py | 2 +- onionr/static-data/www/mail/mail.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/onionr/communicator.py b/onionr/communicator.py index ef94ae14..f10d37da 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -621,7 +621,7 @@ class OnionrCommunicatorDaemon: def detectAPICrash(self): '''exit if the api server crashes/stops''' if self._core._utils.localCommand('ping', silent=False) not in ('pong', 'pong!'): - for i in range(8): + for i in range(12): if self._core._utils.localCommand('ping') in ('pong', 'pong!') or self.shutdown: break # break for loop time.sleep(1) diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index cfbb7e89..09b8714a 100644 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -264,7 +264,7 @@ function getSentbox(){ e.target.parentNode.parentNode.removeChild(e.target.parentNode) return } - showSentboxWindow() + showSentboxWindow(resp[i]['peer'], message) } })(i, resp) threadPart.appendChild(entry) From 50eb095039f16529c335e8b04184dc04418a6cd8 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 11 Apr 2019 17:01:18 -0500 Subject: [PATCH 018/173] work on docs --- README.md | 8 +++---- docs/http-api.md | 52 ++++++++++++++++++++++++++++++++++++++-- docs/specs/block-spec.md | 4 ++-- onionr/api.py | 4 +++- 4 files changed, 59 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ebae2e10..58dac596 100755 --- a/README.md +++ b/README.md @@ -17,11 +17,11 @@ **The main repository for this software is at https://gitlab.com/beardog/Onionr/** -# Summary +# About Onionr is a decentralized, peer-to-peer data storage network, designed to be anonymous and resistant to (meta)data analysis and spam/disruption. -Onionr stores data in independent packages referred to as 'blocks'. The blocks are synced to all other nodes in the network. Blocks and user IDs cannot be easily proven to have been created by particular nodes (only inferred). Even if there is enough evidence to believe a particular node created a block, nodes still operate behind Tor or I2P and as such are not trivially known to be at a particular IP address. +Onionr stores data in independent packages referred to as 'blocks'. The blocks are synced to all other nodes in the network. Blocks and user IDs cannot be easily proven to have been created by a particular user. Even if there is enough evidence to believe that a specific user created a block, nodes still operate behind Tor or I2P and as such cannot be trivially unmasked. Users are identified by ed25519 public keys, which can be used to sign blocks or send encrypted data. @@ -36,7 +36,7 @@ The whitepaper (subject to change prior to first alpha release) is available [he * [X] 🌐 Fully p2p/decentralized, no trackers or other single points of failure * [X] 🔒 End to end encryption of user data * [X] 📢 Optional non-encrypted blocks, useful for blog posts or public file sharing -* [X] 💻 Easy API system for integration to websites +* [X] 💻 Easy API for integration to websites * [X] 🕵️ Metadata analysis resistance and anonymity * [X] 📡 Transport agnosticism (no internet required) @@ -54,7 +54,7 @@ Friend/contact manager Encrypted, metadata-masking mail application screenshot -Encrypted, metadata-masking mail application. +Encrypted, metadata-masking mail application. Perhaps the first distributed mail system to have basic forward secrecy. # Install and Run on Linux diff --git a/docs/http-api.md b/docs/http-api.md index 52a55368..b2fae877 100755 --- a/docs/http-api.md +++ b/docs/http-api.md @@ -1,2 +1,50 @@ -HTTP API -TODO +# Onionr HTTP API + +# About HTTP API + +All HTTP interfaces in the Onionr reference client use the [Flask](http://flask.pocoo.org/) web framework with the [gevent](http://www.gevent.org/) WSGI server. + +## Client & Public difference + +The client API server is a locked down interface intended for authenticated local communication. + +The public API server is available only remotely from Tor & I2P. It is the interface in which peers use to communicate with one another. + +# Client API + +Please note: endpoints that simply provide static web app files are not documented here. + +(Client API docs coming soon) + +# Public API + +* / + - Methods: GET + - Returns a basic HTML informational banner describing Onionr. +* /getblocklist + - Methods: GET + - URI Parameters: + - date: unix epoch timestamp for offset + - Returns a list of block hashes stored on the node since an offset (all blocks if no timestamp is specified) +* /getdata/block-hash + - Methods: GET + - Returns data for a block based on a provided hash +* /www/file-path + - Methods: GET + - Returns file data. Intended for manually sharing file data directly from an Onionr node. +* /ping + - Methods: GET + - Returns 'pong!' +* /pex + - Methods: GET + - Returns a list of peer addresses reached within recent time +* /announce + - Methods: POST + - Accepts form data for 'node' (valid node address) and 'random' which is a nonce when hashed (blake2b_256) in the format `hash(peerAddress+serverAddress+nonce)`, begins with at least 5 zeros. + - Returns 200 with 'Success' if no error occurs. If the post is invalid, 'failure' with code 406 is returned. +* /upload + - Methods: POST + - Accepts form data for 'block' as a 'file' upload. + - Returns 200 with 'success' if no error occurs. If the block cannot be accepted, 'failure' with 400 is returned. + +# Direct Connection API \ No newline at end of file diff --git a/docs/specs/block-spec.md b/docs/specs/block-spec.md index f9cd3509..348cedae 100644 --- a/docs/specs/block-spec.md +++ b/docs/specs/block-spec.md @@ -2,9 +2,9 @@ # Block Description -Onionr 'Blocks' are the primary means of sharing information in Onionr. Blocks are identified by a single hash value of their entire contents. +Onionr Blocks are the primary means of sharing information in Onionr. Blocks are identified by a single hash value of their entire contents, using SHA3_256. -They contain a JSON metadata section followed by a line break, with the main data following. +Blocks contain a JSON metadata section followed by a line break, with the main data section comprising the rest. In the future, the specification will likely be updated to use flags and MessagePack instead of JSON with english keys. diff --git a/onionr/api.py b/onionr/api.py index 415707e5..df8b09f2 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -186,7 +186,7 @@ class PublicAPI: powHash = powHash.decode() except AttributeError: pass - if powHash.startswith('0000'): + if powHash.startswith('00000'): newNode = clientAPI._core._utils.bytesToStr(newNode) if clientAPI._core._utils.validateID(newNode) and not newNode in clientAPI._core.onionrInst.communicatorInst.newPeers: clientAPI._core.onionrInst.communicatorInst.newPeers.append(newNode) @@ -194,6 +194,8 @@ class PublicAPI: else: logger.warn(newNode.decode() + ' failed to meet POW: ' + powHash) resp = Response(resp) + if resp == 'failure': + return resp, 406 return resp @app.route('/upload', methods=['post']) From d403f490e9f1b76fb04f026e3218f4cfd378c81a Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 12 Apr 2019 12:14:16 -0500 Subject: [PATCH 019/173] updated docs and cleaned up api somewhat --- README.md | 7 +++ docs/http-api.md | 60 ++++++++++++++++++++++++-- onionr/api.py | 40 +++-------------- onionr/static-data/www/mail/index.html | 2 +- 4 files changed, 71 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 58dac596..b63fe020 100755 --- a/README.md +++ b/README.md @@ -56,6 +56,13 @@ Friend/contact manager Encrypted, metadata-masking mail application. Perhaps the first distributed mail system to have basic forward secrecy. +# Documentation + +More docs coming soon. + +* [Block specification](docs/specs/block-spec.md) +* [HTTP API](docs/http-api.md) + # Install and Run on Linux The following applies to Ubuntu Bionic. Other distros may have different package or command names. diff --git a/docs/http-api.md b/docs/http-api.md index b2fae877..d2ee56a5 100755 --- a/docs/http-api.md +++ b/docs/http-api.md @@ -1,7 +1,5 @@ # Onionr HTTP API -# About HTTP API - All HTTP interfaces in the Onionr reference client use the [Flask](http://flask.pocoo.org/) web framework with the [gevent](http://www.gevent.org/) WSGI server. ## Client & Public difference @@ -14,10 +12,66 @@ The public API server is available only remotely from Tor & I2P. It is the inter Please note: endpoints that simply provide static web app files are not documented here. -(Client API docs coming soon) +* /serviceactive/pubkey + - Methods: GET + - Returns true or false based on if a given public key has an active direct connection service. +* /queueResponseAdd/key (DEPRECATED) + - Methods: POST + - Accepts form key 'data' to set queue response information from a plugin + - Returns success if no error occurs +* /queueResponse/key (DEPRECATED) + - Methods: GET + - Returns the queue response for a key. Returns failure with a 404 code if a code is not set. +* /ping + - Methods: GET + - Returns "pong!" +* /getblocksbytype/type + - Methods: GET + - Returns a list of stored blocks by a given type +* /getblockbody/hash + - Methods: GET + - Returns the main data section of a block +* /getblockdata/hash + - Methods: GET + - Returns the entire data contents of a block, including metadata. +* /getblockheader/hash + - Methods: GET + - Returns the header (metadata section) of a block. +* /lastconnect + - Methods: GET + - Returns the epoch timestamp of when the last incoming connection to the public API server was logged +* /site/hash + - Methods: GET + - Returns HTML content out of a block +* /waitforshare/hash + - Methods: POST + - Prevents the public API server from listing or sharing a block until it has been uploaded to at least 1 peer. +* /shutdown + - Methods: GET + - Shutdown Onionr. You should probably use /shutdownclean instead. +* /shutdownclean + - Methods: GET + - Tells the communicator daemon to shutdown Onionr. Slower but cleaner. +* /getstats + - Methods: GET + - Returns some JSON serialized statistics +* /getuptime + - Methods: GET + - Returns uptime in seconds +* /getActivePubkey + - Methods: GET + - Returns the current active public key in base32 format +* /getHumanReadable/pubkey + - Methods: GET + - Echos the specified public key in mnemonic format +* /insertblock + - Methods: POST + - Accepts JSON data for creating a new block. 'message' contains the block data, 'to' specifies the peer's public key to encrypt the data to, 'sign' is a boolean for signing the message. # Public API +v0 + * / - Methods: GET - Returns a basic HTML informational banner describing Onionr. diff --git a/onionr/api.py b/onionr/api.py index df8b09f2..ae6368e2 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -270,7 +270,6 @@ class API: logger.info('Running api on %s:%s' % (self.host, self.bindPort)) self.httpServer = '' - self.pluginResponses = {} # Responses for plugin endpoints self.queueResponse = {} onionrInst.setClientAPIInst(self) app.register_blueprint(friendsapi.friends) @@ -369,7 +368,10 @@ class API: pass else: del self.queueResponse[name] - return Response(resp) + if resp == 'failure': + return resp, 404 + else: + return resp @app.route('/ping') def ping(): @@ -522,36 +524,6 @@ class API: pass threading.Thread(target=self._core.insertBlock, args=(message,), kwargs={'header': bType, 'encryptType': encryptType, 'sign':sign, 'asymPeer': to, 'meta': meta}).start() return Response('success') - - @app.route('/apipoints/', methods=['POST', 'GET']) - def pluginEndpoints(subpath=''): - '''Send data to plugins''' - # TODO have a variable for the plugin to set data to that we can use for the response - pluginResponseCode = str(uuid.uuid4()) - resp = 'success' - responseTimeout = 20 - startTime = self._core._utils.getEpoch() - postData = {} - if request.method == 'POST': - postData = request.form['postData'] - if len(subpath) > 1: - data = subpath.split('/') - if len(data) > 1: - plName = data[0] - events.event('pluginRequest', {'name': plName, 'path': subpath, 'pluginResponse': pluginResponseCode, 'postData': postData}, onionr=onionrInst) - while True: - try: - resp = self.pluginResponses[pluginResponseCode] - except KeyError: - time.sleep(0.2) - if self._core._utils.getEpoch() - startTime > responseTimeout: - abort(504) - break - else: - break - else: - abort(404) - return Response(resp) self.httpServer = WSGIServer((self.host, bindPort), app, log=None, handler_class=FDSafeHandler) self.httpServer.serve_forever() @@ -578,8 +550,8 @@ class API: def getUptime(self): while True: try: - return self._utils.getEpoch - startTime - except AttributeError: + return self._utils.getEpoch() - self.startTime + except (AttributeError, NameError): # Don't error on race condition with startup pass diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index f3f337bd..08441075 100644 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -58,7 +58,7 @@
-
+ To: Subject: From e6c24a483f402a7c495599d455add7d418e5db81 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 18 Mar 2019 00:22:31 -0500 Subject: [PATCH 020/173] bumped stem version and started work on new dir conn system --- onionr/netcontroller.py | 14 +---- onionr/onionrservices/__init__.py | 18 ++++++ onionr/onionrservices/bootstrapservice.py | 67 +++++++++++++++++++++++ onionr/onionrservices/connectionserver.py | 4 ++ onionr/onionrutils.py | 6 +- requirements.in | 2 +- requirements.txt | 4 +- 7 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 onionr/onionrservices/__init__.py create mode 100644 onionr/onionrservices/bootstrapservice.py create mode 100644 onionr/onionrservices/connectionserver.py diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py index 4f6281c9..49bf46ac 100755 --- a/onionr/netcontroller.py +++ b/onionr/netcontroller.py @@ -17,12 +17,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' - -import subprocess, os, random, sys, logger, time, signal, config, base64, socket -from stem.control import Controller +import subprocess, os, random, sys, time, signal, base64, socket +from shutil import which +import logger, config from onionrblockapi import Block from dependencies import secrets -from shutil import which def getOpenPort(): # taken from (but modified) https://stackoverflow.com/a/2838309 @@ -67,13 +66,6 @@ class NetController: self.torBinary = 'tor' config.reload() - ''' - if os.path.exists(self.torConfigLocation): - torrc = open(self.torConfigLocation, 'r') - if not str(self.hsPort) in torrc.read(): - os.remove(self.torConfigLocation) - torrc.close() - ''' return diff --git a/onionr/onionrservices/__init__.py b/onionr/onionrservices/__init__.py new file mode 100644 index 00000000..68612cf7 --- /dev/null +++ b/onionr/onionrservices/__init__.py @@ -0,0 +1,18 @@ +import stem +import core +from . import connectionserver, connectionclient, bootstrapservice +class OnionrServices: + def __init__(self, onionr_core): + assert isinstance(onionr_core, core.Core) + self._core = onionr_core + self.servers = {} + self.clients = {} + return + + def create_server(self): + return + + def create_client(self, peer): + # Create ephemeral onion service to bootstrap connection + address = bootstrapservice.bootstrap_client_service(peer) + return address \ No newline at end of file diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py new file mode 100644 index 00000000..7a2c3823 --- /dev/null +++ b/onionr/onionrservices/bootstrapservice.py @@ -0,0 +1,67 @@ +''' + Onionr - P2P Anonymous Storage Network + + Bootstrap onion direct connections for the clients +''' +''' + 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 . +''' +from gevent.pywsgi import WSGIServer, WSGIHandler +from stem.control import Controller +from flask import Flask +import core +from netcontroller import getOpenPort + +def bootstrap_client_service(peer, core_inst=None): + ''' + Bootstrap client services + ''' + if core_inst is None: + core_inst = core.Core() + + if not core_inst._utils.validatePubKey(peer): + raise ValueError('Peer must be valid base32 ed25519 public key') + + http_server = WSGIServer(('127.0.0.1', bootstrap_port), bootstrap_app, log=None) + bootstrap_port = getOpenPort() + bootstrap_app = flask.Flask(__name__) + + bootstrap_address = '' + + @bootstrap_app.route('/ping') + def get_ping(): + return "pong!" + + @bootstrap_app.route('/bs/
', methods=['POST']) + def get_bootstrap(address): + if core_inst._utils.validateID(address): + # Set the bootstrap address then close the server + bootstrap_address = address + http_server.stop() + + with Controller.from_port() as controller: + # Connect to the Tor process for Onionr + controller.authenticate() + # Create the v3 onion service + response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, await_publication = True, key_type='ED25519-V3') + + core_inst.insertBlock(response.hostname, header='con', sign=True, encryptType='asym', + asymPeer=peer, disableForward=True) + + # Run the bootstrap server + http_server.serve_forever() + # This line reached when server is shutdown by being bootstrapped + + # Now that the bootstrap server has received a server, return the address + return bootstrap_address diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py new file mode 100644 index 00000000..d602d8ce --- /dev/null +++ b/onionr/onionrservices/connectionserver.py @@ -0,0 +1,4 @@ +class ConnectionServer: + def __init__(self): + return + \ No newline at end of file diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index f8fd13af..082b14fb 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -329,7 +329,8 @@ class OnionrUtils: retVal = True return retVal - def validateID(self, id): + @staticmethod + def validateID(id): ''' Validate if an address is a valid tor or i2p hidden service ''' @@ -381,7 +382,8 @@ class OnionrUtils: except: return False - def isIntegerString(self, data): + @staticmethod + def isIntegerString(data): '''Check if a string is a valid base10 integer (also returns true if already an int)''' try: int(data) diff --git a/requirements.in b/requirements.in index 6b1cb663..7bf8ff02 100755 --- a/requirements.in +++ b/requirements.in @@ -5,5 +5,5 @@ gevent==1.3.6 defusedxml==0.5.0 Flask==1.0.2 PySocks==1.6.8 -stem==1.6.0 +stem==1.7.1 deadsimplekv==0.0.1 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b2c93d7c..9bb58ea1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -185,8 +185,8 @@ six==1.12.0 \ --hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \ --hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73 \ # via pynacl -stem==1.6.0 \ - --hash=sha256:d7fe1fb13ed5a94d610b5ad77e9f1b3404db0ca0586ded7a34afd323e3b849ed +stem==1.7.1 \ + --hash=sha256:c9eaf3116cb60c15995cbd3dec3a5cbc50e9bb6e062c4d6d42201e566f498ca2 urllib3==1.23 \ --hash=sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf \ --hash=sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5 From 3212768dc77f79b63ad34fc05a27d4a5534266a4 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 19 Mar 2019 00:09:53 -0500 Subject: [PATCH 021/173] work on direct connections --- onionr/communicator.py | 21 +++----------- onionr/core.py | 1 - onionr/onionrservices/__init__.py | 20 ++++++++++--- onionr/onionrservices/bootstrapservice.py | 18 ++++++------ onionr/onionrservices/connectionserver.py | 29 +++++++++++++++++-- .../default-plugins/metadataprocessor/main.py | 26 +++++------------ 6 files changed, 62 insertions(+), 53 deletions(-) diff --git a/onionr/communicator.py b/onionr/communicator.py index 12fbabbc..37c0cbdb 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -24,7 +24,7 @@ from dependencies import secrets from utils import networkmerger import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block from communicatorutils import onionrdaemontools -import onionrsockets, onionr, onionrproofs +import onionrservices, onionr, onionrproofs from communicatorutils import onionrcommunicatortimers, proxypicker OnionrCommunicatorTimers = onionrcommunicatortimers.OnionrCommunicatorTimers @@ -126,9 +126,9 @@ class OnionrCommunicatorDaemon: #forwardSecrecyTimer.count = (forwardSecrecyTimer.frequency - 990) if config.get('general.socket_servers'): - self.socketServer = threading.Thread(target=onionrsockets.OnionrSocketServer, args=(self._core,)) - self.socketServer.start() - self.socketClient = onionrsockets.OnionrSocketClient(self._core) + self.services = onionrservices.OnionrServices(self._core) + else: + self.services = None # Main daemon loop, mainly for calling timers, don't do any complex operations here to avoid locking try: @@ -565,19 +565,6 @@ class OnionrCommunicatorDaemon: i.count = (i.frequency - 1) elif cmd[0] == 'uploadBlock': self.blocksToUpload.append(cmd[1]) - elif cmd[0] == 'startSocket': - # Create our own socket server - socketInfo = json.loads(cmd[1]) - socketInfo['id'] = uuid.uuid4() - self._core.startSocket = socketInfo - elif cmd[0] == 'addSocket': - # Socket server was created for us - socketInfo = json.loads(cmd[1]) - peer = socketInfo['peer'] - reason = socketInfo['reason'] - threading.Thread(target=self.socketClient.startSocket, args=(peer, reason)).start() - else: - logger.info('Recieved daemonQueue command:' + cmd[0]) if cmd[0] not in ('', None): if response != '': diff --git a/onionr/core.py b/onionr/core.py index 1cbce026..ecbe2d73 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -353,7 +353,6 @@ class Core: conn = sqlite3.connect(self.queueDB, timeout=30) c = conn.cursor() t = (command, data, date, responseID) - try: c.execute('INSERT INTO commands (command, data, date, responseID) VALUES(?, ?, ?, ?)', t) conn.commit() diff --git a/onionr/onionrservices/__init__.py b/onionr/onionrservices/__init__.py index 68612cf7..5ea0ef3e 100644 --- a/onionr/onionrservices/__init__.py +++ b/onionr/onionrservices/__init__.py @@ -1,17 +1,29 @@ +import time import stem import core -from . import connectionserver, connectionclient, bootstrapservice +from . import connectionserver, bootstrapservice class OnionrServices: def __init__(self, onionr_core): assert isinstance(onionr_core, core.Core) self._core = onionr_core self.servers = {} self.clients = {} + self.shutdown = False return - def create_server(self): - return - + def create_server(self, peer, address): + assert self._core._utils.validateID(address) + BOOTSTRAP_TRIES = 10 + TRY_WAIT = 3 + for x in range(BOOTSTRAP_TRIES): + if self._core._utils.doGetRequest('http://' + address + '/ping') == 'pong!': + connectionserver.ConnectionServer(peer, address, core_inst=self._core) + return True + else: + time.sleep(TRY_WAIT) + else: + return False + def create_client(self, peer): # Create ephemeral onion service to bootstrap connection address = bootstrapservice.bootstrap_client_service(peer) diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py index 7a2c3823..b0b2fb7d 100644 --- a/onionr/onionrservices/bootstrapservice.py +++ b/onionr/onionrservices/bootstrapservice.py @@ -23,7 +23,7 @@ from flask import Flask import core from netcontroller import getOpenPort -def bootstrap_client_service(peer, core_inst=None): +def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=120): ''' Bootstrap client services ''' @@ -32,10 +32,10 @@ def bootstrap_client_service(peer, core_inst=None): if not core_inst._utils.validatePubKey(peer): raise ValueError('Peer must be valid base32 ed25519 public key') - - http_server = WSGIServer(('127.0.0.1', bootstrap_port), bootstrap_app, log=None) + bootstrap_port = getOpenPort() - bootstrap_app = flask.Flask(__name__) + bootstrap_app = Flask(__name__) + http_server = WSGIServer(('127.0.0.1', bootstrap_port), bootstrap_app, log=None) bootstrap_address = '' @@ -50,14 +50,14 @@ def bootstrap_client_service(peer, core_inst=None): bootstrap_address = address http_server.stop() - with Controller.from_port() as controller: + with Controller.from_port(port=core_inst.config.get('tor.controlPort')) as controller: # Connect to the Tor process for Onionr - controller.authenticate() + controller.authenticate(core_inst.config.get('tor.controlpassword')) # Create the v3 onion service - response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, await_publication = True, key_type='ED25519-V3') + response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, await_publication = True, key_content = 'ED25519-V3') - core_inst.insertBlock(response.hostname, header='con', sign=True, encryptType='asym', - asymPeer=peer, disableForward=True) + core_inst.insertBlock(response.service_id, header='con', sign=True, encryptType='asym', + asymPeer=peer, disableForward=True, expire=(core_inst._utils.getEpoch() + bootstrap_timeout)) # Run the bootstrap server http_server.serve_forever() diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index d602d8ce..54ff03c3 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -1,4 +1,27 @@ +import stem, flask +import core class ConnectionServer: - def __init__(self): - return - \ No newline at end of file + def __init__(self, peer, address, core_inst=None): + if core_inst is None: + self.core_inst = core.Core() + else: + self.core_inst = core_inst + + if not core_inst._utils.validatePubKey(peer): + raise ValueError('Peer must be valid base32 ed25519 public key') + + service_app = flask.Flask(__name__) + service_port = getOpenPort() + http_server = WSGIServer(('127.0.0.1', service_port), service_app, log=None) + + @service_app.route('/ping') + def get_ping(): + return "pong!" + + with Controller.from_port(port=core_inst.config.get('tor.controlPort')) as controller: + # Connect to the Tor process for Onionr + controller.authenticate(core_inst.config.get('tor.controlpassword')) + # Create the v3 onion service + response = controller.create_ephemeral_hidden_service({80: service_port}, await_publication = True, key_type='ED25519-V3') + logger.info('hosting on ' + response.service_id) + http_server.serve_forever() \ No newline at end of file diff --git a/onionr/static-data/default-plugins/metadataprocessor/main.py b/onionr/static-data/default-plugins/metadataprocessor/main.py index e7277c7d..54f7dc18 100755 --- a/onionr/static-data/default-plugins/metadataprocessor/main.py +++ b/onionr/static-data/default-plugins/metadataprocessor/main.py @@ -46,6 +46,8 @@ def on_processblocks(api, data=None): myBlock = api.data['block'] blockType = api.data['type'] logger.info('blockType is ' + blockType) + utils = api.get_utils() + core = api.get_core() # Process specific block types @@ -54,26 +56,12 @@ def on_processblocks(api, data=None): if api.data['validSig'] == True: _processForwardKey(api, myBlock) # socket blocks - elif blockType == 'socket': + elif blockType == 'con': if api.data['validSig'] == True and myBlock.decrypted: # we check if it is decrypted as a way of seeing if it was for us - logger.info('Detected socket advertised to us...') - try: - address = myBlock.getMetadata('address') - except KeyError: - raise onionrexceptions.MissingAddress("Missing address for new socket") - try: - port = myBlock.getMetadata('port') - except KeyError: - raise ValueError("Missing port for new socket") - try: - reason = myBlock.getMetadata('reason') - except KeyError: - raise ValueError("Missing socket reason") - - socketInfo = json.dumps({'peer': api.data['signer'], 'address': address, 'port': port, 'create': False, 'reason': reason}) - api.get_core().daemonQueueAdd('addSocket', socketInfo) - else: - logger.warn("socket is not for us or is invalid") + myBlock.bcontent = utils.bytesToStr(myBlock.bcontent) + if utils.validateID('%s.onion' % (myBlock.bcontent,)): + logger.info('Detected socket advertised to us...') + core.keyStore.put('con', (myBlock.content, myBlock.signer)) def on_init(api, data = None): From ae2d79247195431b6f69265ae0c304eccc80b9f6 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 22 Mar 2019 21:58:09 -0500 Subject: [PATCH 022/173] bootstrap blocks now inserted and detected --- onionr/communicator.py | 2 ++ onionr/communicatorutils/onionrcommunicatortimers.py | 5 +++-- onionr/onionrservices/__init__.py | 1 + onionr/onionrservices/bootstrapservice.py | 5 +++-- onionr/onionrservices/connectionserver.py | 2 +- onionr/onionrutils.py | 2 ++ .../default-plugins/metadataprocessor/main.py | 9 +-------- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/onionr/communicator.py b/onionr/communicator.py index 37c0cbdb..e4a513d6 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -24,6 +24,7 @@ from dependencies import secrets from utils import networkmerger import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block from communicatorutils import onionrdaemontools +from communicatorutils import servicecreator import onionrservices, onionr, onionrproofs from communicatorutils import onionrcommunicatortimers, proxypicker @@ -107,6 +108,7 @@ class OnionrCommunicatorDaemon: OnionrCommunicatorTimers(self, self.uploadBlock, 10, requiresPeer=True, maxThreads=1) OnionrCommunicatorTimers(self, self.daemonCommands, 6, maxThreads=1) OnionrCommunicatorTimers(self, self.detectAPICrash, 30, maxThreads=1) + OnionrCommunicatorTimers(self, servicecreator.service_creator, 5, maxThreads=10, myArgs=(self,)) deniableBlockTimer = OnionrCommunicatorTimers(self, self.daemonTools.insertDeniableBlock, 180, requiresPeer=True, maxThreads=1) netCheckTimer = OnionrCommunicatorTimers(self, self.daemonTools.netCheck, 600) diff --git a/onionr/communicatorutils/onionrcommunicatortimers.py b/onionr/communicatorutils/onionrcommunicatortimers.py index c15f20fc..a94a7821 100755 --- a/onionr/communicatorutils/onionrcommunicatortimers.py +++ b/onionr/communicatorutils/onionrcommunicatortimers.py @@ -20,7 +20,7 @@ ''' import threading, onionrexceptions, logger class OnionrCommunicatorTimers: - def __init__(self, daemonInstance, timerFunction, frequency, makeThread=True, threadAmount=1, maxThreads=5, requiresPeer=False): + def __init__(self, daemonInstance, timerFunction, frequency, makeThread=True, threadAmount=1, maxThreads=5, requiresPeer=False, myArgs=[]): self.timerFunction = timerFunction self.frequency = frequency self.threadAmount = threadAmount @@ -29,6 +29,7 @@ class OnionrCommunicatorTimers: self.daemonInstance = daemonInstance self.maxThreads = maxThreads self._core = self.daemonInstance._core + self.args = myArgs self.daemonInstance.timers.append(self) self.count = 0 @@ -55,7 +56,7 @@ class OnionrCommunicatorTimers: logger.debug('%s is currently using the maximum number of threads, not starting another.' % self.timerFunction.__name__) else: self.daemonInstance.threadCounts[self.timerFunction.__name__] += 1 - newThread = threading.Thread(target=self.timerFunction) + newThread = threading.Thread(target=self.timerFunction, args=self.args) newThread.start() else: self.timerFunction() diff --git a/onionr/onionrservices/__init__.py b/onionr/onionrservices/__init__.py index 5ea0ef3e..7194ea22 100644 --- a/onionr/onionrservices/__init__.py +++ b/onionr/onionrservices/__init__.py @@ -2,6 +2,7 @@ import time import stem import core from . import connectionserver, bootstrapservice + class OnionrServices: def __init__(self, onionr_core): assert isinstance(onionr_core, core.Core) diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py index b0b2fb7d..5dafe0f3 100644 --- a/onionr/onionrservices/bootstrapservice.py +++ b/onionr/onionrservices/bootstrapservice.py @@ -23,7 +23,7 @@ from flask import Flask import core from netcontroller import getOpenPort -def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=120): +def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): ''' Bootstrap client services ''' @@ -54,7 +54,8 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=120): # Connect to the Tor process for Onionr controller.authenticate(core_inst.config.get('tor.controlpassword')) # Create the v3 onion service - response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, await_publication = True, key_content = 'ED25519-V3') + #response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, await_publication = True, key_content = 'ED25519-V3') + response = controller.create_ephemeral_hidden_service(80, key_type = 'NEW', await_publication = True) core_inst.insertBlock(response.service_id, header='con', sign=True, encryptType='asym', asymPeer=peer, disableForward=True, expire=(core_inst._utils.getEpoch() + bootstrap_timeout)) diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index 54ff03c3..32d2640a 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -22,6 +22,6 @@ class ConnectionServer: # Connect to the Tor process for Onionr controller.authenticate(core_inst.config.get('tor.controlpassword')) # Create the v3 onion service - response = controller.create_ephemeral_hidden_service({80: service_port}, await_publication = True, key_type='ED25519-V3') + response = controller.create_ephemeral_hidden_service({80: service_port}, await_publication = True, key_type='NEW') logger.info('hosting on ' + response.service_id) http_server.serve_forever() \ No newline at end of file diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 082b14fb..b9ae61c4 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -182,6 +182,8 @@ class OnionrUtils: pass else: self._core.updateBlockInfo(blockHash, 'expire', expireTime) + if not blockType is None: + self._core.updateBlockInfo(blockHash, 'dataType', blockType) else: pass #logger.debug('Not processing metadata on encrypted block we cannot decrypt.') diff --git a/onionr/static-data/default-plugins/metadataprocessor/main.py b/onionr/static-data/default-plugins/metadataprocessor/main.py index 54f7dc18..787b0174 100755 --- a/onionr/static-data/default-plugins/metadataprocessor/main.py +++ b/onionr/static-data/default-plugins/metadataprocessor/main.py @@ -55,14 +55,7 @@ def on_processblocks(api, data=None): if blockType == 'forwardKey': if api.data['validSig'] == True: _processForwardKey(api, myBlock) - # socket blocks - elif blockType == 'con': - if api.data['validSig'] == True and myBlock.decrypted: # we check if it is decrypted as a way of seeing if it was for us - myBlock.bcontent = utils.bytesToStr(myBlock.bcontent) - if utils.validateID('%s.onion' % (myBlock.bcontent,)): - logger.info('Detected socket advertised to us...') - core.keyStore.put('con', (myBlock.content, myBlock.signer)) - + def on_init(api, data = None): pluginapi = api From d1737216e426833d6f87d9d5e52a70b3668b22cf Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 23 Mar 2019 21:56:46 -0500 Subject: [PATCH 023/173] direct connections closer to working --- onionr/communicator.py | 12 ++++++------ onionr/communicatorutils/servicecreator.py | 17 +++++++++++++++++ onionr/onionrservices/__init__.py | 3 +-- onionr/onionrservices/bootstrapservice.py | 3 +-- onionr/static-data/default_config.json | 2 +- 5 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 onionr/communicatorutils/servicecreator.py diff --git a/onionr/communicator.py b/onionr/communicator.py index e4a513d6..670e08fa 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -108,7 +108,12 @@ class OnionrCommunicatorDaemon: OnionrCommunicatorTimers(self, self.uploadBlock, 10, requiresPeer=True, maxThreads=1) OnionrCommunicatorTimers(self, self.daemonCommands, 6, maxThreads=1) OnionrCommunicatorTimers(self, self.detectAPICrash, 30, maxThreads=1) - OnionrCommunicatorTimers(self, servicecreator.service_creator, 5, maxThreads=10, myArgs=(self,)) + if config.get('general.socket_servers', False): + self.services = onionrservices.OnionrServices(self._core) + self.active_services = [] + OnionrCommunicatorTimers(self, servicecreator.service_creator, 5, maxThreads=10, myArgs=(self,)) + else: + self.services = None deniableBlockTimer = OnionrCommunicatorTimers(self, self.daemonTools.insertDeniableBlock, 180, requiresPeer=True, maxThreads=1) netCheckTimer = OnionrCommunicatorTimers(self, self.daemonTools.netCheck, 600) @@ -127,11 +132,6 @@ class OnionrCommunicatorDaemon: blockCleanupTimer.count = (blockCleanupTimer.frequency - 5) #forwardSecrecyTimer.count = (forwardSecrecyTimer.frequency - 990) - if config.get('general.socket_servers'): - self.services = onionrservices.OnionrServices(self._core) - else: - self.services = None - # Main daemon loop, mainly for calling timers, don't do any complex operations here to avoid locking try: while not self.shutdown: diff --git a/onionr/communicatorutils/servicecreator.py b/onionr/communicatorutils/servicecreator.py new file mode 100644 index 00000000..4914547a --- /dev/null +++ b/onionr/communicatorutils/servicecreator.py @@ -0,0 +1,17 @@ +import communicator, onionrblockapi +def service_creator(daemon): + assert isinstance(daemon, communicator.OnionrCommunicatorDaemon) + core = daemon._core + utils = core._utils + + # Find socket connection blocks + con_blocks = core.getBlocksByType('con') + for b in con_blocks: + if not b in daemon.active_services: + bl = onionrblockapi.Block(b, core=core, decrypt=True) + bs = utils.bytesToStr(bl.bcontent) + '.onion' + if utils.validatePubKey(bl.signer) and utils.validateID(bs): + daemon.active_services.append(b) + daemon.services.create_server(bl.signer, bs) + + daemon.decrementThreadCount('service_creator') \ No newline at end of file diff --git a/onionr/onionrservices/__init__.py b/onionr/onionrservices/__init__.py index 7194ea22..3f0e81e5 100644 --- a/onionr/onionrservices/__init__.py +++ b/onionr/onionrservices/__init__.py @@ -17,9 +17,8 @@ class OnionrServices: BOOTSTRAP_TRIES = 10 TRY_WAIT = 3 for x in range(BOOTSTRAP_TRIES): - if self._core._utils.doGetRequest('http://' + address + '/ping') == 'pong!': + if self._core._utils.doGetRequest('http://' + address + '/ping', port=self._core.config.get('tor.socksport')) == 'pong!': connectionserver.ConnectionServer(peer, address, core_inst=self._core) - return True else: time.sleep(TRY_WAIT) else: diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py index 5dafe0f3..33c4e8b4 100644 --- a/onionr/onionrservices/bootstrapservice.py +++ b/onionr/onionrservices/bootstrapservice.py @@ -55,8 +55,7 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): controller.authenticate(core_inst.config.get('tor.controlpassword')) # Create the v3 onion service #response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, await_publication = True, key_content = 'ED25519-V3') - response = controller.create_ephemeral_hidden_service(80, key_type = 'NEW', await_publication = True) - + response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, key_type = 'NEW', await_publication = True) core_inst.insertBlock(response.service_id, header='con', sign=True, encryptType='asym', asymPeer=peer, disableForward=True, expire=(core_inst._utils.getEpoch() + bootstrap_timeout)) diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index 86261502..2b9d47df 100755 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -4,7 +4,7 @@ "display_header" : false, "minimum_block_pow": 4, "minimum_send_pow": 4, - "socket_servers": false, + "socket_servers": true, "security_level": 0, "max_block_age": 2678400, "bypass_tor_check": false, From 2f01a096efef058a4a12dfbf70158366df6bb476 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 24 Mar 2019 22:32:17 -0500 Subject: [PATCH 024/173] cleanup, work, and bugfixes for direct connections --- onionr/api.py | 7 ++-- onionr/onionrblockapi.py | 5 ++- onionr/onionrservices/__init__.py | 4 ++- onionr/onionrservices/bootstrapservice.py | 12 ++++--- onionr/onionrservices/connectionserver.py | 42 ++++++++++++++++++++--- 5 files changed, 57 insertions(+), 13 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 79694238..4f7276aa 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -39,7 +39,7 @@ class FDSafeHandler(WSGIHandler): except Timeout as ex: raise -def setBindIP(filePath): +def setBindIP(filePath, writeOut=True): '''Set a random localhost IP to a specified file (intended for private or public API localhost IPs)''' if config.get('general.random_bind_ip', True): hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))] @@ -55,8 +55,9 @@ def setBindIP(filePath): s.close() else: data = '127.0.0.1' - with open(filePath, 'w') as bindFile: - bindFile.write(data) + if writeOut: + with open(filePath, 'w') as bindFile: + bindFile.write(data) return data class PublicAPI: diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py index e7a4395b..1f890bcc 100755 --- a/onionr/onionrblockapi.py +++ b/onionr/onionrblockapi.py @@ -215,7 +215,10 @@ class Block: ''' if self.exists(): - os.remove(self.getBlockFile()) + try: + os.remove(self.getBlockFile()) + except TypeError: + pass self.getCore().removeBlock(self.getHash()) return True return False diff --git a/onionr/onionrservices/__init__.py b/onionr/onionrservices/__init__.py index 3f0e81e5..21ded6ec 100644 --- a/onionr/onionrservices/__init__.py +++ b/onionr/onionrservices/__init__.py @@ -16,8 +16,10 @@ class OnionrServices: assert self._core._utils.validateID(address) BOOTSTRAP_TRIES = 10 TRY_WAIT = 3 + base_url = 'http://%s/' % (address,) + socks = self._core.config.get('tor.socksport') for x in range(BOOTSTRAP_TRIES): - if self._core._utils.doGetRequest('http://' + address + '/ping', port=self._core.config.get('tor.socksport')) == 'pong!': + if self._core._utils.doGetRequest(base_url + 'ping', port=socks, ignoreAPI=True) == 'pong!': connectionserver.ConnectionServer(peer, address, core_inst=self._core) else: time.sleep(TRY_WAIT) diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py index 33c4e8b4..f1e0b35c 100644 --- a/onionr/onionrservices/bootstrapservice.py +++ b/onionr/onionrservices/bootstrapservice.py @@ -17,11 +17,12 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' +import time from gevent.pywsgi import WSGIServer, WSGIHandler from stem.control import Controller from flask import Flask import core -from netcontroller import getOpenPort +from netcontroller import getOpenPort def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): ''' @@ -38,6 +39,7 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): http_server = WSGIServer(('127.0.0.1', bootstrap_port), bootstrap_app, log=None) bootstrap_address = '' + shutdown = False @bootstrap_app.route('/ping') def get_ping(): @@ -48,20 +50,22 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): if core_inst._utils.validateID(address): # Set the bootstrap address then close the server bootstrap_address = address - http_server.stop() + shutdown = True + return "success" with Controller.from_port(port=core_inst.config.get('tor.controlPort')) as controller: # Connect to the Tor process for Onionr controller.authenticate(core_inst.config.get('tor.controlpassword')) # Create the v3 onion service - #response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, await_publication = True, key_content = 'ED25519-V3') response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, key_type = 'NEW', await_publication = True) core_inst.insertBlock(response.service_id, header='con', sign=True, encryptType='asym', asymPeer=peer, disableForward=True, expire=(core_inst._utils.getEpoch() + bootstrap_timeout)) # Run the bootstrap server - http_server.serve_forever() + threading.Thread(target=http_server.serve_forever).start() # This line reached when server is shutdown by being bootstrapped + while not shutdown and not core_inst.killSockets: + time.sleep(1) # Now that the bootstrap server has received a server, return the address return bootstrap_address diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index 32d2640a..60aff03e 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -1,5 +1,30 @@ -import stem, flask -import core +''' + Onionr - P2P Anonymous Storage Network + + This module does the second part of the bootstrap block handshake and creates the API server +''' +''' + 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 threading, time +from gevent.pywsgi import WSGIServer, WSGIHandler +from stem.control import Controller +from flask import Flask +import core, logger +from netcontroller import getOpenPort +from api import setBindIP + class ConnectionServer: def __init__(self, peer, address, core_inst=None): if core_inst is None: @@ -10,10 +35,15 @@ class ConnectionServer: if not core_inst._utils.validatePubKey(peer): raise ValueError('Peer must be valid base32 ed25519 public key') - service_app = flask.Flask(__name__) + socks = core_inst.config.get('tor.socksport') # Load config for Tor socks port for proxy + service_app = Flask(__name__) # Setup Flask app for server. service_port = getOpenPort() + service_ip = setBindIP() http_server = WSGIServer(('127.0.0.1', service_port), service_app, log=None) + + # TODO define basic endpoints useful for direct connections like stats + # TODO load endpoints from plugins @service_app.route('/ping') def get_ping(): return "pong!" @@ -23,5 +53,9 @@ class ConnectionServer: controller.authenticate(core_inst.config.get('tor.controlpassword')) # Create the v3 onion service response = controller.create_ephemeral_hidden_service({80: service_port}, await_publication = True, key_type='NEW') + self.core_inst._utils.doPostRequest('http://' + address + '/bs/' + response.service_id, port=socks) logger.info('hosting on ' + response.service_id) - http_server.serve_forever() \ No newline at end of file + threading.Thread(target=http_server.serve_forever).start() + while not self.core_inst.killSockets: + time.sleep(1) + http_server.stop() \ No newline at end of file From 7be654401ba45b5115f8b370bc793e5778495b70 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 25 Mar 2019 18:46:25 -0500 Subject: [PATCH 025/173] bug fixes for direct connnections --- onionr/api.py | 4 ++-- onionr/communicator.py | 2 +- onionr/onionrservices/bootstrapservice.py | 28 +++++++++++++---------- onionr/onionrservices/connectionserver.py | 11 ++++----- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 4f7276aa..e542bd35 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -39,7 +39,7 @@ class FDSafeHandler(WSGIHandler): except Timeout as ex: raise -def setBindIP(filePath, writeOut=True): +def setBindIP(filePath=''): '''Set a random localhost IP to a specified file (intended for private or public API localhost IPs)''' if config.get('general.random_bind_ip', True): hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))] @@ -55,7 +55,7 @@ def setBindIP(filePath, writeOut=True): s.close() else: data = '127.0.0.1' - if writeOut: + if filePath != '': with open(filePath, 'w') as bindFile: bindFile.write(data) return data diff --git a/onionr/communicator.py b/onionr/communicator.py index 670e08fa..bb56ca19 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -111,7 +111,7 @@ class OnionrCommunicatorDaemon: if config.get('general.socket_servers', False): self.services = onionrservices.OnionrServices(self._core) self.active_services = [] - OnionrCommunicatorTimers(self, servicecreator.service_creator, 5, maxThreads=10, myArgs=(self,)) + OnionrCommunicatorTimers(self, servicecreator.service_creator, 5, maxThreads=50, myArgs=(self,)) else: self.services = None deniableBlockTimer = OnionrCommunicatorTimers(self, self.daemonTools.insertDeniableBlock, 180, requiresPeer=True, maxThreads=1) diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py index f1e0b35c..95ae0158 100644 --- a/onionr/onionrservices/bootstrapservice.py +++ b/onionr/onionrservices/bootstrapservice.py @@ -17,10 +17,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import time +import time, threading, uuid from gevent.pywsgi import WSGIServer, WSGIHandler from stem.control import Controller -from flask import Flask +from flask import Flask, Response import core from netcontroller import getOpenPort @@ -40,6 +40,7 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): bootstrap_address = '' shutdown = False + bs_id = str(uuid.uuid4()) @bootstrap_app.route('/ping') def get_ping(): @@ -47,25 +48,28 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): @bootstrap_app.route('/bs/
', methods=['POST']) def get_bootstrap(address): - if core_inst._utils.validateID(address): + if core_inst._utils.validateID(address + '.onion'): # Set the bootstrap address then close the server - bootstrap_address = address - shutdown = True - return "success" + bootstrap_address = address + '.onion' + core_inst.keyStore.put(bs_id, bootstrap_address) + http_server.stop() + return Response("success") + else: + return Response("") with Controller.from_port(port=core_inst.config.get('tor.controlPort')) as controller: # Connect to the Tor process for Onionr controller.authenticate(core_inst.config.get('tor.controlpassword')) # Create the v3 onion service - response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, key_type = 'NEW', await_publication = True) + response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, key_type = 'NEW', key_content = 'ED25519-V3', await_publication = True) core_inst.insertBlock(response.service_id, header='con', sign=True, encryptType='asym', asymPeer=peer, disableForward=True, expire=(core_inst._utils.getEpoch() + bootstrap_timeout)) - # Run the bootstrap server - threading.Thread(target=http_server.serve_forever).start() + try: + http_server.serve_forever() + except TypeError: + pass # This line reached when server is shutdown by being bootstrapped - while not shutdown and not core_inst.killSockets: - time.sleep(1) # Now that the bootstrap server has received a server, return the address - return bootstrap_address + return core_inst.keyStore.get(bs_id) diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index 60aff03e..d8a5f773 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -23,7 +23,7 @@ from stem.control import Controller from flask import Flask import core, logger from netcontroller import getOpenPort -from api import setBindIP +import api class ConnectionServer: def __init__(self, peer, address, core_inst=None): @@ -38,9 +38,8 @@ class ConnectionServer: socks = core_inst.config.get('tor.socksport') # Load config for Tor socks port for proxy service_app = Flask(__name__) # Setup Flask app for server. service_port = getOpenPort() - service_ip = setBindIP() + service_ip = api.setBindIP() http_server = WSGIServer(('127.0.0.1', service_port), service_app, log=None) - # TODO define basic endpoints useful for direct connections like stats # TODO load endpoints from plugins @@ -52,10 +51,8 @@ class ConnectionServer: # Connect to the Tor process for Onionr controller.authenticate(core_inst.config.get('tor.controlpassword')) # Create the v3 onion service - response = controller.create_ephemeral_hidden_service({80: service_port}, await_publication = True, key_type='NEW') + response = controller.create_ephemeral_hidden_service({80: service_port}, await_publication = True, key_type='NEW', key_content = 'ED25519-V3') self.core_inst._utils.doPostRequest('http://' + address + '/bs/' + response.service_id, port=socks) logger.info('hosting on ' + response.service_id) - threading.Thread(target=http_server.serve_forever).start() - while not self.core_inst.killSockets: - time.sleep(1) + http_server.serve_forever() http_server.stop() \ No newline at end of file From cd6eb59023db987eeef2967483d63252ef055d66 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 25 Mar 2019 23:25:46 -0500 Subject: [PATCH 026/173] more work on direct connections --- onionr/api.py | 17 +++-------------- onionr/communicator.py | 4 +++- onionr/onionrservices/bootstrapservice.py | 6 ++++++ onionr/onionrservices/connectionserver.py | 3 ++- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index e542bd35..7e552355 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -27,6 +27,7 @@ from onionrblockapi import Block import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config import httpapi from httpapi import friendsapi, simplecache +from onionrservices import httpheaders import onionr class FDSafeHandler(WSGIHandler): @@ -92,17 +93,9 @@ class PublicAPI: @app.after_request def sendHeaders(resp): '''Send api, access control headers''' - resp.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch, since we can't fully remove the header. - # CSP to prevent XSS. Mainly for client side attacks (if hostname protection could somehow be bypassed) - resp.headers["Content-Security-Policy"] = "default-src 'none'; script-src 'none'; object-src 'none'; style-src data: 'unsafe-inline'; img-src data:; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'none'" - # Prevent click jacking - resp.headers['X-Frame-Options'] = 'deny' - # No sniff is possibly not needed - resp.headers['X-Content-Type-Options'] = "nosniff" + resp = httpheaders.set_default_onionr_http_headers(resp) # Network API version resp.headers['X-API'] = onionr.API_VERSION - # Close connections to limit FD use - resp.headers['Connection'] = "close" self.lastRequest = clientAPI._core._utils.getRoundedEpoch(roundS=5) return resp @@ -300,15 +293,11 @@ class API: @app.after_request def afterReq(resp): # Security headers + resp = httpheaders.set_default_onionr_http_headers(resp) if request.endpoint == 'site': resp.headers['Content-Security-Policy'] = "default-src 'none'; style-src data: 'unsafe-inline'; img-src data:" else: resp.headers['Content-Security-Policy'] = "default-src 'none'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'self'" - resp.headers['X-Frame-Options'] = 'deny' - resp.headers['X-Content-Type-Options'] = "nosniff" - resp.headers['Server'] = '' - resp.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch. - resp.headers['Connection'] = "close" return resp @app.route('/board/', endpoint='board') diff --git a/onionr/communicator.py b/onionr/communicator.py index bb56ca19..47f8e5d6 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -111,6 +111,7 @@ class OnionrCommunicatorDaemon: if config.get('general.socket_servers', False): self.services = onionrservices.OnionrServices(self._core) self.active_services = [] + self.service_greenlets = [] OnionrCommunicatorTimers(self, servicecreator.service_creator, 5, maxThreads=50, myArgs=(self,)) else: self.services = None @@ -148,7 +149,8 @@ class OnionrCommunicatorDaemon: pass logger.info('Goodbye.') - self._core.killSockets = True + for server in self.service_greenlets: + server.stop() self._core._utils.localCommand('shutdown') # shutdown the api time.sleep(0.5) diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py index 95ae0158..292df4ed 100644 --- a/onionr/onionrservices/bootstrapservice.py +++ b/onionr/onionrservices/bootstrapservice.py @@ -37,6 +37,12 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): bootstrap_port = getOpenPort() bootstrap_app = Flask(__name__) http_server = WSGIServer(('127.0.0.1', bootstrap_port), bootstrap_app, log=None) + try: + core_inst.onionrInst.communicatorInst + except AttributeError: + pass + else: + core_inst.onionrInst.communicatorInst.service_greenlets.append(http_server) bootstrap_address = '' shutdown = False diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index d8a5f773..6b04645c 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -40,6 +40,7 @@ class ConnectionServer: service_port = getOpenPort() service_ip = api.setBindIP() http_server = WSGIServer(('127.0.0.1', service_port), service_app, log=None) + core_inst.onionrInst.communicatorInst.service_greenlets.append(http_server) # TODO define basic endpoints useful for direct connections like stats # TODO load endpoints from plugins @@ -53,6 +54,6 @@ class ConnectionServer: # Create the v3 onion service response = controller.create_ephemeral_hidden_service({80: service_port}, await_publication = True, key_type='NEW', key_content = 'ED25519-V3') self.core_inst._utils.doPostRequest('http://' + address + '/bs/' + response.service_id, port=socks) - logger.info('hosting on ' + response.service_id) + logger.info('hosting on %s with %s' % (response.service_id, peer)) http_server.serve_forever() http_server.stop() \ No newline at end of file From 2e75e08879ec38880f18d12fbf7b27bfc409d1c0 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 26 Mar 2019 10:23:31 -0500 Subject: [PATCH 027/173] added httpheaders.py --- onionr/onionrservices/httpheaders.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 onionr/onionrservices/httpheaders.py diff --git a/onionr/onionrservices/httpheaders.py b/onionr/onionrservices/httpheaders.py new file mode 100644 index 00000000..2b2c1770 --- /dev/null +++ b/onionr/onionrservices/httpheaders.py @@ -0,0 +1,28 @@ +''' + Onionr - P2P Anonymous Storage Network + + Set default onionr http headers +''' +''' + 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 . +''' +def set_default_onionr_http_headers(flask_response): + '''Response headers''' + flask_response.headers['Content-Security-Policy'] = "default-src 'none'; style-src data: 'unsafe-inline'; img-src data:" + flask_response.headers['X-Frame-Options'] = 'deny' + flask_response.headers['X-Content-Type-Options'] = "nosniff" + flask_response.headers['Server'] = '' + flask_response.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch. + flask_response.headers['Connection'] = "close" + return flask_response \ No newline at end of file From d3c5fe3a5a74b1a691a4c286441dd0a944c280ad Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 27 Mar 2019 12:38:46 -0500 Subject: [PATCH 028/173] added clandestine --- onionr/onionrservices/bootstrapservice.py | 7 + onionr/onionrservices/connectionserver.py | 7 + onionr/onionrsockets.py | 172 ------------------ .../default-plugins/clandestine/controlapi.py | 26 +++ .../default-plugins/clandestine/info.json | 5 + .../default-plugins/clandestine/main.py | 46 +++++ 6 files changed, 91 insertions(+), 172 deletions(-) delete mode 100755 onionr/onionrsockets.py create mode 100644 onionr/static-data/default-plugins/clandestine/controlapi.py create mode 100755 onionr/static-data/default-plugins/clandestine/info.json create mode 100755 onionr/static-data/default-plugins/clandestine/main.py diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py index 292df4ed..a2952563 100644 --- a/onionr/onionrservices/bootstrapservice.py +++ b/onionr/onionrservices/bootstrapservice.py @@ -23,6 +23,7 @@ from stem.control import Controller from flask import Flask, Response import core from netcontroller import getOpenPort +from . import httpheaders def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): ''' @@ -52,6 +53,12 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): def get_ping(): return "pong!" + @bootstrap_app.after_request + def afterReq(resp): + # Security headers + resp = httpheaders.set_default_onionr_http_headers(resp) + return resp + @bootstrap_app.route('/bs/
', methods=['POST']) def get_bootstrap(address): if core_inst._utils.validateID(address + '.onion'): diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index 6b04645c..ee586e2b 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -24,6 +24,7 @@ from flask import Flask import core, logger from netcontroller import getOpenPort import api +from . import httpheaders class ConnectionServer: def __init__(self, peer, address, core_inst=None): @@ -48,6 +49,12 @@ class ConnectionServer: def get_ping(): return "pong!" + @service_app.after_request + def afterReq(resp): + # Security headers + resp = httpheaders.set_default_onionr_http_headers(resp) + return resp + with Controller.from_port(port=core_inst.config.get('tor.controlPort')) as controller: # Connect to the Tor process for Onionr controller.authenticate(core_inst.config.get('tor.controlpassword')) diff --git a/onionr/onionrsockets.py b/onionr/onionrsockets.py deleted file mode 100755 index aa02c3db..00000000 --- a/onionr/onionrsockets.py +++ /dev/null @@ -1,172 +0,0 @@ -''' - Onionr - P2P Anonymous Storage Network - - Onionr Socket interface -''' -''' - 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 stem.control -import threading -import socks, config, uuid -import onionrexceptions, time, requests, onionrblockapi, logger -from dependencies import secrets -from gevent.pywsgi import WSGIServer -from flask import request, Response, abort -import flask -class OnionrSocketServer: - def __init__(self, coreInst): - self._core = coreInst - app = flask.Flask(__name__) - self._core.socketServerConnData = {} - self.bindPort = 0 - - self.sockets = {} - - while self.bindPort < 1024: - self.bindPort = secrets.randbelow(65535) - - self.responseData = {} - - threading.Thread(target=self.detectShutdown).start() - threading.Thread(target=self.socketStarter).start() - app = flask.Flask(__name__) - self.http_server = WSGIServer(('127.0.0.1', self.bindPort), app) - self.http_server.serve_forever() - - @app.route('/dc/', methods=['POST']) - def acceptConn(self): - data = request.form['data'] - data = self._core._utils.bytesTorStr(data) - data = {'date': self._core._utils.getEpoch(), 'data': data} - myPeer = '' - retData = '' - for peer in self.sockets: - if self.sockets[peer] == request.host: - myPeer = peer - break - else: - return "" - - if request.host in self.sockets: - self._core.socketServerConnData[myPeer].append(data) - else: - self._core.socketServerConnData[myPeer] = [data] - - try: - retData = self._core.socketServerResponseData[myPeer] - except KeyError: - pass - else: - self._core.socketServerResponseData[myPeer] = '' - - return retData - - def socketStarter(self): - while not self._core.killSockets: - try: - self.addSocket(self._core.startSocket['peer'], reason=self._core.startSocket['reason']) - except KeyError: - pass - else: - logger.info('%s socket started with %s' % (self._core.startSocket['reason'], self._core.startSocket['peer'])) - self._core.startSocket = {} - time.sleep(1) - - def detectShutdown(self): - while not self._core.killSockets: - time.sleep(5) - - logger.debug('Killing socket server...') - self.http_server.stop() - - def addSocket(self, peer, reason=''): - bindPort = 1337 - - assert len(reason) <= 12 - - with stem.control.Controller.from_port(port=config.get('tor.controlPort')) as controller: - controller.authenticate(config.get('tor.controlpassword')) - - socket = controller.create_ephemeral_hidden_service({80: bindPort}, await_publication = True) - self.sockets[peer] = socket.service_id + '.onion' - - self.responseData[socket.service_id + '.onion'] = '' - - self._core.insertBlock(str(uuid.uuid4()), header='socket', sign=True, encryptType='asym', asymPeer=peer, meta={'reason': reason, 'address': socket.service_id + '.onion'}) - self._core.socketReasons[peer] = reason - return - -class OnionrSocketClient: - def __init__(self, coreInst): - self.sockets = {} # pubkey: tor address - self.connPool = {} - self.sendData = {} - self._core = coreInst - self.response = '' - self.request = '' - self.connected = False - self.killSocket = False - - def startSocket(self, peer, reason): - address = '' - logger.info('Trying to find socket server for %s' % (peer,)) - # Find the newest open socket for a given peer - for block in self._core.getBlocksByType('socket'): - block = onionrblockapi.Block(block, core=self._core) - if block.decrypt(): - theSigner = block.signer - try: - theSigner = theSigner.decode() - except AttributeError: - pass - if block.verifySig() and theSigner == peer: - address = block.getMetadata('address') - if self._core._utils.validateID(address): - # If we got their address, it is valid, and verified, we can break out - if block.getMetadata('reason') == reason: - break - else: - logger.error('The socket the peer opened is not for %s' % (reason,)) - else: - logger.error('Peer transport id is invalid for socket: %s' % (address,)) - address = '' - else: - logger.warn('Block has invalid sig or id, was for %s' % (theSigner,)) - if address != '': - logger.info('%s socket client started with %s' % (reason, peer)) - self.sockets[peer] = address - data = 'hey' - while not self.killSocket: - try: - data = self.sendData[peer] - logger.info('Sending %s to %s' % (data, peer)) - except KeyError: - pass - else: - self.sendData[peer] = '' - postData = {'data': data} - self.connPool[peer] = {'date': self._core._utils.getEpoch(), 'data': self._core._utils.doPostRequest('http://' + address + '/dc/', data=postData)} - time.sleep(2) - - def getResponse(self, peer): - retData = '' - try: - retData = self.connPool[peer] - except KeyError: - pass - return - - def sendData(self, peer, data): - self.sendData[peer] = data diff --git a/onionr/static-data/default-plugins/clandestine/controlapi.py b/onionr/static-data/default-plugins/clandestine/controlapi.py new file mode 100644 index 00000000..ce341f82 --- /dev/null +++ b/onionr/static-data/default-plugins/clandestine/controlapi.py @@ -0,0 +1,26 @@ +''' + Onionr - P2P Anonymous Storage Network + + HTTP endpoints for controlling IMs +''' +''' + 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 . +''' +from flask import Response, request, redirect, Blueprint, abort + +flask_blueprint = Blueprint('clandenstine', __name__) + +@flask_blueprint.route('/mail/ping') +def ping(): + return 'pong!' diff --git a/onionr/static-data/default-plugins/clandestine/info.json b/onionr/static-data/default-plugins/clandestine/info.json new file mode 100755 index 00000000..7c5a143c --- /dev/null +++ b/onionr/static-data/default-plugins/clandestine/info.json @@ -0,0 +1,5 @@ +{ + "name" : "clandestine", + "version" : "1.0", + "author" : "onionr" +} diff --git a/onionr/static-data/default-plugins/clandestine/main.py b/onionr/static-data/default-plugins/clandestine/main.py new file mode 100755 index 00000000..bc7ca112 --- /dev/null +++ b/onionr/static-data/default-plugins/clandestine/main.py @@ -0,0 +1,46 @@ +''' + Onionr - P2P Anonymous Storage Network + + Instant message conversations with Onionr peers +''' +''' + 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 . +''' + +# Imports some useful libraries +import locale + +locale.setlocale(locale.LC_ALL, '') + +plugin_name = 'clandenstine' +PLUGIN_VERSION = '0.0.0' + +sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) +from . import controlapi +flask_blueprint = controlapi.flask_blueprint + +class Clandenstine: + def __init__(self, pluginapi): + self.myCore = pluginapi.get_core() + +def on_init(api, data = None): + ''' + This event is called after Onionr is initialized, but before the command + inputted is executed. Could be called when daemon is starting or when + just the client is running. + ''' + + pluginapi = api + chat = Clandenstine(pluginapi) + return From 726bbe61acec17c984a43b545bb85d79c198da53 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 27 Mar 2019 13:55:43 -0500 Subject: [PATCH 029/173] improved expiry --- onionr/communicator.py | 9 +++++++-- onionr/etc/onionrvalues.py | 3 ++- onionr/onionrutils.py | 10 +++++----- onionr/static-data/bootstrap-nodes.txt | 1 + onionr/static-data/default-plugins/clandestine/main.py | 2 +- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/onionr/communicator.py b/onionr/communicator.py index 47f8e5d6..ef94ae14 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -149,8 +149,13 @@ class OnionrCommunicatorDaemon: pass logger.info('Goodbye.') - for server in self.service_greenlets: - server.stop() + try: + self.service_greenlets + except AttributeError: + pass + else: + for server in self.service_greenlets: + server.stop() self._core._utils.localCommand('shutdown') # shutdown the api time.sleep(0.5) diff --git a/onionr/etc/onionrvalues.py b/onionr/etc/onionrvalues.py index 84203d88..338dc0d3 100755 --- a/onionr/etc/onionrvalues.py +++ b/onionr/etc/onionrvalues.py @@ -21,4 +21,5 @@ class OnionrValues: def __init__(self): self.passwordLength = 20 - self.blockMetadataLengths = {'meta': 1000, 'sig': 200, 'signer': 200, 'time': 10, 'powRandomToken': 1000, 'encryptType': 4, 'expire': 14} #TODO properly refine values to minimum needed \ No newline at end of file + self.blockMetadataLengths = {'meta': 1000, 'sig': 200, 'signer': 200, 'time': 10, 'powRandomToken': 1000, 'encryptType': 4, 'expire': 14} #TODO properly refine values to minimum needed + self.default_expire = 2678400 \ No newline at end of file diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index b9ae61c4..ea56b593 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -25,7 +25,7 @@ import onionrexceptions from onionr import API_VERSION import onionrevents import storagecounter -from etc import pgpwords +from etc import pgpwords, onionrvalues from onionrusers import onionrusers if sys.version_info < (3, 6): try: @@ -179,8 +179,8 @@ class OnionrUtils: expireTime = myBlock.getHeader('expire') assert len(str(int(expireTime))) < 20 # test that expire time is an integer of sane length (for epoch) except (AssertionError, ValueError, TypeError) as e: - pass - else: + expireTime = onionrvalues.OnionrValues().default_expire + finally: self._core.updateBlockInfo(blockHash, 'expire', expireTime) if not blockType is None: self._core.updateBlockInfo(blockHash, 'dataType', blockType) @@ -253,7 +253,7 @@ class OnionrUtils: pass # Validate metadata dict for invalid keys to sizes that are too large - maxAge = config.get("general.max_block_age", 2678400) + maxAge = config.get("general.max_block_age", onionrvalues.OnionrValues().default_expire) if type(metadata) is dict: for i in metadata: try: @@ -285,7 +285,7 @@ class OnionrUtils: try: assert int(metadata[i]) > self.getEpoch() except AssertionError: - logger.warn('Block is expired: %s greater than %s' % (metadata[i], self.getEpoch())) + logger.warn('Block is expired: %s less than %s' % (metadata[i], self.getEpoch())) break elif i == 'encryptType': try: diff --git a/onionr/static-data/bootstrap-nodes.txt b/onionr/static-data/bootstrap-nodes.txt index e69de29b..374ea7b4 100755 --- a/onionr/static-data/bootstrap-nodes.txt +++ b/onionr/static-data/bootstrap-nodes.txt @@ -0,0 +1 @@ +huei7fugyuesltkf3yzj27an36skz2f2u55g7ganyaq6yjhauwicwfyd.onion diff --git a/onionr/static-data/default-plugins/clandestine/main.py b/onionr/static-data/default-plugins/clandestine/main.py index bc7ca112..93bfd2d3 100755 --- a/onionr/static-data/default-plugins/clandestine/main.py +++ b/onionr/static-data/default-plugins/clandestine/main.py @@ -19,7 +19,7 @@ ''' # Imports some useful libraries -import locale +import locale, sys, os locale.setlocale(locale.LC_ALL, '') From 30630e1d8e0b8af3a0f6b0e708b9d8869e151db3 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 29 Mar 2019 12:37:51 -0500 Subject: [PATCH 030/173] work on clandestine --- onionr/communicatorutils/onionrdaemontools.py | 2 +- onionr/communicatorutils/servicecreator.py | 1 + onionr/core.py | 6 +-- onionr/etc/onionrvalues.py | 2 +- onionr/httpapi/__init__.py | 4 +- onionr/onionrcommands/pubkeymanager.py | 4 +- onionr/onionrservices/connectionserver.py | 8 +++- .../default-plugins/clandestine/controlapi.py | 2 +- .../default-plugins/clandestine/main.py | 9 ++-- .../default-plugins/clandestine/peerserver.py | 42 +++++++++++++++++++ 10 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 onionr/static-data/default-plugins/clandestine/peerserver.py diff --git a/onionr/communicatorutils/onionrdaemontools.py b/onionr/communicatorutils/onionrdaemontools.py index b68c693d..1c4e0220 100755 --- a/onionr/communicatorutils/onionrdaemontools.py +++ b/onionr/communicatorutils/onionrdaemontools.py @@ -98,7 +98,7 @@ class DaemonTools: if len(self.daemon.onlinePeers) == 0: if not netutils.checkNetwork(self.daemon._core._utils, torPort=self.daemon.proxyPort): if not self.daemon.shutdown: - logger.warn('Network check failed, are you connected to the internet?') + logger.warn('Network check failed, are you connected to the Internet, and is Tor working?') self.daemon.isOnline = False else: self.daemon.isOnline = True diff --git a/onionr/communicatorutils/servicecreator.py b/onionr/communicatorutils/servicecreator.py index 4914547a..ecd1543c 100644 --- a/onionr/communicatorutils/servicecreator.py +++ b/onionr/communicatorutils/servicecreator.py @@ -12,6 +12,7 @@ def service_creator(daemon): bs = utils.bytesToStr(bl.bcontent) + '.onion' if utils.validatePubKey(bl.signer) and utils.validateID(bs): daemon.active_services.append(b) + daemon.active_services.append(bl.signer) daemon.services.create_server(bl.signer, bs) daemon.decrementThreadCount('service_creator') \ No newline at end of file diff --git a/onionr/core.py b/onionr/core.py index ecbe2d73..48029555 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -794,11 +794,11 @@ class Core: retData = False else: # Tell the api server through localCommand to wait for the daemon to upload this block to make stastical analysis more difficult - self._utils.localCommand('/waitforshare/' + retData, post=True) + if self._utils.localCommand('/ping', maxWait=10) == 'pong!': + self._utils.localCommand('/waitforshare/' + retData, post=True, maxWait=5) + self.daemonQueueAdd('uploadBlock', retData) self.addToBlockDB(retData, selfInsert=True, dataSaved=True) - #self.setBlockType(retData, meta['type']) self._utils.processBlockMetadata(retData) - self.daemonQueueAdd('uploadBlock', retData) if retData != False: if plaintextPeer == 'OVPCZLOXD6DC5JHX4EQ3PSOGAZ3T24F75HQLIUZSDSMYPEOXCPFA====': diff --git a/onionr/etc/onionrvalues.py b/onionr/etc/onionrvalues.py index 338dc0d3..8627d246 100755 --- a/onionr/etc/onionrvalues.py +++ b/onionr/etc/onionrvalues.py @@ -22,4 +22,4 @@ class OnionrValues: def __init__(self): self.passwordLength = 20 self.blockMetadataLengths = {'meta': 1000, 'sig': 200, 'signer': 200, 'time': 10, 'powRandomToken': 1000, 'encryptType': 4, 'expire': 14} #TODO properly refine values to minimum needed - self.default_expire = 2678400 \ No newline at end of file + self.default_expire = 2592000 \ No newline at end of file diff --git a/onionr/httpapi/__init__.py b/onionr/httpapi/__init__.py index 018d3abb..f38d80bd 100755 --- a/onionr/httpapi/__init__.py +++ b/onionr/httpapi/__init__.py @@ -19,11 +19,11 @@ ''' import onionrplugins -def load_plugin_blueprints(flaskapp): +def load_plugin_blueprints(flaskapp, blueprint='flask_blueprint'): '''Iterate enabled plugins and load any http endpoints they have''' for plugin in onionrplugins.get_enabled_plugins(): plugin = onionrplugins.get_plugin(plugin) try: - flaskapp.register_blueprint(getattr(plugin, 'flask_blueprint')) + flaskapp.register_blueprint(getattr(plugin, blueprint)) except AttributeError: pass \ No newline at end of file diff --git a/onionr/onionrcommands/pubkeymanager.py b/onionr/onionrcommands/pubkeymanager.py index ea55a3fe..00d17175 100644 --- a/onionr/onionrcommands/pubkeymanager.py +++ b/onionr/onionrcommands/pubkeymanager.py @@ -19,7 +19,7 @@ ''' import sys, getpass -import logger +import logger, onionrexceptions from onionrusers import onionrusers def add_ID(o_inst): try: @@ -38,7 +38,7 @@ def add_ID(o_inst): logger.info('Generating deterministic key. This can take a while.') newID, privKey = o_inst.onionrCore._crypto.generateDeterministic(pass1) except onionrexceptions.PasswordStrengthError: - logger.error('Must use at least 25 characters.') + logger.error('Passphrase must use at least %s characters.' % (o_inst.onionrCore._crypto.deterministicRequirement,)) sys.exit(1) else: logger.error('Passwords do not match.') diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index ee586e2b..ad34cae1 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -21,7 +21,7 @@ import threading, time from gevent.pywsgi import WSGIServer, WSGIHandler from stem.control import Controller from flask import Flask -import core, logger +import core, logger, httpapi from netcontroller import getOpenPort import api from . import httpheaders @@ -44,7 +44,9 @@ class ConnectionServer: core_inst.onionrInst.communicatorInst.service_greenlets.append(http_server) # TODO define basic endpoints useful for direct connections like stats - # TODO load endpoints from plugins + + httpapi.load_plugin_blueprints(service_app, blueprint='direct_blueprint') + @service_app.route('/ping') def get_ping(): return "pong!" @@ -60,7 +62,9 @@ class ConnectionServer: controller.authenticate(core_inst.config.get('tor.controlpassword')) # Create the v3 onion service response = controller.create_ephemeral_hidden_service({80: service_port}, await_publication = True, key_type='NEW', key_content = 'ED25519-V3') + self.core_inst.keyStore.put('dc-' + response.service_id, self.core_inst._utils.bytesToStr(peer)) self.core_inst._utils.doPostRequest('http://' + address + '/bs/' + response.service_id, port=socks) logger.info('hosting on %s with %s' % (response.service_id, peer)) http_server.serve_forever() + self.core_inst.keyStore.delete('dc-' + response.service_id) http_server.stop() \ No newline at end of file diff --git a/onionr/static-data/default-plugins/clandestine/controlapi.py b/onionr/static-data/default-plugins/clandestine/controlapi.py index ce341f82..5631a2a4 100644 --- a/onionr/static-data/default-plugins/clandestine/controlapi.py +++ b/onionr/static-data/default-plugins/clandestine/controlapi.py @@ -21,6 +21,6 @@ from flask import Response, request, redirect, Blueprint, abort flask_blueprint = Blueprint('clandenstine', __name__) -@flask_blueprint.route('/mail/ping') +@flask_blueprint.route('/clandenstine/ping') def ping(): return 'pong!' diff --git a/onionr/static-data/default-plugins/clandestine/main.py b/onionr/static-data/default-plugins/clandestine/main.py index 93bfd2d3..9a1e75a2 100755 --- a/onionr/static-data/default-plugins/clandestine/main.py +++ b/onionr/static-data/default-plugins/clandestine/main.py @@ -23,14 +23,15 @@ import locale, sys, os locale.setlocale(locale.LC_ALL, '') -plugin_name = 'clandenstine' +plugin_name = 'clandestine' PLUGIN_VERSION = '0.0.0' sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) -from . import controlapi +from . import controlapi, peerserver flask_blueprint = controlapi.flask_blueprint +direct_blueprint = peerserver.direct_blueprint -class Clandenstine: +class Clandestine: def __init__(self, pluginapi): self.myCore = pluginapi.get_core() @@ -42,5 +43,5 @@ def on_init(api, data = None): ''' pluginapi = api - chat = Clandenstine(pluginapi) + chat = Clandestine(pluginapi) return diff --git a/onionr/static-data/default-plugins/clandestine/peerserver.py b/onionr/static-data/default-plugins/clandestine/peerserver.py new file mode 100644 index 00000000..e10fa016 --- /dev/null +++ b/onionr/static-data/default-plugins/clandestine/peerserver.py @@ -0,0 +1,42 @@ +''' + Onionr - P2P Anonymous Storage Network + + HTTP endpoints for communicating with peers +''' +''' + 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 core +from flask import Response, request, redirect, Blueprint, abort, g + +direct_blueprint = Blueprint('clandestine', __name__) +core_inst = core.Core() + +storage_dir = core_inst.dataDir + +@direct_blueprint.before_request +def request_setup(): + core_inst.keyStore.refresh() + host = request.host + host = host.strip('.b32.i2p') + host = host.strip('.onion') + g.host = host + g.peer = core_inst.keyStore.get('dc-' + g.host) + +@direct_blueprint.route('/clandestine/ping') +def pingdirect(): + return 'pong!' + g.peer + +@direct_blueprint.route('/clandestine/send') +def poll_chat \ No newline at end of file From 29db7e27d9480bcfcd94f4c545da81fc39827a73 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 30 Mar 2019 23:50:54 -0500 Subject: [PATCH 031/173] basic cs messages supported --- .../default-plugins/clandestine/controlapi.py | 51 +++++++++++++++++-- .../default-plugins/clandestine/peerserver.py | 21 ++++++-- 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/onionr/static-data/default-plugins/clandestine/controlapi.py b/onionr/static-data/default-plugins/clandestine/controlapi.py index 5631a2a4..f203c0af 100644 --- a/onionr/static-data/default-plugins/clandestine/controlapi.py +++ b/onionr/static-data/default-plugins/clandestine/controlapi.py @@ -17,10 +17,55 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -from flask import Response, request, redirect, Blueprint, abort +import json +from flask import Response, request, redirect, Blueprint, g +import core -flask_blueprint = Blueprint('clandenstine', __name__) +core_inst = core.Core() +flask_blueprint = Blueprint('clandestine_control', __name__) -@flask_blueprint.route('/clandenstine/ping') +@flask_blueprint.route('/clandestine/ping') def ping(): return 'pong!' + +@flask_blueprint.route('/clandestine/send/', methods=['POST']) +def send_message(peer): + data = request.get_json(force=True) + core_inst.keyStore.refresh() + existing = core_inst.keyStore.get('s' + peer) + if existing is None: + existing = [] + existing.append(data) + core_inst.keyStore.put('s' + peer, existing) + core_inst.keyStore.flush() + return Response('success') + +@flask_blueprint.route('/clandestine/gets/') +def get_sent(peer): + sent = core_inst.keyStore.get('s' + peer) + if sent is None: + sent = [] + return Response(json.dumps(sent)) + +@flask_blueprint.route('/clandestine/addrec/', methods=['POST']) +def add_rec(peer): + data = request.get_json(force=True) + core_inst.keyStore.refresh() + existing = core_inst.keyStore.get('r' + peer) + if existing is None: + existing = [] + existing.append(data) + core_inst.keyStore.put('r' + peer, existing) + core_inst.keyStore.flush() + return Response('success') + +@flask_blueprint.route('/clandestine/getrec/') +def get_messages(peer): + core_inst.keyStore.refresh() + existing = core_inst.keyStore.get('r' + peer) + if existing is None: + existing = [] + else: + existing = list(existing) + core_inst.keyStore.delete('r' + peer) + return Response(json.dumps(existing)) \ No newline at end of file diff --git a/onionr/static-data/default-plugins/clandestine/peerserver.py b/onionr/static-data/default-plugins/clandestine/peerserver.py index e10fa016..b5fb3913 100644 --- a/onionr/static-data/default-plugins/clandestine/peerserver.py +++ b/onionr/static-data/default-plugins/clandestine/peerserver.py @@ -17,9 +17,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' +import sys, os, json import core from flask import Response, request, redirect, Blueprint, abort, g - +sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) direct_blueprint = Blueprint('clandestine', __name__) core_inst = core.Core() @@ -36,7 +37,19 @@ def request_setup(): @direct_blueprint.route('/clandestine/ping') def pingdirect(): - return 'pong!' + g.peer + return 'pong!' -@direct_blueprint.route('/clandestine/send') -def poll_chat \ No newline at end of file +@direct_blueprint.route('/clandestine/sendto', methods=['POST', 'GET']) +def sendto(): + try: + msg = request.get_json(force=True) + except: + msg = '' + if msg == None or msg == '': + msg = json.dumps({'m': 'hello world', 't': core_inst._utils.getEpoch()}) + core_inst._utils.localCommand('/clandestine/addrec/%s' % (g.peer,), post=True, postData=msg) + return Response('success') + +@direct_blueprint.route('/clandestine/poll') +def poll_chat(): + return Response(core_inst._utils.localCommand('/clandestine/gets/%s' % (g.peer,))) \ No newline at end of file From 3d1b967f1f123511f952cd7d3ed0ee3cde298669 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 31 Mar 2019 12:16:09 -0500 Subject: [PATCH 032/173] plugin example of connecting to dir conn --- onionr/onionrservices/bootstrapservice.py | 4 ++-- onionr/onionrservices/connectionserver.py | 6 +++++ .../default-plugins/clandestine/main.py | 24 ++++++++++++++++++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py index a2952563..160b716d 100644 --- a/onionr/onionrservices/bootstrapservice.py +++ b/onionr/onionrservices/bootstrapservice.py @@ -39,8 +39,8 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): bootstrap_app = Flask(__name__) http_server = WSGIServer(('127.0.0.1', bootstrap_port), bootstrap_app, log=None) try: - core_inst.onionrInst.communicatorInst - except AttributeError: + assert core_inst.onionrInst.communicatorInst is not None + except (AttributeError, AssertionError) as e: pass else: core_inst.onionrInst.communicatorInst.service_greenlets.append(http_server) diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index ad34cae1..dea9f393 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -50,6 +50,12 @@ class ConnectionServer: @service_app.route('/ping') def get_ping(): return "pong!" + + @service_app.route('/shutdown') + def shutdown_server(): + core_inst.onionrInst.communicatorInst.service_greenlets.remove(http_server) + http_server.stop() + return Response('goodbye') @service_app.after_request def afterReq(resp): diff --git a/onionr/static-data/default-plugins/clandestine/main.py b/onionr/static-data/default-plugins/clandestine/main.py index 9a1e75a2..3fab2dd0 100755 --- a/onionr/static-data/default-plugins/clandestine/main.py +++ b/onionr/static-data/default-plugins/clandestine/main.py @@ -20,8 +20,9 @@ # Imports some useful libraries import locale, sys, os - locale.setlocale(locale.LC_ALL, '') +import onionrservices, logger +from onionrservices import bootstrapservice plugin_name = 'clandestine' PLUGIN_VERSION = '0.0.0' @@ -31,9 +32,29 @@ from . import controlapi, peerserver flask_blueprint = controlapi.flask_blueprint direct_blueprint = peerserver.direct_blueprint +def exit_with_error(text=''): + if text != '': + logger.error(text) + sys.exit(1) + class Clandestine: def __init__(self, pluginapi): self.myCore = pluginapi.get_core() + + def create(self): + try: + peer = sys.argv[2] + if not self.myCore._utils.validatePubKey(peer): + exit_with_error('Invalid public key specified') + except IndexError: + exit_with_error('You must specify a peer public key') + + # Ask peer for transport address by creating block for them + peer_transport_address = bootstrapservice.bootstrap_client_service(peer, self.myCore) + + print(peer_transport_address) + if self.myCore._utils.doGetRequest('http://%s/ping' % (peer_transport_address,), ignoreAPI=True, port=self.myCore.config.get('tor.socksport')) == 'pong!': + print('connected', peer_transport_address) def on_init(api, data = None): ''' @@ -44,4 +65,5 @@ def on_init(api, data = None): pluginapi = api chat = Clandestine(pluginapi) + api.commands.register(['clandestine'], chat.create) return From f7ae054d098c3357a1f3a362c0f72289f609ab44 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 2 Apr 2019 11:50:09 -0500 Subject: [PATCH 033/173] work on direct connections and clandestine --- onionr/api.py | 10 ++++++ onionr/communicatorutils/servicecreator.py | 5 +-- onionr/onionrservices/connectionserver.py | 2 +- .../default-plugins/clandestine/main.py | 34 ++++++++++++++++--- .../default-plugins/clandestine/peerserver.py | 2 ++ 5 files changed, 46 insertions(+), 7 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 7e552355..3df4741b 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -318,6 +318,16 @@ class API: @app.route('/friends/', endpoint='friendsindex') def loadContacts(): return send_from_directory('static-data/www/friends/', 'index.html') + + @app.route('/serviceactive/') + def serviceActive(pubkey): + try: + if pubkey in self._core.onionrInst.communicatorInst.active_services: + return Response('true') + except AttributeError as e: + print('attribute error', str(e)) + pass + return Response('false') @app.route('/board/', endpoint='boardContent') def boardContent(path): diff --git a/onionr/communicatorutils/servicecreator.py b/onionr/communicatorutils/servicecreator.py index ecd1543c..fbb4c5b9 100644 --- a/onionr/communicatorutils/servicecreator.py +++ b/onionr/communicatorutils/servicecreator.py @@ -11,8 +11,9 @@ def service_creator(daemon): bl = onionrblockapi.Block(b, core=core, decrypt=True) bs = utils.bytesToStr(bl.bcontent) + '.onion' if utils.validatePubKey(bl.signer) and utils.validateID(bs): + signer = utils.bytesToStr(bl.signer) daemon.active_services.append(b) - daemon.active_services.append(bl.signer) - daemon.services.create_server(bl.signer, bs) + daemon.active_services.append(signer) + daemon.services.create_server(signer, bs) daemon.decrementThreadCount('service_creator') \ No newline at end of file diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index dea9f393..27a0b55f 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -70,7 +70,7 @@ class ConnectionServer: response = controller.create_ephemeral_hidden_service({80: service_port}, await_publication = True, key_type='NEW', key_content = 'ED25519-V3') self.core_inst.keyStore.put('dc-' + response.service_id, self.core_inst._utils.bytesToStr(peer)) self.core_inst._utils.doPostRequest('http://' + address + '/bs/' + response.service_id, port=socks) - logger.info('hosting on %s with %s' % (response.service_id, peer)) + logger.info('hosting on %s with %s' % (response.service_id, peer)) http_server.serve_forever() self.core_inst.keyStore.delete('dc-' + response.service_id) http_server.stop() \ No newline at end of file diff --git a/onionr/static-data/default-plugins/clandestine/main.py b/onionr/static-data/default-plugins/clandestine/main.py index 3fab2dd0..8f202dac 100755 --- a/onionr/static-data/default-plugins/clandestine/main.py +++ b/onionr/static-data/default-plugins/clandestine/main.py @@ -19,7 +19,7 @@ ''' # Imports some useful libraries -import locale, sys, os +import locale, sys, os, threading, json locale.setlocale(locale.LC_ALL, '') import onionrservices, logger from onionrservices import bootstrapservice @@ -40,7 +40,30 @@ def exit_with_error(text=''): class Clandestine: def __init__(self, pluginapi): self.myCore = pluginapi.get_core() + self.peer = None + self.transport = None + self.shutdown = False + def _sender_loop(self): + print('Enter a message to send, with ctrl-d or -s on a new line.') + print('-c on a new line or ctrl-c stops') + message = '' + while not self.shutdown: + try: + message += input() + if message == '-s': + raise EOFError + elif message == '-c': + raise KeyboardInterrupt + else: + message += '\n' + except EOFError: + message = json.dumps({'m': message, 't': self.myCore._utils.getEpoch()}) + print(self.myCore._utils.doPostRequest('http://%s/clandestine/sendto' % (self.transport,), port=self.socks, data=message)) + message = '' + except KeyboardInterrupt: + self.shutdown = True + def create(self): try: peer = sys.argv[2] @@ -48,13 +71,16 @@ class Clandestine: exit_with_error('Invalid public key specified') except IndexError: exit_with_error('You must specify a peer public key') - + self.peer = peer # Ask peer for transport address by creating block for them peer_transport_address = bootstrapservice.bootstrap_client_service(peer, self.myCore) + self.transport = peer_transport_address + self.socks = self.myCore.config.get('tor.socksport') - print(peer_transport_address) - if self.myCore._utils.doGetRequest('http://%s/ping' % (peer_transport_address,), ignoreAPI=True, port=self.myCore.config.get('tor.socksport')) == 'pong!': + print('connected with', peer, 'on', peer_transport_address) + if self.myCore._utils.doGetRequest('http://%s/ping' % (peer_transport_address,), ignoreAPI=True, port=self.socks) == 'pong!': print('connected', peer_transport_address) + threading.Thread(target=self._sender_loop).start() def on_init(api, data = None): ''' diff --git a/onionr/static-data/default-plugins/clandestine/peerserver.py b/onionr/static-data/default-plugins/clandestine/peerserver.py index b5fb3913..64e4eaeb 100644 --- a/onionr/static-data/default-plugins/clandestine/peerserver.py +++ b/onionr/static-data/default-plugins/clandestine/peerserver.py @@ -47,6 +47,8 @@ def sendto(): msg = '' if msg == None or msg == '': msg = json.dumps({'m': 'hello world', 't': core_inst._utils.getEpoch()}) + else: + msg = json.dumps(msg) core_inst._utils.localCommand('/clandestine/addrec/%s' % (g.peer,), post=True, postData=msg) return Response('success') From ad94c8a4efb89faee49eb16679a0a19c5993706c Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 4 Apr 2019 16:56:18 -0500 Subject: [PATCH 034/173] added first block spec draft --- docs/block-spec.md | 64 ++++++++++++++++++++++++++++++++++++++ onionr/api.py | 1 - onionr/etc/onionrvalues.py | 2 +- onionr/onionrproofs.py | 2 +- onionr/subprocesspow.py | 2 +- 5 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 docs/block-spec.md diff --git a/docs/block-spec.md b/docs/block-spec.md new file mode 100644 index 00000000..0a5f1491 --- /dev/null +++ b/docs/block-spec.md @@ -0,0 +1,64 @@ +# Onionr Block Spec v1.0.0 + +# Block Description + +Onionr 'Blocks' are the primary means of sharing information in Onionr. Blocks are identified by a single hash value of their entire contents. + +They contain a JSON metadata section followed by a line break, with the main data following. + +In the future, the spec will be updated to use flags and MessagePack instead of JSON with english keys. + + +# Encryption and Signatures + +Onionr blocks may be encrypted or signed. In the reference client, this is done with libsodium, for both asymmetric and symmetric encryption. + +Unlike many similar projects, blocks may completely be in plaintext, making Onionr suitable for sharing information publicly. + +# Metadata Section + +The metadata section has the following fields. If a block contains any other field, it must be considered invalid. All metadata fields are technically optional, but many are useful and essentially necessary for most use cases. + +## meta + +Max byte size: 1000 + +Meta is a string field which can contain arbitrary sub fields. It is intended for applications and plugins to use it for arbitrary metadata information. In the reference client, if the data section is encrypted or signed, the meta section also is. + +Common meta fields, such as 'type' are used by the reference Onionr client to describe the type of a block. + +## sig + +Max byte size: 200 + +Sig is a field for storing public key signatures of the block, typically ed25519. In the reference client, this field is a base64 encoded signature of the meta field combined with the block data. (**Therefore, information outside of the meta and data fields cannot be trusted to be placed there by the signer, although it can still be assured that the particular block has not been modified.**) + +Note: the max field size is larger than a EdDSA signature (which is what is typically used) in order to allow other primitives for signing in alternative implementations or future versions. + +## signer + +Max byte size: 200 + +Signer is a field for specifying the public key which signed the block. In the reference client this is a base64 encoded ed25519 public key. + +## time + +Max byte size: 10 + +Time is an integer field for specifying the time of which a block was created. The trustworthiness of this field is based on one's trust of the block creator, however blocks with a time field set in the future (past a reasonable clock skew) are thrown out by the reference client. + +## expire + +Max byte size: 10 + +Expire is an integer field for specifying the time of which the block creator has indicated that the block should be deleted. The purpose of this is for voluntarily freeing the burden of unwanted blocks on the Onionr network, rather than security/privacy (since blocks could be trivially kept past expiration). Regardless, the reference client deletes blocks after a preset time if the expire field is either not set or longer than the preset time. + +## pow + +Max byte size: 1000 + +Pow is a field for placing the nonce found to make a block meet a target proof of work. In theory, a block could meet a target without a random token in this field. + +## encryptType + +encryptType is a field to specify the mode of encryption for a block. The values supported by Onionr are 'asym' and 'sym'. diff --git a/onionr/api.py b/onionr/api.py index 3df4741b..bba548c5 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -325,7 +325,6 @@ class API: if pubkey in self._core.onionrInst.communicatorInst.active_services: return Response('true') except AttributeError as e: - print('attribute error', str(e)) pass return Response('false') diff --git a/onionr/etc/onionrvalues.py b/onionr/etc/onionrvalues.py index 8627d246..9f636147 100755 --- a/onionr/etc/onionrvalues.py +++ b/onionr/etc/onionrvalues.py @@ -21,5 +21,5 @@ class OnionrValues: def __init__(self): self.passwordLength = 20 - self.blockMetadataLengths = {'meta': 1000, 'sig': 200, 'signer': 200, 'time': 10, 'powRandomToken': 1000, 'encryptType': 4, 'expire': 14} #TODO properly refine values to minimum needed + self.blockMetadataLengths = {'meta': 1000, 'sig': 200, 'signer': 200, 'time': 10, 'pow': 1000, 'encryptType': 4, 'expire': 14} #TODO properly refine values to minimum needed self.default_expire = 2592000 \ No newline at end of file diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index 62dc215c..53c6d881 100755 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -247,7 +247,7 @@ class POW: startNonce = nonce while self.hashing: #token = nacl.hash.blake2b(rand + self.data).decode() - self.metadata['powRandomToken'] = nonce + self.metadata['pow'] = nonce payload = json.dumps(self.metadata).encode() + b'\n' + self.data token = myCore._crypto.sha3Hash(payload) try: diff --git a/onionr/subprocesspow.py b/onionr/subprocesspow.py index a2fee7c6..fee2f2dd 100755 --- a/onionr/subprocesspow.py +++ b/onionr/subprocesspow.py @@ -75,7 +75,7 @@ class SubprocessPOW: difficulty = self.difficulty mcore = core.Core() while True: - metadata['powRandomToken'] = nonce + metadata['pow'] = nonce payload = json.dumps(metadata).encode() + b'\n' + data token = mcore._crypto.sha3Hash(payload) try: From 45ddbe5e69efca0d1e189638a524b42aa06cb578 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 9 Apr 2019 12:30:54 -0500 Subject: [PATCH 035/173] fixes for mail, work on specs, and added files for profile viewer --- README.md | 2 + docs/{api.md => http-api.md} | 0 docs/{ => specs}/block-spec.md | 3 +- onionr/api.py | 14 +++++- onionr/httpapi/profilesapi/__init__.py | 28 ++++++++++++ onionr/httpapi/profilesapi/profiles.py | 2 + onionr/static-data/bootstrap-nodes.txt | 2 +- .../default-plugins/pms/mailapi.py | 25 ++++++++--- onionr/static-data/default_config.json | 4 +- onionr/static-data/www/mail/index.html | 5 ++- onionr/static-data/www/mail/mail.css | 2 +- onionr/static-data/www/mail/mail.js | 44 +++++++++++-------- onionr/static-data/www/profiles/index.html | 20 +++++++++ onionr/static-data/www/profiles/profiles.js | 0 14 files changed, 119 insertions(+), 32 deletions(-) rename docs/{api.md => http-api.md} (100%) rename docs/{ => specs}/block-spec.md (96%) create mode 100644 onionr/httpapi/profilesapi/__init__.py create mode 100644 onionr/httpapi/profilesapi/profiles.py create mode 100644 onionr/static-data/www/profiles/index.html create mode 100644 onionr/static-data/www/profiles/profiles.js diff --git a/README.md b/README.md index 8b0de5c2..ebae2e10 100755 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ Users are identified by ed25519 public keys, which can be used to sign blocks or Onionr can be used for mail, as a social network, instant messenger, file sharing software, or for encrypted group discussion. +The whitepaper (subject to change prior to first alpha release) is available [here](docs/whitepaper.md). + ![Tor stinks slide image](docs/tor-stinks-02.png) ## Main Features diff --git a/docs/api.md b/docs/http-api.md similarity index 100% rename from docs/api.md rename to docs/http-api.md diff --git a/docs/block-spec.md b/docs/specs/block-spec.md similarity index 96% rename from docs/block-spec.md rename to docs/specs/block-spec.md index 0a5f1491..f9cd3509 100644 --- a/docs/block-spec.md +++ b/docs/specs/block-spec.md @@ -6,8 +6,7 @@ Onionr 'Blocks' are the primary means of sharing information in Onionr. Blocks a They contain a JSON metadata section followed by a line break, with the main data following. -In the future, the spec will be updated to use flags and MessagePack instead of JSON with english keys. - +In the future, the specification will likely be updated to use flags and MessagePack instead of JSON with english keys. # Encryption and Signatures diff --git a/onionr/api.py b/onionr/api.py index bba548c5..415707e5 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -26,7 +26,7 @@ import core from onionrblockapi import Block import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config import httpapi -from httpapi import friendsapi, simplecache +from httpapi import friendsapi, simplecache, profilesapi from onionrservices import httpheaders import onionr @@ -256,7 +256,8 @@ class API: self.bindPort = bindPort # Be extremely mindful of this. These are endpoints available without a password - self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'board', 'boardContent', 'sharedContent', 'mail', 'mailindex', 'friends', 'friendsindex') + self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'board', 'profiles', 'profilesindex', + 'boardContent', 'sharedContent', 'mail', 'mailindex', 'friends', 'friendsindex') self.clientToken = config.get('client.webpassword') self.timeBypassToken = base64.b16encode(os.urandom(32)).decode() @@ -272,6 +273,7 @@ class API: onionrInst.setClientAPIInst(self) app.register_blueprint(friendsapi.friends) app.register_blueprint(simplecache.simplecache) + app.register_blueprint(profilesapi.profile_BP) httpapi.load_plugin_blueprints(app) @app.before_request @@ -318,6 +320,14 @@ class API: @app.route('/friends/', endpoint='friendsindex') def loadContacts(): return send_from_directory('static-data/www/friends/', 'index.html') + + @app.route('/profiles/', endpoint='profiles') + def loadContacts(path): + return send_from_directory('static-data/www/profiles/', path) + + @app.route('/profiles/', endpoint='profilesindex') + def loadContacts(): + return send_from_directory('static-data/www/profiles/', 'index.html') @app.route('/serviceactive/') def serviceActive(pubkey): diff --git a/onionr/httpapi/profilesapi/__init__.py b/onionr/httpapi/profilesapi/__init__.py new file mode 100644 index 00000000..f3c3ba2f --- /dev/null +++ b/onionr/httpapi/profilesapi/__init__.py @@ -0,0 +1,28 @@ +''' + Onionr - P2P Anonymous Storage Network + + This file creates http endpoints for user profile pages +''' +''' + 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 core +from flask import Blueprint, Response, request, abort +from . import profiles + +profile_BP = Blueprint('profile_BP', __name__) + +@profile_BP.route('/profile/get/', endpoint='profiles') +def get_profile_page(pubkey): + return Response(pubkey) \ No newline at end of file diff --git a/onionr/httpapi/profilesapi/profiles.py b/onionr/httpapi/profilesapi/profiles.py new file mode 100644 index 00000000..11a73b13 --- /dev/null +++ b/onionr/httpapi/profilesapi/profiles.py @@ -0,0 +1,2 @@ +def get_latest_user_profile(pubkey): + return '' \ No newline at end of file diff --git a/onionr/static-data/bootstrap-nodes.txt b/onionr/static-data/bootstrap-nodes.txt index 374ea7b4..52622b12 100755 --- a/onionr/static-data/bootstrap-nodes.txt +++ b/onionr/static-data/bootstrap-nodes.txt @@ -1 +1 @@ -huei7fugyuesltkf3yzj27an36skz2f2u55g7ganyaq6yjhauwicwfyd.onion +ecaufnpxx67xmvzwcriohapmmnqbj665h3ynemvwrr2juxl5oa5g7cad.onion diff --git a/onionr/static-data/default-plugins/pms/mailapi.py b/onionr/static-data/default-plugins/pms/mailapi.py index 87cdb678..0a60e67c 100644 --- a/onionr/static-data/default-plugins/pms/mailapi.py +++ b/onionr/static-data/default-plugins/pms/mailapi.py @@ -42,6 +42,7 @@ def mail_delete(block): if block not in existing: existing.append(block) kv.put('deleted_mail', existing) + kv.flush() return 'success' @flask_blueprint.route('/mail/getinbox') @@ -50,16 +51,30 @@ def list_inbox(): @flask_blueprint.route('/mail/getsentbox') def list_sentbox(): + kv.refresh() sentbox_list = sentboxdb.SentBox(c).listSent() - sentbox_list_copy = list(sentbox_list) + list_copy = list(sentbox_list) deleted = kv.get('deleted_mail') if deleted is None: deleted = [] - for x in range(len(sentbox_list_copy) - 1): + for x in list_copy: + if x['hash'] in deleted: + sentbox_list.remove(x) + return json.dumps(sentbox_list) +''' +@flask_blueprint.route('/mail/getsentbox') +def list_sentbox(): + sentbox_list = sentboxdb.SentBox(c).listSent() + sentbox_list_copy = list(sentbox_list) + kv.refresh() + deleted = kv.get('deleted_mail') + if deleted is None: + deleted = [] + for x in range(len(sentbox_list_copy)): if sentbox_list_copy[x]['hash'] in deleted: - x -= 1 - sentbox_list.pop(x) + sentbox_list.remove(sentbox_list_copy[x]['hash']) else: sentbox_list[x]['name'] = contactmanager.ContactManager(c, sentbox_list_copy[x]['peer'], saveUser=False).get_info('name') - return json.dumps(sentbox_list) \ No newline at end of file + return json.dumps(sentbox_list) +''' \ No newline at end of file diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index 2b9d47df..988e91f0 100755 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -4,12 +4,12 @@ "display_header" : false, "minimum_block_pow": 4, "minimum_send_pow": 4, - "socket_servers": true, + "socket_servers": false, "security_level": 0, "max_block_age": 2678400, "bypass_tor_check": false, "public_key": "", - "random_bind_ip": true + "random_bind_ip": false }, "www" : { diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index 8af6f035..f3f337bd 100644 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -36,8 +36,11 @@
From: Signature:
+
+ Subject: +
- +
diff --git a/onionr/static-data/www/mail/mail.css b/onionr/static-data/www/mail/mail.css index af80deb1..88afc0b9 100644 --- a/onionr/static-data/www/mail/mail.css +++ b/onionr/static-data/www/mail/mail.css @@ -92,7 +92,7 @@ input{ color: black; } -#replyBtn{ +.break-up{ margin-top: 1em; } diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index 302c84f6..cfbb7e89 100644 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -26,7 +26,7 @@ threadContent = {} myPub = httpGet('/getActivePubkey') replyBtn = document.getElementById('replyBtn') -function openReply(bHash){ +function openReply(bHash, quote, subject){ var inbox = document.getElementsByClassName('threadEntry') var entry = '' var friendName = '' @@ -41,13 +41,23 @@ function openReply(bHash){ } key = entry.getAttribute('data-pubkey') document.getElementById('draftID').value = key + document.getElementById('draftSubject').value = 'RE: ' + subject + + // Add quoted reply + var splitQuotes = quote.split('\n') + for (var x = 0; x < splitQuotes.length; x++){ + splitQuotes[x] = '>' + splitQuotes[x] + } + quote = splitQuotes.join('\n') + document.getElementById('draftText').value = quote setActiveTab('send message') } -function openThread(bHash, sender, date, sigBool, pubkey){ +function openThread(bHash, sender, date, sigBool, pubkey, subjectLine){ var messageDisplay = document.getElementById('threadDisplay') var blockContent = httpGet('/getblockbody/' + bHash) document.getElementById('fromUser').value = sender + document.getElementById('subjectView').innerText = subjectLine messageDisplay.innerText = blockContent var sigEl = document.getElementById('sigValid') var sigMsg = 'signature' @@ -64,7 +74,7 @@ function openThread(bHash, sender, date, sigBool, pubkey){ sigEl.innerText = sigMsg overlay('messageDisplay') replyBtn.onclick = function(){ - openReply(bHash) + openReply(bHash, messageDisplay.innerText, subjectLine) } } @@ -174,7 +184,7 @@ function loadInboxEntries(bHash){ if (event.target.classList.contains('deleteBtn')){ return } - openThread(entry.getAttribute('data-hash'), senderInput.value, dateStr.innerText, resp['meta']['validSig'], entry.getAttribute('data-pubkey')) + openThread(entry.getAttribute('data-hash'), senderInput.value, dateStr.innerText, resp['meta']['validSig'], entry.getAttribute('data-pubkey'), subjectLine.innerText) } deleteBtn.onclick = function(){ @@ -216,7 +226,7 @@ function getSentbox(){ if (keys.length == 0){ threadPart.innerHTML = "nothing to show here yet." } - for (var i = 0; i < keys.length; i++){ + for (var i = 0; i < keys.length; i++) (function(i, resp){ var entry = document.createElement('div') var obj = resp[i] var toLabel = document.createElement('span') @@ -245,19 +255,18 @@ function getSentbox(){ entry.appendChild(toEl) entry.appendChild(preview) entry.appendChild(sentDate) - entry.onclick = (function(tree, el, msg) {return function() { - console.log(resp) - if (! entry.classList.contains('deleteBtn')){ - showSentboxWindow(el.value, msg) - } - };})(entry, toEl, message); - - deleteBtn.onclick = function(){ - entry.parentNode.removeChild(entry); - deleteMessage(entry.getAttribute('data-hash')) - } + threadPart.appendChild(entry) - } + + entry.onclick = function(e){ + if (e.target.classList.contains('deleteBtn')){ + deleteMessage(e.target.parentNode.getAttribute('data-hash')) + e.target.parentNode.parentNode.removeChild(e.target.parentNode) + return + } + showSentboxWindow() + } + })(i, resp) threadPart.appendChild(entry) }.bind(threadPart)) } @@ -304,7 +313,6 @@ for (var i = 0; i < document.getElementsByClassName('refresh').length; i++){ document.getElementsByClassName('refresh')[i].style.float = 'right' } - fetch('/friends/list', { headers: { "token": webpass diff --git a/onionr/static-data/www/profiles/index.html b/onionr/static-data/www/profiles/index.html new file mode 100644 index 00000000..a4dad9b7 --- /dev/null +++ b/onionr/static-data/www/profiles/index.html @@ -0,0 +1,20 @@ + + + + + + Onionr Profiles + + + + + + +
+

+
+ + + + + \ No newline at end of file diff --git a/onionr/static-data/www/profiles/profiles.js b/onionr/static-data/www/profiles/profiles.js new file mode 100644 index 00000000..e69de29b From bd275abe429760b158806f42f12e9f42d2dbf230 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 9 Apr 2019 12:35:40 -0500 Subject: [PATCH 036/173] fixes for mail, work on specs, and added files for profile viewer --- onionr/communicator.py | 2 +- onionr/static-data/www/mail/mail.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/onionr/communicator.py b/onionr/communicator.py index ef94ae14..f10d37da 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -621,7 +621,7 @@ class OnionrCommunicatorDaemon: def detectAPICrash(self): '''exit if the api server crashes/stops''' if self._core._utils.localCommand('ping', silent=False) not in ('pong', 'pong!'): - for i in range(8): + for i in range(12): if self._core._utils.localCommand('ping') in ('pong', 'pong!') or self.shutdown: break # break for loop time.sleep(1) diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index cfbb7e89..09b8714a 100644 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -264,7 +264,7 @@ function getSentbox(){ e.target.parentNode.parentNode.removeChild(e.target.parentNode) return } - showSentboxWindow() + showSentboxWindow(resp[i]['peer'], message) } })(i, resp) threadPart.appendChild(entry) From 7c4e8bfa7385f0a64c74f0560fe2ac11017df20b Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 11 Apr 2019 17:01:18 -0500 Subject: [PATCH 037/173] work on docs --- README.md | 8 +++---- docs/http-api.md | 52 ++++++++++++++++++++++++++++++++++++++-- docs/specs/block-spec.md | 4 ++-- onionr/api.py | 4 +++- 4 files changed, 59 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ebae2e10..58dac596 100755 --- a/README.md +++ b/README.md @@ -17,11 +17,11 @@ **The main repository for this software is at https://gitlab.com/beardog/Onionr/** -# Summary +# About Onionr is a decentralized, peer-to-peer data storage network, designed to be anonymous and resistant to (meta)data analysis and spam/disruption. -Onionr stores data in independent packages referred to as 'blocks'. The blocks are synced to all other nodes in the network. Blocks and user IDs cannot be easily proven to have been created by particular nodes (only inferred). Even if there is enough evidence to believe a particular node created a block, nodes still operate behind Tor or I2P and as such are not trivially known to be at a particular IP address. +Onionr stores data in independent packages referred to as 'blocks'. The blocks are synced to all other nodes in the network. Blocks and user IDs cannot be easily proven to have been created by a particular user. Even if there is enough evidence to believe that a specific user created a block, nodes still operate behind Tor or I2P and as such cannot be trivially unmasked. Users are identified by ed25519 public keys, which can be used to sign blocks or send encrypted data. @@ -36,7 +36,7 @@ The whitepaper (subject to change prior to first alpha release) is available [he * [X] 🌐 Fully p2p/decentralized, no trackers or other single points of failure * [X] 🔒 End to end encryption of user data * [X] 📢 Optional non-encrypted blocks, useful for blog posts or public file sharing -* [X] 💻 Easy API system for integration to websites +* [X] 💻 Easy API for integration to websites * [X] 🕵️ Metadata analysis resistance and anonymity * [X] 📡 Transport agnosticism (no internet required) @@ -54,7 +54,7 @@ Friend/contact manager Encrypted, metadata-masking mail application screenshot -Encrypted, metadata-masking mail application. +Encrypted, metadata-masking mail application. Perhaps the first distributed mail system to have basic forward secrecy. # Install and Run on Linux diff --git a/docs/http-api.md b/docs/http-api.md index 52a55368..b2fae877 100755 --- a/docs/http-api.md +++ b/docs/http-api.md @@ -1,2 +1,50 @@ -HTTP API -TODO +# Onionr HTTP API + +# About HTTP API + +All HTTP interfaces in the Onionr reference client use the [Flask](http://flask.pocoo.org/) web framework with the [gevent](http://www.gevent.org/) WSGI server. + +## Client & Public difference + +The client API server is a locked down interface intended for authenticated local communication. + +The public API server is available only remotely from Tor & I2P. It is the interface in which peers use to communicate with one another. + +# Client API + +Please note: endpoints that simply provide static web app files are not documented here. + +(Client API docs coming soon) + +# Public API + +* / + - Methods: GET + - Returns a basic HTML informational banner describing Onionr. +* /getblocklist + - Methods: GET + - URI Parameters: + - date: unix epoch timestamp for offset + - Returns a list of block hashes stored on the node since an offset (all blocks if no timestamp is specified) +* /getdata/block-hash + - Methods: GET + - Returns data for a block based on a provided hash +* /www/file-path + - Methods: GET + - Returns file data. Intended for manually sharing file data directly from an Onionr node. +* /ping + - Methods: GET + - Returns 'pong!' +* /pex + - Methods: GET + - Returns a list of peer addresses reached within recent time +* /announce + - Methods: POST + - Accepts form data for 'node' (valid node address) and 'random' which is a nonce when hashed (blake2b_256) in the format `hash(peerAddress+serverAddress+nonce)`, begins with at least 5 zeros. + - Returns 200 with 'Success' if no error occurs. If the post is invalid, 'failure' with code 406 is returned. +* /upload + - Methods: POST + - Accepts form data for 'block' as a 'file' upload. + - Returns 200 with 'success' if no error occurs. If the block cannot be accepted, 'failure' with 400 is returned. + +# Direct Connection API \ No newline at end of file diff --git a/docs/specs/block-spec.md b/docs/specs/block-spec.md index f9cd3509..348cedae 100644 --- a/docs/specs/block-spec.md +++ b/docs/specs/block-spec.md @@ -2,9 +2,9 @@ # Block Description -Onionr 'Blocks' are the primary means of sharing information in Onionr. Blocks are identified by a single hash value of their entire contents. +Onionr Blocks are the primary means of sharing information in Onionr. Blocks are identified by a single hash value of their entire contents, using SHA3_256. -They contain a JSON metadata section followed by a line break, with the main data following. +Blocks contain a JSON metadata section followed by a line break, with the main data section comprising the rest. In the future, the specification will likely be updated to use flags and MessagePack instead of JSON with english keys. diff --git a/onionr/api.py b/onionr/api.py index 415707e5..df8b09f2 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -186,7 +186,7 @@ class PublicAPI: powHash = powHash.decode() except AttributeError: pass - if powHash.startswith('0000'): + if powHash.startswith('00000'): newNode = clientAPI._core._utils.bytesToStr(newNode) if clientAPI._core._utils.validateID(newNode) and not newNode in clientAPI._core.onionrInst.communicatorInst.newPeers: clientAPI._core.onionrInst.communicatorInst.newPeers.append(newNode) @@ -194,6 +194,8 @@ class PublicAPI: else: logger.warn(newNode.decode() + ' failed to meet POW: ' + powHash) resp = Response(resp) + if resp == 'failure': + return resp, 406 return resp @app.route('/upload', methods=['post']) From 3bea32a91172379a3dc86263c0f6620264009960 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 12 Apr 2019 12:14:16 -0500 Subject: [PATCH 038/173] updated docs and cleaned up api somewhat --- README.md | 7 +++ docs/http-api.md | 60 ++++++++++++++++++++++++-- onionr/api.py | 40 +++-------------- onionr/static-data/www/mail/index.html | 2 +- 4 files changed, 71 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 58dac596..b63fe020 100755 --- a/README.md +++ b/README.md @@ -56,6 +56,13 @@ Friend/contact manager Encrypted, metadata-masking mail application. Perhaps the first distributed mail system to have basic forward secrecy. +# Documentation + +More docs coming soon. + +* [Block specification](docs/specs/block-spec.md) +* [HTTP API](docs/http-api.md) + # Install and Run on Linux The following applies to Ubuntu Bionic. Other distros may have different package or command names. diff --git a/docs/http-api.md b/docs/http-api.md index b2fae877..d2ee56a5 100755 --- a/docs/http-api.md +++ b/docs/http-api.md @@ -1,7 +1,5 @@ # Onionr HTTP API -# About HTTP API - All HTTP interfaces in the Onionr reference client use the [Flask](http://flask.pocoo.org/) web framework with the [gevent](http://www.gevent.org/) WSGI server. ## Client & Public difference @@ -14,10 +12,66 @@ The public API server is available only remotely from Tor & I2P. It is the inter Please note: endpoints that simply provide static web app files are not documented here. -(Client API docs coming soon) +* /serviceactive/pubkey + - Methods: GET + - Returns true or false based on if a given public key has an active direct connection service. +* /queueResponseAdd/key (DEPRECATED) + - Methods: POST + - Accepts form key 'data' to set queue response information from a plugin + - Returns success if no error occurs +* /queueResponse/key (DEPRECATED) + - Methods: GET + - Returns the queue response for a key. Returns failure with a 404 code if a code is not set. +* /ping + - Methods: GET + - Returns "pong!" +* /getblocksbytype/type + - Methods: GET + - Returns a list of stored blocks by a given type +* /getblockbody/hash + - Methods: GET + - Returns the main data section of a block +* /getblockdata/hash + - Methods: GET + - Returns the entire data contents of a block, including metadata. +* /getblockheader/hash + - Methods: GET + - Returns the header (metadata section) of a block. +* /lastconnect + - Methods: GET + - Returns the epoch timestamp of when the last incoming connection to the public API server was logged +* /site/hash + - Methods: GET + - Returns HTML content out of a block +* /waitforshare/hash + - Methods: POST + - Prevents the public API server from listing or sharing a block until it has been uploaded to at least 1 peer. +* /shutdown + - Methods: GET + - Shutdown Onionr. You should probably use /shutdownclean instead. +* /shutdownclean + - Methods: GET + - Tells the communicator daemon to shutdown Onionr. Slower but cleaner. +* /getstats + - Methods: GET + - Returns some JSON serialized statistics +* /getuptime + - Methods: GET + - Returns uptime in seconds +* /getActivePubkey + - Methods: GET + - Returns the current active public key in base32 format +* /getHumanReadable/pubkey + - Methods: GET + - Echos the specified public key in mnemonic format +* /insertblock + - Methods: POST + - Accepts JSON data for creating a new block. 'message' contains the block data, 'to' specifies the peer's public key to encrypt the data to, 'sign' is a boolean for signing the message. # Public API +v0 + * / - Methods: GET - Returns a basic HTML informational banner describing Onionr. diff --git a/onionr/api.py b/onionr/api.py index df8b09f2..ae6368e2 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -270,7 +270,6 @@ class API: logger.info('Running api on %s:%s' % (self.host, self.bindPort)) self.httpServer = '' - self.pluginResponses = {} # Responses for plugin endpoints self.queueResponse = {} onionrInst.setClientAPIInst(self) app.register_blueprint(friendsapi.friends) @@ -369,7 +368,10 @@ class API: pass else: del self.queueResponse[name] - return Response(resp) + if resp == 'failure': + return resp, 404 + else: + return resp @app.route('/ping') def ping(): @@ -522,36 +524,6 @@ class API: pass threading.Thread(target=self._core.insertBlock, args=(message,), kwargs={'header': bType, 'encryptType': encryptType, 'sign':sign, 'asymPeer': to, 'meta': meta}).start() return Response('success') - - @app.route('/apipoints/', methods=['POST', 'GET']) - def pluginEndpoints(subpath=''): - '''Send data to plugins''' - # TODO have a variable for the plugin to set data to that we can use for the response - pluginResponseCode = str(uuid.uuid4()) - resp = 'success' - responseTimeout = 20 - startTime = self._core._utils.getEpoch() - postData = {} - if request.method == 'POST': - postData = request.form['postData'] - if len(subpath) > 1: - data = subpath.split('/') - if len(data) > 1: - plName = data[0] - events.event('pluginRequest', {'name': plName, 'path': subpath, 'pluginResponse': pluginResponseCode, 'postData': postData}, onionr=onionrInst) - while True: - try: - resp = self.pluginResponses[pluginResponseCode] - except KeyError: - time.sleep(0.2) - if self._core._utils.getEpoch() - startTime > responseTimeout: - abort(504) - break - else: - break - else: - abort(404) - return Response(resp) self.httpServer = WSGIServer((self.host, bindPort), app, log=None, handler_class=FDSafeHandler) self.httpServer.serve_forever() @@ -578,8 +550,8 @@ class API: def getUptime(self): while True: try: - return self._utils.getEpoch - startTime - except AttributeError: + return self._utils.getEpoch() - self.startTime + except (AttributeError, NameError): # Don't error on race condition with startup pass diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index f3f337bd..08441075 100644 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -58,7 +58,7 @@
- + To: Subject: From 7be884987e758db697c3fb57f3dffb300cc9cc6f Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 12 Apr 2019 12:19:01 -0500 Subject: [PATCH 039/173] fixed introduce not having enough pow --- onionr/communicatorutils/onionrdaemontools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionr/communicatorutils/onionrdaemontools.py b/onionr/communicatorutils/onionrdaemontools.py index 1c4e0220..85c97747 100755 --- a/onionr/communicatorutils/onionrdaemontools.py +++ b/onionr/communicatorutils/onionrdaemontools.py @@ -73,7 +73,7 @@ class DaemonTools: data['random'] = existingRand else: self.announceProgress[peer] = True - proof = onionrproofs.DataPOW(combinedNodes, forceDifficulty=4) + proof = onionrproofs.DataPOW(combinedNodes, forceDifficulty=5) del self.announceProgress[peer] try: data['random'] = base64.b64encode(proof.waitForResult()[1]) From 96219c81ab7925c050848ce5cafabbfa6afa8e5b Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 12 Apr 2019 18:45:22 -0500 Subject: [PATCH 040/173] minor bug fixes --- onionr/onionrblockapi.py | 2 +- onionr/static-data/default-plugins/clandestine/main.py | 3 +-- .../static-data/default-plugins/metadataprocessor/main.py | 3 --- requirements.in | 3 ++- requirements.txt | 7 +++---- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py index 1f890bcc..df17aa40 100755 --- a/onionr/onionrblockapi.py +++ b/onionr/onionrblockapi.py @@ -90,7 +90,7 @@ class Block: try: if self.core._utils.getEpoch() - self.core.getBlockDate(self.hash) < 60: assert self.core._crypto.replayTimestampValidation(self.bmetadata['rply']) - except (AssertionError, KeyError) as e: + except (AssertionError, KeyError, TypeError) as e: if not self.bypassReplayCheck: # Zero out variables to prevent reading of replays self.bmetadata = {} diff --git a/onionr/static-data/default-plugins/clandestine/main.py b/onionr/static-data/default-plugins/clandestine/main.py index 8f202dac..17007f0e 100755 --- a/onionr/static-data/default-plugins/clandestine/main.py +++ b/onionr/static-data/default-plugins/clandestine/main.py @@ -26,9 +26,8 @@ from onionrservices import bootstrapservice plugin_name = 'clandestine' PLUGIN_VERSION = '0.0.0' - sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) -from . import controlapi, peerserver +import controlapi, peerserver flask_blueprint = controlapi.flask_blueprint direct_blueprint = peerserver.direct_blueprint diff --git a/onionr/static-data/default-plugins/metadataprocessor/main.py b/onionr/static-data/default-plugins/metadataprocessor/main.py index 787b0174..346600ff 100755 --- a/onionr/static-data/default-plugins/metadataprocessor/main.py +++ b/onionr/static-data/default-plugins/metadataprocessor/main.py @@ -45,9 +45,6 @@ def on_processblocks(api, data=None): # Generally fired by utils. myBlock = api.data['block'] blockType = api.data['type'] - logger.info('blockType is ' + blockType) - utils = api.get_utils() - core = api.get_core() # Process specific block types diff --git a/requirements.in b/requirements.in index 7bf8ff02..014499dd 100755 --- a/requirements.in +++ b/requirements.in @@ -6,4 +6,5 @@ defusedxml==0.5.0 Flask==1.0.2 PySocks==1.6.8 stem==1.7.1 -deadsimplekv==0.0.1 \ No newline at end of file +deadsimplekv==0.0.1 +jinja2==2.10.1 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 9bb58ea1..de720793 100644 --- a/requirements.txt +++ b/requirements.txt @@ -107,10 +107,9 @@ itsdangerous==1.1.0 \ --hash=sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19 \ --hash=sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749 \ # via flask -jinja2==2.10 \ - --hash=sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd \ - --hash=sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4 \ - # via flask +jinja2==2.10.1 \ + --hash=sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013 \ + --hash=sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b markupsafe==1.1.1 \ --hash=sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473 \ --hash=sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161 \ From 9ee31684f606640fb770112f2b369674101edea2 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 13 Apr 2019 17:18:38 -0500 Subject: [PATCH 041/173] fixed default expiration not using sum of date and made mail ui show inbox after closing message draft dialog --- onionr/onionrutils.py | 3 ++- onionr/static-data/www/mail/mail.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index ea56b593..fa75f72a 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -157,6 +157,7 @@ class OnionrUtils: ''' Read metadata from a block and cache it to the block database ''' + curTime = self.getRoundedEpoch(roundS=60) myBlock = Block(blockHash, self._core) if myBlock.isEncrypted: myBlock.decrypt() @@ -179,7 +180,7 @@ class OnionrUtils: expireTime = myBlock.getHeader('expire') assert len(str(int(expireTime))) < 20 # test that expire time is an integer of sane length (for epoch) except (AssertionError, ValueError, TypeError) as e: - expireTime = onionrvalues.OnionrValues().default_expire + expireTime = onionrvalues.OnionrValues().default_expire + curTime finally: self._core.updateBlockInfo(blockHash, 'expire', expireTime) if not blockType is None: diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index 09b8714a..06cb080a 100644 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -89,6 +89,7 @@ function setActiveTab(tabName){ break case 'send message': overlay('sendMessage') + setActiveTab('inbox') break } } From 2fde909d86437c6dc1ca92586c343bf50fc30ce1 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 15 Apr 2019 18:48:11 -0500 Subject: [PATCH 042/173] bug fixes and ui appearance improvements --- docs/http-api.md | 5 ++++- .../default-plugins/clandestine/peerserver.py | 4 +--- onionr/static-data/www/mail/mail.js | 11 ----------- onionr/static-data/www/private/index.html | 18 +++++++++++------- onionr/static-data/www/shared/main/style.css | 2 +- onionr/static-data/www/shared/misc.js | 12 ++++++++++++ 6 files changed, 29 insertions(+), 23 deletions(-) diff --git a/docs/http-api.md b/docs/http-api.md index d2ee56a5..2fe1728d 100755 --- a/docs/http-api.md +++ b/docs/http-api.md @@ -101,4 +101,7 @@ v0 - Accepts form data for 'block' as a 'file' upload. - Returns 200 with 'success' if no error occurs. If the block cannot be accepted, 'failure' with 400 is returned. -# Direct Connection API \ No newline at end of file +# Direct Connection API + +These are constant endpoints available on direct connection servers. Plugin endpoints for direct connections are not documented here. + diff --git a/onionr/static-data/default-plugins/clandestine/peerserver.py b/onionr/static-data/default-plugins/clandestine/peerserver.py index 64e4eaeb..5fe92fe9 100644 --- a/onionr/static-data/default-plugins/clandestine/peerserver.py +++ b/onionr/static-data/default-plugins/clandestine/peerserver.py @@ -45,11 +45,9 @@ def sendto(): msg = request.get_json(force=True) except: msg = '' - if msg == None or msg == '': - msg = json.dumps({'m': 'hello world', 't': core_inst._utils.getEpoch()}) else: msg = json.dumps(msg) - core_inst._utils.localCommand('/clandestine/addrec/%s' % (g.peer,), post=True, postData=msg) + core_inst._utils.localCommand('/clandestine/addrec/%s' % (g.peer,), post=True, postData=msg) return Response('success') @direct_blueprint.route('/clandestine/poll') diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index 06cb080a..cdc013e1 100644 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -23,7 +23,6 @@ threadPart = document.getElementById('threads') threadPlaceholder = document.getElementById('threadPlaceholder') tabBtns = document.getElementById('tabBtns') threadContent = {} -myPub = httpGet('/getActivePubkey') replyBtn = document.getElementById('replyBtn') function openReply(bHash, quote, subject){ @@ -300,16 +299,6 @@ tabBtns.onclick = function(event){ setActiveTab(event.target.innerText.toLowerCase()) } -var idStrings = document.getElementsByClassName('myPub') -for (var i = 0; i < idStrings.length; i++){ - if (idStrings[i].tagName.toLowerCase() == 'input'){ - idStrings[i].value = myPub - } - else{ - idStrings[i].innerText = myPub - } -} - for (var i = 0; i < document.getElementsByClassName('refresh').length; i++){ document.getElementsByClassName('refresh')[i].style.float = 'right' } diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index 8d35f070..b1e50e12 100644 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -19,17 +19,21 @@ Onionr Web Control Panel

- +
🕵️‍♂️ Current Used Identity:
+
+

- +

Onionr Services

+


Mail - Friend Manager +


Stats

-

Uptime:

-

Last Received Connection: Unknown

-

Stored Blocks:

-

Blocks in queue:

-

Connected nodes:

+

🕰️ Uptime:

+

🖇️ Last Received Connection: Unknown

+

💾 Stored Blocks:

+

📨 Blocks in queue:

+

🔗 Outgoing Connections:


         
diff --git a/onionr/static-data/www/shared/main/style.css b/onionr/static-data/www/shared/main/style.css index 90d4af13..a673ea7b 100644 --- a/onionr/static-data/www/shared/main/style.css +++ b/onionr/static-data/www/shared/main/style.css @@ -152,7 +152,7 @@ body{ padding: 5px; } - .btn, .warnBtn, .dangerBtn, .successBtn{ + .btn, .warnBtn, .dangerBtn, .successBtn, .primaryBtn{ padding: 5px; border-radius: 5px; border: 2px solid black; diff --git a/onionr/static-data/www/shared/misc.js b/onionr/static-data/www/shared/misc.js index 3c960c8f..eeaddf71 100644 --- a/onionr/static-data/www/shared/misc.js +++ b/onionr/static-data/www/shared/misc.js @@ -20,6 +20,8 @@ webpass = document.location.hash.replace('#', '') nowebpass = false +myPub = httpGet('/getActivePubkey') + function post_to_url(path, params) { var form = document.createElement("form") @@ -91,4 +93,14 @@ for (var i = 0; i < document.getElementsByClassName('closeOverlay').length; i++) document.getElementsByClassName('closeOverlay')[i].onclick = function(e){ document.getElementById(e.target.getAttribute('overlay')).style.visibility = 'hidden' } +} + +var idStrings = document.getElementsByClassName('myPub') +for (var i = 0; i < idStrings.length; i++){ + if (idStrings[i].tagName.toLowerCase() == 'input'){ + idStrings[i].value = myPub + } + else{ + idStrings[i].innerText = myPub + } } \ No newline at end of file From 9604849dbba416769eff408186335f3e499632c7 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 16 Apr 2019 00:02:09 -0500 Subject: [PATCH 043/173] work on making basic board --- onionr/static-data/www/board/board.js | 18 ++---------------- onionr/static-data/www/board/index.html | 8 ++------ onionr/static-data/www/private/index.html | 2 +- 3 files changed, 5 insertions(+), 23 deletions(-) diff --git a/onionr/static-data/www/board/board.js b/onionr/static-data/www/board/board.js index 7f513357..a1ec5dc2 100644 --- a/onionr/static-data/www/board/board.js +++ b/onionr/static-data/www/board/board.js @@ -1,11 +1,7 @@ -webpassword = '' requested = [] -document.getElementById('webpassWindow').style.display = 'block'; - var windowHeight = window.innerHeight; -document.getElementById('webpassWindow').style.height = windowHeight + "px"; - +webpassword = webpass function httpGet(theUrl) { var xmlHttp = new XMLHttpRequest() xmlHttp.open( "GET", theUrl, false ) // false for synchronous request @@ -35,23 +31,13 @@ function getBlocks(){ var blockList = feedText.split(',') for (i = 0; i < blockList.length; i++){ if (! requested.includes(blockList[i])){ - bl = httpGet('/gethtmlsafeblockdata/' + blockList[i]) + bl = httpGet('/getblockdata/' + blockList[i]) appendMessages(bl) requested.push(blockList[i]) } } } -document.getElementById('registerPassword').onclick = function(){ - webpassword = document.getElementById('webpassword').value - if (httpGet('/ping') === 'pong!'){ - document.getElementById('webpassWindow').style.display = 'none' - getBlocks() - } - else{ - alert('Sorry, but that password appears invalid.') - } -} document.getElementById('refreshFeed').onclick = function(){ getBlocks() diff --git a/onionr/static-data/www/board/index.html b/onionr/static-data/www/board/index.html index 7486a910..054f6a15 100644 --- a/onionr/static-data/www/board/index.html +++ b/onionr/static-data/www/board/index.html @@ -9,14 +9,10 @@ - +
None Yet :)
+ \ No newline at end of file diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index b1e50e12..60ce3804 100644 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -26,7 +26,7 @@

Onionr Services


-

Mail - Friend Manager +

Mail - Friend Manager - Boards


Stats

🕰️ Uptime:

From 705359d3a6d60070bc0dd3c3c6e22422204d34c9 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 16 Apr 2019 12:04:51 -0500 Subject: [PATCH 044/173] board appearance and functionality work --- onionr/static-data/www/board/board.js | 32 +++++++++++++++---------- onionr/static-data/www/board/index.html | 23 ++++++++++++++---- onionr/static-data/www/board/theme.css | 12 +++++++--- 3 files changed, 46 insertions(+), 21 deletions(-) diff --git a/onionr/static-data/www/board/board.js b/onionr/static-data/www/board/board.js index a1ec5dc2..d9f35bce 100644 --- a/onionr/static-data/www/board/board.js +++ b/onionr/static-data/www/board/board.js @@ -2,22 +2,25 @@ requested = [] var windowHeight = window.innerHeight; webpassword = webpass -function httpGet(theUrl) { - var xmlHttp = new XMLHttpRequest() - xmlHttp.open( "GET", theUrl, false ) // false for synchronous request - xmlHttp.setRequestHeader('token', webpassword) - xmlHttp.send( null ) - if (xmlHttp.status == 200){ - return xmlHttp.responseText +newPostForm = document.getElementById('addMsg') + +function appendMessages(msg){ + var humanDate = new Date(0) + var msg = JSON.parse(msg) + var dateEl = document.createElement('span') + var el = document.createElement('div') + var msgDate = msg['meta']['time'] + if (msgDate === undefined){ + msgDate = 'unknown' } else{ - return ""; + humanDate.setUTCSeconds(msgDate) + msgDate = humanDate.toDateString() + ' ' + humanDate.toTimeString() } -} -function appendMessages(msg){ - el = document.createElement('div') + dateEl.textContent = msgDate el.className = 'entry' - el.innerText = msg + el.innerText = msg['content'] + document.getElementById('feed').appendChild(dateEl) document.getElementById('feed').appendChild(el) document.getElementById('feed').appendChild(document.createElement('br')) } @@ -38,7 +41,10 @@ function getBlocks(){ } } - document.getElementById('refreshFeed').onclick = function(){ getBlocks() +} + +newPostForm.onsubmit = function(){ + return false } \ No newline at end of file diff --git a/onionr/static-data/www/board/index.html b/onionr/static-data/www/board/index.html index 054f6a15..180bf0b0 100644 --- a/onionr/static-data/www/board/index.html +++ b/onionr/static-data/www/board/index.html @@ -4,14 +4,27 @@ - OnionrBoard + Circle + + - - - -
None Yet :)
+ +

Circle

+

+ Anonymous message board +

+ + +

+ + +

+
+ +

+ None Yet :)
diff --git a/onionr/static-data/www/board/theme.css b/onionr/static-data/www/board/theme.css index 766e4407..1062e49b 100644 --- a/onionr/static-data/www/board/theme.css +++ b/onionr/static-data/www/board/theme.css @@ -1,6 +1,8 @@ -h1, h2, h3{ - font-family: sans-serif; +body{ + background-color: white; + color: black; } + .hidden{ display: none; } @@ -18,7 +20,7 @@ p{ } .entry{ - color: red; + color: green; } #feed{ @@ -28,4 +30,8 @@ p{ border: 2px solid black; padding: 5px; min-height: 50px; +} + +.successBtn{ + min-width: 100px; } \ No newline at end of file From 2219be9ae35ad52fcb5e3ee4ea4af47f88a4fd17 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 20 Apr 2019 17:55:54 -0500 Subject: [PATCH 045/173] + Work on boards * Handle ctrl-c better in main loop --- onionr/api.py | 5 +++++ onionr/communicator.py | 2 +- onionr/core.py | 4 ++++ onionr/onionrcommands/daemonlaunch.py | 22 ++++++++++++++-------- onionr/static-data/www/board/board.js | 19 +++++++++++++++++++ onionr/static-data/www/board/index.html | 2 +- 6 files changed, 44 insertions(+), 10 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index ae6368e2..2f320fbc 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -497,6 +497,11 @@ class API: encrypt = False bData = request.get_json(force=True) message = bData['message'] + + # Detect if message (block body) is not specified + if type(message) is None: + return 'failure', 406 + subject = 'temp' encryptType = '' sign = True diff --git a/onionr/communicator.py b/onionr/communicator.py index f10d37da..24907cf2 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -148,7 +148,7 @@ class OnionrCommunicatorDaemon: self.shutdown = True pass - logger.info('Goodbye.') + logger.info('Goodbye. (Onionr is cleaning up, and will exit)') try: self.service_greenlets except AttributeError: diff --git a/onionr/core.py b/onionr/core.py index 48029555..47f00fa7 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -686,9 +686,13 @@ class Core: return False retData = False + if type(data) is None: + raise ValueError('Data cannot be none') + createTime = self._utils.getRoundedEpoch() # check nonce + print(data) dataNonce = self._utils.bytesToStr(self._crypto.sha3Hash(data)) try: with open(self.dataNonceFile, 'r') as nonces: diff --git a/onionr/onionrcommands/daemonlaunch.py b/onionr/onionrcommands/daemonlaunch.py index b5334068..dac35115 100644 --- a/onionr/onionrcommands/daemonlaunch.py +++ b/onionr/onionrcommands/daemonlaunch.py @@ -18,7 +18,7 @@ along with this program. If not, see . ''' -import os, time, sys, platform, sqlite3 +import os, time, sys, platform, sqlite3, signal from threading import Thread import onionr, api, logger, communicator import onionrevents as events @@ -84,9 +84,12 @@ def daemon(o_inst): logger.debug('Started communicator.') events.event('daemon_start', onionr = o_inst) - try: - while True: + while True: + try: time.sleep(3) + except KeyboardInterrupt: + o_inst.communicatorInst.shutdown = True + finally: # Debug to print out used FDs (regular and net) #proc = psutil.Process() #print('api-files:',proc.open_files(), len(psutil.net_connections())) @@ -95,16 +98,19 @@ def daemon(o_inst): break if o_inst.killed: break # Break out if sigterm for clean exit - except KeyboardInterrupt: - pass - finally: - o_inst.onionrCore.daemonQueueAdd('shutdown') - o_inst.onionrUtils.localCommand('shutdown') + + signal.signal(signal.SIGINT, _ignore_sigint) + o_inst.onionrCore.daemonQueueAdd('shutdown') + o_inst.onionrUtils.localCommand('shutdown') + net.killTor() time.sleep(3) o_inst.deleteRunFiles() return +def _ignore_sigint(sig, frame): + return + def kill_daemon(o_inst): ''' Shutdown the Onionr daemon diff --git a/onionr/static-data/www/board/board.js b/onionr/static-data/www/board/board.js index d9f35bce..7d70f9db 100644 --- a/onionr/static-data/www/board/board.js +++ b/onionr/static-data/www/board/board.js @@ -6,6 +6,9 @@ newPostForm = document.getElementById('addMsg') function appendMessages(msg){ var humanDate = new Date(0) + if (msg.length == 0){ + return + } var msg = JSON.parse(msg) var dateEl = document.createElement('span') var el = document.createElement('div') @@ -46,5 +49,21 @@ document.getElementById('refreshFeed').onclick = function(){ } newPostForm.onsubmit = function(){ + var message = document.getElementById('newMsgText').value + var postData = {'message': message, 'type': 'txt', 'encrypt': false} + postData = JSON.stringify(postData) + newPostForm.style.display = 'none' + fetch('/insertblock', { + method: 'POST', + body: postData, + headers: { + "content-type": "application/json", + "token": webpass + }}) + .then((resp) => resp.text()) // Transform the data into json + .then(function(data) { + newPostForm.style.display = 'block' + alert('Queued for submission!') + }) return false } \ No newline at end of file diff --git a/onionr/static-data/www/board/index.html b/onionr/static-data/www/board/index.html index 180bf0b0..37d22ea9 100644 --- a/onionr/static-data/www/board/index.html +++ b/onionr/static-data/www/board/index.html @@ -16,7 +16,7 @@ Anonymous message board

- +

From c674e2d281f2534169b0c2522deba38820c0fba1 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 20 Apr 2019 19:31:14 -0500 Subject: [PATCH 046/173] Fixed spam for block processing --- onionr/api.py | 1 - onionr/onionrutils.py | 2 +- onionr/static-data/default-plugins/metadataprocessor/main.py | 3 +-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 2f320fbc..c9b3a865 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -579,7 +579,6 @@ class API: else: validSig = False signer = self._core._utils.bytesToStr(bl.signer) - #print(signer, bl.isSigned(), self._core._utils.validatePubKey(signer), bl.isSigner(signer)) if bl.isSigned() and self._core._utils.validatePubKey(signer) and bl.isSigner(signer): validSig = True bl.bheader['validSig'] = validSig diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index fa75f72a..2637b905 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -157,6 +157,7 @@ class OnionrUtils: ''' Read metadata from a block and cache it to the block database ''' + onionrevents.event('processblocks', data = {'block': myBlock, 'type': blockType, 'signer': signer, 'validSig': valid}, onionr = None) curTime = self.getRoundedEpoch(roundS=60) myBlock = Block(blockHash, self._core) if myBlock.isEncrypted: @@ -171,7 +172,6 @@ class OnionrUtils: try: if len(blockType) <= 10: self._core.updateBlockInfo(blockHash, 'dataType', blockType) - onionrevents.event('processblocks', data = {'block': myBlock, 'type': blockType, 'signer': signer, 'validSig': valid}, onionr = None) except TypeError: logger.warn("Missing block information") pass diff --git a/onionr/static-data/default-plugins/metadataprocessor/main.py b/onionr/static-data/default-plugins/metadataprocessor/main.py index 346600ff..5feb4305 100755 --- a/onionr/static-data/default-plugins/metadataprocessor/main.py +++ b/onionr/static-data/default-plugins/metadataprocessor/main.py @@ -39,13 +39,12 @@ def _processForwardKey(api, myBlock): if api.get_utils().validatePubKey(key): peer.addForwardKey(key) else: - raise onionrexceptions.InvalidPubkey("%s is nota valid pubkey key" % (key,)) + raise onionrexceptions.InvalidPubkey("%s is not a valid pubkey key" % (key,)) def on_processblocks(api, data=None): # Generally fired by utils. myBlock = api.data['block'] blockType = api.data['type'] - # Process specific block types # forwardKey blocks, add a new forward secrecy key for a peer From 71aadb7767cb21ee863474ee79fca4e7f8d08e41 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 22 Apr 2019 21:02:09 -0500 Subject: [PATCH 047/173] fixed dumb print leftover spamming hashes --- onionr/api.py | 13 +++++++------ onionr/communicator.py | 2 +- onionr/core.py | 2 +- onionr/onionrplugins.py | 6 ++++-- onionr/onionrproofs.py | 1 - onionr/onionrutils.py | 2 +- onionr/static-data/bootstrap-nodes.txt | 1 - onionr/static-data/default_config.json | 1 + 8 files changed, 15 insertions(+), 13 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index c9b3a865..a77e95aa 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -114,10 +114,11 @@ class PublicAPI: # Provide a list of our blocks, with a date offset dateAdjust = request.args.get('date') bList = clientAPI._core.getBlockList(dateRec=dateAdjust) - for b in self.hideBlocks: - if b in bList: - # Don't share blocks we created if they haven't been *uploaded* yet, makes it harder to find who created a block - bList.remove(b) + if config.get('general.hide_created_blocks', True): + for b in self.hideBlocks: + if b in bList: + # Don't share blocks we created if they haven't been *uploaded* yet, makes it harder to find who created a block + bList.remove(b) return Response('\n'.join(bList)) @app.route('/getdata/') @@ -126,7 +127,7 @@ class PublicAPI: resp = '' data = name if clientAPI._utils.validateHash(data): - if data not in self.hideBlocks: + if not config.get('general.hide_created_blocks', True) or data not in self.hideBlocks: if data in clientAPI._core.getBlockList(): block = clientAPI.getBlockData(data, raw=True) try: @@ -244,7 +245,7 @@ class API: ''' Initialize the api server, preping variables for later use - This initilization defines all of the API entry points and handlers for the endpoints and errors + This initialization defines all of the API entry points and handlers for the endpoints and errors This also saves the used host (random localhost IP address) to the data folder in host.txt ''' diff --git a/onionr/communicator.py b/onionr/communicator.py index 24907cf2..94f8a4fa 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -621,7 +621,7 @@ class OnionrCommunicatorDaemon: def detectAPICrash(self): '''exit if the api server crashes/stops''' if self._core._utils.localCommand('ping', silent=False) not in ('pong', 'pong!'): - for i in range(12): + for i in range(20): if self._core._utils.localCommand('ping') in ('pong', 'pong!') or self.shutdown: break # break for loop time.sleep(1) diff --git a/onionr/core.py b/onionr/core.py index 47f00fa7..3c04ceb4 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -692,7 +692,7 @@ class Core: createTime = self._utils.getRoundedEpoch() # check nonce - print(data) + #print(data) dataNonce = self._utils.bytesToStr(self._crypto.sha3Hash(data)) try: with open(self.dataNonceFile, 'r') as nonces: diff --git a/onionr/onionrplugins.py b/onionr/onionrplugins.py index 7595805b..1a7c18d8 100755 --- a/onionr/onionrplugins.py +++ b/onionr/onionrplugins.py @@ -260,6 +260,8 @@ def check(): if not os.path.exists(os.path.dirname(get_plugins_folder())): logger.debug('Generating plugin data folder...') - os.makedirs(os.path.dirname(get_plugins_folder())) - + try: + os.makedirs(os.path.dirname(get_plugins_folder())) + except FileExistsError: + pass return diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index 53c6d881..bd0600cf 100755 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -259,7 +259,6 @@ class POW: self.hashing = False iFound = True self.result = payload - print('count', nonce - startNonce) break nonce += 1 diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 2637b905..27c7fa2b 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -157,7 +157,6 @@ class OnionrUtils: ''' Read metadata from a block and cache it to the block database ''' - onionrevents.event('processblocks', data = {'block': myBlock, 'type': blockType, 'signer': signer, 'validSig': valid}, onionr = None) curTime = self.getRoundedEpoch(roundS=60) myBlock = Block(blockHash, self._core) if myBlock.isEncrypted: @@ -185,6 +184,7 @@ class OnionrUtils: self._core.updateBlockInfo(blockHash, 'expire', expireTime) if not blockType is None: self._core.updateBlockInfo(blockHash, 'dataType', blockType) + onionrevents.event('processblocks', data = {'block': myBlock, 'type': blockType, 'signer': signer, 'validSig': valid}, onionr = None) else: pass #logger.debug('Not processing metadata on encrypted block we cannot decrypt.') diff --git a/onionr/static-data/bootstrap-nodes.txt b/onionr/static-data/bootstrap-nodes.txt index 52622b12..e69de29b 100755 --- a/onionr/static-data/bootstrap-nodes.txt +++ b/onionr/static-data/bootstrap-nodes.txt @@ -1 +0,0 @@ -ecaufnpxx67xmvzwcriohapmmnqbj665h3ynemvwrr2juxl5oa5g7cad.onion diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index 988e91f0..aba71a3f 100755 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -6,6 +6,7 @@ "minimum_send_pow": 4, "socket_servers": false, "security_level": 0, + "hide_created_blocks": true, "max_block_age": 2678400, "bypass_tor_check": false, "public_key": "", From 47ee28f74f17885f334f30a4ba364d999b2c5039 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 25 Apr 2019 00:38:15 -0500 Subject: [PATCH 048/173] * Fixed duplicate plugin events --- docs/http-api.md | 8 ++++++++ docs/specs/block-spec.md | 4 ++-- onionr/onionrcommands/onionrstatistics.py | 5 ++++- onionr/onionrevents.py | 1 + onionr/onionrplugins.py | 11 ++++++----- onionr/onionrservices/connectionserver.py | 2 +- onionr/static-data/default-plugins/encrypt/main.py | 1 + onionr/static-data/default-plugins/pms/main.py | 1 - onionr/static-data/index.html | 2 +- 9 files changed, 24 insertions(+), 11 deletions(-) diff --git a/docs/http-api.md b/docs/http-api.md index 2fe1728d..241320ae 100755 --- a/docs/http-api.md +++ b/docs/http-api.md @@ -105,3 +105,11 @@ v0 These are constant endpoints available on direct connection servers. Plugin endpoints for direct connections are not documented here. +* /ping + - Methods: GET + - Returns 200 with 'pong!' + +* /close + - Methods: GET + - Kills the direct connection server, destroying the onion address. + - Returns 200 with 'goodbye' \ No newline at end of file diff --git a/docs/specs/block-spec.md b/docs/specs/block-spec.md index 348cedae..26867e42 100644 --- a/docs/specs/block-spec.md +++ b/docs/specs/block-spec.md @@ -1,4 +1,4 @@ -# Onionr Block Spec v1.0.0 +# Onionr Block Spec v1.1.0 # Block Description @@ -60,4 +60,4 @@ Pow is a field for placing the nonce found to make a block meet a target proof o ## encryptType -encryptType is a field to specify the mode of encryption for a block. The values supported by Onionr are 'asym' and 'sym'. +encryptType is a field to specify the mode of encryption for a block. The values supported by Onionr are 'asym' and 'sym'. \ No newline at end of file diff --git a/onionr/onionrcommands/onionrstatistics.py b/onionr/onionrcommands/onionrstatistics.py index 04264655..8430a524 100644 --- a/onionr/onionrcommands/onionrstatistics.py +++ b/onionr/onionrcommands/onionrstatistics.py @@ -104,7 +104,10 @@ def show_peers(o_inst): if not type(peers) is None: if peers not in ('', 'failure', None): if peers != False: - print(peers) + if peers == 'none': + print('No current outgoing connections.') + else: + print(peers) else: print('Daemon probably not running. Unable to list connected peers.') break \ No newline at end of file diff --git a/onionr/onionrevents.py b/onionr/onionrevents.py index 3301a3ac..c6ebcc40 100755 --- a/onionr/onionrevents.py +++ b/onionr/onionrevents.py @@ -67,6 +67,7 @@ def call(plugin, event_name, data = None, pluginapi = None): return True except Exception as e: + logger.error(str(e)) return False else: return True diff --git a/onionr/onionrplugins.py b/onionr/onionrplugins.py index 1a7c18d8..c334ea09 100755 --- a/onionr/onionrplugins.py +++ b/onionr/onionrplugins.py @@ -17,9 +17,8 @@ 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 +import os, re, importlib +import onionrevents as events, config, logger # set data dir dataDir = os.environ.get('ONIONR_HOME', os.environ.get('DATA_DIR', 'data/')) @@ -71,7 +70,7 @@ def enable(name, onionr = None, start_event = True): events.call(get_plugin(name), 'enable', onionr) except ImportError: # Was getting import error on Gitlab CI test "data" # NOTE: If you are experiencing issues with plugins not being enabled, it might be this resulting from an error in the module - # can happen inconsistenly (especially between versions) + # can happen inconsistently (especially between versions) return False else: enabled_plugins.append(name) @@ -170,6 +169,7 @@ def import_module_from_file(full_path_to_module): module_dir, module_file = os.path.split(full_path_to_module) module_name, module_ext = os.path.splitext(module_file) + module_name = module_dir # Module name must be unique otherwise it will get written in other imports # Get module "spec" from filename spec = importlib.util.spec_from_file_location(module_name,full_path_to_module) @@ -187,7 +187,7 @@ def get_plugin(name): if str(name).lower() in _instances: return _instances[str(name).lower()] else: - _instances[str(name).lower()] = import_module_from_file(get_plugins_folder(name, False) + 'main.py') + _instances[str(name).lower()] = import_module_from_file(get_plugins_folder(str(name).lower(), False) + 'main.py') return get_plugin(name) def get_plugins(): @@ -233,6 +233,7 @@ def get_plugins_folder(name = None, absolute = True): path = _pluginsfolder else: # only allow alphanumeric characters + #path = _pluginsfolder + str(name.lower()) path = _pluginsfolder + re.sub('[^0-9a-zA-Z_]+', '', str(name).lower()) if absolute is True: diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index 27a0b55f..31e429f8 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -51,7 +51,7 @@ class ConnectionServer: def get_ping(): return "pong!" - @service_app.route('/shutdown') + @service_app.route('/close') def shutdown_server(): core_inst.onionrInst.communicatorInst.service_greenlets.remove(http_server) http_server.stop() diff --git a/onionr/static-data/default-plugins/encrypt/main.py b/onionr/static-data/default-plugins/encrypt/main.py index f917d72a..15697ac2 100755 --- a/onionr/static-data/default-plugins/encrypt/main.py +++ b/onionr/static-data/default-plugins/encrypt/main.py @@ -24,6 +24,7 @@ from onionrblockapi import Block import onionrexceptions, onionrusers import locale locale.setlocale(locale.LC_ALL, '') +plugin_name = 'encrypt' class PlainEncryption: def __init__(self, api): diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py index 363c1259..2a4c15a5 100755 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -307,7 +307,6 @@ def on_insertblock(api, data={}): meta = json.loads(data['meta']) sentboxTools.addToSent(data['hash'], data['peer'], data['content'], meta['subject']) - def on_init(api, data = None): ''' This event is called after Onionr is initialized, but before the command diff --git a/onionr/static-data/index.html b/onionr/static-data/index.html index 916bfe9c..d4dd7f75 100755 --- a/onionr/static-data/index.html +++ b/onionr/static-data/index.html @@ -4,4 +4,4 @@

Onionr is a decentralized peer-to-peer data storage system.

-

To learn more about Onionr, see the website at https://Onionr.VoidNet.tech/

+

To learn more about Onionr, see the website at https://Onionr.net/

From 1b22a993a0e669a67f2e2b020ab585fb59e4d3e4 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 27 Apr 2019 10:43:16 -0500 Subject: [PATCH 049/173] work on ui --- onionr/api.py | 19 +++--- onionr/config.py | 2 +- onionr/httpapi/configapi/__init__.py | 62 +++++++++++++++++++ onionr/onionrutils.py | 6 +- .../default-plugins/flow/flowapi.py | 27 ++++++++ .../static-data/default-plugins/flow/main.py | 43 +++++++------ .../default-plugins/pms/mailapi.py | 17 ----- .../static-data/default-plugins/pms/main.py | 2 +- onionr/static-data/www/mail/index.html | 2 +- onionr/static-data/www/mail/mail.js | 6 -- onionr/static-data/www/private/index.html | 10 ++- onionr/static-data/www/private/main.css | 8 +++ onionr/static-data/www/shared/configeditor.js | 18 ++++++ 13 files changed, 168 insertions(+), 54 deletions(-) create mode 100644 onionr/httpapi/configapi/__init__.py create mode 100644 onionr/static-data/default-plugins/flow/flowapi.py create mode 100644 onionr/static-data/www/private/main.css create mode 100644 onionr/static-data/www/shared/configeditor.js diff --git a/onionr/api.py b/onionr/api.py index a77e95aa..bf42348a 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -26,7 +26,7 @@ import core from onionrblockapi import Block import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config import httpapi -from httpapi import friendsapi, simplecache, profilesapi +from httpapi import friendsapi, simplecache, profilesapi, configapi from onionrservices import httpheaders import onionr @@ -259,7 +259,7 @@ class API: self.bindPort = bindPort # Be extremely mindful of this. These are endpoints available without a password - self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'board', 'profiles', 'profilesindex', + self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'homedata', 'board', 'profiles', 'profilesindex', 'boardContent', 'sharedContent', 'mail', 'mailindex', 'friends', 'friendsindex') self.clientToken = config.get('client.webpassword') @@ -276,6 +276,7 @@ class API: app.register_blueprint(friendsapi.friends) app.register_blueprint(simplecache.simplecache) app.register_blueprint(profilesapi.profile_BP) + app.register_blueprint(configapi.config_BP) httpapi.load_plugin_blueprints(app) @app.before_request @@ -347,6 +348,15 @@ class API: def sharedContent(path): return send_from_directory('static-data/www/shared/', path) + @app.route('/', endpoint='onionrhome') + def hello(): + # ui home + return send_from_directory('static-data/www/private/', 'index.html') + + @app.route('/private/', endpoint='homedata') + def homedata(path): + return send_from_directory('static-data/www/private/', path) + @app.route('/www/', endpoint='www') def wwwPublic(path): if not config.get("www.private.run", True): @@ -378,11 +388,6 @@ class API: def ping(): # Used to check if client api is working return Response("pong!") - - @app.route('/', endpoint='onionrhome') - def hello(): - # ui home - return send_from_directory('static-data/www/private/', 'index.html') @app.route('/getblocksbytype/') def getBlocksByType(name): diff --git a/onionr/config.py b/onionr/config.py index fe093b57..11319b85 100755 --- a/onionr/config.py +++ b/onionr/config.py @@ -115,7 +115,7 @@ def save(): try: with open(get_config_file(), 'w', encoding="utf8") as configfile: json.dump(get_config(), configfile, indent=2) - except: + except json.JSONDecodeError: logger.warn('Failed to write to configuration file.') def reload(): diff --git a/onionr/httpapi/configapi/__init__.py b/onionr/httpapi/configapi/__init__.py new file mode 100644 index 00000000..5f5588af --- /dev/null +++ b/onionr/httpapi/configapi/__init__.py @@ -0,0 +1,62 @@ +''' + Onionr - P2P Anonymous Storage Network + + This file handles configuration setting and getting from the HTTP API +''' +''' + 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 json +from flask import Blueprint, request, Response, abort +import config, onionrutils +config.reload() + +config_BP = Blueprint('config_BP', __name__) + +@config_BP.route('/config/get') +def get_all_config(): + '''Simply return all configuration as JSON string''' + return Response(json.dumps(config.get_config())) + +@config_BP.route('/config/get/') +def get_by_key(key): + '''Return a config setting by key''' + return Response(json.dumps(config.get(key))) + +@config_BP.route('/config/setall', methods=['POST']) +def set_all_config(): + '''Overwrite existing JSON config with new JSON string''' + new_config = request.get_json(force=True) + try: + new_config = json.loads(new_config) + except json.JSONDecodeError: + abort(400) + else: + config.set_config(new_config) + return Response('success') + +@config_BP.route('/config/set/', methods=['POST']) +def set_by_key(key): + '''Overwrite/set only 1 config key''' + ''' + { + 'data': data + } + ''' + try: + data = json.loads(onionrutils.OnionrUtils.bytesToStr(request.data))['data'] + except (json.JSONDecodeError, KeyError): + abort(400) + config.set(key, data, True) + return Response('success') \ No newline at end of file diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 27c7fa2b..e3b9e49e 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -530,13 +530,15 @@ class OnionrUtils: else: return retData - def strToBytes(self, data): + @staticmethod + def strToBytes(data): try: data = data.encode() except AttributeError: pass return data - def bytesToStr(self, data): + @staticmethod + def bytesToStr(data): try: data = data.decode() except AttributeError: diff --git a/onionr/static-data/default-plugins/flow/flowapi.py b/onionr/static-data/default-plugins/flow/flowapi.py new file mode 100644 index 00000000..95d5fb4f --- /dev/null +++ b/onionr/static-data/default-plugins/flow/flowapi.py @@ -0,0 +1,27 @@ +''' + Onionr - P2P Microblogging Platform & Social network + + This file primarily serves to allow specific fetching of flow board messages +''' +''' + 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 . +''' + +from flask import Response, request, redirect, Blueprint, abort + +flask_blueprint = Blueprint('flow', __name__) + +@flask_blueprint.route('/flow/getpostsbyboard/') +def get_post_by_board(board): + return Response('WIP') \ No newline at end of file diff --git a/onionr/static-data/default-plugins/flow/main.py b/onionr/static-data/default-plugins/flow/main.py index 2587df93..de406207 100755 --- a/onionr/static-data/default-plugins/flow/main.py +++ b/onionr/static-data/default-plugins/flow/main.py @@ -19,10 +19,17 @@ ''' # Imports some useful libraries -import logger, config, threading, time +import threading, time, locale, sys, os from onionrblockapi import Block +import logger, config +locale.setlocale(locale.LC_ALL, '') + +sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) +import flowapi # import after path insert +flask_blueprint = flowapi.flask_blueprint plugin_name = 'flow' +PLUGIN_VERSION = '0.0.1' class OnionrFlow: def __init__(self): @@ -64,23 +71,23 @@ class OnionrFlow: time.sleep(1) try: while self.flowRunning: - for block in self.myCore.getBlocksByType('txt'): - block = Block(block) - if block.getMetadata('ch') != self.channel: - #print('not chan', block.getMetadata('ch')) - continue - if block.getHash() in self.alreadyOutputed: - #print('already') - continue - if not self.flowRunning: - break - logger.info('\n------------------------', prompt = False) - content = block.getContent() - # Escape new lines, remove trailing whitespace, and escape ansi sequences - content = self.myCore._utils.escapeAnsi(content.replace('\n', '\\n').replace('\r', '\\r').strip()) - logger.info(block.getDate().strftime("%m/%d %H:%M") + ' - ' + logger.colors.reset + content, prompt = False) - self.alreadyOutputed.append(block.getHash()) - time.sleep(5) + for block in self.myCore.getBlocksByType('txt'): + block = Block(block) + if block.getMetadata('ch') != self.channel: + #print('not chan', block.getMetadata('ch')) + continue + if block.getHash() in self.alreadyOutputed: + #print('already') + continue + if not self.flowRunning: + break + logger.info('\n------------------------', prompt = False) + content = block.getContent() + # Escape new lines, remove trailing whitespace, and escape ansi sequences + content = self.myCore._utils.escapeAnsi(content.replace('\n', '\\n').replace('\r', '\\r').strip()) + logger.info(block.getDate().strftime("%m/%d %H:%M") + ' - ' + logger.colors.reset + content, prompt = False) + self.alreadyOutputed.append(block.getHash()) + time.sleep(5) except KeyboardInterrupt: self.flowRunning = False diff --git a/onionr/static-data/default-plugins/pms/mailapi.py b/onionr/static-data/default-plugins/pms/mailapi.py index 0a60e67c..c9df3a52 100644 --- a/onionr/static-data/default-plugins/pms/mailapi.py +++ b/onionr/static-data/default-plugins/pms/mailapi.py @@ -61,20 +61,3 @@ def list_sentbox(): if x['hash'] in deleted: sentbox_list.remove(x) return json.dumps(sentbox_list) -''' -@flask_blueprint.route('/mail/getsentbox') -def list_sentbox(): - sentbox_list = sentboxdb.SentBox(c).listSent() - sentbox_list_copy = list(sentbox_list) - kv.refresh() - deleted = kv.get('deleted_mail') - if deleted is None: - deleted = [] - for x in range(len(sentbox_list_copy)): - if sentbox_list_copy[x]['hash'] in deleted: - sentbox_list.remove(sentbox_list_copy[x]['hash']) - else: - sentbox_list[x]['name'] = contactmanager.ContactManager(c, sentbox_list_copy[x]['peer'], saveUser=False).get_info('name') - - return json.dumps(sentbox_list) -''' \ No newline at end of file diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py index 2a4c15a5..ce58c58e 100755 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -35,7 +35,7 @@ import sentboxdb, mailapi, loadinbox # import after path insert flask_blueprint = mailapi.flask_blueprint def draw_border(text): - #https://stackoverflow.com/a/20757491 + # This function taken from https://stackoverflow.com/a/20757491 by https://stackoverflow.com/users/816449/bunyk, under https://creativecommons.org/licenses/by-sa/3.0/ lines = text.splitlines() width = max(len(s) for s in lines) res = ['┌' + '─' * width + '┐'] diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index 08441075..81a0128e 100644 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -10,7 +10,7 @@ - +
diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index cdc013e1..8c9b13d1 100644 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -327,12 +327,6 @@ fetch('/friends/list', { } friendSelectParent.appendChild(option) } - - for (var i = 0; i < keys.length; i++){ - - //friendSelectParent - //alert(resp[keys[i]]['name']) - } }) setActiveTab('inbox') diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index 60ce3804..2b54cc99 100644 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -6,8 +6,8 @@ Onionr - +
@@ -28,6 +28,13 @@


Mail - Friend Manager - Boards


+
+ Edit Configuration +
+ + +
+

Stats

🕰️ Uptime:

🖇️ Last Received Connection: Unknown

@@ -39,6 +46,7 @@ + \ No newline at end of file diff --git a/onionr/static-data/www/private/main.css b/onionr/static-data/www/private/main.css new file mode 100644 index 00000000..91a7b588 --- /dev/null +++ b/onionr/static-data/www/private/main.css @@ -0,0 +1,8 @@ +.configEditor{ + width: 100%; + height: 300px; +} + +.saveConfig{ + margin-top: 1em; +} \ No newline at end of file diff --git a/onionr/static-data/www/shared/configeditor.js b/onionr/static-data/www/shared/configeditor.js new file mode 100644 index 00000000..45a97d46 --- /dev/null +++ b/onionr/static-data/www/shared/configeditor.js @@ -0,0 +1,18 @@ +var saveBtns = document.getElementsByClassName('saveConfig') +var saveBtn = document.getElementsByClassName('saveConfig')[0] +var configEditor = document.getElementsByClassName('configEditor')[0] +var config = {} + +fetch('/config/get', { +headers: { + "token": webpass +}}) +.then((resp) => resp.json()) // Transform the data into json +.then(function(resp) { + config = resp + configEditor.value = JSON.stringify(config) +}) + +saveBtn.onclick = function(){ + +} \ No newline at end of file From 95750b6b3c16d56c56408d74d41bdb7d9ca41d07 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 28 Apr 2019 21:08:10 -0500 Subject: [PATCH 050/173] finished config editor --- onionr/httpapi/configapi/__init__.py | 6 +-- onionr/static-data/www/private/index.html | 2 + onionr/static-data/www/shared/configeditor.js | 44 +++++++++++++++++-- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/onionr/httpapi/configapi/__init__.py b/onionr/httpapi/configapi/__init__.py index 5f5588af..3f5484f8 100644 --- a/onionr/httpapi/configapi/__init__.py +++ b/onionr/httpapi/configapi/__init__.py @@ -27,7 +27,7 @@ config_BP = Blueprint('config_BP', __name__) @config_BP.route('/config/get') def get_all_config(): '''Simply return all configuration as JSON string''' - return Response(json.dumps(config.get_config())) + return Response(json.dumps(config.get_config(), indent=4, sort_keys=True)) @config_BP.route('/config/get/') def get_by_key(key): @@ -37,13 +37,13 @@ def get_by_key(key): @config_BP.route('/config/setall', methods=['POST']) def set_all_config(): '''Overwrite existing JSON config with new JSON string''' - new_config = request.get_json(force=True) try: - new_config = json.loads(new_config) + new_config = request.get_json(force=True) except json.JSONDecodeError: abort(400) else: config.set_config(new_config) + config.save() return Response('success') @config_BP.route('/config/set/', methods=['POST']) diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index 2b54cc99..32c4c497 100644 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -31,6 +31,8 @@
Edit Configuration
+

Warning: Some values can be dangerous to change. Use caution.

+
diff --git a/onionr/static-data/www/shared/configeditor.js b/onionr/static-data/www/shared/configeditor.js index 45a97d46..6c9913b1 100644 --- a/onionr/static-data/www/shared/configeditor.js +++ b/onionr/static-data/www/shared/configeditor.js @@ -1,3 +1,22 @@ +/* + Onionr - P2P Anonymous Storage Network + + This file is for configuration editing in the web interface + + 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 . +*/ + var saveBtns = document.getElementsByClassName('saveConfig') var saveBtn = document.getElementsByClassName('saveConfig')[0] var configEditor = document.getElementsByClassName('configEditor')[0] @@ -7,12 +26,29 @@ fetch('/config/get', { headers: { "token": webpass }}) -.then((resp) => resp.json()) // Transform the data into json +.then((resp) => resp.text()) // Transform the data into text .then(function(resp) { - config = resp - configEditor.value = JSON.stringify(config) + configEditor.value = resp + config = JSON.parse(resp) //parse here so we can set the text field to pretty json }) saveBtn.onclick = function(){ - + var postData = configEditor.value + try { + JSON.parse(postData) + } catch (e) { + alert('Configuration is not valid JSON') + return false + } + fetch('/config/setall', { + method: 'POST', + body: postData, + headers: { + "content-type": "application/json", + "token": webpass + }}) + .then((resp) => resp.text()) // Transform the data into text + .then(function(data) { + alert('Config saved') + }) } \ No newline at end of file From a4d6dc5fa5827e19b4bbb9eceb4ceaae59d9eefb Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 7 May 2019 12:56:20 -0500 Subject: [PATCH 051/173] bug fixes and refactoring --- README.md | 12 +-- onionr/api.py | 86 ++----------------- onionr/communicatorutils/onionrdaemontools.py | 7 +- onionr/etc/onionrvalues.py | 3 +- onionr/httpapi/miscpublicapi/__init__.py | 6 ++ onionr/httpapi/miscpublicapi/announce.py | 63 ++++++++++++++ onionr/httpapi/miscpublicapi/getblocks.py | 50 +++++++++++ onionr/httpapi/miscpublicapi/upload.py | 43 ++++++++++ onionr/onionr.py | 2 + onionr/static-data/www/private/index.html | 12 +-- 10 files changed, 189 insertions(+), 95 deletions(-) create mode 100644 onionr/httpapi/miscpublicapi/__init__.py create mode 100644 onionr/httpapi/miscpublicapi/announce.py create mode 100644 onionr/httpapi/miscpublicapi/getblocks.py create mode 100644 onionr/httpapi/miscpublicapi/upload.py diff --git a/README.md b/README.md index b63fe020..2fb180e0 100755 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

- Anonymous P2P storage network 🕵️ + Anonymous P2P communication network 🕵️

(***pre-alpha & experimental, not well tested or easy to use yet***) @@ -19,7 +19,7 @@ # About -Onionr is a decentralized, peer-to-peer data storage network, designed to be anonymous and resistant to (meta)data analysis and spam/disruption. +Onionr is a decentralized, peer-to-peer communication and storage network, designed to be anonymous and resistant to (meta)data analysis and spam/disruption. Onionr stores data in independent packages referred to as 'blocks'. The blocks are synced to all other nodes in the network. Blocks and user IDs cannot be easily proven to have been created by a particular user. Even if there is enough evidence to believe that a specific user created a block, nodes still operate behind Tor or I2P and as such cannot be trivially unmasked. @@ -29,7 +29,7 @@ Onionr can be used for mail, as a social network, instant messenger, file sharin The whitepaper (subject to change prior to first alpha release) is available [here](docs/whitepaper.md). -![Tor stinks slide image](docs/tor-stinks-02.png) +![node web illustration](docs/onionr-web.png) ## Main Features @@ -54,7 +54,7 @@ Friend/contact manager Encrypted, metadata-masking mail application screenshot -Encrypted, metadata-masking mail application. Perhaps the first distributed mail system to have basic forward secrecy. +Encrypted, metadata-masking mail application. One of the first distributed mail systems to have basic forward secrecy. # Documentation @@ -115,4 +115,6 @@ The 'open source badge' is by Maik Ellerbrock and is licensed under a Creative C The Onionr logo was created by [Anhar Ismail](https://github.com/anharismail) under the [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/). -If you modify and redistribute our code ("forking"), please use a different logo and project name to avoid confusion. Please do not use our logo in a way that makes it seem like we endorse you without permission. \ No newline at end of file +If you modify and redistribute our code ("forking"), please use a different logo and project name to avoid confusion. Please do not use our logo in a way that makes it seem like we endorse you without permission. + +![Tor stinks slide image](docs/tor-stinks-02.png) diff --git a/onionr/api.py b/onionr/api.py index bf42348a..11197a0a 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -26,7 +26,7 @@ import core from onionrblockapi import Block import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config import httpapi -from httpapi import friendsapi, simplecache, profilesapi, configapi +from httpapi import friendsapi, simplecache, profilesapi, configapi, miscpublicapi from onionrservices import httpheaders import onionr @@ -111,36 +111,12 @@ class PublicAPI: @app.route('/getblocklist') def getBlockList(): - # Provide a list of our blocks, with a date offset - dateAdjust = request.args.get('date') - bList = clientAPI._core.getBlockList(dateRec=dateAdjust) - if config.get('general.hide_created_blocks', True): - for b in self.hideBlocks: - if b in bList: - # Don't share blocks we created if they haven't been *uploaded* yet, makes it harder to find who created a block - bList.remove(b) - return Response('\n'.join(bList)) + return httpapi.miscpublicapi.public_block_list(clientAPI, self, request) @app.route('/getdata/') def getBlockData(name): # Share data for a block if we have it - resp = '' - data = name - if clientAPI._utils.validateHash(data): - if not config.get('general.hide_created_blocks', True) or data not in self.hideBlocks: - if data in clientAPI._core.getBlockList(): - block = clientAPI.getBlockData(data, raw=True) - try: - block = block.encode() - except AttributeError: - abort(404) - block = clientAPI._core._utils.strToBytes(block) - resp = block - #resp = base64.b64encode(block).decode() - if len(resp) == 0: - abort(404) - resp = "" - return Response(resp, mimetype='application/octet-stream') + return httpapi.miscpublicapi.public_get_block_data(clientAPI, self, name) @app.route('/www/') def wwwPublic(path): @@ -163,40 +139,7 @@ class PublicAPI: @app.route('/announce', methods=['post']) def acceptAnnounce(): - resp = 'failure' - powHash = '' - randomData = '' - newNode = '' - ourAdder = clientAPI._core.hsAddress.encode() - try: - newNode = request.form['node'].encode() - except KeyError: - logger.warn('No node specified for upload') - pass - else: - try: - randomData = request.form['random'] - randomData = base64.b64decode(randomData) - except KeyError: - logger.warn('No random data specified for upload') - else: - nodes = newNode + clientAPI._core.hsAddress.encode() - nodes = clientAPI._core._crypto.blake2bHash(nodes) - powHash = clientAPI._core._crypto.blake2bHash(randomData + nodes) - try: - powHash = powHash.decode() - except AttributeError: - pass - if powHash.startswith('00000'): - newNode = clientAPI._core._utils.bytesToStr(newNode) - if clientAPI._core._utils.validateID(newNode) and not newNode in clientAPI._core.onionrInst.communicatorInst.newPeers: - clientAPI._core.onionrInst.communicatorInst.newPeers.append(newNode) - resp = 'Success' - else: - logger.warn(newNode.decode() + ' failed to meet POW: ' + powHash) - resp = Response(resp) - if resp == 'failure': - return resp, 406 + resp = httpapi.miscpublicapi.announce(clientAPI, request) return resp @app.route('/upload', methods=['post']) @@ -204,26 +147,7 @@ class PublicAPI: '''Accept file uploads. In the future this will be done more often than on creation to speed up block sync ''' - resp = 'failure' - try: - data = request.form['block'] - except KeyError: - logger.warn('No block specified for upload') - pass - else: - if sys.getsizeof(data) < 100000000: - try: - if blockimporter.importBlockFromData(data, clientAPI._core): - resp = 'success' - else: - logger.warn('Error encountered importing uploaded block') - except onionrexceptions.BlacklistedBlock: - logger.debug('uploaded block is blacklisted') - pass - if resp == 'failure': - abort(400) - resp = Response(resp) - return resp + return httpapi.miscpublicapi.upload(clientAPI, request) # Set instances, then startup our public api server clientAPI.setPublicAPIInstance(self) diff --git a/onionr/communicatorutils/onionrdaemontools.py b/onionr/communicatorutils/onionrdaemontools.py index 85c97747..54ee2703 100755 --- a/onionr/communicatorutils/onionrdaemontools.py +++ b/onionr/communicatorutils/onionrdaemontools.py @@ -23,6 +23,8 @@ import base64, sqlite3, os from dependencies import secrets from utils import netutils from onionrusers import onionrusers +from etc import onionrvalues +ov = onionrvalues.OnionrValues() class DaemonTools: ''' @@ -64,7 +66,8 @@ class DaemonTools: if ourID != 1: #TODO: Extend existingRand for i2p existingRand = self.daemon._core.getAddressInfo(peer, 'powValue') - if type(existingRand) is type(None): + # Reset existingRand if it no longer meets the minimum POW + if type(existingRand) is type(None) or not existingRand.endswith(b'0' * ov.announce_pow): existingRand = '' if peer in self.announceCache: @@ -73,7 +76,7 @@ class DaemonTools: data['random'] = existingRand else: self.announceProgress[peer] = True - proof = onionrproofs.DataPOW(combinedNodes, forceDifficulty=5) + proof = onionrproofs.DataPOW(combinedNodes, forceDifficulty=ov.announce_pow) del self.announceProgress[peer] try: data['random'] = base64.b64encode(proof.waitForResult()[1]) diff --git a/onionr/etc/onionrvalues.py b/onionr/etc/onionrvalues.py index 9f636147..f55f5ac4 100755 --- a/onionr/etc/onionrvalues.py +++ b/onionr/etc/onionrvalues.py @@ -22,4 +22,5 @@ class OnionrValues: def __init__(self): self.passwordLength = 20 self.blockMetadataLengths = {'meta': 1000, 'sig': 200, 'signer': 200, 'time': 10, 'pow': 1000, 'encryptType': 4, 'expire': 14} #TODO properly refine values to minimum needed - self.default_expire = 2592000 \ No newline at end of file + self.default_expire = 2592000 + self.announce_pow = 5 \ No newline at end of file diff --git a/onionr/httpapi/miscpublicapi/__init__.py b/onionr/httpapi/miscpublicapi/__init__.py new file mode 100644 index 00000000..f03d3808 --- /dev/null +++ b/onionr/httpapi/miscpublicapi/__init__.py @@ -0,0 +1,6 @@ +from . import announce, upload, getblocks + +announce = announce.handle_announce # endpoint handler for accepting peer announcements +upload = upload.accept_upload # endpoint handler for accepting public uploads +public_block_list = getblocks.get_public_block_list # endpoint handler for getting block lists +public_get_block_data = getblocks.get_block_data # endpoint handler for responding to peers requests for block data \ No newline at end of file diff --git a/onionr/httpapi/miscpublicapi/announce.py b/onionr/httpapi/miscpublicapi/announce.py new file mode 100644 index 00000000..a6d9513f --- /dev/null +++ b/onionr/httpapi/miscpublicapi/announce.py @@ -0,0 +1,63 @@ +''' + Onionr - P2P Microblogging Platform & Social network + + Handle announcements to the public API server +''' +''' + 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 base64 +from flask import Response +import logger +from etc import onionrvalues +def handle_announce(clientAPI, request): + ''' + accept announcement posts, validating POW + clientAPI should be an instance of the clientAPI server running, request is a instance of a flask request + ''' + resp = 'failure' + powHash = '' + randomData = '' + newNode = '' + ourAdder = clientAPI._core.hsAddress.encode() + try: + newNode = request.form['node'].encode() + except KeyError: + logger.warn('No node specified for upload') + pass + else: + try: + randomData = request.form['random'] + randomData = base64.b64decode(randomData) + except KeyError: + logger.warn('No random data specified for upload') + else: + nodes = newNode + clientAPI._core.hsAddress.encode() + nodes = clientAPI._core._crypto.blake2bHash(nodes) + powHash = clientAPI._core._crypto.blake2bHash(randomData + nodes) + try: + powHash = powHash.decode() + except AttributeError: + pass + if powHash.startswith('0' * onionrvalues.OnionrValues().announce_pow): + newNode = clientAPI._core._utils.bytesToStr(newNode) + if clientAPI._core._utils.validateID(newNode) and not newNode in clientAPI._core.onionrInst.communicatorInst.newPeers: + clientAPI._core.onionrInst.communicatorInst.newPeers.append(newNode) + resp = 'Success' + else: + logger.warn(newNode.decode() + ' failed to meet POW: ' + powHash) + resp = Response(resp) + if resp == 'failure': + return resp, 406 + return resp \ No newline at end of file diff --git a/onionr/httpapi/miscpublicapi/getblocks.py b/onionr/httpapi/miscpublicapi/getblocks.py new file mode 100644 index 00000000..1ab8f949 --- /dev/null +++ b/onionr/httpapi/miscpublicapi/getblocks.py @@ -0,0 +1,50 @@ +''' + Onionr - P2P Microblogging Platform & Social network + + Public endpoints to get block data and lists +''' +''' + 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 . +''' +from flask import Response, abort +import config +def get_public_block_list(clientAPI, publicAPI, request): + # Provide a list of our blocks, with a date offset + dateAdjust = request.args.get('date') + bList = clientAPI._core.getBlockList(dateRec=dateAdjust) + if config.get('general.hide_created_blocks', True): + for b in publicAPI.hideBlocks: + if b in bList: + # Don't share blocks we created if they haven't been *uploaded* yet, makes it harder to find who created a block + bList.remove(b) + return Response('\n'.join(bList)) + +def get_block_data(clientAPI, publicAPI, data): + '''data is the block hash in hex''' + resp = '' + if clientAPI._utils.validateHash(data): + if not config.get('general.hide_created_blocks', True) or data not in publicAPI.hideBlocks: + if data in clientAPI._core.getBlockList(): + block = clientAPI.getBlockData(data, raw=True) + try: + block = block.encode() # Encode in case data is binary + except AttributeError: + abort(404) + block = clientAPI._core._utils.strToBytes(block) + resp = block + if len(resp) == 0: + abort(404) + resp = "" + # Has to be octet stream, otherwise binary data fails hash check + return Response(resp, mimetype='application/octet-stream') \ No newline at end of file diff --git a/onionr/httpapi/miscpublicapi/upload.py b/onionr/httpapi/miscpublicapi/upload.py new file mode 100644 index 00000000..0561ed08 --- /dev/null +++ b/onionr/httpapi/miscpublicapi/upload.py @@ -0,0 +1,43 @@ +''' + Onionr - P2P Microblogging Platform & Social network + + Accept block uploads to the public API server +''' +''' + 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 +from flask import Response, abort +import blockimporter, onionrexceptions, logger +def accept_upload(clientAPI, request): + resp = 'failure' + try: + data = request.form['block'] + except KeyError: + logger.warn('No block specified for upload') + pass + else: + if sys.getsizeof(data) < 100000000: + try: + if blockimporter.importBlockFromData(data, clientAPI._core): + resp = 'success' + else: + logger.warn('Error encountered importing uploaded block') + except onionrexceptions.BlacklistedBlock: + logger.debug('uploaded block is blacklisted') + pass + if resp == 'failure': + abort(400) + resp = Response(resp) + return resp \ No newline at end of file diff --git a/onionr/onionr.py b/onionr/onionr.py index 532c87d5..1c19c6d5 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -401,6 +401,8 @@ class Onionr: ''' Starts the Onionr daemon ''' + if config.get('general.dev_mode', False): + override = True commands.daemonlaunch.start(self, input, override) def setClientAPIInst(self, inst): diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index 32c4c497..7b1aa1e5 100644 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -29,12 +29,12 @@

Mail - Friend Manager - Boards


- Edit Configuration -
-

Warning: Some values can be dangerous to change. Use caution.

-
- - + Edit Configuration +
+

Warning: Some values can be dangerous to change. Use caution.

+
+ +

Stats

From 2879595c66a1ff8fc6f1ae96c7a1d289acf379ec Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 7 May 2019 22:28:06 -0500 Subject: [PATCH 052/173] refactored communicator a bit --- onionr/communicatorutils/__init__.py | 0 onionr/communicatorutils/downloadblocks.py | 95 ++++++++++++++++++++++ onionr/communicatorutils/lookupblocks.py | 60 ++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 onionr/communicatorutils/__init__.py create mode 100644 onionr/communicatorutils/downloadblocks.py create mode 100644 onionr/communicatorutils/lookupblocks.py diff --git a/onionr/communicatorutils/__init__.py b/onionr/communicatorutils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/onionr/communicatorutils/downloadblocks.py b/onionr/communicatorutils/downloadblocks.py new file mode 100644 index 00000000..f8ab7d54 --- /dev/null +++ b/onionr/communicatorutils/downloadblocks.py @@ -0,0 +1,95 @@ +import communicator, onionrexceptions +import logger + +def download_blocks_from_communicator(comm_inst): + assert isinstance(comm_inst, communicator.OnionrCommunicatorDaemon) + for blockHash in list(comm_inst.blockQueue): + triedQueuePeers = [] # List of peers we've tried for a block + try: + blockPeers = list(comm_inst.blockQueue[blockHash]) + except KeyError: + blockPeers = [] + removeFromQueue = True + if comm_inst.shutdown or not comm_inst.isOnline: + # Exit loop if shutting down or offline + break + # Do not download blocks being downloaded or that are already saved (edge cases) + if blockHash in comm_inst.currentDownloading: + #logger.debug('Already downloading block %s...' % blockHash) + continue + if blockHash in comm_inst._core.getBlockList(): + #logger.debug('Block %s is already saved.' % (blockHash,)) + try: + del comm_inst.blockQueue[blockHash] + except KeyError: + pass + continue + if comm_inst._core._blacklist.inBlacklist(blockHash): + continue + if comm_inst._core._utils.storageCounter.isFull(): + break + comm_inst.currentDownloading.append(blockHash) # So we can avoid concurrent downloading in other threads of same block + if len(blockPeers) == 0: + peerUsed = comm_inst.pickOnlinePeer() + else: + blockPeers = comm_inst._core._crypto.randomShuffle(blockPeers) + peerUsed = blockPeers.pop(0) + + if not comm_inst.shutdown and peerUsed.strip() != '': + logger.info("Attempting to download %s from %s..." % (blockHash[:12], peerUsed)) + content = comm_inst.peerAction(peerUsed, 'getdata/' + blockHash) # block content from random peer (includes metadata) + if content != False and len(content) > 0: + try: + content = content.encode() + except AttributeError: + pass + + realHash = comm_inst._core._crypto.sha3Hash(content) + try: + realHash = realHash.decode() # bytes on some versions for some reason + except AttributeError: + pass + if realHash == blockHash: + content = content.decode() # decode here because sha3Hash needs bytes above + metas = comm_inst._core._utils.getBlockMetadataFromData(content) # returns tuple(metadata, meta), meta is also in metadata + metadata = metas[0] + if comm_inst._core._utils.validateMetadata(metadata, metas[2]): # check if metadata is valid, and verify nonce + if comm_inst._core._crypto.verifyPow(content): # check if POW is enough/correct + logger.info('Attempting to save block %s...' % blockHash[:12]) + try: + comm_inst._core.setData(content) + except onionrexceptions.DiskAllocationReached: + logger.error('Reached disk allocation allowance, cannot save block %s.' % blockHash) + removeFromQueue = False + else: + comm_inst._core.addToBlockDB(blockHash, dataSaved=True) + comm_inst._core._utils.processBlockMetadata(blockHash) # caches block metadata values to block database + else: + logger.warn('POW failed for block %s.' % blockHash) + else: + if comm_inst._core._blacklist.inBlacklist(realHash): + logger.warn('Block %s is blacklisted.' % (realHash,)) + else: + logger.warn('Metadata for block %s is invalid.' % blockHash) + comm_inst._core._blacklist.addToDB(blockHash) + else: + # if block didn't meet expected hash + tempHash = comm_inst._core._crypto.sha3Hash(content) # lazy hack, TODO use var + try: + tempHash = tempHash.decode() + except AttributeError: + pass + # Punish peer for sharing invalid block (not always malicious, but is bad regardless) + onionrpeers.PeerProfiles(peerUsed, comm_inst._core).addScore(-50) + if tempHash != 'ed55e34cb828232d6c14da0479709bfa10a0923dca2b380496e6b2ed4f7a0253': + # Dumb hack for 404 response from peer. Don't log it if 404 since its likely not malicious or a critical error. + logger.warn('Block hash validation failed for ' + blockHash + ' got ' + tempHash) + else: + removeFromQueue = False # Don't remove from queue if 404 + if removeFromQueue: + try: + del comm_inst.blockQueue[blockHash] # remove from block queue both if success or false + except KeyError: + pass + comm_inst.currentDownloading.remove(blockHash) + comm_inst.decrementThreadCount('getBlocks') \ No newline at end of file diff --git a/onionr/communicatorutils/lookupblocks.py b/onionr/communicatorutils/lookupblocks.py new file mode 100644 index 00000000..c8b17b56 --- /dev/null +++ b/onionr/communicatorutils/lookupblocks.py @@ -0,0 +1,60 @@ +import logger, onionrproofs +def lookup_blocks_from_communicator(comm_inst): + logger.info('Looking up new blocks...') + tryAmount = 2 + newBlocks = '' + existingBlocks = comm_inst._core.getBlockList() + triedPeers = [] # list of peers we've tried this time around + maxBacklog = 1560 # Max amount of *new* block hashes to have already in queue, to avoid memory exhaustion + lastLookupTime = 0 # Last time we looked up a particular peer's list + for i in range(tryAmount): + listLookupCommand = 'getblocklist' # This is defined here to reset it each time + if len(comm_inst.blockQueue) >= maxBacklog: + break + if not comm_inst.isOnline: + break + # check if disk allocation is used + if comm_inst._core._utils.storageCounter.isFull(): + logger.debug('Not looking up new blocks due to maximum amount of allowed disk space used') + break + peer = comm_inst.pickOnlinePeer() # select random online peer + # if we've already tried all the online peers this time around, stop + if peer in triedPeers: + if len(comm_inst.onlinePeers) == len(triedPeers): + break + else: + continue + triedPeers.append(peer) + + # Get the last time we looked up a peer's stamp to only fetch blocks since then. + # Saved in memory only for privacy reasons + try: + lastLookupTime = comm_inst.dbTimestamps[peer] + except KeyError: + lastLookupTime = 0 + else: + listLookupCommand += '?date=%s' % (lastLookupTime,) + try: + newBlocks = comm_inst.peerAction(peer, listLookupCommand) # get list of new block hashes + except Exception as error: + logger.warn('Could not get new blocks from %s.' % peer, error = error) + newBlocks = False + else: + comm_inst.dbTimestamps[peer] = comm_inst._core._utils.getRoundedEpoch(roundS=60) + if newBlocks != False: + # if request was a success + for i in newBlocks.split('\n'): + if comm_inst._core._utils.validateHash(i): + # if newline seperated string is valid hash + if not i in existingBlocks: + # if block does not exist on disk and is not already in block queue + if i not in comm_inst.blockQueue: + if onionrproofs.hashMeetsDifficulty(i) and not comm_inst._core._blacklist.inBlacklist(i): + if len(comm_inst.blockQueue) <= 1000000: + comm_inst.blockQueue[i] = [peer] # add blocks to download queue + else: + if peer not in comm_inst.blockQueue[i]: + if len(comm_inst.blockQueue[i]) < 10: + comm_inst.blockQueue[i].append(peer) + comm_inst.decrementThreadCount('lookupBlocks') + return \ No newline at end of file From d17970b181ca5681bb40b5dc919cfeafd0c87233 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 9 May 2019 00:27:15 -0500 Subject: [PATCH 053/173] + Have Tor reject socks requests to non-onion services as a small security measure * More refactoring --- onionr/communicator.py | 259 ++------------------ onionr/communicatorutils/connectnewpeers.py | 80 ++++++ onionr/communicatorutils/downloadblocks.py | 19 ++ onionr/communicatorutils/lookupblocks.py | 19 ++ onionr/communicatorutils/reversesync.py | 4 + onionr/communicatorutils/uploadblocks.py | 52 ++++ onionr/netcontroller.py | 3 +- onionr/onionrutils.py | 3 +- 8 files changed, 192 insertions(+), 247 deletions(-) create mode 100644 onionr/communicatorutils/connectnewpeers.py create mode 100644 onionr/communicatorutils/reversesync.py create mode 100644 onionr/communicatorutils/uploadblocks.py diff --git a/onionr/communicator.py b/onionr/communicator.py index 94f8a4fa..7fe3fcb3 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -21,12 +21,11 @@ ''' import sys, os, core, config, json, requests, time, logger, threading, base64, onionr, uuid, binascii from dependencies import secrets -from utils import networkmerger import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block -from communicatorutils import onionrdaemontools -from communicatorutils import servicecreator +from communicatorutils import onionrdaemontools, servicecreator, onionrcommunicatortimers +from communicatorutils import proxypicker, downloadblocks, lookupblocks +from communicatorutils import servicecreator, connectnewpeers, uploadblocks import onionrservices, onionr, onionrproofs -from communicatorutils import onionrcommunicatortimers, proxypicker OnionrCommunicatorTimers = onionrcommunicatortimers.OnionrCommunicatorTimers @@ -181,6 +180,7 @@ class OnionrCommunicatorDaemon: for x in newPeers: x = x.strip() if not self._core._utils.validateID(x) or x in self.newPeers or x == self._core.hsAddress: + # avoid adding if its our address invalid.append(x) for x in invalid: newPeers.remove(x) @@ -189,158 +189,19 @@ class OnionrCommunicatorDaemon: def lookupBlocks(self): '''Lookup new blocks & add them to download queue''' - logger.info('Looking up new blocks...') - tryAmount = 2 - newBlocks = '' - existingBlocks = self._core.getBlockList() - triedPeers = [] # list of peers we've tried this time around - maxBacklog = 1560 # Max amount of *new* block hashes to have already in queue, to avoid memory exhaustion - lastLookupTime = 0 # Last time we looked up a particular peer's list - for i in range(tryAmount): - listLookupCommand = 'getblocklist' # This is defined here to reset it each time - if len(self.blockQueue) >= maxBacklog: - break - if not self.isOnline: - break - # check if disk allocation is used - if self._core._utils.storageCounter.isFull(): - logger.debug('Not looking up new blocks due to maximum amount of allowed disk space used') - break - peer = self.pickOnlinePeer() # select random online peer - # if we've already tried all the online peers this time around, stop - if peer in triedPeers: - if len(self.onlinePeers) == len(triedPeers): - break - else: - continue - triedPeers.append(peer) - - # Get the last time we looked up a peer's stamp to only fetch blocks since then. - # Saved in memory only for privacy reasons - try: - lastLookupTime = self.dbTimestamps[peer] - except KeyError: - lastLookupTime = 0 - else: - listLookupCommand += '?date=%s' % (lastLookupTime,) - try: - newBlocks = self.peerAction(peer, listLookupCommand) # get list of new block hashes - except Exception as error: - logger.warn('Could not get new blocks from %s.' % peer, error = error) - newBlocks = False - else: - self.dbTimestamps[peer] = self._core._utils.getRoundedEpoch(roundS=60) - if newBlocks != False: - # if request was a success - for i in newBlocks.split('\n'): - if self._core._utils.validateHash(i): - # if newline seperated string is valid hash - if not i in existingBlocks: - # if block does not exist on disk and is not already in block queue - if i not in self.blockQueue: - if onionrproofs.hashMeetsDifficulty(i) and not self._core._blacklist.inBlacklist(i): - if len(self.blockQueue) <= 1000000: - self.blockQueue[i] = [peer] # add blocks to download queue - else: - if peer not in self.blockQueue[i]: - if len(self.blockQueue[i]) < 10: - self.blockQueue[i].append(peer) - self.decrementThreadCount('lookupBlocks') - return + lookupblocks.lookup_blocks_from_communicator(self) def getBlocks(self): '''download new blocks in queue''' - for blockHash in list(self.blockQueue): - triedQueuePeers = [] # List of peers we've tried for a block - try: - blockPeers = list(self.blockQueue[blockHash]) - except KeyError: - blockPeers = [] - removeFromQueue = True - if self.shutdown or not self.isOnline: - # Exit loop if shutting down or offline - break - # Do not download blocks being downloaded or that are already saved (edge cases) - if blockHash in self.currentDownloading: - #logger.debug('Already downloading block %s...' % blockHash) - continue - if blockHash in self._core.getBlockList(): - #logger.debug('Block %s is already saved.' % (blockHash,)) - try: - del self.blockQueue[blockHash] - except KeyError: - pass - continue - if self._core._blacklist.inBlacklist(blockHash): - continue - if self._core._utils.storageCounter.isFull(): - break - self.currentDownloading.append(blockHash) # So we can avoid concurrent downloading in other threads of same block - if len(blockPeers) == 0: - peerUsed = self.pickOnlinePeer() - else: - blockPeers = self._core._crypto.randomShuffle(blockPeers) - peerUsed = blockPeers.pop(0) + downloadblocks.download_blocks_from_communicator(self) - if not self.shutdown and peerUsed.strip() != '': - logger.info("Attempting to download %s from %s..." % (blockHash[:12], peerUsed)) - content = self.peerAction(peerUsed, 'getdata/' + blockHash) # block content from random peer (includes metadata) - if content != False and len(content) > 0: - try: - content = content.encode() - except AttributeError: - pass - - realHash = self._core._crypto.sha3Hash(content) - try: - realHash = realHash.decode() # bytes on some versions for some reason - except AttributeError: - pass - if realHash == blockHash: - content = content.decode() # decode here because sha3Hash needs bytes above - metas = self._core._utils.getBlockMetadataFromData(content) # returns tuple(metadata, meta), meta is also in metadata - metadata = metas[0] - if self._core._utils.validateMetadata(metadata, metas[2]): # check if metadata is valid, and verify nonce - if self._core._crypto.verifyPow(content): # check if POW is enough/correct - logger.info('Attempting to save block %s...' % blockHash[:12]) - try: - self._core.setData(content) - except onionrexceptions.DiskAllocationReached: - logger.error('Reached disk allocation allowance, cannot save block %s.' % blockHash) - removeFromQueue = False - else: - self._core.addToBlockDB(blockHash, dataSaved=True) - self._core._utils.processBlockMetadata(blockHash) # caches block metadata values to block database - else: - logger.warn('POW failed for block %s.' % blockHash) - else: - if self._core._blacklist.inBlacklist(realHash): - logger.warn('Block %s is blacklisted.' % (realHash,)) - else: - logger.warn('Metadata for block %s is invalid.' % blockHash) - self._core._blacklist.addToDB(blockHash) - else: - # if block didn't meet expected hash - tempHash = self._core._crypto.sha3Hash(content) # lazy hack, TODO use var - try: - tempHash = tempHash.decode() - except AttributeError: - pass - # Punish peer for sharing invalid block (not always malicious, but is bad regardless) - onionrpeers.PeerProfiles(peerUsed, self._core).addScore(-50) - if tempHash != 'ed55e34cb828232d6c14da0479709bfa10a0923dca2b380496e6b2ed4f7a0253': - # Dumb hack for 404 response from peer. Don't log it if 404 since its likely not malicious or a critical error. - logger.warn('Block hash validation failed for ' + blockHash + ' got ' + tempHash) - else: - removeFromQueue = False # Don't remove from queue if 404 - if removeFromQueue: - try: - del self.blockQueue[blockHash] # remove from block queue both if success or false - except KeyError: - pass - self.currentDownloading.remove(blockHash) - self.decrementThreadCount('getBlocks') - return + def decrementThreadCount(self, threadName): + '''Decrement amount of a thread name if more than zero, called when a function meant to be run in a thread ends''' + try: + if self.threadCounts[threadName] > 0: + self.threadCounts[threadName] -= 1 + except KeyError: + pass def pickOnlinePeer(self): '''randomly picks peer from pool without bias (using secrets module)''' @@ -358,14 +219,6 @@ class OnionrCommunicatorDaemon: break return retData - def decrementThreadCount(self, threadName): - '''Decrement amount of a thread name if more than zero, called when a function meant to be run in a thread ends''' - try: - if self.threadCounts[threadName] > 0: - self.threadCounts[threadName] -= 1 - except KeyError: - pass - def clearOfflinePeer(self): '''Removes the longest offline peer to retry later''' try: @@ -411,62 +264,7 @@ class OnionrCommunicatorDaemon: def connectNewPeer(self, peer='', useBootstrap=False): '''Adds a new random online peer to self.onlinePeers''' - retData = False - tried = self.offlinePeers - if peer != '': - if self._core._utils.validateID(peer): - peerList = [peer] - else: - raise onionrexceptions.InvalidAddress('Will not attempt connection test to invalid address') - else: - peerList = self._core.listAdders() - - mainPeerList = self._core.listAdders() - peerList = onionrpeers.getScoreSortedPeerList(self._core) - - if len(peerList) < 8 or secrets.randbelow(4) == 3: - tryingNew = [] - for x in self.newPeers: - if x not in peerList: - peerList.append(x) - tryingNew.append(x) - for i in tryingNew: - self.newPeers.remove(i) - - if len(peerList) == 0 or useBootstrap: - # Avoid duplicating bootstrap addresses in peerList - self.addBootstrapListToPeerList(peerList) - - for address in peerList: - if not config.get('tor.v3onions') and len(address) == 62: - continue - if address == self._core.hsAddress: - continue - if len(address) == 0 or address in tried or address in self.onlinePeers or address in self.cooldownPeer: - continue - if self.shutdown: - return - if self.peerAction(address, 'ping') == 'pong!': - time.sleep(0.1) - if address not in mainPeerList: - networkmerger.mergeAdders(address, self._core) - if address not in self.onlinePeers: - logger.info('Connected to ' + address) - self.onlinePeers.append(address) - self.connectTimes[address] = self._core._utils.getEpoch() - retData = address - - # add peer to profile list if they're not in it - for profile in self.peerProfiles: - if profile.address == address: - break - else: - self.peerProfiles.append(onionrpeers.PeerProfiles(address, self._core)) - break - else: - tried.append(address) - logger.debug('Failed to connect to ' + address) - return retData + connectnewpeers.connect_new_peer_to_communicator(self, peer, useBootstrap) def removeOnlinePeer(self, peer): '''Remove an online peer''' @@ -584,34 +382,7 @@ class OnionrCommunicatorDaemon: def uploadBlock(self): '''Upload our block to a few peers''' - # when inserting a block, we try to upload it to a few peers to add some deniability - triedPeers = [] - finishedUploads = [] - self.blocksToUpload = self._core._crypto.randomShuffle(self.blocksToUpload) - if len(self.blocksToUpload) != 0: - for bl in self.blocksToUpload: - if not self._core._utils.validateHash(bl): - logger.warn('Requested to upload invalid block') - self.decrementThreadCount('uploadBlock') - return - for i in range(min(len(self.onlinePeers), 6)): - peer = self.pickOnlinePeer() - if peer in triedPeers: - continue - triedPeers.append(peer) - url = 'http://' + peer + '/upload' - data = {'block': block.Block(bl).getRaw()} - proxyType = proxypicker.pick_proxy(peer) - logger.info("Uploading block to " + peer) - if not self._core._utils.doPostRequest(url, data=data, proxyType=proxyType) == False: - self._core._utils.localCommand('waitforshare/' + bl, post=True) - finishedUploads.append(bl) - for x in finishedUploads: - try: - self.blocksToUpload.remove(x) - except ValueError: - pass - self.decrementThreadCount('uploadBlock') + uploadblocks.upload_blocks_from_communicator(self) def announce(self, peer): '''Announce to peers our address''' diff --git a/onionr/communicatorutils/connectnewpeers.py b/onionr/communicatorutils/connectnewpeers.py new file mode 100644 index 00000000..43e91c9a --- /dev/null +++ b/onionr/communicatorutils/connectnewpeers.py @@ -0,0 +1,80 @@ +''' + Onionr - P2P Microblogging Platform & Social network + + Connect a new peer to our communicator instance. Does so randomly if no peer is specified +''' +''' + 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 time +import onionrexceptions, logger, onionrpeers +from utils import networkmerger +def connect_new_peer_to_communicator(comm_inst, peer='', useBootstrap=False): + config = comm_inst._core.config + retData = False + tried = comm_inst.offlinePeers + if peer != '': + if comm_inst._core._utils.validateID(peer): + peerList = [peer] + else: + raise onionrexceptions.InvalidAddress('Will not attempt connection test to invalid address') + else: + peerList = comm_inst._core.listAdders() + + mainPeerList = comm_inst._core.listAdders() + peerList = onionrpeers.getScoreSortedPeerList(comm_inst._core) + + if len(peerList) < 8 or secrets.randbelow(4) == 3: + tryingNew = [] + for x in comm_inst.newPeers: + if x not in peerList: + peerList.append(x) + tryingNew.append(x) + for i in tryingNew: + comm_inst.newPeers.remove(i) + + if len(peerList) == 0 or useBootstrap: + # Avoid duplicating bootstrap addresses in peerList + comm_inst.addBootstrapListToPeerList(peerList) + + for address in peerList: + if not config.get('tor.v3onions') and len(address) == 62: + continue + if address == comm_inst._core.hsAddress: + continue + if len(address) == 0 or address in tried or address in comm_inst.onlinePeers or address in comm_inst.cooldownPeer: + continue + if comm_inst.shutdown: + return + if comm_inst.peerAction(address, 'ping') == 'pong!': + time.sleep(0.1) + if address not in mainPeerList: + networkmerger.mergeAdders(address, comm_inst._core) + if address not in comm_inst.onlinePeers: + logger.info('Connected to ' + address) + comm_inst.onlinePeers.append(address) + comm_inst.connectTimes[address] = comm_inst._core._utils.getEpoch() + retData = address + + # add peer to profile list if they're not in it + for profile in comm_inst.peerProfiles: + if profile.address == address: + break + else: + comm_inst.peerProfiles.append(onionrpeers.PeerProfiles(address, comm_inst._core)) + break + else: + tried.append(address) + logger.debug('Failed to connect to ' + address) + return retData \ No newline at end of file diff --git a/onionr/communicatorutils/downloadblocks.py b/onionr/communicatorutils/downloadblocks.py index f8ab7d54..d6df074c 100644 --- a/onionr/communicatorutils/downloadblocks.py +++ b/onionr/communicatorutils/downloadblocks.py @@ -1,3 +1,22 @@ +''' + Onionr - P2P Microblogging Platform & Social network + + Download blocks using the communicator instance +''' +''' + 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 communicator, onionrexceptions import logger diff --git a/onionr/communicatorutils/lookupblocks.py b/onionr/communicatorutils/lookupblocks.py index c8b17b56..e3e7ab2e 100644 --- a/onionr/communicatorutils/lookupblocks.py +++ b/onionr/communicatorutils/lookupblocks.py @@ -1,3 +1,22 @@ +''' + Onionr - P2P Microblogging Platform & Social network + + Lookup new blocks with the communicator using a random connected peer +''' +''' + 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 logger, onionrproofs def lookup_blocks_from_communicator(comm_inst): logger.info('Looking up new blocks...') diff --git a/onionr/communicatorutils/reversesync.py b/onionr/communicatorutils/reversesync.py new file mode 100644 index 00000000..55355a88 --- /dev/null +++ b/onionr/communicatorutils/reversesync.py @@ -0,0 +1,4 @@ +class ReverseSync: + def __init__(self, communicator_inst): + return + \ No newline at end of file diff --git a/onionr/communicatorutils/uploadblocks.py b/onionr/communicatorutils/uploadblocks.py new file mode 100644 index 00000000..fe6392b4 --- /dev/null +++ b/onionr/communicatorutils/uploadblocks.py @@ -0,0 +1,52 @@ +''' + Onionr - P2P Microblogging Platform & Social network + + Upload blocks in the upload queue to peers from the communicator +''' +''' + 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 logger +from communicatorutils import proxypicker +import onionrblockapi as block + +def upload_blocks_from_communicator(comm_inst): + # when inserting a block, we try to upload it to a few peers to add some deniability + triedPeers = [] + finishedUploads = [] + comm_inst.blocksToUpload = comm_inst._core._crypto.randomShuffle(comm_inst.blocksToUpload) + if len(comm_inst.blocksToUpload) != 0: + for bl in comm_inst.blocksToUpload: + if not comm_inst._core._utils.validateHash(bl): + logger.warn('Requested to upload invalid block') + comm_inst.decrementThreadCount('uploadBlock') + return + for i in range(min(len(comm_inst.onlinePeers), 6)): + peer = comm_inst.pickOnlinePeer() + if peer in triedPeers: + continue + triedPeers.append(peer) + url = 'http://' + peer + '/upload' + data = {'block': block.Block(bl).getRaw()} + proxyType = proxypicker.pick_proxy(peer) + logger.info("Uploading block to " + peer) + if not comm_inst._core._utils.doPostRequest(url, data=data, proxyType=proxyType) == False: + comm_inst._core._utils.localCommand('waitforshare/' + bl, post=True) + finishedUploads.append(bl) + for x in finishedUploads: + try: + comm_inst.blocksToUpload.remove(x) + except ValueError: + pass + comm_inst.decrementThreadCount('uploadBlock') \ No newline at end of file diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py index 49bf46ac..4d582be2 100755 --- a/onionr/netcontroller.py +++ b/onionr/netcontroller.py @@ -96,7 +96,7 @@ class NetController: if 'warn' not in password: break - torrcData = '''SocksPort ''' + str(self.socksPort) + ''' + torrcData = '''SocksPort ''' + str(self.socksPort) + ''' OnionTrafficOnly DataDirectory ''' + self.dataDir + '''tordata/ CookieAuthentication 1 ControlPort ''' + str(controlPort) + ''' @@ -110,7 +110,6 @@ HiddenServicePort 80 ''' + self.apiServerIP + ''':''' + str(self.hsPort) torrc = open(self.torConfigLocation, 'w') torrc.write(torrcData) torrc.close() - return def startTor(self): diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index e3b9e49e..9ab1dfa3 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -193,7 +193,8 @@ class OnionrUtils: ''' Remove ANSI escape codes from a string with regex - taken or adapted from: https://stackoverflow.com/a/38662876 + taken or adapted from: https://stackoverflow.com/a/38662876 by user https://stackoverflow.com/users/802365/%c3%89douard-lopez + cc-by-sa-3 license https://creativecommons.org/licenses/by-sa/3.0/ ''' ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]') return ansi_escape.sub('', line) From d4191b2cb5fd49618bbf6ed086343b343c1a5942 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 11 May 2019 13:32:56 -0500 Subject: [PATCH 054/173] communicator refactoring and better documentation --- README.md | 2 +- onionr/communicator.py | 97 +++++++------------ .../communicatorutils/daemonqueuehandler.py | 55 +++++++++++ onionr/communicatorutils/lookupadders.py | 48 +++++++++ onionr/communicatorutils/onionrdaemontools.py | 3 +- onionr/netcontroller.py | 3 +- 6 files changed, 142 insertions(+), 66 deletions(-) create mode 100644 onionr/communicatorutils/daemonqueuehandler.py create mode 100644 onionr/communicatorutils/lookupadders.py diff --git a/README.md b/README.md index 2fb180e0..cc666543 100755 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Onionr is a decentralized, peer-to-peer communication and storage network, desig Onionr stores data in independent packages referred to as 'blocks'. The blocks are synced to all other nodes in the network. Blocks and user IDs cannot be easily proven to have been created by a particular user. Even if there is enough evidence to believe that a specific user created a block, nodes still operate behind Tor or I2P and as such cannot be trivially unmasked. -Users are identified by ed25519 public keys, which can be used to sign blocks or send encrypted data. +Users are identified by ed25519/curve25519 public keys, which can be used to sign blocks or send encrypted data. Onionr can be used for mail, as a social network, instant messenger, file sharing software, or for encrypted group discussion. diff --git a/onionr/communicator.py b/onionr/communicator.py index 7fe3fcb3..b8b9e3f1 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -23,8 +23,9 @@ import sys, os, core, config, json, requests, time, logger, threading, base64, o from dependencies import secrets import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block from communicatorutils import onionrdaemontools, servicecreator, onionrcommunicatortimers -from communicatorutils import proxypicker, downloadblocks, lookupblocks +from communicatorutils import proxypicker, downloadblocks, lookupblocks, lookupadders from communicatorutils import servicecreator, connectnewpeers, uploadblocks +from communicatorutils import daemonqueuehandler import onionrservices, onionr, onionrproofs OnionrCommunicatorTimers = onionrcommunicatortimers.OnionrCommunicatorTimers @@ -98,15 +99,33 @@ class OnionrCommunicatorDaemon: # requiresPeer True means the timer function won't fire if we have no connected peers peerPoolTimer = OnionrCommunicatorTimers(self, self.getOnlinePeers, 60, maxThreads=1) OnionrCommunicatorTimers(self, self.runCheck, 2, maxThreads=1) + + # Timers to periodically lookup new blocks and download them OnionrCommunicatorTimers(self, self.lookupBlocks, self._core.config.get('timers.lookupBlocks'), requiresPeer=True, maxThreads=1) OnionrCommunicatorTimers(self, self.getBlocks, self._core.config.get('timers.getBlocks'), requiresPeer=True, maxThreads=2) + + # Timer to reset the longest offline peer so contact can be attempted again OnionrCommunicatorTimers(self, self.clearOfflinePeer, 58) + + # Timer to cleanup old blocks blockCleanupTimer = OnionrCommunicatorTimers(self, self.daemonTools.cleanOldBlocks, 65) + + # Timer to discover new peers OnionrCommunicatorTimers(self, self.lookupAdders, 60, requiresPeer=True) + + # Timer for adjusting which peers we actively communicate to at any given time, to avoid over-using peers OnionrCommunicatorTimers(self, self.daemonTools.cooldownPeer, 30, requiresPeer=True) + + # Timer to read the upload queue and upload the entries to peers OnionrCommunicatorTimers(self, self.uploadBlock, 10, requiresPeer=True, maxThreads=1) + + # Timer to process the daemon command queue OnionrCommunicatorTimers(self, self.daemonCommands, 6, maxThreads=1) + + # Timer that kills Onionr if the API server crashes OnionrCommunicatorTimers(self, self.detectAPICrash, 30, maxThreads=1) + + # Setup direct connections if config.get('general.socket_servers', False): self.services = onionrservices.OnionrServices(self._core) self.active_services = [] @@ -114,23 +133,32 @@ class OnionrCommunicatorDaemon: OnionrCommunicatorTimers(self, servicecreator.service_creator, 5, maxThreads=50, myArgs=(self,)) else: self.services = None + + # This timer creates deniable blocks, in an attempt to further obfuscate block insertion metadata deniableBlockTimer = OnionrCommunicatorTimers(self, self.daemonTools.insertDeniableBlock, 180, requiresPeer=True, maxThreads=1) + # Timer to check for connectivity, through Tor to various high-profile onion services netCheckTimer = OnionrCommunicatorTimers(self, self.daemonTools.netCheck, 600) - if config.get('general.security_level') == 0: + + # Announce the public API server transport address to other nodes if security level allows + if config.get('general.security_level', 1) == 0: + # Default to high security level incase config breaks announceTimer = OnionrCommunicatorTimers(self, self.daemonTools.announceNode, 3600, requiresPeer=True, maxThreads=1) announceTimer.count = (announceTimer.frequency - 120) else: logger.debug('Will not announce node.') + + # Timer to delete malfunctioning or long-dead peers cleanupTimer = OnionrCommunicatorTimers(self, self.peerCleanup, 300, requiresPeer=True) + + # Timer to cleanup dead ephemeral forward secrecy keys forwardSecrecyTimer = OnionrCommunicatorTimers(self, self.daemonTools.cleanKeys, 15, maxThreads=1) - # set loop to execute instantly to load up peer pool (replaced old pool init wait) + # Adjust initial timer triggers peerPoolTimer.count = (peerPoolTimer.frequency - 1) cleanupTimer.count = (cleanupTimer.frequency - 60) deniableBlockTimer.count = (deniableBlockTimer.frequency - 175) blockCleanupTimer.count = (blockCleanupTimer.frequency - 5) - #forwardSecrecyTimer.count = (forwardSecrecyTimer.frequency - 990) # Main daemon loop, mainly for calling timers, don't do any complex operations here to avoid locking try: @@ -160,32 +188,7 @@ class OnionrCommunicatorDaemon: def lookupAdders(self): '''Lookup new peer addresses''' - logger.info('Looking up new addresses...') - tryAmount = 1 - newPeers = [] - for i in range(tryAmount): - # Download new peer address list from random online peers - if len(newPeers) > 10000: - # Dont get new peers if we have too many queued up - break - peer = self.pickOnlinePeer() - newAdders = self.peerAction(peer, action='pex') - try: - newPeers = newAdders.split(',') - except AttributeError: - pass - else: - # Validate new peers are good format and not already in queue - invalid = [] - for x in newPeers: - x = x.strip() - if not self._core._utils.validateID(x) or x in self.newPeers or x == self._core.hsAddress: - # avoid adding if its our address - invalid.append(x) - for x in invalid: - newPeers.remove(x) - self.newPeers.extend(newPeers) - self.decrementThreadCount('lookupAdders') + lookupadders.lookup_new_peer_transports_with_communicator(self) def lookupBlocks(self): '''Lookup new blocks & add them to download queue''' @@ -346,39 +349,7 @@ class OnionrCommunicatorDaemon: ''' Process daemon commands from daemonQueue ''' - cmd = self._core.daemonQueue() - response = '' - if cmd is not False: - events.event('daemon_command', onionr = None, data = {'cmd' : cmd}) - if cmd[0] == 'shutdown': - self.shutdown = True - elif cmd[0] == 'announceNode': - if len(self.onlinePeers) > 0: - self.announce(cmd[1]) - else: - logger.debug("No nodes connected. Will not introduce node.") - elif cmd[0] == 'runCheck': # deprecated - logger.debug('Status check; looks good.') - open(self._core.dataDir + '.runcheck', 'w+').close() - elif cmd[0] == 'connectedPeers': - response = '\n'.join(list(self.onlinePeers)).strip() - if response == '': - response = 'none' - elif cmd[0] == 'localCommand': - response = self._core._utils.localCommand(cmd[1]) - elif cmd[0] == 'pex': - for i in self.timers: - if i.timerFunction.__name__ == 'lookupAdders': - i.count = (i.frequency - 1) - elif cmd[0] == 'uploadBlock': - self.blocksToUpload.append(cmd[1]) - - if cmd[0] not in ('', None): - if response != '': - self._core._utils.localCommand('queueResponseAdd/' + cmd[4], post=True, postData={'data': response}) - response = '' - - self.decrementThreadCount('daemonCommands') + daemonqueuehandler.handle_daemon_commands(self) def uploadBlock(self): '''Upload our block to a few peers''' diff --git a/onionr/communicatorutils/daemonqueuehandler.py b/onionr/communicatorutils/daemonqueuehandler.py new file mode 100644 index 00000000..7b602f02 --- /dev/null +++ b/onionr/communicatorutils/daemonqueuehandler.py @@ -0,0 +1,55 @@ +''' + Onionr - P2P Microblogging Platform & Social network + + Handle daemon queue commands in the communicator +''' +''' + 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 logger +import onionrevents as events +def handle_daemon_commands(comm_inst): + cmd = comm_inst._core.daemonQueue() + response = '' + if cmd is not False: + events.event('daemon_command', onionr = None, data = {'cmd' : cmd}) + if cmd[0] == 'shutdown': + comm_inst.shutdown = True + elif cmd[0] == 'announceNode': + if len(comm_inst.onlinePeers) > 0: + comm_inst.announce(cmd[1]) + else: + logger.debug("No nodes connected. Will not introduce node.") + elif cmd[0] == 'runCheck': # deprecated + logger.debug('Status check; looks good.') + open(comm_inst._core.dataDir + '.runcheck', 'w+').close() + elif cmd[0] == 'connectedPeers': + response = '\n'.join(list(comm_inst.onlinePeers)).strip() + if response == '': + response = 'none' + elif cmd[0] == 'localCommand': + response = comm_inst._core._utils.localCommand(cmd[1]) + elif cmd[0] == 'pex': + for i in comm_inst.timers: + if i.timerFunction.__name__ == 'lookupAdders': + i.count = (i.frequency - 1) + elif cmd[0] == 'uploadBlock': + comm_inst.blocksToUpload.append(cmd[1]) + + if cmd[0] not in ('', None): + if response != '': + comm_inst._core._utils.localCommand('queueResponseAdd/' + cmd[4], post=True, postData={'data': response}) + response = '' + + comm_inst.decrementThreadCount('daemonCommands') \ No newline at end of file diff --git a/onionr/communicatorutils/lookupadders.py b/onionr/communicatorutils/lookupadders.py new file mode 100644 index 00000000..14a2d6e0 --- /dev/null +++ b/onionr/communicatorutils/lookupadders.py @@ -0,0 +1,48 @@ +''' + Onionr - P2P Microblogging Platform & Social network + + Lookup new peer transport addresses using the communicator +''' +''' + 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 logger + +def lookup_new_peer_transports_with_communicator(comm_inst): + logger.info('Looking up new addresses...') + tryAmount = 1 + newPeers = [] + for i in range(tryAmount): + # Download new peer address list from random online peers + if len(newPeers) > 10000: + # Dont get new peers if we have too many queued up + break + peer = comm_inst.pickOnlinePeer() + newAdders = comm_inst.peerAction(peer, action='pex') + try: + newPeers = newAdders.split(',') + except AttributeError: + pass + else: + # Validate new peers are good format and not already in queue + invalid = [] + for x in newPeers: + x = x.strip() + if not comm_inst._core._utils.validateID(x) or x in comm_inst.newPeers or x == comm_inst._core.hsAddress: + # avoid adding if its our address + invalid.append(x) + for x in invalid: + newPeers.remove(x) + comm_inst.newPeers.extend(newPeers) + comm_inst.decrementThreadCount('lookupAdders') \ No newline at end of file diff --git a/onionr/communicatorutils/onionrdaemontools.py b/onionr/communicatorutils/onionrdaemontools.py index 54ee2703..6148041f 100755 --- a/onionr/communicatorutils/onionrdaemontools.py +++ b/onionr/communicatorutils/onionrdaemontools.py @@ -210,8 +210,9 @@ class DaemonTools: fakePeer = '' chance = 10 if secrets.randbelow(chance) == (chance - 1): + # This assumes on the libsodium primitives to have key-privacy fakePeer = 'OVPCZLOXD6DC5JHX4EQ3PSOGAZ3T24F75HQLIUZSDSMYPEOXCPFA====' - data = secrets.token_hex(secrets.randbelow(500) + 1) + data = secrets.token_hex(secrets.randbelow(1024) + 1) self.daemon._core.insertBlock(data, header='pm', encryptType='asym', asymPeer=fakePeer, meta={'subject': 'foo'}) self.daemon.decrementThreadCount('insertDeniableBlock') return \ No newline at end of file diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py index 4d582be2..15986cf1 100755 --- a/onionr/netcontroller.py +++ b/onionr/netcontroller.py @@ -24,7 +24,8 @@ from onionrblockapi import Block from dependencies import secrets def getOpenPort(): - # taken from (but modified) https://stackoverflow.com/a/2838309 + # taken from (but modified) https://stackoverflow.com/a/2838309 by https://stackoverflow.com/users/133374/albert ccy-by-sa-3 https://creativecommons.org/licenses/by-sa/3.0/ + # changes from source: import moved to top of file, bind specifically to localhost s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(("127.0.0.1",0)) s.listen(1) From 4fcf3d69cdaa325060e55330a9e3feaacac4cb30 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 12 May 2019 09:21:17 -0500 Subject: [PATCH 055/173] fix config setup not returning bool for existing data directory --- .../communicatorutils/daemonqueuehandler.py | 2 +- onionr/communicatorutils/lookupadders.py | 2 +- onionr/onionr.py | 2 +- onionr/onionrservices/__init__.py | 19 +++++++++++++++++++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/onionr/communicatorutils/daemonqueuehandler.py b/onionr/communicatorutils/daemonqueuehandler.py index 7b602f02..5c906f64 100644 --- a/onionr/communicatorutils/daemonqueuehandler.py +++ b/onionr/communicatorutils/daemonqueuehandler.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - P2P Anonymous Storage Network Handle daemon queue commands in the communicator ''' diff --git a/onionr/communicatorutils/lookupadders.py b/onionr/communicatorutils/lookupadders.py index 14a2d6e0..6b9f474d 100644 --- a/onionr/communicatorutils/lookupadders.py +++ b/onionr/communicatorutils/lookupadders.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - P2P Anonymous Storage Network Lookup new peer transport addresses using the communicator ''' diff --git a/onionr/onionr.py b/onionr/onionr.py index 1c19c6d5..bc176c12 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -150,7 +150,7 @@ class Onionr: self.killed = True def setupConfig(dataDir, self = None): - setupconfig.setup_config(dataDir, self) + return setupconfig.setup_config(dataDir, self) def cmdHeader(self): if len(sys.argv) >= 3: diff --git a/onionr/onionrservices/__init__.py b/onionr/onionrservices/__init__.py index 21ded6ec..cae33ab2 100644 --- a/onionr/onionrservices/__init__.py +++ b/onionr/onionrservices/__init__.py @@ -1,3 +1,22 @@ +''' + Onionr - P2P Anonymous Storage Network + + Onionr services provide the server component to direct connections +''' +''' + 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 time import stem import core From b49199c7d19ec560a6f8b2ec3b478a6fae74bbc3 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 12 May 2019 09:41:36 -0500 Subject: [PATCH 056/173] makedirs, not mkdirs --- onionr/config.py | 14 +++++--------- onionr/onionr.py | 7 +++---- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/onionr/config.py b/onionr/config.py index 11319b85..67bd0bb3 100755 --- a/onionr/config.py +++ b/onionr/config.py @@ -96,15 +96,11 @@ def check(): Checks if the configuration file exists, creates it if not ''' - try: - if not os.path.exists(os.path.dirname(get_config_file())): - os.path.mkdirs(os.path.dirname(get_config_file())) - if not os.path.isfile(get_config_file()): - open(get_config_file(), 'a', encoding="utf8").close() - save() - except: - pass - #logger.debug('Failed to check configuration file.') + if not os.path.exists(os.path.dirname(get_config_file())): + os.path.makedirs(os.path.dirname(get_config_file())) + if not os.path.isfile(get_config_file()): + open(get_config_file(), 'a', encoding="utf8").close() + save() def save(): ''' diff --git a/onionr/onionr.py b/onionr/onionr.py index bc176c12..b8374905 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -77,10 +77,9 @@ class Onionr: 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/') + # If block data folder does not exist + 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()): From e8baafa3b8893838d725fab2b9d00dd821f61b9e Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 12 May 2019 09:44:02 -0500 Subject: [PATCH 057/173] makedirs, not mkdirs --- onionr/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionr/config.py b/onionr/config.py index 67bd0bb3..a546de17 100755 --- a/onionr/config.py +++ b/onionr/config.py @@ -97,7 +97,7 @@ def check(): ''' if not os.path.exists(os.path.dirname(get_config_file())): - os.path.makedirs(os.path.dirname(get_config_file())) + os.makedirs(os.path.dirname(get_config_file())) if not os.path.isfile(get_config_file()): open(get_config_file(), 'a', encoding="utf8").close() save() From 4e3ad27485f30bce67bea68360cd78d138e8a866 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 13 May 2019 00:01:37 -0500 Subject: [PATCH 058/173] newline in quoted mail --- README.md | 6 ++++-- onionr/static-data/www/mail/mail.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cc666543..f3be8f12 100755 --- a/README.md +++ b/README.md @@ -103,7 +103,9 @@ Note: probably not tax deductible beardog [ at ] mailbox.org -## Disclaimer +## Disclaimers and legal + +Onionr is published under the GNU GPL v3 license. The Tor Project and I2P developers do not own, create, or endorse this project, and are not otherwise involved. @@ -115,6 +117,6 @@ The 'open source badge' is by Maik Ellerbrock and is licensed under a Creative C The Onionr logo was created by [Anhar Ismail](https://github.com/anharismail) under the [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/). -If you modify and redistribute our code ("forking"), please use a different logo and project name to avoid confusion. Please do not use our logo in a way that makes it seem like we endorse you without permission. +If you modify and redistribute our code ("forking"), please use a different logo and project name to avoid confusion. Please do not use our logo in a way that makes it seem like we endorse you without our permission. ![Tor stinks slide image](docs/tor-stinks-02.png) diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index 8c9b13d1..f02a3110 100644 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -47,7 +47,7 @@ function openReply(bHash, quote, subject){ for (var x = 0; x < splitQuotes.length; x++){ splitQuotes[x] = '>' + splitQuotes[x] } - quote = splitQuotes.join('\n') + quote = '\n' + splitQuotes.join('\n') document.getElementById('draftText').value = quote setActiveTab('send message') } From b582377c8c8dcc66e41e449fb7899057de3be85c Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 15 May 2019 18:25:36 -0500 Subject: [PATCH 059/173] fixed config and other bugs, improved connection server --- README.md | 8 +++---- onionr/api.py | 2 +- onionr/communicator.py | 1 + onionr/communicatorutils/downloadblocks.py | 4 +++- onionr/config.py | 3 --- onionr/netcontroller.py | 2 +- onionr/onionr.py | 3 +-- onionr/onionrcommands/daemonlaunch.py | 4 ++-- onionr/onionrservices/__init__.py | 13 +++++++++-- onionr/onionrservices/connectionserver.py | 27 ++++++++++++++++------ onionr/setupconfig.py | 23 +++++++++--------- 11 files changed, 56 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index f3be8f12..5dc71daf 100755 --- a/README.md +++ b/README.md @@ -27,10 +27,12 @@ Users are identified by ed25519/curve25519 public keys, which can be used to sig Onionr can be used for mail, as a social network, instant messenger, file sharing software, or for encrypted group discussion. -The whitepaper (subject to change prior to first alpha release) is available [here](docs/whitepaper.md). +The whitepaper (subject to change prior to alpha release) is available [here](docs/whitepaper.md). ![node web illustration](docs/onionr-web.png) +![Tor stinks slide image](docs/tor-stinks-02.png) + ## Main Features * [X] 🌐 Fully p2p/decentralized, no trackers or other single points of failure @@ -117,6 +119,4 @@ The 'open source badge' is by Maik Ellerbrock and is licensed under a Creative C The Onionr logo was created by [Anhar Ismail](https://github.com/anharismail) under the [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/). -If you modify and redistribute our code ("forking"), please use a different logo and project name to avoid confusion. Please do not use our logo in a way that makes it seem like we endorse you without our permission. - -![Tor stinks slide image](docs/tor-stinks-02.png) +If you modify and redistribute our code ("forking"), please use a different logo and project name to avoid confusion. Please do not use our logo in a way that makes it seem like we endorse you without our permission. \ No newline at end of file diff --git a/onionr/api.py b/onionr/api.py index 11197a0a..f85501dd 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -81,7 +81,7 @@ class PublicAPI: def validateRequest(): '''Validate request has the correct hostname''' # If high security level, deny requests to public (HS should be disabled anyway for Tor, but might not be for I2P) - if config.get('general.security_level', default=0) > 0: + if config.get('general.security_level', default=1) > 0: abort(403) if type(self.torAdder) is None and type(self.i2pAdder) is None: # abort if our hs addresses are not known diff --git a/onionr/communicator.py b/onionr/communicator.py index b8b9e3f1..d018a53a 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -33,6 +33,7 @@ OnionrCommunicatorTimers = onionrcommunicatortimers.OnionrCommunicatorTimers config.reload() class OnionrCommunicatorDaemon: def __init__(self, onionrInst, proxyPort, developmentMode=config.get('general.dev_mode', False)): + config.reload() onionrInst.communicatorInst = self # configure logger and stuff onionr.Onionr.setupConfig('data/', self = self) diff --git a/onionr/communicatorutils/downloadblocks.py b/onionr/communicatorutils/downloadblocks.py index d6df074c..a2d4908c 100644 --- a/onionr/communicatorutils/downloadblocks.py +++ b/onionr/communicatorutils/downloadblocks.py @@ -18,11 +18,13 @@ along with this program. If not, see . ''' import communicator, onionrexceptions -import logger +import logger, onionrpeers def download_blocks_from_communicator(comm_inst): assert isinstance(comm_inst, communicator.OnionrCommunicatorDaemon) for blockHash in list(comm_inst.blockQueue): + if len(comm_inst.onlinePeers) == 0: + break triedQueuePeers = [] # List of peers we've tried for a block try: blockPeers = list(comm_inst.blockQueue[blockHash]) diff --git a/onionr/config.py b/onionr/config.py index a546de17..5576a1d3 100755 --- a/onionr/config.py +++ b/onionr/config.py @@ -98,9 +98,6 @@ def check(): if not os.path.exists(os.path.dirname(get_config_file())): os.makedirs(os.path.dirname(get_config_file())) - if not os.path.isfile(get_config_file()): - open(get_config_file(), 'a', encoding="utf8").close() - save() def save(): ''' diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py index 15986cf1..fa64c938 100755 --- a/onionr/netcontroller.py +++ b/onionr/netcontroller.py @@ -103,7 +103,7 @@ CookieAuthentication 1 ControlPort ''' + str(controlPort) + ''' HashedControlPassword ''' + str(password) + ''' ''' - if config.get('general.security_level') == 0: + if config.get('general.security_level', 1) == 0: torrcData += '''\nHiddenServiceDir ''' + self.dataDir + '''hs/ \n''' + hsVer + '''\n HiddenServicePort 80 ''' + self.apiServerIP + ''':''' + str(self.hsPort) diff --git a/onionr/onionr.py b/onionr/onionr.py index b8374905..30ec5184 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -71,7 +71,7 @@ class Onionr: logger.set_file(os.environ.get('LOG_DIR', 'data') + '/onionr.log') # Load global configuration data - data_exists = Onionr.setupConfig(self.dataDir, self = self) + data_exists = Onionr.setupConfig(self.dataDir, self) if netcontroller.torBinary() is None: logger.error('Tor is not installed') @@ -122,7 +122,6 @@ class Onionr: 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) diff --git a/onionr/onionrcommands/daemonlaunch.py b/onionr/onionrcommands/daemonlaunch.py index dac35115..9dab9f57 100644 --- a/onionr/onionrcommands/daemonlaunch.py +++ b/onionr/onionrcommands/daemonlaunch.py @@ -50,7 +50,7 @@ def daemon(o_inst): except FileNotFoundError: pass time.sleep(0.5) - onionr.Onionr.setupConfig('data/', self = o_inst) + #onionr.Onionr.setupConfig('data/', self = o_inst) if o_inst._developmentMode: logger.warn('DEVELOPMENT MODE ENABLED (NOT RECOMMENDED)', timestamp = False) @@ -59,7 +59,7 @@ def daemon(o_inst): if not net.startTor(): o_inst.onionrUtils.localCommand('shutdown') sys.exit(1) - if len(net.myID) > 0 and o_inst.onionrCore.config.get('general.security_level') == 0: + if len(net.myID) > 0 and o_inst.onionrCore.config.get('general.security_level', 1) == 0: logger.debug('Started .onion service: %s' % (logger.colors.underline + net.myID)) else: logger.debug('.onion service disabled') diff --git a/onionr/onionrservices/__init__.py b/onionr/onionrservices/__init__.py index cae33ab2..2faa4cdd 100644 --- a/onionr/onionrservices/__init__.py +++ b/onionr/onionrservices/__init__.py @@ -23,6 +23,9 @@ import core from . import connectionserver, bootstrapservice class OnionrServices: + ''' + Create a client or server for connecting to peer interfaces + ''' def __init__(self, onionr_core): assert isinstance(onionr_core, core.Core) self._core = onionr_core @@ -32,13 +35,19 @@ class OnionrServices: return def create_server(self, peer, address): + ''' + When a client wants to connect, contact their bootstrap address and tell them our + ephemeral address for our service by creating a new ConnectionServer instance + ''' assert self._core._utils.validateID(address) - BOOTSTRAP_TRIES = 10 - TRY_WAIT = 3 + BOOTSTRAP_TRIES = 10 # How many times to attempt contacting the bootstrap server + TRY_WAIT = 3 # Seconds to wait before trying bootstrap again + # HTTP is fine because .onion/i2p is encrypted/authenticated base_url = 'http://%s/' % (address,) socks = self._core.config.get('tor.socksport') for x in range(BOOTSTRAP_TRIES): if self._core._utils.doGetRequest(base_url + 'ping', port=socks, ignoreAPI=True) == 'pong!': + # if bootstrap sever is online, tell them our service address connectionserver.ConnectionServer(peer, address, core_inst=self._core) else: time.sleep(TRY_WAIT) diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index 31e429f8..0878b100 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -22,6 +22,7 @@ from gevent.pywsgi import WSGIServer, WSGIHandler from stem.control import Controller from flask import Flask import core, logger, httpapi +import onionrexceptions from netcontroller import getOpenPort import api from . import httpheaders @@ -66,11 +67,23 @@ class ConnectionServer: with Controller.from_port(port=core_inst.config.get('tor.controlPort')) as controller: # Connect to the Tor process for Onionr controller.authenticate(core_inst.config.get('tor.controlpassword')) - # Create the v3 onion service + # Create the v3 onion service for the peer to connect to response = controller.create_ephemeral_hidden_service({80: service_port}, await_publication = True, key_type='NEW', key_content = 'ED25519-V3') - self.core_inst.keyStore.put('dc-' + response.service_id, self.core_inst._utils.bytesToStr(peer)) - self.core_inst._utils.doPostRequest('http://' + address + '/bs/' + response.service_id, port=socks) - logger.info('hosting on %s with %s' % (response.service_id, peer)) - http_server.serve_forever() - self.core_inst.keyStore.delete('dc-' + response.service_id) - http_server.stop() \ No newline at end of file + + try: + for x in range(3): + attempt = self.core_inst._utils.doPostRequest('http://' + address + '/bs/' + response.service_id, port=socks) + if attempt == 'success': + break + else: + raise ConnectionError + except ConnectionError: + # Re-raise + raise ConnectionError('Could not reach %s bootstrap address %s' % (peer, address)) + else: + # If no connection error, create the service and save it to local global key store + self.core_inst.keyStore.put('dc-' + response.service_id, self.core_inst._utils.bytesToStr(peer)) + logger.info('hosting on %s with %s' % (response.service_id, peer)) + http_server.serve_forever() + http_server.stop() + self.core_inst.keyStore.delete('dc-' + response.service_id) \ No newline at end of file diff --git a/onionr/setupconfig.py b/onionr/setupconfig.py index 1229faeb..e439845a 100644 --- a/onionr/setupconfig.py +++ b/onionr/setupconfig.py @@ -3,21 +3,22 @@ import config, logger def setup_config(dataDir, o_inst = None): data_exists = os.path.exists(dataDir) - if not data_exists: os.mkdir(dataDir) + config.reload() + + + if not os.path.exists(config._configfile): + 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 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): From 1dcc82b86c38d30774bee9882d0d662fea649f53 Mon Sep 17 00:00:00 2001 From: Arinerron Date: Thu, 16 May 2019 16:34:37 -0700 Subject: [PATCH 060/173] Minor grammar fixes --- docs/whitepaper.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/whitepaper.md b/docs/whitepaper.md index ca2eb080..737cc764 100755 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -39,11 +39,11 @@ When designing Onionr we had these main goals in mind: At its core, Onionr is merely a description for storing data in self-verifying packages ("blocks"). These blocks can be encrypted to a user (or for one's self), encrypted symmetrically, or not at all. Blocks can be signed by their creator, but regardless, they are self-verifying due to being identified by a sha3-256 hash value; once a block is created, it cannot be modified. -Onionr exchanges a list of blocks between all nodes. By default, all nodes download and share all other blocks, however this is configurable. Blocks do not rely on any particular order of receipt or transport mechanism. +Onionr exchanges a list of blocks between all nodes. By default, all nodes download and share all other blocks, however, this is configurable. Blocks do not rely on any particular order of receipt or transport mechanism. ## User IDs -User IDs are simply Ed25519 public keys. They are represented in Base32 format, or encoded using the [PGP Word List](https://en.wikipedia.org/wiki/PGP_word_list). +User IDs are simply Ed25519 public keys. They are represented in Base32 format or encoded using the [PGP Word List](https://en.wikipedia.org/wiki/PGP_word_list). Public keys can be generated deterministically with a password using a key derivation function (Argon2id). This password can be shared between many users in order to share data anonymously among a group, using only 1 password. This is useful in some cases, but is risky, as if one user causes the key to be compromised and does not notify the group or revoke the key, there is no way to know. @@ -53,7 +53,7 @@ Although Onionr is transport agnostic, the only supported transports in the refe ### Node Profiling -To mitigate maliciously slow or unreliable nodes, Onionr builds a profile on nodes it connects to. Nodes are assigned a score, which raises based on the amount of successful block transfers, speed, and reliability of a node, and reduces the score based on how unreliable a node is. If a node is unreachable for over 24 hours after contact, it is forgotten. Onionr can also prioritize connection to 'friend' nodes. +To mitigate maliciously slow or unreliable nodes, Onionr builds a profile on nodes it connects to. Nodes are assigned a score, which raises based on the number of successful block transfers, speed, and reliability of a node, and reduces the score based on how unreliable a node is. If a node is unreachable for over 24 hours after contact, it is forgotten. Onionr can also prioritize connections to 'friend' nodes. ## Block Format @@ -65,7 +65,7 @@ Optionally, a random token can be inserted into the metadata for use in Proof of ### Block Encryption -For encryption, Onionr uses ephemeral Curve25519 keys for key exchange and XSalsa20-Poly1305 as a symmetric cipher, or optionally using only XSalsa20-Poly1305 with a pre-shared key. +For encryption, Onionr uses ephemeral Curve25519 keys for key exchange and XSalsa20-Poly1305 as a symmetric cipher or optionally using only XSalsa20-Poly1305 with a pre-shared key. Regardless of encryption, blocks can be signed internally using Ed25519. @@ -91,7 +91,7 @@ In addition, randomness beacons such as the one operated by [NIST](https://beaco # Direct Connections -We propose a method of using Onionr's block sync system to enable direct connections between peers by having one peer request to connect to another using the peer's public key. Since the request is within a standard block, proof of work must be used to request connection. If the requested peer is available and wishes to accept the connection, Onionr will generate a temporary .onion address for the other peer to connect to. Alternatively, a reverse connection may be formed, which is faster to establish but requires a message brokering system instead of a standard socket. +We propose a method of using Onionr's block sync system to enable direct connections between peers by having one peer request to connect to another using the peer's public key. Since the request is within a standard block, proof of work must be used to request a connection. If the requested peer is available and wishes to accept the connection, Onionr will generate a temporary .onion address for the other peer to connect to. Alternatively, a reverse connection may be formed, which is faster to establish but requires a message brokering system instead of a standard socket. The benefits of such a system are increased privacy, and the ability to anonymously communicate from multiple devices at once. In a traditional onion service, one's online status can be monitored and more easily correlated. @@ -119,7 +119,9 @@ We seek to protect the following information: * Physical location/IP address of nodes on the network * All block data from tampering -### Data we cannot or do not protect +### Unprotected Data + +Onionr does not protect the following: * Data specifically inserted as plaintext is available to the public * The public key of signed plaintext blocks @@ -133,7 +135,7 @@ We assume that Tor onion services (v3) and I2P services cannot be trivially dean Once quantum safe algorithms are more mature and have decent high level libraries, they will be deployed. -# Comparisons to other P2P software +# P2P Software Comparison Since Onionr is far from the first to implement many of these ideas (on their own), this section compares Onionr to other networks, using points we consider to be the most important. @@ -141,4 +143,4 @@ Since Onionr is far from the first to implement many of these ideas (on their ow # Conclusion -If successful, Onionr will be a complete decentralized platform for anonymous computing, complete with limited metadata exposure, both node and user anonymity, and spam prevention \ No newline at end of file +If successful, Onionr will be a complete decentralized platform for anonymous computing, complete with limited metadata exposure, both node and user anonymity, and spam prevention From f52ece371ef4cf4816ea104862679708673cb8e8 Mon Sep 17 00:00:00 2001 From: Arinerron Date: Thu, 16 May 2019 16:34:55 -0700 Subject: [PATCH 061/173] Replace "stretched" logos with "regular" ones --- docs/onionr-icon.png | Bin 87335 -> 41748 bytes docs/onionr-logo.png | Bin 193823 -> 35296 bytes 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 docs/onionr-icon.png mode change 100755 => 100644 docs/onionr-logo.png diff --git a/docs/onionr-icon.png b/docs/onionr-icon.png old mode 100755 new mode 100644 index 861af295863d90c9d29f5defc674f905a2a03762..65c279dbef70d90fce174b8bb55f2423a93a27d9 GIT binary patch literal 41748 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 Cg(v0! diff --git a/docs/onionr-logo.png b/docs/onionr-logo.png old mode 100755 new mode 100644 index 31a9524137cc4ec5d173c16c7cebd30e113eed89..493095bedc349dfbcfbc9a2e1f6e39ab57c9bf31 GIT binary patch literal 35296 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 L%HP0CYq|a(7G0h+ literal 193823 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%0DI;Ml^w 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

i45Z1XNWMx8M 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 h3>N%${ From 075467ad2038b95e1dc98190b75d675f367235bb Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 18 May 2019 13:46:48 -0500 Subject: [PATCH 062/173] * updated contributing.md with better explanations * added clandestine endpoint and web files, basic page layout * better comply with gplv3 in pgpwords module --- CONTRIBUTING.md | 8 +++--- onionr/api.py | 10 ++++++- onionr/etc/pgpwords.py | 8 ++++++ .../default-plugins/clandestine/controlapi.py | 2 +- onionr/static-data/www/clandestine/index.html | 22 ++++++++++++++++ onionr/static-data/www/clandestine/js/main.js | 26 +++++++++++++++++++ onionr/static-data/www/private/index.html | 3 ++- 7 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 onionr/static-data/www/clandestine/index.html create mode 100644 onionr/static-data/www/clandestine/js/main.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f5a31c5d..ea21aeef 100755 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ This file serves to provide guidelines on how to successfully contribute to Onio ## Code of Conduct -See our [Code of Conduct](https://github.com/beardog108/onionr/blob/master/CODE_OF_CONDUCT.md) +Contributors in project-related spaces/contexts are expected to follow the [Code of Conduct](https://github.com/beardog108/onionr/blob/master/CODE_OF_CONDUCT.md) ## Reporting Bugs @@ -23,11 +23,13 @@ Please provide the following information when reporting a bug: If a bug is a security issue, please contact us privately. -And most importantly, please be patient. Onionr is an open source project done by volunteers. +And most importantly, please be patient. Onionr is a free open source project maintained by volunteers in their free time. ## Asking Questions -If you need help with Onionr, you can contact the devs (be polite and remember this is a volunteer-driven non-profit project). +If you need help with Onionr, you can contact the devs (be polite and remember this is a volunteer-driven project). + +Do your best to use good english. ## Contributing Code diff --git a/onionr/api.py b/onionr/api.py index f85501dd..50bc83b5 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -184,7 +184,8 @@ class API: # Be extremely mindful of this. These are endpoints available without a password self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'homedata', 'board', 'profiles', 'profilesindex', - 'boardContent', 'sharedContent', 'mail', 'mailindex', 'friends', 'friendsindex') + 'boardContent', 'sharedContent', 'mail', 'mailindex', 'friends', 'friendsindex', + 'clandestine', 'clandestineIndex') self.clientToken = config.get('client.webpassword') self.timeBypassToken = base64.b16encode(os.urandom(32)).decode() @@ -239,6 +240,13 @@ class API: @app.route('/mail/', endpoint='mailindex') def loadMailIndex(): return send_from_directory('static-data/www/mail/', 'index.html') + + @app.route('/clandestine/', endpoint='clandestine') + def loadClandestine(path): + return send_from_directory('static-data/www/clandestine/', path) + @app.route('/clandestine/', endpoint='clandestineIndex') + def loadClandestineIndex(): + return send_from_directory('static-data/www/clandestine/', 'index.html') @app.route('/friends/', endpoint='friends') def loadContacts(path): diff --git a/onionr/etc/pgpwords.py b/onionr/etc/pgpwords.py index d9738f3f..8e4ddb8e 100755 --- a/onionr/etc/pgpwords.py +++ b/onionr/etc/pgpwords.py @@ -3,6 +3,14 @@ '''This file is adapted from https://github.com/thblt/pgp-words by github user 'thblt' ('Thibault Polge), GPL v3 license''' +''' +Changes made for Onionr: +Minor changes such as slight word adjustment, line breaks + +CLI commands/usage function removed +hexify function added +''' + import os, re, sys, binascii _words = [ diff --git a/onionr/static-data/default-plugins/clandestine/controlapi.py b/onionr/static-data/default-plugins/clandestine/controlapi.py index f203c0af..773e9c36 100644 --- a/onionr/static-data/default-plugins/clandestine/controlapi.py +++ b/onionr/static-data/default-plugins/clandestine/controlapi.py @@ -18,7 +18,7 @@ along with this program. If not, see . ''' import json -from flask import Response, request, redirect, Blueprint, g +from flask import Response, request, redirect, Blueprint, send_from_directory import core core_inst = core.Core() diff --git a/onionr/static-data/www/clandestine/index.html b/onionr/static-data/www/clandestine/index.html new file mode 100644 index 00000000..2badec8d --- /dev/null +++ b/onionr/static-data/www/clandestine/index.html @@ -0,0 +1,22 @@ + + + + + + Clandestine + + + + + + +
+ + Clandestine + +
    +
    + + + + \ No newline at end of file diff --git a/onionr/static-data/www/clandestine/js/main.js b/onionr/static-data/www/clandestine/js/main.js new file mode 100644 index 00000000..0e7071a0 --- /dev/null +++ b/onionr/static-data/www/clandestine/js/main.js @@ -0,0 +1,26 @@ +friendList = [] +convoListElement = document.getElementsByClassName('conversationList')[0] + +function createConvoList(){ + for (var x = 0; x < friendList.length; x++){ + var convoEntry = document.createElement('div') + convoEntry.classList.add('convoEntry') + convoEntry.setAttribute('data-pubkey', friendList[x]) + convoEntry.innerText = friendList[x] + convoListElement.append(convoEntry) + } +} + +fetch('/friends/list', { + headers: { + "token": webpass + }}) +.then((resp) => resp.json()) // Transform the data into json +.then(function(resp) { + var keys = [] + for(var k in resp) keys.push(k) + for (var i = 0; i < keys.length; i++){ + friendList.push(keys[i]) + } + createConvoList() +}) \ No newline at end of file diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index 7b1aa1e5..0d27d2a5 100644 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -26,7 +26,8 @@

    Onionr Services


    -

    Mail - Friend Manager - Boards +

    Mail - Friend Manager - Boards - + Clandestine


    Edit Configuration From 7e85b4a995e4a75b746a9c13d5d26c86af6667dc Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 18 May 2019 13:49:25 -0500 Subject: [PATCH 063/173] added current BS node --- onionr/static-data/bootstrap-nodes.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/onionr/static-data/bootstrap-nodes.txt b/onionr/static-data/bootstrap-nodes.txt index e69de29b..faec3008 100755 --- a/onionr/static-data/bootstrap-nodes.txt +++ b/onionr/static-data/bootstrap-nodes.txt @@ -0,0 +1 @@ +7abghj4dhb4p7ytet4wm4hsejuf2c5ufaznypoifkcacvlzs6yhc6gyd.onion \ No newline at end of file From 3322778ff4fa59248813a8be50b345dcbf1749fc Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 1 Jun 2019 11:54:36 -0500 Subject: [PATCH 064/173] refactored onionr commands and other small adjustments --- CODE_OF_CONDUCT.md | 6 +-- README.md | 4 +- onionr/onionr.py | 51 ++++---------------------- onionr/onionrcommands/__init__.py | 4 +- onionr/onionrcommands/banblocks.py | 39 ++++++++++++++++++++ onionr/onionrcommands/exportblocks.py | 42 +++++++++++++++++++++ onionr/onionrservices/serviceblocks.py | 0 7 files changed, 96 insertions(+), 50 deletions(-) create mode 100644 onionr/onionrcommands/banblocks.py create mode 100644 onionr/onionrcommands/exportblocks.py create mode 100644 onionr/onionrservices/serviceblocks.py diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index ccd047ea..3b624005 100755 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -40,7 +40,7 @@ Project maintainers who do not follow or enforce the Code of Conduct in good fai ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +[homepage]: https://contributor-covenant.org +[version]: https://contributor-covenant.org/version/1/4/ diff --git a/README.md b/README.md index 5dc71daf..e5f18105 100755 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

    - Anonymous P2P communication network 🕵️ + Private P2P Communication network 🕵️

    (***pre-alpha & experimental, not well tested or easy to use yet***) @@ -19,7 +19,7 @@ # About -Onionr is a decentralized, peer-to-peer communication and storage network, designed to be anonymous and resistant to (meta)data analysis and spam/disruption. +Onionr is a decentralized, peer-to-peer communication and storage network, designed to be anonymous and resistant to (meta)data analysis, spam, and corruption. Onionr stores data in independent packages referred to as 'blocks'. The blocks are synced to all other nodes in the network. Blocks and user IDs cannot be easily proven to have been created by a particular user. Even if there is enough evidence to believe that a specific user created a block, nodes still operate behind Tor or I2P and as such cannot be trivially unmasked. diff --git a/onionr/onionr.py b/onionr/onionr.py index 30ec5184..402b7bd0 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication - Onionr is the name for both the protocol and the original/reference software. + This file initializes Onionr when ran to be a daemon or with commands Run with 'help' for usage. ''' @@ -23,14 +23,14 @@ 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.stderr.write('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 +import netcontroller from netcontroller import NetController from onionrblockapi import Block import onionrproofs, onionrexceptions, communicator, setupconfig @@ -42,7 +42,7 @@ try: 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_TAGLINE = 'Private P2P Communication - 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. @@ -165,17 +165,6 @@ class Onionr: if not message is None: logger.info(logger.colors.fg.lightgreen + '-> ' + str(message) + logger.colors.reset + logger.colors.fg.lightgreen + ' <-\n', sensitive=True) - 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 deleteRunFiles(self): try: os.remove(self.onionrCore.publicApiHostFile) @@ -211,19 +200,11 @@ class Onionr: return columns ''' - THIS SECTION HANDLES THE COMMANDS + Handle command line commands ''' 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) + commands.exportblocks(self) def showDetails(self): commands.onionrstatistics.show_details(self) @@ -247,23 +228,7 @@ class Onionr: commands.pubkeymanager.friend_command(self) 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') + commands.banblocks.ban_block(self) def listConn(self): commands.onionrstatistics.show_peers(self) diff --git a/onionr/onionrcommands/__init__.py b/onionr/onionrcommands/__init__.py index bf10f4ed..e929499f 100644 --- a/onionr/onionrcommands/__init__.py +++ b/onionr/onionrcommands/__init__.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This module defines commands for CLI usage ''' @@ -20,7 +20,7 @@ import webbrowser, sys import logger -from . import pubkeymanager, onionrstatistics, daemonlaunch, filecommands, plugincommands, keyadders +from . import pubkeymanager, onionrstatistics, daemonlaunch, filecommands, plugincommands, keyadders, banblocks, exportblocks def show_help(o_inst, command): diff --git a/onionr/onionrcommands/banblocks.py b/onionr/onionrcommands/banblocks.py new file mode 100644 index 00000000..474353f6 --- /dev/null +++ b/onionr/onionrcommands/banblocks.py @@ -0,0 +1,39 @@ +''' + Onionr - P2P Anonymous Storage Network + + This file contains the command for banning blocks from the node +''' +''' + 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 +import logger +def ban_block(o_inst): + try: + ban = sys.argv[2] + except IndexError: + ban = logger.readline('Enter a block hash:') + if o_inst.onionrUtils.validateHash(ban): + if not o_inst.onionrCore._blacklist.inBlacklist(ban): + try: + o_inst.onionrCore._blacklist.addToDB(ban) + o_inst.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') \ No newline at end of file diff --git a/onionr/onionrcommands/exportblocks.py b/onionr/onionrcommands/exportblocks.py new file mode 100644 index 00000000..243ee752 --- /dev/null +++ b/onionr/onionrcommands/exportblocks.py @@ -0,0 +1,42 @@ +''' + Onionr - Private P2P Communication + + This file handles the command for exporting blocks to disk +''' +''' + 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 +import logger, onionrstorage +def doExport(o_inst, bHash): + exportDir = o_inst.dataDir + 'block-export/' + if not os.path.exists(exportDir): + if os.path.exists(o_inst.dataDir): + os.mkdir(exportDir) + else: + logger.error('Onionr Not initialized') + data = onionrstorage.getData(o_inst.onionrCore, bHash) + with open('%s/%s.dat' % (exportDir, bHash), 'wb') as exportFile: + exportFile.write(data) + +def export_block(o_inst): + exportDir = o_inst.dataDir + 'block-export/' + try: + assert o_inst.onionrUtils.validateHash(sys.argv[2]) + except (IndexError, AssertionError): + logger.error('No valid block hash specified.') + sys.exit(1) + else: + bHash = sys.argv[2] + o_inst.doExport(bHash) \ No newline at end of file diff --git a/onionr/onionrservices/serviceblocks.py b/onionr/onionrservices/serviceblocks.py new file mode 100644 index 00000000..e69de29b From 7e058b29e0909f8f445dc33556096eb6c5991b06 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 7 Jun 2019 00:09:14 -0500 Subject: [PATCH 065/173] * Removed hard coded data dir in daemon launch * Documented subprocess pow better * Calculate block size better for pow + Added monero donation address --- README.md | 8 +++- onionr/onionrcommands/daemonlaunch.py | 4 +- onionr/onionrproofs.py | 11 +---- onionr/subprocesspow.py | 65 +++++++++++++++------------ 4 files changed, 46 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index e5f18105..5de6fc78 100755 --- a/README.md +++ b/README.md @@ -69,12 +69,14 @@ More docs coming soon. The following applies to Ubuntu Bionic. Other distros may have different package or command names. +`$ sudo apt install python3-pip python3-dev tor` + * Have python3.6+, python3-pip, Tor (daemon, not browser) installed (python3-dev recommended) * Clone the git repo: `$ git clone https://gitlab.com/beardog/onionr` * cd into install direction: `$ cd onionr/` * Install the Python dependencies ([virtualenv strongly recommended](https://virtualenv.pypa.io/en/stable/userguide/)): `$ pip3 install --require-hashes -r requirements.txt` -(--require-hashes is intended to prevent exploitation via compromise of Pypi/CA certificates) +(--require-hashes is intended to prevent exploitation via compromise of PyPi/CA certificates) ## Help out @@ -95,7 +97,9 @@ Contribute money: Donating at least $5 gets you cool Onionr stickers. Get in touch if you want them. -Bitcoin: [1onion55FXzm6h8KQw3zFw2igpHcV7LPq](bitcoin:1onion55FXzm6h8KQw3zFw2igpHcV7LPq) (Contact us for privacy coins like Monero) +Bitcoin: [1onion55FXzm6h8KQw3zFw2igpHcV7LPq](bitcoin:1onion55FXzm6h8KQw3zFw2igpHcV7LPq) (Contact us for a unique address or for other coins) + +Monero: 4B5BA24d1P3R5aWEpkGY5TP7buJJcn2aSGBVRQCHhpiahxeB4aWsu15XwmuTjC6VF62NApZeJGTS248RMVECP8aW73Uj2ax USD (Card/Paypal): [Ko-Fi](https://www.ko-fi.com/beardogkf) diff --git a/onionr/onionrcommands/daemonlaunch.py b/onionr/onionrcommands/daemonlaunch.py index 9dab9f57..98708258 100644 --- a/onionr/onionrcommands/daemonlaunch.py +++ b/onionr/onionrcommands/daemonlaunch.py @@ -29,9 +29,9 @@ def daemon(o_inst): ''' # remove runcheck if it exists - if os.path.isfile('data/.runcheck'): + if os.path.isfile('%s/.runcheck' % (o_inst.onionrCore.dataDir,)): logger.debug('Runcheck file found on daemon start, deleting in advance.') - os.remove('data/.runcheck') + os.remove('%s/.runcheck' % (o_inst.onionrCore.dataDir,)) Thread(target=api.API, args=(o_inst, o_inst.debug, onionr.API_VERSION)).start() Thread(target=api.PublicAPI, args=[o_inst.getClientApi()]).start() diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index bd0600cf..287ba30f 100755 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -53,14 +53,9 @@ def getDifficultyForNewBlock(data, ourBlock=True): dataSize = 0 if isinstance(data, onionrblockapi.Block): dataSize = len(data.getRaw().encode('utf-8')) - elif isinstance(data, str): - dataSize = len(data.encode('utf-8')) - elif isinstance(data, bytes): - dataSize = len(data) - elif isinstance(data, int): - dataSize = data else: - raise ValueError('not Block, str, or int') + dataSize = len(onionrutils.OnionrUtils.strToBytes(data)) + if ourBlock: minDifficulty = config.get('general.minimum_send_pow', 4) else: @@ -131,8 +126,6 @@ class DataPOW: for i in range(max(1, threadCount)): t = threading.Thread(name = 'thread%s' % i, target = self.pow, args = (True,myCore)) t.start() - - return def pow(self, reporting = False, myCore = None): startTime = math.floor(time.time()) diff --git a/onionr/subprocesspow.py b/onionr/subprocesspow.py index fee2f2dd..6df08ca2 100755 --- a/onionr/subprocesspow.py +++ b/onionr/subprocesspow.py @@ -1,34 +1,39 @@ #!/usr/bin/env python3 -import subprocess, sys, os -import multiprocessing, threading, time, json, math, binascii +import subprocess, os +import multiprocessing, threading, time, json, math from multiprocessing import Pipe, Process import core, onionrblockapi, config, onionrutils, logger, onionrproofs class SubprocessPOW: - def __init__(self, data, metadata, core_inst=None, subprocCount=None): + def __init__(self, data, metadata, core_inst=None, subproc_count=None): + ''' + Onionr proof of work using multiple processes + Accepts block data, block metadata + and optionally an onionr core library instance. + if subproc_count is not set, os.cpu_count() is used to determine the number of processes + + Do to Python GIL multiprocessing or use of external libraries is necessary to accelerate CPU bound tasks + ''' + # Option to accept existing core instance to save memory if core_inst is None: core_inst = core.Core() - if subprocCount is None: - subprocCount = os.cpu_count() - self.subprocCount = subprocCount + # No known benefit to using more processes than there are cores. + # Note: os.cpu_count perhaps not always accurate + if subproc_count is None: + subproc_count = os.cpu_count() + self.subproc_count = subproc_count self.result = '' self.shutdown = False self.core_inst = core_inst self.data = data self.metadata = metadata - dataLen = len(data) + len(json.dumps(metadata)) + # dump dict to measure bytes of json metadata. Cannot reuse later because the pow token must be added + json_metadata = json.dumps(metadata).encode() - #if forceDifficulty > 0: - # self.difficulty = forceDifficulty - #else: - # Calculate difficulty. Dumb for now, may use good algorithm in the future. - self.difficulty = onionrproofs.getDifficultyForNewBlock(dataLen) - - try: - self.data = self.data.encode() - except AttributeError: - pass + self.data = onionrutils.OnionrUtils.strToBytes(data) + # Calculate difficulty. Dumb for now, may use good algorithm in the future. + self.difficulty = onionrproofs.getDifficultyForNewBlock(bytes(json_metadata + b'\n' + self.data)) logger.info('Computing POW (difficulty: %s)...' % self.difficulty) @@ -38,9 +43,10 @@ class SubprocessPOW: self.payload = None def start(self): - startTime = self.core_inst._utils.getEpoch() - for x in range(self.subprocCount): + # Create a new thread for each subprocess + for x in range(self.subproc_count): threading.Thread(target=self._spawn_proc).start() + # Monitor the processes for a payload, shut them down when its found while True: if self.payload is None: time.sleep(0.1) @@ -49,6 +55,7 @@ class SubprocessPOW: return self.payload def _spawn_proc(self): + # Create a child proof of work process, wait for data and send shutdown signal when its found parent_conn, child_conn = Pipe() p = Process(target=self.do_pow, args=(child_conn,)) p.start() @@ -67,24 +74,24 @@ class SubprocessPOW: self.payload = payload def do_pow(self, pipe): - nonce = int(binascii.hexlify(os.urandom(2)), 16) + nonce = -10000000 # Start nonce at negative 10 million so that the chosen nonce is likely to be small in length nonceStart = nonce data = self.data metadata = self.metadata puzzle = self.puzzle difficulty = self.difficulty - mcore = core.Core() + mcore = core.Core() # I think we make a new core here because of multiprocess bugs while True: - metadata['pow'] = nonce - payload = json.dumps(metadata).encode() + b'\n' + data - token = mcore._crypto.sha3Hash(payload) - try: - # on some versions, token is bytes - token = token.decode() - except AttributeError: - pass + # Break if shutdown received if pipe.poll() and pipe.recv() == 'shutdown': break + # Load nonce into block metadata + metadata['pow'] = nonce + # Serialize metadata, combine with block data + payload = json.dumps(metadata).encode() + b'\n' + data + # Check sha3_256 hash of block, compare to puzzle. Send payload if puzzle finished + token = mcore._crypto.sha3Hash(payload) + token = onionrutils.OnionrUtils.bytesToStr(token) # ensure token is string if puzzle == token[0:difficulty]: pipe.send(payload) break From 6e1f26dc911d2ca83ddb2ea2eda77e648c1f78e5 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 10 Jun 2019 22:19:31 -0500 Subject: [PATCH 066/173] dont allow anonymous to be set as a friends alias in the interface --- README.md | 12 ++++++------ onionr/static-data/www/friends/friends.js | 4 ++++ onionr/static-data/www/mail/mail.js | 3 ++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5de6fc78..af61cc2a 100755 --- a/README.md +++ b/README.md @@ -29,8 +29,6 @@ Onionr can be used for mail, as a social network, instant messenger, file sharin The whitepaper (subject to change prior to alpha release) is available [here](docs/whitepaper.md). -![node web illustration](docs/onionr-web.png) - ![Tor stinks slide image](docs/tor-stinks-02.png) ## Main Features @@ -78,9 +76,9 @@ The following applies to Ubuntu Bionic. Other distros may have different package (--require-hashes is intended to prevent exploitation via compromise of PyPi/CA certificates) -## Help out +# Help out -Everyone is welcome to help out. Help is wanted for the following: +Everyone is welcome to contribute. Help is wanted for the following: * Development (Get in touch first) * Creation of a shared lib for use from other languages and faster proof-of-work @@ -107,7 +105,7 @@ Note: probably not tax deductible ## Contact -beardog [ at ] mailbox.org +Email: beardog [ at ] mailbox.org ## Disclaimers and legal @@ -123,4 +121,6 @@ The 'open source badge' is by Maik Ellerbrock and is licensed under a Creative C The Onionr logo was created by [Anhar Ismail](https://github.com/anharismail) under the [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/). -If you modify and redistribute our code ("forking"), please use a different logo and project name to avoid confusion. Please do not use our logo in a way that makes it seem like we endorse you without our permission. \ No newline at end of file +If you modify and redistribute our code ("forking"), please use a different logo and project name to avoid confusion. Please do not use the project name or logo in a way that makes it seem like we endorse you without our permission. + +![node web illustration](docs/onionr-web.png) \ No newline at end of file diff --git a/onionr/static-data/www/friends/friends.js b/onionr/static-data/www/friends/friends.js index cbefae4d..48e3fd6b 100755 --- a/onionr/static-data/www/friends/friends.js +++ b/onionr/static-data/www/friends/friends.js @@ -27,6 +27,10 @@ function removeFriend(pubkey){ addForm.onsubmit = function(){ var friend = document.getElementsByName('addKey')[0] var alias = document.getElementsByName('data')[0] + if (alias.value.toLowerCase() == 'anonymous'){ + alert('Anonymous is a reserved name') + return false + } fetch('/friends/add/' + friend.value, { method: 'POST', diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index f02a3110..1109e433 100644 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -55,7 +55,8 @@ function openReply(bHash, quote, subject){ function openThread(bHash, sender, date, sigBool, pubkey, subjectLine){ var messageDisplay = document.getElementById('threadDisplay') var blockContent = httpGet('/getblockbody/' + bHash) - document.getElementById('fromUser').value = sender + + document.getElementById('fromUser').value = sender || 'Anonymous' document.getElementById('subjectView').innerText = subjectLine messageDisplay.innerText = blockContent var sigEl = document.getElementById('sigValid') From af6834cfd018706f0a932241cfc8beb1ba20e9ff Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 10 Jun 2019 23:21:44 -0500 Subject: [PATCH 067/173] added missing secrets import --- onionr/communicatorutils/connectnewpeers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/onionr/communicatorutils/connectnewpeers.py b/onionr/communicatorutils/connectnewpeers.py index 43e91c9a..ca71d310 100644 --- a/onionr/communicatorutils/connectnewpeers.py +++ b/onionr/communicatorutils/connectnewpeers.py @@ -20,6 +20,11 @@ import time import onionrexceptions, logger, onionrpeers from utils import networkmerger +# secrets module was added into standard lib in 3.6+ +if sys.version_info[0] == 3 and sys.version_info[1] < 6: + from dependencies import secrets +elif sys.version_info[0] == 3 and sys.version_info[1] >= 6: + import secrets def connect_new_peer_to_communicator(comm_inst, peer='', useBootstrap=False): config = comm_inst._core.config retData = False From d18f56eee24e9614ef7f13d72d90b438269dc0c1 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 10 Jun 2019 23:22:35 -0500 Subject: [PATCH 068/173] added missing secrets import --- onionr/communicatorutils/connectnewpeers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionr/communicatorutils/connectnewpeers.py b/onionr/communicatorutils/connectnewpeers.py index ca71d310..88d8151d 100644 --- a/onionr/communicatorutils/connectnewpeers.py +++ b/onionr/communicatorutils/connectnewpeers.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import time +import time, sys import onionrexceptions, logger, onionrpeers from utils import networkmerger # secrets module was added into standard lib in 3.6+ From 2d20ecdfd3251b172fc0d6d184c81a25bad89b3f Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 11 Jun 2019 01:08:44 -0500 Subject: [PATCH 069/173] * send introduce command instead of using duplicated logic * remove unnecessary imports * improve if self-address peer comparison to avoid some config reads --- onionr/communicator.py | 6 +++--- onionr/core.py | 38 +++++++++++--------------------------- onionr/onionr.py | 4 +--- onionr/onionrcrypto.py | 4 +++- onionr/onionrproofs.py | 2 +- onionr/onionrutils.py | 5 +++-- onionr/serializeddata.py | 2 +- onionr/subprocesspow.py | 2 +- 8 files changed, 24 insertions(+), 39 deletions(-) diff --git a/onionr/communicator.py b/onionr/communicator.py index d018a53a..602f7277 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -19,11 +19,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import sys, os, core, config, json, requests, time, logger, threading, base64, onionr, uuid, binascii -from dependencies import secrets +import sys, os, time +import core, config, logger, onionr import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block from communicatorutils import onionrdaemontools, servicecreator, onionrcommunicatortimers -from communicatorutils import proxypicker, downloadblocks, lookupblocks, lookupadders +from communicatorutils import downloadblocks, lookupblocks, lookupadders from communicatorutils import servicecreator, connectnewpeers, uploadblocks from communicatorutils import daemonqueuehandler import onionrservices, onionr, onionrproofs diff --git a/onionr/core.py b/onionr/core.py index 3c04ceb4..4246f7fe 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import sqlite3, os, sys, time, math, base64, tarfile, nacl, logger, json, netcontroller, math, config, uuid +import sqlite3, os, sys, time, json, uuid +import logger, netcontroller, config from onionrblockapi import Block import deadsimplekv as simplekv import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions @@ -160,11 +161,11 @@ class Core: Add an address to the address database (only tor currently) ''' - if address == config.get('i2p.ownAddr', None) or address == self.hsAddress: - return False if type(address) is None or len(address) == 0: return False if self._utils.validateID(address): + if address == config.get('i2p.ownAddr', None) or address == self.hsAddress: + return False conn = sqlite3.connect(self.addressDB, timeout=30) c = conn.cursor() # check if address is in database @@ -226,6 +227,8 @@ class Core: conn.close() dataSize = sys.getsizeof(onionrstorage.getData(self, block)) self._utils.storageCounter.removeBytes(dataSize) + else: + raise onionrexceptions.InvalidHexHash def createAddressDB(self): ''' @@ -797,7 +800,7 @@ class Core: logger.error(allocationReachedMessage) retData = False else: - # Tell the api server through localCommand to wait for the daemon to upload this block to make stastical analysis more difficult + # Tell the api server through localCommand to wait for the daemon to upload this block to make statistical analysis more difficult if self._utils.localCommand('/ping', maxWait=10) == 'pong!': self._utils.localCommand('/waitforshare/' + retData, post=True, maxWait=5) self.daemonQueueAdd('uploadBlock', retData) @@ -815,27 +818,8 @@ class Core: ''' Introduces our node into the network by telling X many nodes our HS address ''' - - if(self._utils.isCommunicatorRunning(timeout=30)): - announceAmount = 2 - nodeList = self.listAdders() - - if len(nodeList) == 0: - for i in self.bootstrapList: - if self._utils.validateID(i): - self.addAddress(i) - nodeList.append(i) - - if announceAmount > len(nodeList): - announceAmount = len(nodeList) - - for i in range(announceAmount): - self.daemonQueueAdd('announceNode', nodeList[i]) - - events.event('introduction', onionr = None) - - return True + if self._utils.localCommand('/ping', maxWait=10) == 'pong!': + self.daemonQueueAdd('announceNode') + logger.info('Introduction command will be processed.') else: - logger.error('Onionr daemon is not running.') - return False - return + logger.warn('No running node detected. Cannot introduce.') \ No newline at end of file diff --git a/onionr/onionr.py b/onionr/onionr.py index 402b7bd0..a12250cc 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -25,8 +25,7 @@ MIN_PY_VERSION = 6 if sys.version_info[0] == 2 or sys.version_info[1] < MIN_PY_VERSION: sys.stderr.write('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 +import os, base64, random, shutil, time, platform, signal from threading import Thread import api, core, config, logger, onionrplugins as plugins, onionrevents as events import onionrutils @@ -34,7 +33,6 @@ import netcontroller from netcontroller import NetController from onionrblockapi import Block import onionrproofs, onionrexceptions, communicator, setupconfig -from onionrusers import onionrusers import onionrcommands as commands # Many command definitions are here try: diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py index 4b5a72e1..ddff1993 100755 --- a/onionr/onionrcrypto.py +++ b/onionr/onionrcrypto.py @@ -17,7 +17,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import nacl.signing, nacl.encoding, nacl.public, nacl.hash, nacl.pwhash, nacl.utils, nacl.secret, os, binascii, base64, hashlib, logger, onionrproofs, time, math, sys, hmac +import os, binascii, base64, hashlib, time, sys, hmac +import nacl.signing, nacl.encoding, nacl.public, nacl.hash, nacl.pwhash, nacl.utils, nacl.secret +import logger, onionrproofs import onionrexceptions, keymanager, core # secrets module was added into standard lib in 3.6+ if sys.version_info[0] == 3 and sys.version_info[1] < 6: diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index 287ba30f..20e10403 100755 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import multiprocessing, nacl.encoding, nacl.hash, nacl.utils, time, math, threading, binascii, sys, base64, json +import multiprocessing, nacl.encoding, nacl.hash, nacl.utils, time, math, threading, binascii, sys, json import core, onionrutils, config, logger, onionrblockapi def getDifficultyModifier(coreOrUtilsInst=None): diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 9ab1dfa3..31521059 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -18,10 +18,11 @@ along with this program. If not, see . ''' # Misc functions that do not fit in the main api, but are useful -import getpass, sys, requests, os, socket, hashlib, logger, sqlite3, config, binascii, time, base64, json, glob, shutil, math, json, re, urllib.parse, string +import sys, os, sqlite3, binascii, time, base64, json, glob, shutil, math, re, urllib.parse, string +import requests import nacl.signing, nacl.encoding from onionrblockapi import Block -import onionrexceptions +import onionrexceptions, config, logger from onionr import API_VERSION import onionrevents import storagecounter diff --git a/onionr/serializeddata.py b/onionr/serializeddata.py index 1587e74e..3efe150a 100644 --- a/onionr/serializeddata.py +++ b/onionr/serializeddata.py @@ -18,7 +18,7 @@ along with this program. If not, see . ''' -import core, api, uuid, json +import core, json class SerializedData: def __init__(self, coreInst): diff --git a/onionr/subprocesspow.py b/onionr/subprocesspow.py index 6df08ca2..f1b23c7f 100755 --- a/onionr/subprocesspow.py +++ b/onionr/subprocesspow.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import subprocess, os -import multiprocessing, threading, time, json, math +import multiprocessing, threading, time, json from multiprocessing import Pipe, Process import core, onionrblockapi, config, onionrutils, logger, onionrproofs From 335db6409694bde4adae582867178fe31a04eaf1 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 11 Jun 2019 19:05:15 -0500 Subject: [PATCH 070/173] * Progress in adjust tagline in files, license boilerplate fixes * Import adjustments --- CONTRIBUTING.md | 2 +- onionr/api.py | 2 +- onionr/blockimporter.py | 2 +- onionr/communicator.py | 2 +- onionr/communicatorutils/connectnewpeers.py | 2 +- onionr/config.py | 2 +- onionr/core.py | 2 +- onionr/dbcreator.py | 2 +- onionr/keymanager.py | 2 +- onionr/onionrcrypto.py | 2 +- onionr/onionrpeers.py | 3 ++- onionr/onionrproofs.py | 2 +- onionr/onionrservices/connectionserver.py | 5 ++--- onionr/onionrservices/serviceblocks.py | 0 onionr/onionrutils.py | 2 +- onionr/serializeddata.py | 2 +- onionr/static-data/www/friends/friends.js | 2 +- onionr/static-data/www/mail/mail.js | 2 +- onionr/static-data/www/mail/sendmail.js | 2 +- onionr/static-data/www/shared/configeditor.js | 2 +- onionr/static-data/www/shared/misc.js | 2 +- onionr/subprocesspow.py | 20 +++++++++++++++++++ 22 files changed, 42 insertions(+), 22 deletions(-) delete mode 100644 onionr/onionrservices/serviceblocks.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea21aeef..9f083d3d 100755 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to Onionr -One of the great things about open source projects is that they allow for many people to contribute to the project. +One of the great things about open source projects is that they allow for anyone to contribute to the project. This file serves to provide guidelines on how to successfully contribute to Onionr. diff --git a/onionr/api.py b/onionr/api.py index 50bc83b5..66464e9b 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file handles all incoming http requests to the client, using Flask ''' diff --git a/onionr/blockimporter.py b/onionr/blockimporter.py index ce1cd1fe..ccfc77db 100755 --- a/onionr/blockimporter.py +++ b/onionr/blockimporter.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication Import block data and save it ''' diff --git a/onionr/communicator.py b/onionr/communicator.py index 602f7277..5f31fc3c 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file contains both the OnionrCommunicate class for communcating with peers and code to operate as a daemon, getting commands from the command queue database (see core.Core.daemonQueue) diff --git a/onionr/communicatorutils/connectnewpeers.py b/onionr/communicatorutils/connectnewpeers.py index 88d8151d..0ba358f6 100644 --- a/onionr/communicatorutils/connectnewpeers.py +++ b/onionr/communicatorutils/connectnewpeers.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication Connect a new peer to our communicator instance. Does so randomly if no peer is specified ''' diff --git a/onionr/config.py b/onionr/config.py index 5576a1d3..6cb75050 100755 --- a/onionr/config.py +++ b/onionr/config.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication This file deals with configuration management. ''' diff --git a/onionr/core.py b/onionr/core.py index 4246f7fe..58c95d9f 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication Core Onionr library, useful for external programs. Handles peer & data processing ''' diff --git a/onionr/dbcreator.py b/onionr/dbcreator.py index b84794d3..6c26fa97 100755 --- a/onionr/dbcreator.py +++ b/onionr/dbcreator.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Data Storage & Sharing + Onionr - Private P2P Communication DBCreator, creates sqlite3 databases used by Onionr ''' diff --git a/onionr/keymanager.py b/onionr/keymanager.py index ff89401b..9e2a1703 100755 --- a/onionr/keymanager.py +++ b/onionr/keymanager.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication Load, save, and delete the user's public key pairs (does not handle peer keys) ''' diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py index ddff1993..2ce90bf6 100755 --- a/onionr/onionrcrypto.py +++ b/onionr/onionrcrypto.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file handles Onionr's cryptography. ''' diff --git a/onionr/onionrpeers.py b/onionr/onionrpeers.py index 8a88d649..ad60d543 100755 --- a/onionr/onionrpeers.py +++ b/onionr/onionrpeers.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import core, config, logger, sqlite3 +import sqlite3 +import core, config, logger class PeerProfiles: ''' PeerProfiles diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index 20e10403..ac64575f 100755 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication Proof of work module ''' diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index 0878b100..e676e496 100644 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This module does the second part of the bootstrap block handshake and creates the API server ''' @@ -17,8 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import threading, time -from gevent.pywsgi import WSGIServer, WSGIHandler +from gevent.pywsgi import WSGIServer from stem.control import Controller from flask import Flask import core, logger, httpapi diff --git a/onionr/onionrservices/serviceblocks.py b/onionr/onionrservices/serviceblocks.py deleted file mode 100644 index e69de29b..00000000 diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 31521059..824c2415 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication OnionrUtils offers various useful functions to Onionr. Relatively misc. ''' diff --git a/onionr/serializeddata.py b/onionr/serializeddata.py index 3efe150a..ef58c304 100644 --- a/onionr/serializeddata.py +++ b/onionr/serializeddata.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This module serializes various data pieces for use in other modules, in particular the web api ''' diff --git a/onionr/static-data/www/friends/friends.js b/onionr/static-data/www/friends/friends.js index 48e3fd6b..28e2a2da 100755 --- a/onionr/static-data/www/friends/friends.js +++ b/onionr/static-data/www/friends/friends.js @@ -1,5 +1,5 @@ /* - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file handles the UI for managing friends/contacts diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index 1109e433..d7b62765 100644 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -1,5 +1,5 @@ /* - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file handles the mail interface diff --git a/onionr/static-data/www/mail/sendmail.js b/onionr/static-data/www/mail/sendmail.js index 704574e6..f8ff49bc 100755 --- a/onionr/static-data/www/mail/sendmail.js +++ b/onionr/static-data/www/mail/sendmail.js @@ -1,5 +1,5 @@ /* - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file handles the mail interface diff --git a/onionr/static-data/www/shared/configeditor.js b/onionr/static-data/www/shared/configeditor.js index 6c9913b1..172b3d5a 100644 --- a/onionr/static-data/www/shared/configeditor.js +++ b/onionr/static-data/www/shared/configeditor.js @@ -1,5 +1,5 @@ /* - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file is for configuration editing in the web interface diff --git a/onionr/static-data/www/shared/misc.js b/onionr/static-data/www/shared/misc.js index eeaddf71..ca0cb58b 100644 --- a/onionr/static-data/www/shared/misc.js +++ b/onionr/static-data/www/shared/misc.js @@ -1,5 +1,5 @@ /* - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file handles the mail interface diff --git a/onionr/subprocesspow.py b/onionr/subprocesspow.py index f1b23c7f..e7b78d5f 100755 --- a/onionr/subprocesspow.py +++ b/onionr/subprocesspow.py @@ -1,4 +1,24 @@ #!/usr/bin/env python3 +''' + Onionr - Private P2P Communication + + Multiprocess proof of work +''' +''' + 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 subprocess, os import multiprocessing, threading, time, json from multiprocessing import Pipe, Process From 6305e5d2fdabbd25bc3a4420ea706641591df6ab Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 11 Jun 2019 19:42:29 -0500 Subject: [PATCH 071/173] Create FUNDING.yml --- .github/FUNDING.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..66ad2049 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +ko_fi: beardogkf + From bc75211bb1c81baca8cc35cf41247c617fe808ce Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 11 Jun 2019 20:01:33 -0500 Subject: [PATCH 072/173] bumped requests and urllib3 version to fix urllib3 CA cert security issue --- requirements.in | 6 +++--- requirements.txt | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/requirements.in b/requirements.in index 014499dd..2a255e58 100755 --- a/requirements.in +++ b/requirements.in @@ -1,5 +1,5 @@ -urllib3==1.23 -requests==2.20.0 +urllib3==1.24.2 +requests==2.21.0 PyNaCl==1.2.1 gevent==1.3.6 defusedxml==0.5.0 @@ -7,4 +7,4 @@ Flask==1.0.2 PySocks==1.6.8 stem==1.7.1 deadsimplekv==0.0.1 -jinja2==2.10.1 \ No newline at end of file +jinja2==2.10.1 diff --git a/requirements.txt b/requirements.txt index de720793..5facf81b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -177,18 +177,18 @@ pynacl==1.2.1 \ --hash=sha256:fabf73d5d0286f9e078774f3435601d2735c94ce9e514ac4fb945701edead7e4 pysocks==1.6.8 \ --hash=sha256:3fe52c55890a248676fd69dc9e3c4e811718b777834bcaab7a8125cf9deac672 -requests==2.20.0 \ - --hash=sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c \ - --hash=sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279 +requests==2.21.0 \ + --hash=sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e \ + --hash=sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b six==1.12.0 \ --hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \ --hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73 \ # via pynacl stem==1.7.1 \ --hash=sha256:c9eaf3116cb60c15995cbd3dec3a5cbc50e9bb6e062c4d6d42201e566f498ca2 -urllib3==1.23 \ - --hash=sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf \ - --hash=sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5 +urllib3==1.24.2 \ + --hash=sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0 \ + --hash=sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3 werkzeug==0.14.1 \ --hash=sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c \ --hash=sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b \ From 89624441ec46714f19ea14491a0c4a47d130a15c Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 11 Jun 2019 20:49:02 -0500 Subject: [PATCH 073/173] Create SECURITY.md --- SECURITY.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..ce612cca --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,37 @@ +# Security Policy + +# Scope + +The Onionr software and any nodes you control are within scope. + +Avoid social engineering, volume-based denial of service and disrupting or harming the Onionr network. Do not attempt to exploit any machines/servers you do not own or otherwise have permission to do so. + +The following exploits are of particular interest: + +* Arbitrary code execution +* API authentication bypass (such as accessing local API from public interface) +* Deanonymization: + * Easily associating public keys with server addresses + * Discovering true server IPs when behind Tor/I2P (aside from Tor/i2p-level attacks) + * Easily discovering which nodes are the block creator +* XSS, CSRF, clickjacking +* Timing attacks against the local http server ([see blog post](https://www.chaoswebs.net/blog/timebleed-breaking-privacy-with-a-simple-timing-attack.html)) +* Discovering direct connection servers as a non participant. +* Cryptography/protocol issues +* Denying nodes access to the network by segmenting them out with Sybil nodes + +We do not consider non-network based same-machine attacks to be very significant, but we are still willing to listen. + +# Rewards + +Onionr is a student-owned hobby project, resources are not available for large rewards. + +Stickers or other reasonable & negotiable rewards are available. We reserve the right to refuse rewards for any reason. + +Public recognition can be given upon request. + +# Contact + +Email: beardog [ at ] mailbox.org + +PGP (optional): F61A 4DBB 0B3D F172 1F65 0EDF 0D41 4D0F E405 B63B From 89f11d930f59c88eae3443d3861cea04d74347fe Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 11 Jun 2019 21:31:54 -0500 Subject: [PATCH 074/173] removed network comparison from whitepaper --- docs/network-comparison.png | Bin 64942 -> 0 bytes docs/whitepaper.md | 6 ------ 2 files changed, 6 deletions(-) delete mode 100644 docs/network-comparison.png diff --git a/docs/network-comparison.png b/docs/network-comparison.png deleted file mode 100644 index 0d1f42e6c8d331379fc7764a9ee0ddee41c7c962..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64942 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 Q5d Date: Wed, 12 Jun 2019 01:44:15 -0500 Subject: [PATCH 075/173] boilerplate and comment changes, added first subdirectory readme (for communicatorutils) --- README.md | 18 ++++++++++++++- onionr/communicatorutils/README.md | 23 +++++++++++++++++++ onionr/communicatorutils/connectnewpeers.py | 6 +++++ onionr/communicatorutils/downloadblocks.py | 2 +- onionr/communicatorutils/lookupadders.py | 4 ++-- onionr/communicatorutils/lookupblocks.py | 2 +- .../onionrcommunicatortimers.py | 3 +-- onionr/communicatorutils/onionrdaemontools.py | 4 +++- onionr/communicatorutils/proxypicker.py | 7 +++--- onionr/communicatorutils/reversesync.py | 4 ---- onionr/communicatorutils/servicecreator.py | 20 ++++++++++++++++ onionr/communicatorutils/uploadblocks.py | 2 +- 12 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 onionr/communicatorutils/README.md delete mode 100644 onionr/communicatorutils/reversesync.py diff --git a/README.md b/README.md index af61cc2a..34491fad 100755 --- a/README.md +++ b/README.md @@ -36,10 +36,26 @@ The whitepaper (subject to change prior to alpha release) is available [here](do * [X] 🌐 Fully p2p/decentralized, no trackers or other single points of failure * [X] 🔒 End to end encryption of user data * [X] 📢 Optional non-encrypted blocks, useful for blog posts or public file sharing -* [X] 💻 Easy API for integration to websites +* [X] 💻 Easy HTTP API for integration to websites * [X] 🕵️ Metadata analysis resistance and anonymity * [X] 📡 Transport agnosticism (no internet required) +## Software Suite + +Onionr ships with various application plugins ready for use out of the box: + +Currently usable: + +* Mail +* Public anonymous chat +* Simple webpage hosting (Will be greatly extended) +* File sharing (Work in progress) + +Not yet usable: + +* Instant messaging +* Forum/BBS + **Onionr API and functionality is subject to non-backwards compatible change during pre-alpha development** # Screenshots diff --git a/onionr/communicatorutils/README.md b/onionr/communicatorutils/README.md new file mode 100644 index 00000000..2bd7caf1 --- /dev/null +++ b/onionr/communicatorutils/README.md @@ -0,0 +1,23 @@ +# communicatorutils + +The files in this submodule handle various subtasks and utilities for the onionr communicator. + +## Files: + +connectnewpeers.py: takes a communicator instance and has it connect to as many peers as needed, and/or to a new specified peer. + +daemonqueuehandler.py: checks for new commands in the daemon queue and processes them accordingly. + +downloadblocks.py: iterates a communicator instance's block download queue and attempts to download the blocks from online peers + +lookupadders.py: ask connected peers to share their list of peer transport addresses + +onionrcommunicataortimers.py: create a timer for a function to be launched on an interval. Control how many possible instances of a timer may be running a function at once and control if the timer should be ran in a thread or not. + +onionrdaemontools.py: contains the DaemonTools class which has a lot of etc functions useful for the communicator. Deprecated. + +proxypicker.py: returns a string name for the appropriate proxy to be used with a particular peer transport address. + +servicecreator.py: iterate connection blocks and create new direct connection servers for them. + +uploadblocks.py: iterate a communicator's upload queue and upload the blocks to connected peers \ No newline at end of file diff --git a/onionr/communicatorutils/connectnewpeers.py b/onionr/communicatorutils/connectnewpeers.py index 0ba358f6..ee1eb468 100644 --- a/onionr/communicatorutils/connectnewpeers.py +++ b/onionr/communicatorutils/connectnewpeers.py @@ -40,6 +40,7 @@ def connect_new_peer_to_communicator(comm_inst, peer='', useBootstrap=False): mainPeerList = comm_inst._core.listAdders() peerList = onionrpeers.getScoreSortedPeerList(comm_inst._core) + # If we don't have enough peers connected or random chance, select new peers to try if len(peerList) < 8 or secrets.randbelow(4) == 3: tryingNew = [] for x in comm_inst.newPeers: @@ -56,15 +57,19 @@ def connect_new_peer_to_communicator(comm_inst, peer='', useBootstrap=False): for address in peerList: if not config.get('tor.v3onions') and len(address) == 62: continue + # Don't connect to our own address if address == comm_inst._core.hsAddress: continue + # Don't connect to invalid address or if its already been tried/connected, or if its cooled down if len(address) == 0 or address in tried or address in comm_inst.onlinePeers or address in comm_inst.cooldownPeer: continue if comm_inst.shutdown: return + # Ping a peer, if comm_inst.peerAction(address, 'ping') == 'pong!': time.sleep(0.1) if address not in mainPeerList: + # Add a peer to our list if it isn't already since it successfully connected networkmerger.mergeAdders(address, comm_inst._core) if address not in comm_inst.onlinePeers: logger.info('Connected to ' + address) @@ -80,6 +85,7 @@ def connect_new_peer_to_communicator(comm_inst, peer='', useBootstrap=False): comm_inst.peerProfiles.append(onionrpeers.PeerProfiles(address, comm_inst._core)) break else: + # Mark a peer as tried if they failed to respond to ping tried.append(address) logger.debug('Failed to connect to ' + address) return retData \ No newline at end of file diff --git a/onionr/communicatorutils/downloadblocks.py b/onionr/communicatorutils/downloadblocks.py index a2d4908c..720e0eae 100644 --- a/onionr/communicatorutils/downloadblocks.py +++ b/onionr/communicatorutils/downloadblocks.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication Download blocks using the communicator instance ''' diff --git a/onionr/communicatorutils/lookupadders.py b/onionr/communicatorutils/lookupadders.py index 6b9f474d..4f861682 100644 --- a/onionr/communicatorutils/lookupadders.py +++ b/onionr/communicatorutils/lookupadders.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication Lookup new peer transport addresses using the communicator ''' @@ -26,7 +26,7 @@ def lookup_new_peer_transports_with_communicator(comm_inst): for i in range(tryAmount): # Download new peer address list from random online peers if len(newPeers) > 10000: - # Dont get new peers if we have too many queued up + # Don't get new peers if we have too many queued up break peer = comm_inst.pickOnlinePeer() newAdders = comm_inst.peerAction(peer, action='pex') diff --git a/onionr/communicatorutils/lookupblocks.py b/onionr/communicatorutils/lookupblocks.py index e3e7ab2e..0994d15c 100644 --- a/onionr/communicatorutils/lookupblocks.py +++ b/onionr/communicatorutils/lookupblocks.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication Lookup new blocks with the communicator using a random connected peer ''' diff --git a/onionr/communicatorutils/onionrcommunicatortimers.py b/onionr/communicatorutils/onionrcommunicatortimers.py index a94a7821..b7255da4 100755 --- a/onionr/communicatorutils/onionrcommunicatortimers.py +++ b/onionr/communicatorutils/onionrcommunicatortimers.py @@ -1,6 +1,5 @@ -#!/usr/bin/env python3 ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file contains timer control for the communicator ''' diff --git a/onionr/communicatorutils/onionrdaemontools.py b/onionr/communicatorutils/onionrdaemontools.py index 6148041f..78e1f3dd 100755 --- a/onionr/communicatorutils/onionrdaemontools.py +++ b/onionr/communicatorutils/onionrdaemontools.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication Contains the CommunicatorUtils class which contains useful functions for the communicator daemon ''' @@ -18,6 +18,8 @@ along with this program. If not, see . ''' +# MODULE DEPRECATED + import onionrexceptions, onionrpeers, onionrproofs, logger import base64, sqlite3, os from dependencies import secrets diff --git a/onionr/communicatorutils/proxypicker.py b/onionr/communicatorutils/proxypicker.py index 7e1d1e38..a6461b5c 100644 --- a/onionr/communicatorutils/proxypicker.py +++ b/onionr/communicatorutils/proxypicker.py @@ -1,7 +1,7 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication - Just picks a proxy + Just picks a proxy to use based on a peer's address ''' ''' This program is free software: you can redistribute it and/or modify @@ -22,4 +22,5 @@ def pick_proxy(peer_address): if peer_address.endswith('.onion'): return 'tor' elif peer_address.endswith('.i2p'): - return 'i2p' \ No newline at end of file + return 'i2p' + raise ValueError("Peer address was not string ending with acceptable value") \ No newline at end of file diff --git a/onionr/communicatorutils/reversesync.py b/onionr/communicatorutils/reversesync.py deleted file mode 100644 index 55355a88..00000000 --- a/onionr/communicatorutils/reversesync.py +++ /dev/null @@ -1,4 +0,0 @@ -class ReverseSync: - def __init__(self, communicator_inst): - return - \ No newline at end of file diff --git a/onionr/communicatorutils/servicecreator.py b/onionr/communicatorutils/servicecreator.py index fbb4c5b9..c1f2c7e2 100644 --- a/onionr/communicatorutils/servicecreator.py +++ b/onionr/communicatorutils/servicecreator.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + Creates an onionr direct connection service by scanning all connection blocks +''' +''' + 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 communicator, onionrblockapi def service_creator(daemon): assert isinstance(daemon, communicator.OnionrCommunicatorDaemon) @@ -5,6 +24,7 @@ def service_creator(daemon): utils = core._utils # Find socket connection blocks + # TODO cache blocks and only look at recently received ones con_blocks = core.getBlocksByType('con') for b in con_blocks: if not b in daemon.active_services: diff --git a/onionr/communicatorutils/uploadblocks.py b/onionr/communicatorutils/uploadblocks.py index fe6392b4..69d0353d 100644 --- a/onionr/communicatorutils/uploadblocks.py +++ b/onionr/communicatorutils/uploadblocks.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication Upload blocks in the upload queue to peers from the communicator ''' From 268325c07d2904426900d469174be2d1f6812b8c Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 12 Jun 2019 15:12:56 -0500 Subject: [PATCH 076/173] - removed simplecache for now * fixed existingRand bytes check in announce in onionrdaemontools * moved openhome to its own file * slight comment and boilerplate changes + added more readme files for subdirectories --- onionr/api.py | 3 +- onionr/communicatorutils/onionrdaemontools.py | 7 +- onionr/etc/README.md | 5 ++ onionr/etc/onionrvalues.py | 2 +- onionr/etc/pgpwords.py | 2 +- onionr/httpapi/README.md | 13 ++++ onionr/httpapi/configapi/__init__.py | 2 +- onionr/httpapi/friendsapi/__init__.py | 2 +- onionr/httpapi/miscpublicapi/__init__.py | 19 +++++ onionr/httpapi/miscpublicapi/announce.py | 2 +- onionr/httpapi/miscpublicapi/getblocks.py | 2 +- onionr/httpapi/miscpublicapi/upload.py | 2 +- onionr/httpapi/profilesapi/__init__.py | 2 +- onionr/onionr.py | 2 +- onionr/onionrcommands/README.md | 25 +++++++ onionr/onionrcommands/__init__.py | 13 +--- onionr/onionrcommands/banblocks.py | 2 +- onionr/onionrcommands/daemonlaunch.py | 2 +- onionr/onionrcommands/filecommands.py | 20 +++++ onionr/onionrcommands/keyadders.py | 2 +- onionr/onionrcommands/onionrstatistics.py | 2 +- .../openwebinterface.py} | 27 ++++--- onionr/onionrcommands/plugincommands.py | 3 +- onionr/onionrcommands/pubkeymanager.py | 15 ++-- onionr/onionrfragment/__init__.py | 73 ------------------- onionr/onionrservices/__init__.py | 2 +- onionr/onionrservices/bootstrapservice.py | 2 +- onionr/onionrservices/httpheaders.py | 2 +- 28 files changed, 126 insertions(+), 129 deletions(-) create mode 100644 onionr/etc/README.md create mode 100644 onionr/httpapi/README.md create mode 100644 onionr/onionrcommands/README.md rename onionr/{httpapi/simplecache/__init__.py => onionrcommands/openwebinterface.py} (55%) mode change 100755 => 100644 delete mode 100755 onionr/onionrfragment/__init__.py diff --git a/onionr/api.py b/onionr/api.py index 66464e9b..daefd6ef 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -26,7 +26,7 @@ import core from onionrblockapi import Block import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config import httpapi -from httpapi import friendsapi, simplecache, profilesapi, configapi, miscpublicapi +from httpapi import friendsapi, profilesapi, configapi, miscpublicapi from onionrservices import httpheaders import onionr @@ -199,7 +199,6 @@ class API: self.queueResponse = {} onionrInst.setClientAPIInst(self) app.register_blueprint(friendsapi.friends) - app.register_blueprint(simplecache.simplecache) app.register_blueprint(profilesapi.profile_BP) app.register_blueprint(configapi.config_BP) httpapi.load_plugin_blueprints(app) diff --git a/onionr/communicatorutils/onionrdaemontools.py b/onionr/communicatorutils/onionrdaemontools.py index 78e1f3dd..b18edd87 100755 --- a/onionr/communicatorutils/onionrdaemontools.py +++ b/onionr/communicatorutils/onionrdaemontools.py @@ -67,9 +67,9 @@ class DaemonTools: combinedNodes = ourID + peer if ourID != 1: #TODO: Extend existingRand for i2p - existingRand = self.daemon._core.getAddressInfo(peer, 'powValue') + existingRand = self.daemon._core._utils.bytesToStr(self.daemon._core.getAddressInfo(peer, 'powValue')) # Reset existingRand if it no longer meets the minimum POW - if type(existingRand) is type(None) or not existingRand.endswith(b'0' * ov.announce_pow): + if type(existingRand) is type(None) or not existingRand.endswith('0' * ov.announce_pow): existingRand = '' if peer in self.announceCache: @@ -216,5 +216,4 @@ class DaemonTools: fakePeer = 'OVPCZLOXD6DC5JHX4EQ3PSOGAZ3T24F75HQLIUZSDSMYPEOXCPFA====' data = secrets.token_hex(secrets.randbelow(1024) + 1) self.daemon._core.insertBlock(data, header='pm', encryptType='asym', asymPeer=fakePeer, meta={'subject': 'foo'}) - self.daemon.decrementThreadCount('insertDeniableBlock') - return \ No newline at end of file + self.daemon.decrementThreadCount('insertDeniableBlock') \ No newline at end of file diff --git a/onionr/etc/README.md b/onionr/etc/README.md new file mode 100644 index 00000000..c5c054ea --- /dev/null +++ b/onionr/etc/README.md @@ -0,0 +1,5 @@ +# etc + +pgpwords.py: represent data using the pgp word list: https://en.wikipedia.org/wiki/PGP_word_list + +onionrvalues.py: spec values for onionr blocks and other things \ No newline at end of file diff --git a/onionr/etc/onionrvalues.py b/onionr/etc/onionrvalues.py index f55f5ac4..e3e1421d 100755 --- a/onionr/etc/onionrvalues.py +++ b/onionr/etc/onionrvalues.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication This file defines values and requirements used by Onionr ''' diff --git a/onionr/etc/pgpwords.py b/onionr/etc/pgpwords.py index 8e4ddb8e..d045b65a 100755 --- a/onionr/etc/pgpwords.py +++ b/onionr/etc/pgpwords.py @@ -4,7 +4,7 @@ '''This file is adapted from https://github.com/thblt/pgp-words by github user 'thblt' ('Thibault Polge), GPL v3 license''' ''' -Changes made for Onionr: +Changes made for Onionr by Kevin Froman in 2018-2019: Minor changes such as slight word adjustment, line breaks CLI commands/usage function removed diff --git a/onionr/httpapi/README.md b/onionr/httpapi/README.md new file mode 100644 index 00000000..b048b2c3 --- /dev/null +++ b/onionr/httpapi/README.md @@ -0,0 +1,13 @@ +# httpapi + +The httpapi contains collections of endpoints for the client API server. + +## Files: + +configapi: manage onionr configuration from the client http api + +friendsapi: add, remove and list friends from the client http api + +miscpublicapi: misculanious onionr network interaction from the **public** httpapi, such as announcements, block fetching and uploading. + +profilesapi: work in progress in returning a profile page for an Onionr user \ No newline at end of file diff --git a/onionr/httpapi/configapi/__init__.py b/onionr/httpapi/configapi/__init__.py index 3f5484f8..d760ff7b 100644 --- a/onionr/httpapi/configapi/__init__.py +++ b/onionr/httpapi/configapi/__init__.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file handles configuration setting and getting from the HTTP API ''' diff --git a/onionr/httpapi/friendsapi/__init__.py b/onionr/httpapi/friendsapi/__init__.py index c935ded5..8d6b4122 100755 --- a/onionr/httpapi/friendsapi/__init__.py +++ b/onionr/httpapi/friendsapi/__init__.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file creates http endpoints for friend management ''' diff --git a/onionr/httpapi/miscpublicapi/__init__.py b/onionr/httpapi/miscpublicapi/__init__.py index f03d3808..b29454f7 100644 --- a/onionr/httpapi/miscpublicapi/__init__.py +++ b/onionr/httpapi/miscpublicapi/__init__.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + Public endpoints to do various block sync actions and announcement +''' +''' + 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 . +''' from . import announce, upload, getblocks announce = announce.handle_announce # endpoint handler for accepting peer announcements diff --git a/onionr/httpapi/miscpublicapi/announce.py b/onionr/httpapi/miscpublicapi/announce.py index a6d9513f..8a25b635 100644 --- a/onionr/httpapi/miscpublicapi/announce.py +++ b/onionr/httpapi/miscpublicapi/announce.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication Handle announcements to the public API server ''' diff --git a/onionr/httpapi/miscpublicapi/getblocks.py b/onionr/httpapi/miscpublicapi/getblocks.py index 1ab8f949..fa9dde14 100644 --- a/onionr/httpapi/miscpublicapi/getblocks.py +++ b/onionr/httpapi/miscpublicapi/getblocks.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication Public endpoints to get block data and lists ''' diff --git a/onionr/httpapi/miscpublicapi/upload.py b/onionr/httpapi/miscpublicapi/upload.py index 0561ed08..5fd0f32c 100644 --- a/onionr/httpapi/miscpublicapi/upload.py +++ b/onionr/httpapi/miscpublicapi/upload.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication Accept block uploads to the public API server ''' diff --git a/onionr/httpapi/profilesapi/__init__.py b/onionr/httpapi/profilesapi/__init__.py index f3c3ba2f..681212be 100644 --- a/onionr/httpapi/profilesapi/__init__.py +++ b/onionr/httpapi/profilesapi/__init__.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file creates http endpoints for user profile pages ''' diff --git a/onionr/onionr.py b/onionr/onionr.py index a12250cc..c73edb4b 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -208,7 +208,7 @@ class Onionr: commands.onionrstatistics.show_details(self) def openHome(self): - commands.open_home(self) + commands.openwebinterface.open_home(self) def addID(self): commands.pubkeymanager.add_ID(self) diff --git a/onionr/onionrcommands/README.md b/onionr/onionrcommands/README.md new file mode 100644 index 00000000..a817cc7f --- /dev/null +++ b/onionr/onionrcommands/README.md @@ -0,0 +1,25 @@ +# onionrcommands + +This module contains handlers/functions for Onionr cli interface commands. + +## Files + +__init__.py: stores the command references (aside from plugins) and help info. + +banblocks.py: command handler for manually removing blocks from one's node + +daemonlaunch.py: command to run Onionr (start the api servers, tor and communicator) + +exportblocks.py: command to export an onionr block to the export folder. Exported blocks can be manually shared outside of the Onionr network + +filecommands.py commands to insert and fetch files from the Onionr network + +keyadders.py: commands to add an onionr user key or transport address + +onionrstatistics.py: commands to print out various information about one's Onionr node + +openwebinterface.py: command to open the web interface (useful because it requires a randomly generated token) + +plugincommands.py: commands to enable/disable/reload plugins + +pubkeymanager.py: commands to generate a new onionr user id, change the active id, or add/remove/list friends \ No newline at end of file diff --git a/onionr/onionrcommands/__init__.py b/onionr/onionrcommands/__init__.py index e929499f..e561292c 100644 --- a/onionr/onionrcommands/__init__.py +++ b/onionr/onionrcommands/__init__.py @@ -20,7 +20,8 @@ import webbrowser, sys import logger -from . import pubkeymanager, onionrstatistics, daemonlaunch, filecommands, plugincommands, keyadders, banblocks, exportblocks +from . import pubkeymanager, onionrstatistics, daemonlaunch, filecommands, plugincommands, keyadders +from . import banblocks, exportblocks, openwebinterface def show_help(o_inst, command): @@ -39,16 +40,6 @@ def show_help(o_inst, command): for command, helpmessage in helpmenu.items(): o_inst.showHelp(command) -def open_home(o_inst): - try: - url = o_inst.onionrUtils.getClientAPIServer() - except FileNotFoundError: - logger.error('Onionr seems to not be running (could not get api host)') - else: - url = 'http://%s/#%s' % (url, o_inst.onionrCore.config.get('client.webpassword')) - print('If Onionr does not open automatically, use this URL:', url) - webbrowser.open_new_tab(url) - def get_commands(onionr_inst): return {'': onionr_inst.showHelpSuggestion, 'help': onionr_inst.showHelp, diff --git a/onionr/onionrcommands/banblocks.py b/onionr/onionrcommands/banblocks.py index 474353f6..a9caa867 100644 --- a/onionr/onionrcommands/banblocks.py +++ b/onionr/onionrcommands/banblocks.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file contains the command for banning blocks from the node ''' diff --git a/onionr/onionrcommands/daemonlaunch.py b/onionr/onionrcommands/daemonlaunch.py index 98708258..4b343ede 100644 --- a/onionr/onionrcommands/daemonlaunch.py +++ b/onionr/onionrcommands/daemonlaunch.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication launch the api server and communicator ''' diff --git a/onionr/onionrcommands/filecommands.py b/onionr/onionrcommands/filecommands.py index f9d05f01..444fd147 100644 --- a/onionr/onionrcommands/filecommands.py +++ b/onionr/onionrcommands/filecommands.py @@ -1,3 +1,23 @@ +''' + Onionr - Private P2P Communication + + This file handles the commands for adding and getting files from the Onionr network +''' +''' + 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 base64, sys, os import logger from onionrblockapi import Block diff --git a/onionr/onionrcommands/keyadders.py b/onionr/onionrcommands/keyadders.py index d52b81f9..0edda6b0 100644 --- a/onionr/onionrcommands/keyadders.py +++ b/onionr/onionrcommands/keyadders.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication add keys (transport and pubkey) ''' diff --git a/onionr/onionrcommands/onionrstatistics.py b/onionr/onionrcommands/onionrstatistics.py index 8430a524..0974b290 100644 --- a/onionr/onionrcommands/onionrstatistics.py +++ b/onionr/onionrcommands/onionrstatistics.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This module defines commands to show stats/details about the local node ''' diff --git a/onionr/httpapi/simplecache/__init__.py b/onionr/onionrcommands/openwebinterface.py old mode 100755 new mode 100644 similarity index 55% rename from onionr/httpapi/simplecache/__init__.py rename to onionr/onionrcommands/openwebinterface.py index 75a645a0..823703a0 --- a/onionr/httpapi/simplecache/__init__.py +++ b/onionr/onionrcommands/openwebinterface.py @@ -1,7 +1,7 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication - This file creates http endpoints for friend management + Open the web interface properly into a web browser ''' ''' This program is free software: you can redistribute it and/or modify @@ -17,15 +17,14 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import core -from flask import Blueprint, Response, request, abort - -simplecache = Blueprint('simplecache', __name__) - -@simplecache.route('/get/') -def get_key(key): - return - -@simplecache.route('/set/', methods=['POST']) -def set_key(key): - return \ No newline at end of file +import webbrowser +import logger +def open_home(o_inst): + try: + url = o_inst.onionrUtils.getClientAPIServer() + except FileNotFoundError: + logger.error('Onionr seems to not be running (could not get api host)') + else: + url = 'http://%s/#%s' % (url, o_inst.onionrCore.config.get('client.webpassword')) + print('If Onionr does not open automatically, use this URL:', url) + webbrowser.open_new_tab(url) \ No newline at end of file diff --git a/onionr/onionrcommands/plugincommands.py b/onionr/onionrcommands/plugincommands.py index c357956f..e4f88d96 100644 --- a/onionr/onionrcommands/plugincommands.py +++ b/onionr/onionrcommands/plugincommands.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication plugin CLI commands ''' @@ -30,7 +30,6 @@ def enable_plugin(o_inst): logger.info('%s %s ' % (sys.argv[0], sys.argv[1])) def disable_plugin(o_inst): - if len(sys.argv) >= 3: plugin_name = sys.argv[2] logger.info('Disabling plugin "%s"...' % plugin_name) diff --git a/onionr/onionrcommands/pubkeymanager.py b/onionr/onionrcommands/pubkeymanager.py index 00d17175..a294396b 100644 --- a/onionr/onionrcommands/pubkeymanager.py +++ b/onionr/onionrcommands/pubkeymanager.py @@ -1,7 +1,7 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication - This module defines ID-related CLI commands + This module defines user ID-related CLI commands ''' ''' This program is free software: you can redistribute it and/or modify @@ -51,7 +51,7 @@ def change_ID(o_inst): try: key = sys.argv[2] except IndexError: - logger.error('Specify pubkey to use') + logger.warn('Specify pubkey to use') else: if o_inst.onionrUtils.validatePubKey(key): if key in o_inst.onionrCore._crypto.keyManager.getPubkeyList(): @@ -60,9 +60,9 @@ def change_ID(o_inst): logger.info('Set active key to: %s' % (key,)) logger.info('Restart Onionr if it is running.') else: - logger.error('That key does not exist') + logger.warn('That key does not exist') else: - logger.error('Invalid key %s' % (key,)) + logger.warn('Invalid key %s' % (key,)) def friend_command(o_inst): friend = '' @@ -86,7 +86,8 @@ def friend_command(o_inst): raise onionrexceptions.KeyNotKnown friend = onionrusers.OnionrUser(o_inst.onionrCore, friend) except IndexError: - logger.error('Friend ID is required.') + logger.warn('Friend ID is required.') + action = 'error' # set to 'error' so that the finally block does not process anything except onionrexceptions.KeyNotKnown: o_inst.onionrCore.addPeer(friend) friend = onionrusers.OnionrUser(o_inst.onionrCore, friend) @@ -94,7 +95,7 @@ def friend_command(o_inst): if action == 'add': friend.setTrust(1) logger.info('Added %s as friend.' % (friend.publicKey,)) - else: + elif action == 'remove': friend.setTrust(0) logger.info('Removed %s as friend.' % (friend.publicKey,)) else: diff --git a/onionr/onionrfragment/__init__.py b/onionr/onionrfragment/__init__.py deleted file mode 100755 index c8386465..00000000 --- a/onionr/onionrfragment/__init__.py +++ /dev/null @@ -1,73 +0,0 @@ -''' - Onionr - P2P Anonymous Storage Network - - This file contains the OnionrFragment class which implements the fragment system -''' -''' - 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 . -''' - -# onionr:10ch+10ch+10chgdecryptionkey -import core, sys, binascii, os - -FRAGMENT_SIZE = 0.25 -TRUNCATE_LENGTH = 30 - -class OnionrFragment: - def __init__(self, uri=None): - uri = uri.replace('onionr:', '') - count = 0 - blocks = [] - appendData = '' - key = '' - for x in uri: - if x == 'k': - key = uri[uri.index('k') + 1:] - appendData += x - if count == TRUNCATE_LENGTH: - blocks.append(appendData) - appendData = '' - count = 0 - count += 1 - self.key = key - self.blocks = blocks - return - - @staticmethod - def generateFragments(data=None, coreInst=None): - if coreInst is None: - coreInst = core.Core() - - key = os.urandom(32) - data = coreInst._crypto.symmetricEncrypt(data, key).decode() - blocks = [] - blockData = b"" - uri = "onionr:" - total = sys.getsizeof(data) - for x in data: - blockData += x.encode() - if round(len(blockData) / len(data), 3) > FRAGMENT_SIZE: - blocks.append(core.Core().insertBlock(blockData)) - blockData = b"" - - for bl in blocks: - uri += bl[:TRUNCATE_LENGTH] - uri += "k" - uri += binascii.hexlify(key).decode() - return (uri, key) - -if __name__ == '__main__': - uri = OnionrFragment.generateFragments("test")[0] - print(uri) - OnionrFragment(uri) \ No newline at end of file diff --git a/onionr/onionrservices/__init__.py b/onionr/onionrservices/__init__.py index 2faa4cdd..2792f7e3 100644 --- a/onionr/onionrservices/__init__.py +++ b/onionr/onionrservices/__init__.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication Onionr services provide the server component to direct connections ''' diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py index 160b716d..077b4657 100644 --- a/onionr/onionrservices/bootstrapservice.py +++ b/onionr/onionrservices/bootstrapservice.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication Bootstrap onion direct connections for the clients ''' diff --git a/onionr/onionrservices/httpheaders.py b/onionr/onionrservices/httpheaders.py index 2b2c1770..63c4a6ad 100644 --- a/onionr/onionrservices/httpheaders.py +++ b/onionr/onionrservices/httpheaders.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication Set default onionr http headers ''' From 3d93a37d0c063b0fbfea7978d16ae02709c668c4 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 12 Jun 2019 21:35:30 -0500 Subject: [PATCH 077/173] start removing daemontools by moving node announcement to communicatorutils file, deniable block insertion now config option --- onionr/communicator.py | 15 ++-- onionr/communicatorutils/README.md | 2 + onionr/communicatorutils/announcenode.py | 84 +++++++++++++++++++ onionr/communicatorutils/onionrdaemontools.py | 63 +------------- 4 files changed, 96 insertions(+), 68 deletions(-) create mode 100644 onionr/communicatorutils/announcenode.py diff --git a/onionr/communicator.py b/onionr/communicator.py index 5f31fc3c..e958d387 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -25,7 +25,7 @@ import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as p from communicatorutils import onionrdaemontools, servicecreator, onionrcommunicatortimers from communicatorutils import downloadblocks, lookupblocks, lookupadders from communicatorutils import servicecreator, connectnewpeers, uploadblocks -from communicatorutils import daemonqueuehandler +from communicatorutils import daemonqueuehandler, announcenode import onionrservices, onionr, onionrproofs OnionrCommunicatorTimers = onionrcommunicatortimers.OnionrCommunicatorTimers @@ -60,6 +60,8 @@ class OnionrCommunicatorDaemon: self.connectTimes = {} self.peerProfiles = [] # list of peer's profiles (onionrpeers.PeerProfile instances) self.newPeers = [] # Peers merged to us. Don't add to db until we know they're reachable + self.announceProgress = {} + self.announceCache = {} # amount of threads running by name, used to prevent too many self.threadCounts = {} @@ -121,7 +123,7 @@ class OnionrCommunicatorDaemon: OnionrCommunicatorTimers(self, self.uploadBlock, 10, requiresPeer=True, maxThreads=1) # Timer to process the daemon command queue - OnionrCommunicatorTimers(self, self.daemonCommands, 6, maxThreads=1) + OnionrCommunicatorTimers(self, self.daemonCommands, 6, maxThreads=3) # Timer that kills Onionr if the API server crashes OnionrCommunicatorTimers(self, self.detectAPICrash, 30, maxThreads=1) @@ -136,7 +138,9 @@ class OnionrCommunicatorDaemon: self.services = None # This timer creates deniable blocks, in an attempt to further obfuscate block insertion metadata - deniableBlockTimer = OnionrCommunicatorTimers(self, self.daemonTools.insertDeniableBlock, 180, requiresPeer=True, maxThreads=1) + if config.get('general.insert_deniable_blocks', True): + deniableBlockTimer = OnionrCommunicatorTimers(self, self.daemonTools.insertDeniableBlock, 180, requiresPeer=True, maxThreads=1) + deniableBlockTimer.count = (deniableBlockTimer.frequency - 175) # Timer to check for connectivity, through Tor to various high-profile onion services netCheckTimer = OnionrCommunicatorTimers(self, self.daemonTools.netCheck, 600) @@ -144,7 +148,7 @@ class OnionrCommunicatorDaemon: # Announce the public API server transport address to other nodes if security level allows if config.get('general.security_level', 1) == 0: # Default to high security level incase config breaks - announceTimer = OnionrCommunicatorTimers(self, self.daemonTools.announceNode, 3600, requiresPeer=True, maxThreads=1) + announceTimer = OnionrCommunicatorTimers(self, announcenode.announce_node, 3600, myArgs=[self], requiresPeer=True, maxThreads=1) announceTimer.count = (announceTimer.frequency - 120) else: logger.debug('Will not announce node.') @@ -158,7 +162,6 @@ class OnionrCommunicatorDaemon: # Adjust initial timer triggers peerPoolTimer.count = (peerPoolTimer.frequency - 1) cleanupTimer.count = (cleanupTimer.frequency - 60) - deniableBlockTimer.count = (deniableBlockTimer.frequency - 175) blockCleanupTimer.count = (blockCleanupTimer.frequency - 5) # Main daemon loop, mainly for calling timers, don't do any complex operations here to avoid locking @@ -358,7 +361,7 @@ class OnionrCommunicatorDaemon: def announce(self, peer): '''Announce to peers our address''' - if self.daemonTools.announceNode() == False: + if announcenode.announce_node(self) == False: logger.warn('Could not introduce node.') def detectAPICrash(self): diff --git a/onionr/communicatorutils/README.md b/onionr/communicatorutils/README.md index 2bd7caf1..5f37a122 100644 --- a/onionr/communicatorutils/README.md +++ b/onionr/communicatorutils/README.md @@ -4,6 +4,8 @@ The files in this submodule handle various subtasks and utilities for the onionr ## Files: +announcenode.py: Uses a communicator instance to announce our transport address to connected nodes + connectnewpeers.py: takes a communicator instance and has it connect to as many peers as needed, and/or to a new specified peer. daemonqueuehandler.py: checks for new commands in the daemon queue and processes them accordingly. diff --git a/onionr/communicatorutils/announcenode.py b/onionr/communicatorutils/announcenode.py new file mode 100644 index 00000000..6ba136ba --- /dev/null +++ b/onionr/communicatorutils/announcenode.py @@ -0,0 +1,84 @@ +''' + Onionr - Private P2P Communication + + Use a communicator instance to announce our transport address to connected nodes +''' +''' + 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 base64 +import onionrproofs, logger +from etc import onionrvalues + +def announce_node(daemon): + '''Announce our node to our peers''' + ov = onionrvalues.OnionrValues() + retData = False + announceFail = False + + # Do not let announceCache get too large + if len(daemon.announceCache) >= 10000: + daemon.announceCache.popitem() + + if daemon._core.config.get('general.security_level', 0) == 0: + # Announce to random online peers + for i in daemon.onlinePeers: + if not i in daemon.announceCache and not i in daemon.announceProgress: + peer = i + break + else: + peer = daemon.pickOnlinePeer() + + for x in range(1): + if x == 1 and daemon._core.config.get('i2p.host'): + ourID = daemon._core.config.get('i2p.own_addr').strip() + else: + ourID = daemon._core.hsAddress.strip() + + url = 'http://' + peer + '/announce' + data = {'node': ourID} + + combinedNodes = ourID + peer + if ourID != 1: + #TODO: Extend existingRand for i2p + existingRand = daemon._core._utils.bytesToStr(daemon._core.getAddressInfo(peer, 'powValue')) + # Reset existingRand if it no longer meets the minimum POW + if type(existingRand) is type(None) or not existingRand.endswith('0' * ov.announce_pow): + existingRand = '' + + if peer in daemon.announceCache: + data['random'] = self.announceCache[peer] + elif len(existingRand) > 0: + data['random'] = existingRand + else: + daemon.announceProgress[peer] = True + proof = onionrproofs.DataPOW(combinedNodes, forceDifficulty=ov.announce_pow) + del daemon.announceProgress[peer] + try: + data['random'] = base64.b64encode(proof.waitForResult()[1]) + except TypeError: + # Happens when we failed to produce a proof + logger.error("Failed to produce a pow for announcing to " + peer) + announceFail = True + else: + daemon.announceCache[peer] = data['random'] + if not announceFail: + logger.info('Announcing node to ' + url) + if daemon._core._utils.doPostRequest(url, data) == 'Success': + logger.info('Successfully introduced node to ' + peer) + retData = True + daemon._core.setAddressInfo(peer, 'introduced', 1) + daemon._core.setAddressInfo(peer, 'powValue', data['random']) + daemon.decrementThreadCount('announceNode') + return retData \ No newline at end of file diff --git a/onionr/communicatorutils/onionrdaemontools.py b/onionr/communicatorutils/onionrdaemontools.py index b18edd87..c0e74df3 100755 --- a/onionr/communicatorutils/onionrdaemontools.py +++ b/onionr/communicatorutils/onionrdaemontools.py @@ -1,7 +1,7 @@ ''' Onionr - Private P2P Communication - Contains the CommunicatorUtils class which contains useful functions for the communicator daemon + Contains the DaemonTools class ''' ''' This program is free software: you can redistribute it and/or modify @@ -37,67 +37,6 @@ class DaemonTools: self.announceProgress = {} self.announceCache = {} - def announceNode(self): - '''Announce our node to our peers''' - retData = False - announceFail = False - - # Do not let announceCache get too large - if len(self.announceCache) >= 10000: - self.announceCache.popitem() - - if self.daemon._core.config.get('general.security_level', 0) == 0: - # Announce to random online peers - for i in self.daemon.onlinePeers: - if not i in self.announceCache and not i in self.announceProgress: - peer = i - break - else: - peer = self.daemon.pickOnlinePeer() - - for x in range(1): - if x == 1 and self.daemon._core.config.get('i2p.host'): - ourID = self.daemon._core.config.get('i2p.own_addr').strip() - else: - ourID = self.daemon._core.hsAddress.strip() - - url = 'http://' + peer + '/announce' - data = {'node': ourID} - - combinedNodes = ourID + peer - if ourID != 1: - #TODO: Extend existingRand for i2p - existingRand = self.daemon._core._utils.bytesToStr(self.daemon._core.getAddressInfo(peer, 'powValue')) - # Reset existingRand if it no longer meets the minimum POW - if type(existingRand) is type(None) or not existingRand.endswith('0' * ov.announce_pow): - existingRand = '' - - if peer in self.announceCache: - data['random'] = self.announceCache[peer] - elif len(existingRand) > 0: - data['random'] = existingRand - else: - self.announceProgress[peer] = True - proof = onionrproofs.DataPOW(combinedNodes, forceDifficulty=ov.announce_pow) - del self.announceProgress[peer] - try: - data['random'] = base64.b64encode(proof.waitForResult()[1]) - except TypeError: - # Happens when we failed to produce a proof - logger.error("Failed to produce a pow for announcing to " + peer) - announceFail = True - else: - self.announceCache[peer] = data['random'] - if not announceFail: - logger.info('Announcing node to ' + url) - if self.daemon._core._utils.doPostRequest(url, data) == 'Success': - logger.info('Successfully introduced node to ' + peer) - retData = True - self.daemon._core.setAddressInfo(peer, 'introduced', 1) - self.daemon._core.setAddressInfo(peer, 'powValue', data['random']) - self.daemon.decrementThreadCount('announceNode') - return retData - def netCheck(self): '''Check if we are connected to the internet or not when we can't connect to any peers''' if len(self.daemon.onlinePeers) == 0: From 64944f6f7c24783aa968e05d2f11de654aa87027 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 13 Jun 2019 01:58:17 -0500 Subject: [PATCH 078/173] - removed onionrdaemontools (split into many files, mostly into communicatorutils) - removed included secrets.py since 3.6 is required now anyways --- onionr/communicator.py | 37 +- onionr/communicatorutils/README.md | 12 +- onionr/communicatorutils/announcenode.py | 2 +- onionr/communicatorutils/cooldownpeer.py | 51 +++ onionr/communicatorutils/deniableinserts.py | 31 ++ onionr/communicatorutils/housekeeping.py | 59 ++++ onionr/communicatorutils/netcheck.py | 32 ++ onionr/communicatorutils/onionrdaemontools.py | 158 --------- onionr/core.py | 2 +- onionr/dependencies/secrets.py | 331 ------------------ onionr/etc/humanreadabletime.py | 38 ++ onionr/etc/onionrvalues.py | 2 +- onionr/netcontroller.py | 3 +- onionr/onionrcrypto.py | 7 +- 14 files changed, 247 insertions(+), 518 deletions(-) create mode 100644 onionr/communicatorutils/cooldownpeer.py create mode 100644 onionr/communicatorutils/deniableinserts.py create mode 100644 onionr/communicatorutils/housekeeping.py create mode 100644 onionr/communicatorutils/netcheck.py delete mode 100755 onionr/communicatorutils/onionrdaemontools.py delete mode 100755 onionr/dependencies/secrets.py create mode 100644 onionr/etc/humanreadabletime.py diff --git a/onionr/communicator.py b/onionr/communicator.py index e958d387..e9eeb28f 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -22,10 +22,12 @@ import sys, os, time import core, config, logger, onionr import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block -from communicatorutils import onionrdaemontools, servicecreator, onionrcommunicatortimers +from communicatorutils import servicecreator, onionrcommunicatortimers from communicatorutils import downloadblocks, lookupblocks, lookupadders from communicatorutils import servicecreator, connectnewpeers, uploadblocks -from communicatorutils import daemonqueuehandler, announcenode +from communicatorutils import daemonqueuehandler, announcenode, deniableinserts +from communicatorutils import cooldownpeer, housekeeping, netcheck +from etc import humanreadabletime import onionrservices, onionr, onionrproofs OnionrCommunicatorTimers = onionrcommunicatortimers.OnionrCommunicatorTimers @@ -88,10 +90,6 @@ class OnionrCommunicatorDaemon: # Loads in and starts the enabled plugins plugins.reload() - # daemon tools are misc daemon functions, e.g. announce to online peers - # intended only for use by OnionrCommunicatorDaemon - self.daemonTools = onionrdaemontools.DaemonTools(self) - # time app started running for info/statistics purposes self.startTime = self._core._utils.getEpoch() @@ -111,13 +109,13 @@ class OnionrCommunicatorDaemon: OnionrCommunicatorTimers(self, self.clearOfflinePeer, 58) # Timer to cleanup old blocks - blockCleanupTimer = OnionrCommunicatorTimers(self, self.daemonTools.cleanOldBlocks, 65) + blockCleanupTimer = OnionrCommunicatorTimers(self, housekeeping.clean_old_blocks, 65, myArgs=[self]) # Timer to discover new peers OnionrCommunicatorTimers(self, self.lookupAdders, 60, requiresPeer=True) # Timer for adjusting which peers we actively communicate to at any given time, to avoid over-using peers - OnionrCommunicatorTimers(self, self.daemonTools.cooldownPeer, 30, requiresPeer=True) + OnionrCommunicatorTimers(self, cooldownpeer.cooldown_peer, 30, myArgs=[self], requiresPeer=True) # Timer to read the upload queue and upload the entries to peers OnionrCommunicatorTimers(self, self.uploadBlock, 10, requiresPeer=True, maxThreads=1) @@ -133,17 +131,17 @@ class OnionrCommunicatorDaemon: self.services = onionrservices.OnionrServices(self._core) self.active_services = [] self.service_greenlets = [] - OnionrCommunicatorTimers(self, servicecreator.service_creator, 5, maxThreads=50, myArgs=(self,)) + OnionrCommunicatorTimers(self, servicecreator.service_creator, 5, maxThreads=50, myArgs=[self]) else: self.services = None # This timer creates deniable blocks, in an attempt to further obfuscate block insertion metadata if config.get('general.insert_deniable_blocks', True): - deniableBlockTimer = OnionrCommunicatorTimers(self, self.daemonTools.insertDeniableBlock, 180, requiresPeer=True, maxThreads=1) + deniableBlockTimer = OnionrCommunicatorTimers(self, deniableinserts.insert_deniable_block, 180, myArgs=[self], requiresPeer=True, maxThreads=1) deniableBlockTimer.count = (deniableBlockTimer.frequency - 175) # Timer to check for connectivity, through Tor to various high-profile onion services - netCheckTimer = OnionrCommunicatorTimers(self, self.daemonTools.netCheck, 600) + netCheckTimer = OnionrCommunicatorTimers(self, netcheck.net_check, 600, myArgs=[self]) # Announce the public API server transport address to other nodes if security level allows if config.get('general.security_level', 1) == 0: @@ -157,7 +155,7 @@ class OnionrCommunicatorDaemon: cleanupTimer = OnionrCommunicatorTimers(self, self.peerCleanup, 300, requiresPeer=True) # Timer to cleanup dead ephemeral forward secrecy keys - forwardSecrecyTimer = OnionrCommunicatorTimers(self, self.daemonTools.cleanKeys, 15, maxThreads=1) + forwardSecrecyTimer = OnionrCommunicatorTimers(self, housekeeping.clean_keys, 15, myArgs=[self], maxThreads=1) # Adjust initial timer triggers peerPoolTimer.count = (peerPoolTimer.frequency - 1) @@ -255,7 +253,7 @@ class OnionrCommunicatorDaemon: break else: if len(self.onlinePeers) == 0: - logger.debug('Couldn\'t connect to any peers.' + (' Last node seen %s ago.' % self.daemonTools.humanReadableTime(time.time() - self.lastNodeSeen) if not self.lastNodeSeen is None else '')) + logger.debug('Couldn\'t connect to any peers.' + (' Last node seen %s ago.' % humanreadabletime.human_readable_time(time.time() - self.lastNodeSeen) if not self.lastNodeSeen is None else '')) else: self.lastNodeSeen = time.time() self.decrementThreadCount('getOnlinePeers') @@ -346,7 +344,7 @@ class OnionrCommunicatorDaemon: def heartbeat(self): '''Show a heartbeat debug message''' - logger.debug('Heartbeat. Node running for %s.' % self.daemonTools.humanReadableTime(self.getUptime())) + logger.debug('Heartbeat. Node running for %s.' % humanreadabletime.human_readable_time(self.getUptime())) self.decrementThreadCount('heartbeat') def daemonCommands(self): @@ -379,10 +377,17 @@ class OnionrCommunicatorDaemon: self.decrementThreadCount('detectAPICrash') def runCheck(self): - if self.daemonTools.runCheck(): + if run_file_exists(self): logger.debug('Status check; looks good.') self.decrementThreadCount('runCheck') def startCommunicator(onionrInst, proxyPort): - OnionrCommunicatorDaemon(onionrInst, proxyPort) \ No newline at end of file + OnionrCommunicatorDaemon(onionrInst, proxyPort) + +def run_file_exists(daemon): + if os.path.isfile(daemon._core.dataDir + '.runcheck'): + os.remove(daemon._core.dataDir + '.runcheck') + return True + + return False \ No newline at end of file diff --git a/onionr/communicatorutils/README.md b/onionr/communicatorutils/README.md index 5f37a122..393328c3 100644 --- a/onionr/communicatorutils/README.md +++ b/onionr/communicatorutils/README.md @@ -8,15 +8,23 @@ announcenode.py: Uses a communicator instance to announce our transport address connectnewpeers.py: takes a communicator instance and has it connect to as many peers as needed, and/or to a new specified peer. +cooldownpeer.py: randomly selects a connected peer in a communicator and disconnects them for the purpose of security and network balancing. + daemonqueuehandler.py: checks for new commands in the daemon queue and processes them accordingly. +deniableinserts.py: insert fake blocks with the communicator for plausible deniability + downloadblocks.py: iterates a communicator instance's block download queue and attempts to download the blocks from online peers +housekeeping.py: cleans old blocks and forward secrecy keys + lookupadders.py: ask connected peers to share their list of peer transport addresses -onionrcommunicataortimers.py: create a timer for a function to be launched on an interval. Control how many possible instances of a timer may be running a function at once and control if the timer should be ran in a thread or not. +lookupblocks.py: lookup new blocks from connected peers from the communicator -onionrdaemontools.py: contains the DaemonTools class which has a lot of etc functions useful for the communicator. Deprecated. +netcheck.py: check if the node is online based on communicator status and onion server ping results + +onionrcommunicataortimers.py: create a timer for a function to be launched on an interval. Control how many possible instances of a timer may be running a function at once and control if the timer should be ran in a thread or not. proxypicker.py: returns a string name for the appropriate proxy to be used with a particular peer transport address. diff --git a/onionr/communicatorutils/announcenode.py b/onionr/communicatorutils/announcenode.py index 6ba136ba..e3f7d268 100644 --- a/onionr/communicatorutils/announcenode.py +++ b/onionr/communicatorutils/announcenode.py @@ -80,5 +80,5 @@ def announce_node(daemon): retData = True daemon._core.setAddressInfo(peer, 'introduced', 1) daemon._core.setAddressInfo(peer, 'powValue', data['random']) - daemon.decrementThreadCount('announceNode') + daemon.decrementThreadCount('announce_node') return retData \ No newline at end of file diff --git a/onionr/communicatorutils/cooldownpeer.py b/onionr/communicatorutils/cooldownpeer.py new file mode 100644 index 00000000..26cf2dd9 --- /dev/null +++ b/onionr/communicatorutils/cooldownpeer.py @@ -0,0 +1,51 @@ +''' + Onionr - Private P2P Communication + + Select a random online peer in a communicator instance and have them "cool down" +''' +''' + 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 . +''' +def cooldown_peer(comm_inst): + '''Randomly add an online peer to cooldown, so we can connect a new one''' + onlinePeerAmount = len(comm_inst.onlinePeers) + minTime = 300 + cooldownTime = 600 + toCool = '' + tempConnectTimes = dict(comm_inst.connectTimes) + + # Remove peers from cooldown that have been there long enough + tempCooldown = dict(comm_inst.cooldownPeer) + for peer in tempCooldown: + if (comm_inst._core._utils.getEpoch() - tempCooldown[peer]) >= cooldownTime: + del comm_inst.cooldownPeer[peer] + + # Cool down a peer, if we have max connections alive for long enough + if onlinePeerAmount >= comm_inst._core.config.get('peers.max_connect', 10, save = True): + finding = True + + while finding: + try: + toCool = min(tempConnectTimes, key=tempConnectTimes.get) + if (comm_inst._core._utils.getEpoch() - tempConnectTimes[toCool]) < minTime: + del tempConnectTimes[toCool] + else: + finding = False + except ValueError: + break + else: + comm_inst.removeOnlinePeer(toCool) + comm_inst.cooldownPeer[toCool] = comm_inst._core._utils.getEpoch() + + comm_inst.decrementThreadCount('cooldown_peer') \ No newline at end of file diff --git a/onionr/communicatorutils/deniableinserts.py b/onionr/communicatorutils/deniableinserts.py new file mode 100644 index 00000000..f993e6d0 --- /dev/null +++ b/onionr/communicatorutils/deniableinserts.py @@ -0,0 +1,31 @@ +''' + Onionr - Private P2P Communication + + Use the communicator to insert fake mail messages +''' +''' + 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 secrets +from etc import onionrvalues +def insert_deniable_block(comm_inst): + '''Insert a fake block in order to make it more difficult to track real blocks''' + fakePeer = '' + chance = 10 + if secrets.randbelow(chance) == (chance - 1): + # This assumes on the libsodium primitives to have key-privacy + fakePeer = onionrvalues.DENIABLE_PEER_ADDRESS + data = secrets.token_hex(secrets.randbelow(1024) + 1) + comm_inst._core.insertBlock(data, header='pm', encryptType='asym', asymPeer=fakePeer, meta={'subject': 'foo'}) + comm_inst.decrementThreadCount('insert_deniable_block') \ No newline at end of file diff --git a/onionr/communicatorutils/housekeeping.py b/onionr/communicatorutils/housekeeping.py new file mode 100644 index 00000000..829564ab --- /dev/null +++ b/onionr/communicatorutils/housekeeping.py @@ -0,0 +1,59 @@ +''' + Onionr - Private P2P Communication + + Cleanup old Onionr blocks and forward secrecy keys using the communicator. Ran from a timer usually +''' +''' + 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 sqlite3 +import logger +from onionrusers import onionrusers +def clean_old_blocks(comm_inst): + '''Delete old blocks if our disk allocation is full/near full, and also expired blocks''' + + # Delete expired blocks + for bHash in comm_inst._core.getExpiredBlocks(): + comm_inst._core._blacklist.addToDB(bHash) + comm_inst._core.removeBlock(bHash) + logger.info('Deleted block: %s' % (bHash,)) + + while comm_inst._core._utils.storageCounter.isFull(): + oldest = comm_inst._core.getBlockList()[0] + comm_inst._core._blacklist.addToDB(oldest) + comm_inst._core.removeBlock(oldest) + logger.info('Deleted block: %s' % (oldest,)) + + comm_inst.decrementThreadCount('clean_old_blocks') + +def clean_keys(comm_inst): + '''Delete expired forward secrecy keys''' + conn = sqlite3.connect(comm_inst._core.peerDB, timeout=10) + c = conn.cursor() + time = comm_inst._core._utils.getEpoch() + deleteKeys = [] + + for entry in c.execute("SELECT * FROM forwardKeys WHERE expire <= ?", (time,)): + logger.debug('Forward key: %s' % entry[1]) + deleteKeys.append(entry[1]) + + for key in deleteKeys: + logger.debug('Deleting forward key %s' % key) + c.execute("DELETE from forwardKeys where forwardKey = ?", (key,)) + conn.commit() + conn.close() + + onionrusers.deleteExpiredKeys(comm_inst._core) + + comm_inst.decrementThreadCount('clean_keys') \ No newline at end of file diff --git a/onionr/communicatorutils/netcheck.py b/onionr/communicatorutils/netcheck.py new file mode 100644 index 00000000..36a3a304 --- /dev/null +++ b/onionr/communicatorutils/netcheck.py @@ -0,0 +1,32 @@ +''' + Onionr - Private P2P Communication + + Determine if our node is able to use Tor based on the status of a communicator instance + and the result of pinging onion http servers +''' +''' + 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 logger +from utils import netutils +def net_check(comm_inst): + '''Check if we are connected to the internet or not when we can't connect to any peers''' + if len(comm_inst.onlinePeers) == 0: + if not netutils.checkNetwork(comm_inst._core._utils, torPort=comm_inst.proxyPort): + if not comm_inst.shutdown: + logger.warn('Network check failed, are you connected to the Internet, and is Tor working?') + comm_inst.isOnline = False + else: + comm_inst.isOnline = True + comm_inst.decrementThreadCount('net_check') \ No newline at end of file diff --git a/onionr/communicatorutils/onionrdaemontools.py b/onionr/communicatorutils/onionrdaemontools.py deleted file mode 100755 index c0e74df3..00000000 --- a/onionr/communicatorutils/onionrdaemontools.py +++ /dev/null @@ -1,158 +0,0 @@ -''' - Onionr - Private P2P Communication - - Contains the DaemonTools class -''' -''' - 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 . -''' - -# MODULE DEPRECATED - -import onionrexceptions, onionrpeers, onionrproofs, logger -import base64, sqlite3, os -from dependencies import secrets -from utils import netutils -from onionrusers import onionrusers -from etc import onionrvalues -ov = onionrvalues.OnionrValues() - -class DaemonTools: - ''' - Class intended for use by Onionr Communicator - ''' - def __init__(self, daemon): - self.daemon = daemon - self.announceProgress = {} - self.announceCache = {} - - def netCheck(self): - '''Check if we are connected to the internet or not when we can't connect to any peers''' - if len(self.daemon.onlinePeers) == 0: - if not netutils.checkNetwork(self.daemon._core._utils, torPort=self.daemon.proxyPort): - if not self.daemon.shutdown: - logger.warn('Network check failed, are you connected to the Internet, and is Tor working?') - self.daemon.isOnline = False - else: - self.daemon.isOnline = True - self.daemon.decrementThreadCount('netCheck') - - def cleanOldBlocks(self): - '''Delete old blocks if our disk allocation is full/near full, and also expired blocks''' - - # Delete expired blocks - for bHash in self.daemon._core.getExpiredBlocks(): - self.daemon._core._blacklist.addToDB(bHash) - self.daemon._core.removeBlock(bHash) - logger.info('Deleted block: %s' % (bHash,)) - - while self.daemon._core._utils.storageCounter.isFull(): - oldest = self.daemon._core.getBlockList()[0] - self.daemon._core._blacklist.addToDB(oldest) - self.daemon._core.removeBlock(oldest) - logger.info('Deleted block: %s' % (oldest,)) - - self.daemon.decrementThreadCount('cleanOldBlocks') - - def cleanKeys(self): - '''Delete expired forward secrecy keys''' - conn = sqlite3.connect(self.daemon._core.peerDB, timeout=10) - c = conn.cursor() - time = self.daemon._core._utils.getEpoch() - deleteKeys = [] - - for entry in c.execute("SELECT * FROM forwardKeys WHERE expire <= ?", (time,)): - logger.debug('Forward key: %s' % entry[1]) - deleteKeys.append(entry[1]) - - for key in deleteKeys: - logger.debug('Deleting forward key %s' % key) - c.execute("DELETE from forwardKeys where forwardKey = ?", (key,)) - conn.commit() - conn.close() - - onionrusers.deleteExpiredKeys(self.daemon._core) - - self.daemon.decrementThreadCount('cleanKeys') - - def cooldownPeer(self): - '''Randomly add an online peer to cooldown, so we can connect a new one''' - onlinePeerAmount = len(self.daemon.onlinePeers) - minTime = 300 - cooldownTime = 600 - toCool = '' - tempConnectTimes = dict(self.daemon.connectTimes) - - # Remove peers from cooldown that have been there long enough - tempCooldown = dict(self.daemon.cooldownPeer) - for peer in tempCooldown: - if (self.daemon._core._utils.getEpoch() - tempCooldown[peer]) >= cooldownTime: - del self.daemon.cooldownPeer[peer] - - # Cool down a peer, if we have max connections alive for long enough - if onlinePeerAmount >= self.daemon._core.config.get('peers.max_connect', 10, save = True): - finding = True - - while finding: - try: - toCool = min(tempConnectTimes, key=tempConnectTimes.get) - if (self.daemon._core._utils.getEpoch() - tempConnectTimes[toCool]) < minTime: - del tempConnectTimes[toCool] - else: - finding = False - except ValueError: - break - else: - self.daemon.removeOnlinePeer(toCool) - self.daemon.cooldownPeer[toCool] = self.daemon._core._utils.getEpoch() - - self.daemon.decrementThreadCount('cooldownPeer') - - def runCheck(self): - if os.path.isfile(self.daemon._core.dataDir + '.runcheck'): - os.remove(self.daemon._core.dataDir + '.runcheck') - return True - - return False - - def humanReadableTime(self, seconds): - build = '' - - units = { - 'year' : 31557600, - 'month' : (31557600 / 12), - 'day' : 86400, - 'hour' : 3600, - 'minute' : 60, - 'second' : 1 - } - - for unit in units: - amnt_unit = int(seconds / units[unit]) - if amnt_unit >= 1: - seconds -= amnt_unit * units[unit] - build += '%s %s' % (amnt_unit, unit) + ('s' if amnt_unit != 1 else '') + ' ' - - return build.strip() - - def insertDeniableBlock(self): - '''Insert a fake block in order to make it more difficult to track real blocks''' - fakePeer = '' - chance = 10 - if secrets.randbelow(chance) == (chance - 1): - # This assumes on the libsodium primitives to have key-privacy - fakePeer = 'OVPCZLOXD6DC5JHX4EQ3PSOGAZ3T24F75HQLIUZSDSMYPEOXCPFA====' - data = secrets.token_hex(secrets.randbelow(1024) + 1) - self.daemon._core.insertBlock(data, header='pm', encryptType='asym', asymPeer=fakePeer, meta={'subject': 'foo'}) - self.daemon.decrementThreadCount('insertDeniableBlock') \ No newline at end of file diff --git a/onionr/core.py b/onionr/core.py index 58c95d9f..53d834f6 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -808,7 +808,7 @@ class Core: self._utils.processBlockMetadata(retData) if retData != False: - if plaintextPeer == 'OVPCZLOXD6DC5JHX4EQ3PSOGAZ3T24F75HQLIUZSDSMYPEOXCPFA====': + if plaintextPeer == onionrvalues.DENIABLE_PEER_ADDRESS: events.event('insertdeniable', {'content': plaintext, 'meta': plaintextMeta, 'hash': retData, 'peer': self._utils.bytesToStr(asymPeer)}, onionr = self.onionrInst, threaded = True) else: events.event('insertblock', {'content': plaintext, 'meta': plaintextMeta, 'hash': retData, 'peer': self._utils.bytesToStr(asymPeer)}, onionr = self.onionrInst, threaded = True) diff --git a/onionr/dependencies/secrets.py b/onionr/dependencies/secrets.py deleted file mode 100755 index 9b2eb61b..00000000 --- a/onionr/dependencies/secrets.py +++ /dev/null @@ -1,331 +0,0 @@ -"""Generate cryptographically strong pseudo-random numbers suitable for -managing secrets such as account authentication, tokens, and similar. - -See PEP 506 for more information. -https://www.python.org/dev/peps/pep-0506/ - - -A. HISTORY OF THE SOFTWARE -========================== - -Python was created in the early 1990s by Guido van Rossum at Stichting -Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands -as a successor of a language called ABC. Guido remains Python's -principal author, although it includes many contributions from others. - -In 1995, Guido continued his work on Python at the Corporation for -National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) -in Reston, Virginia where he released several versions of the -software. - -In May 2000, Guido and the Python core development team moved to -BeOpen.com to form the BeOpen PythonLabs team. In October of the same -year, the PythonLabs team moved to Digital Creations, which became -Zope Corporation. In 2001, the Python Software Foundation (PSF, see -https://www.python.org/psf/) was formed, a non-profit organization -created specifically to own Python-related Intellectual Property. -Zope Corporation was a sponsoring member of the PSF. - -All Python releases are Open Source (see http://www.opensource.org for -the Open Source Definition). Historically, most, but not all, Python -releases have also been GPL-compatible; the table below summarizes -the various releases. - - Release Derived Year Owner GPL- - from compatible? (1) - - 0.9.0 thru 1.2 1991-1995 CWI yes - 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes - 1.6 1.5.2 2000 CNRI no - 2.0 1.6 2000 BeOpen.com no - 1.6.1 1.6 2001 CNRI yes (2) - 2.1 2.0+1.6.1 2001 PSF no - 2.0.1 2.0+1.6.1 2001 PSF yes - 2.1.1 2.1+2.0.1 2001 PSF yes - 2.1.2 2.1.1 2002 PSF yes - 2.1.3 2.1.2 2002 PSF yes - 2.2 and above 2.1.1 2001-now PSF yes - -Footnotes: - -(1) GPL-compatible doesn't mean that we're distributing Python under - the GPL. All Python licenses, unlike the GPL, let you distribute - a modified version without making your changes open source. The - GPL-compatible licenses make it possible to combine Python with - other software that is released under the GPL; the others don't. - -(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, - because its license has a choice of law clause. According to - CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 - is "not incompatible" with the GPL. - -Thanks to the many outside volunteers who have worked under Guido's -direction to make these releases possible. - - -B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON -=============================================================== - -PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 --------------------------------------------- - -1. This LICENSE AGREEMENT is between the Python Software Foundation -("PSF"), and the Individual or Organization ("Licensee") accessing and -otherwise using this software ("Python") in source or binary form and -its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, PSF hereby -grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, -analyze, test, perform and/or display publicly, prepare derivative works, -distribute, and otherwise use Python alone or in any derivative version, -provided, however, that PSF's License Agreement and PSF's notice of copyright, -i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 Python Software Foundation; All -Rights Reserved" are retained in Python alone or in any derivative version -prepared by Licensee. - -3. In the event Licensee prepares a derivative work that is based on -or incorporates Python or any part thereof, and wants to make -the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to Python. - -4. PSF is making Python available to Licensee on an "AS IS" -basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. Nothing in this License Agreement shall be deemed to create any -relationship of agency, partnership, or joint venture between PSF and -Licensee. This License Agreement does not grant permission to use PSF -trademarks or trade name in a trademark sense to endorse or promote -products or services of Licensee, or any third party. - -8. By copying, installing or otherwise using Python, Licensee -agrees to be bound by the terms and conditions of this License -Agreement. - - -BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 -------------------------------------------- - -BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 - -1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an -office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the -Individual or Organization ("Licensee") accessing and otherwise using -this software in source or binary form and its associated -documentation ("the Software"). - -2. Subject to the terms and conditions of this BeOpen Python License -Agreement, BeOpen hereby grants Licensee a non-exclusive, -royalty-free, world-wide license to reproduce, analyze, test, perform -and/or display publicly, prepare derivative works, distribute, and -otherwise use the Software alone or in any derivative version, -provided, however, that the BeOpen Python License is retained in the -Software, alone or in any derivative version prepared by Licensee. - -3. BeOpen is making the Software available to Licensee on an "AS IS" -basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE -SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS -AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY -DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -5. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -6. This License Agreement shall be governed by and interpreted in all -respects by the law of the State of California, excluding conflict of -law provisions. Nothing in this License Agreement shall be deemed to -create any relationship of agency, partnership, or joint venture -between BeOpen and Licensee. This License Agreement does not grant -permission to use BeOpen trademarks or trade names in a trademark -sense to endorse or promote products or services of Licensee, or any -third party. As an exception, the "BeOpen Python" logos available at -http://www.pythonlabs.com/logos.html may be used according to the -permissions granted on that web page. - -7. By copying, installing or otherwise using the software, Licensee -agrees to be bound by the terms and conditions of this License -Agreement. - - -CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 ---------------------------------------- - -1. This LICENSE AGREEMENT is between the Corporation for National -Research Initiatives, having an office at 1895 Preston White Drive, -Reston, VA 20191 ("CNRI"), and the Individual or Organization -("Licensee") accessing and otherwise using Python 1.6.1 software in -source or binary form and its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, CNRI -hereby grants Licensee a nonexclusive, royalty-free, world-wide -license to reproduce, analyze, test, perform and/or display publicly, -prepare derivative works, distribute, and otherwise use Python 1.6.1 -alone or in any derivative version, provided, however, that CNRI's -License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) -1995-2001 Corporation for National Research Initiatives; All Rights -Reserved" are retained in Python 1.6.1 alone or in any derivative -version prepared by Licensee. Alternately, in lieu of CNRI's License -Agreement, Licensee may substitute the following text (omitting the -quotes): "Python 1.6.1 is made available subject to the terms and -conditions in CNRI's License Agreement. This Agreement together with -Python 1.6.1 may be located on the Internet using the following -unique, persistent identifier (known as a handle): 1895.22/1013. This -Agreement may also be obtained from a proxy server on the Internet -using the following URL: http://hdl.handle.net/1895.22/1013". - -3. In the event Licensee prepares a derivative work that is based on -or incorporates Python 1.6.1 or any part thereof, and wants to make -the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to Python 1.6.1. - -4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" -basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. This License Agreement shall be governed by the federal -intellectual property law of the United States, including without -limitation the federal copyright law, and, to the extent such -U.S. federal law does not apply, by the law of the Commonwealth of -Virginia, excluding Virginia's conflict of law provisions. -Notwithstanding the foregoing, with regard to derivative works based -on Python 1.6.1 that incorporate non-separable material that was -previously distributed under the GNU General Public License (GPL), the -law of the Commonwealth of Virginia shall govern this License -Agreement only as to issues arising under or with respect to -Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this -License Agreement shall be deemed to create any relationship of -agency, partnership, or joint venture between CNRI and Licensee. This -License Agreement does not grant permission to use CNRI trademarks or -trade name in a trademark sense to endorse or promote products or -services of Licensee, or any third party. - -8. By clicking on the "ACCEPT" button where indicated, or by copying, -installing or otherwise using Python 1.6.1, Licensee agrees to be -bound by the terms and conditions of this License Agreement. - - ACCEPT - - -CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 --------------------------------------------------- - -Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, -The Netherlands. All rights reserved. - -Permission to use, copy, modify, and distribute this software and its -documentation for any purpose and without fee is hereby granted, -provided that the above copyright notice appear in all copies and that -both that copyright notice and this permission notice appear in -supporting documentation, and that the name of Stichting Mathematisch -Centrum or CWI not be used in advertising or publicity pertaining to -distribution of the software without specific, written prior -permission. - -STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO -THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE -FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT -OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - -""" - -__all__ = ['choice', 'randbelow', 'randbits', 'SystemRandom', - 'token_bytes', 'token_hex', 'token_urlsafe', - 'compare_digest', - ] - - -import base64 -import binascii -import os - -from hmac import compare_digest -from random import SystemRandom - -_sysrand = SystemRandom() - -randbits = _sysrand.getrandbits -choice = _sysrand.choice - -def randbelow(exclusive_upper_bound): - """Return a random int in the range [0, n).""" - if exclusive_upper_bound <= 0: - raise ValueError("Upper bound must be positive.") - return _sysrand._randbelow(exclusive_upper_bound) - -DEFAULT_ENTROPY = 32 # number of bytes to return by default - -def token_bytes(nbytes=None): - """Return a random byte string containing *nbytes* bytes. - - If *nbytes* is ``None`` or not supplied, a reasonable - default is used. - - >>> token_bytes(16) #doctest:+SKIP - b'\\xebr\\x17D*t\\xae\\xd4\\xe3S\\xb6\\xe2\\xebP1\\x8b' - - """ - if nbytes is None: - nbytes = DEFAULT_ENTROPY - return os.urandom(nbytes) - -def token_hex(nbytes=None): - """Return a random text string, in hexadecimal. - - The string has *nbytes* random bytes, each byte converted to two - hex digits. If *nbytes* is ``None`` or not supplied, a reasonable - default is used. - - >>> token_hex(16) #doctest:+SKIP - 'f9bf78b9a18ce6d46a0cd2b0b86df9da' - - """ - return binascii.hexlify(token_bytes(nbytes)).decode('ascii') - -def token_urlsafe(nbytes=None): - """Return a random URL-safe text string, in Base64 encoding. - - The string has *nbytes* random bytes. If *nbytes* is ``None`` - or not supplied, a reasonable default is used. - - >>> token_urlsafe(16) #doctest:+SKIP - 'Drmhze6EPcv0fN_81Bj-nA' - - """ - tok = token_bytes(nbytes) - return base64.urlsafe_b64encode(tok).rstrip(b'=').decode('ascii') - diff --git a/onionr/etc/humanreadabletime.py b/onionr/etc/humanreadabletime.py new file mode 100644 index 00000000..7e3ff3bc --- /dev/null +++ b/onionr/etc/humanreadabletime.py @@ -0,0 +1,38 @@ +''' + Onionr - Private P2P Communication + + human_readable_time takes integer seconds and returns a human readable string +''' +''' + 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 . +''' +def human_readable_time(seconds): + build = '' + + units = { + 'year' : 31557600, + 'month' : (31557600 / 12), + 'day' : 86400, + 'hour' : 3600, + 'minute' : 60, + 'second' : 1 + } + + for unit in units: + amnt_unit = int(seconds / units[unit]) + if amnt_unit >= 1: + seconds -= amnt_unit * units[unit] + build += '%s %s' % (amnt_unit, unit) + ('s' if amnt_unit != 1 else '') + ' ' + + return build.strip() \ No newline at end of file diff --git a/onionr/etc/onionrvalues.py b/onionr/etc/onionrvalues.py index e3e1421d..5df394db 100755 --- a/onionr/etc/onionrvalues.py +++ b/onionr/etc/onionrvalues.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' - +DENIABLE_PEER_ADDRESS = "OVPCZLOXD6DC5JHX4EQ3PSOGAZ3T24F75HQLIUZSDSMYPEOXCPFA====" class OnionrValues: def __init__(self): self.passwordLength = 20 diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py index fa64c938..6edbd474 100755 --- a/onionr/netcontroller.py +++ b/onionr/netcontroller.py @@ -17,11 +17,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import subprocess, os, random, sys, time, signal, base64, socket +import subprocess, os, sys, time, signal, base64, socket from shutil import which import logger, config from onionrblockapi import Block -from dependencies import secrets def getOpenPort(): # taken from (but modified) https://stackoverflow.com/a/2838309 by https://stackoverflow.com/users/133374/albert ccy-by-sa-3 https://creativecommons.org/licenses/by-sa/3.0/ diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py index 2ce90bf6..dab3e8b7 100755 --- a/onionr/onionrcrypto.py +++ b/onionr/onionrcrypto.py @@ -17,15 +17,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import os, binascii, base64, hashlib, time, sys, hmac +import os, binascii, base64, hashlib, time, sys, hmac, secrets import nacl.signing, nacl.encoding, nacl.public, nacl.hash, nacl.pwhash, nacl.utils, nacl.secret import logger, onionrproofs import onionrexceptions, keymanager, core -# secrets module was added into standard lib in 3.6+ -if sys.version_info[0] == 3 and sys.version_info[1] < 6: - from dependencies import secrets -elif sys.version_info[0] == 3 and sys.version_info[1] >= 6: - import secrets import config class OnionrCrypto: From 5edf0e234abf839d9dd8a14222e9ac4d3b0c5833 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 13 Jun 2019 11:24:34 -0500 Subject: [PATCH 079/173] added more subdirectory readmes --- onionr/etc/README.md | 6 ++++++ onionr/httpapi/README.md | 2 +- onionr/onionrservices/README.md | 13 +++++++++++++ onionr/onionrusers/contactmanager.py | 2 +- onionr/onionrusers/onionrusers.py | 2 +- 5 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 onionr/onionrservices/README.md diff --git a/onionr/etc/README.md b/onionr/etc/README.md index c5c054ea..9b310d28 100644 --- a/onionr/etc/README.md +++ b/onionr/etc/README.md @@ -1,5 +1,11 @@ # etc +Files that don't really fit anywhere else, but aren't used very frequently. + +## Files + +humanreadabletime.py: take integer seconds and return a human readable time string + pgpwords.py: represent data using the pgp word list: https://en.wikipedia.org/wiki/PGP_word_list onionrvalues.py: spec values for onionr blocks and other things \ No newline at end of file diff --git a/onionr/httpapi/README.md b/onionr/httpapi/README.md index b048b2c3..a4cf7eeb 100644 --- a/onionr/httpapi/README.md +++ b/onionr/httpapi/README.md @@ -1,6 +1,6 @@ # httpapi -The httpapi contains collections of endpoints for the client API server. +The httpapi contains collections of endpoints for the client and public API servers. ## Files: diff --git a/onionr/onionrservices/README.md b/onionr/onionrservices/README.md new file mode 100644 index 00000000..2022231c --- /dev/null +++ b/onionr/onionrservices/README.md @@ -0,0 +1,13 @@ +# onionrservices + +onionservices is a submodule to handle direct connections to Onionr peers, using the Onionr network to broker them. + +## Files + +__init__.py: Contains the OnionrServices class which can create direct connection servers or clients. + +bootstrapservice.py: Creates a bootstrap server for a peer and announces the connection by creating a block encrypted to the peer we want to connect to. + +connectionserver.py: Creates a direct connection server for a peer + +httpheaders.py: Modifies a Flask response object http response headers for security purposes. \ No newline at end of file diff --git a/onionr/onionrusers/contactmanager.py b/onionr/onionrusers/contactmanager.py index c47bbe45..0524ed64 100755 --- a/onionr/onionrusers/contactmanager.py +++ b/onionr/onionrusers/contactmanager.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication Sets more abstract information related to a peer. Can be thought of as traditional 'contact' system ''' diff --git a/onionr/onionrusers/onionrusers.py b/onionr/onionrusers/onionrusers.py index e05aac0e..9e26d047 100755 --- a/onionr/onionrusers/onionrusers.py +++ b/onionr/onionrusers/onionrusers.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication Contains abstractions for interacting with users of Onionr ''' From c85bb73d776cc1f1ef935e0683e0f27121c66f66 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 13 Jun 2019 14:53:20 -0500 Subject: [PATCH 080/173] documented onionrusers better with readme --- onionr/onionrusers/README.md | 9 +++++++++ onionr/onionrusers/__init__.py | 0 2 files changed, 9 insertions(+) create mode 100644 onionr/onionrusers/README.md create mode 100644 onionr/onionrusers/__init__.py diff --git a/onionr/onionrusers/README.md b/onionr/onionrusers/README.md new file mode 100644 index 00000000..e85c0952 --- /dev/null +++ b/onionr/onionrusers/README.md @@ -0,0 +1,9 @@ +# onionrusers + +onionrusers is a small collection of classes for interacting with onionr public keys, such as encrypting messages to them with forward secrecy, interacting with their settings, or else. + +## Files + +onionrusers.py: OnionrUsers class can be used to encrypt/decrypt messages to a particular Onionr user (incl. forward secrecy), view information about them, and get our friend list. + +contactmanager.py: Inheriting from OnionrUsers, ContactManager allows arbitrary information to be associated with an Onionr user. \ No newline at end of file diff --git a/onionr/onionrusers/__init__.py b/onionr/onionrusers/__init__.py new file mode 100644 index 00000000..e69de29b From 2d78d3d017296f10830de1ddcdc4bfd08680ee2d Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 13 Jun 2019 18:40:45 -0500 Subject: [PATCH 081/173] fixed announce bug from refactor and added static data readme --- onionr/communicatorutils/announcenode.py | 2 +- onionr/static-data/README.md | 19 +++++++++++++++++++ onionr/static-data/connect-check.txt | 2 +- onionr/static-data/default_config.json | 1 + 4 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 onionr/static-data/README.md diff --git a/onionr/communicatorutils/announcenode.py b/onionr/communicatorutils/announcenode.py index e3f7d268..ef29c3c7 100644 --- a/onionr/communicatorutils/announcenode.py +++ b/onionr/communicatorutils/announcenode.py @@ -58,7 +58,7 @@ def announce_node(daemon): existingRand = '' if peer in daemon.announceCache: - data['random'] = self.announceCache[peer] + data['random'] = daemon.announceCache[peer] elif len(existingRand) > 0: data['random'] = existingRand else: diff --git a/onionr/static-data/README.md b/onionr/static-data/README.md new file mode 100644 index 00000000..90b2156a --- /dev/null +++ b/onionr/static-data/README.md @@ -0,0 +1,19 @@ +# static-data + +This folder contains whatever static files are needed by Onionr and default plugins. + +default-plugins/: default plugin program files to be installed on first Onionr run. + +www/: onionr web ui static files including those of default plugins + +bootstrap-nodes.txt: comma separated list of bootstrap nodes + +connect-check.txt: non-onionr .onion sites to check Tor connectivity with + +default_config.json: default configuration values for Onionr nodes + +default_plugin.py: template for people to make new plugins + +header.txt: Onionr ASCII art logo + +index.html: Notice shown to people who try to visit Onionr nodes public interfaces in their browser \ No newline at end of file diff --git a/onionr/static-data/connect-check.txt b/onionr/static-data/connect-check.txt index 7be85aa0..c776e53e 100755 --- a/onionr/static-data/connect-check.txt +++ b/onionr/static-data/connect-check.txt @@ -1 +1 @@ -https://3g2upl4pq6kufc4m.onion/robots.txt,http://expyuzz4wqqyqhjn.onion/robots.txt,http://archivecaslytosk.onion/robots.txt +https://3g2upl4pq6kufc4m.onion/robots.txt,http://expyuzz4wqqyqhjn.onion/robots.txt,http://archivecaslytosk.onion/robots.txt,http://cockmailwwfvrtqj.onion/robots.txt \ No newline at end of file diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index aba71a3f..2f1954f8 100755 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -7,6 +7,7 @@ "socket_servers": false, "security_level": 0, "hide_created_blocks": true, + "insert_deniable_blocks": true, "max_block_age": 2678400, "bypass_tor_check": false, "public_key": "", From c4fa0c15f086b6fbef566416318fff5bd63631bf Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 14 Jun 2019 00:28:21 -0500 Subject: [PATCH 082/173] * doubled clock skew for blocks to 120 secs * added onionr .onion to public api disclaimer page --- onionr/onionrutils.py | 2 +- onionr/static-data/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 824c2415..b196fc22 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -246,7 +246,7 @@ class OnionrUtils: '''Validate metadata meets onionr spec (does not validate proof value computation), take in either dictionary or json string''' # TODO, make this check sane sizes retData = False - maxClockDifference = 60 + maxClockDifference = 120 # convert to dict if it is json string if type(metadata) is str: diff --git a/onionr/static-data/index.html b/onionr/static-data/index.html index d4dd7f75..926f1979 100755 --- a/onionr/static-data/index.html +++ b/onionr/static-data/index.html @@ -4,4 +4,4 @@

    Onionr is a decentralized peer-to-peer data storage system.

    -

    To learn more about Onionr, see the website at https://Onionr.net/

    +

    To learn more about Onionr, see the website at http://onionr.onionkvc5ibm37bmxwr56bdxcdnb6w3wm4bdghh5qo6f6za7gn7styid.onion or Onionr.net

    From faa79bb35ccc639cfa1bc70616866c435b33d801 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 14 Jun 2019 02:05:58 -0500 Subject: [PATCH 083/173] netcheck now also uses last incomming connection time --- onionr/communicatorutils/netcheck.py | 9 ++++++++- onionr/static-data/www/private/index.html | 2 +- onionr/static-data/www/shared/main/stats.js | 5 ++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/onionr/communicatorutils/netcheck.py b/onionr/communicatorutils/netcheck.py index 36a3a304..c5e95906 100644 --- a/onionr/communicatorutils/netcheck.py +++ b/onionr/communicatorutils/netcheck.py @@ -22,8 +22,15 @@ import logger from utils import netutils def net_check(comm_inst): '''Check if we are connected to the internet or not when we can't connect to any peers''' + rec = False # for detecting if we have received incoming connections recently if len(comm_inst.onlinePeers) == 0: - if not netutils.checkNetwork(comm_inst._core._utils, torPort=comm_inst.proxyPort): + try: + if (comm_inst._core._utils.getEpoch() - int(comm_inst._core._utils.localCommand('/lastconnect'))) <= 60: + comm_inst.isOnline = True + rec = True + except ValueError: + pass + if not rec and not netutils.checkNetwork(comm_inst._core._utils, torPort=comm_inst.proxyPort): if not comm_inst.shutdown: logger.warn('Network check failed, are you connected to the Internet, and is Tor working?') comm_inst.isOnline = False diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index 0d27d2a5..2bb74000 100644 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -40,7 +40,7 @@

    Stats

    🕰️ Uptime:

    -

    🖇️ Last Received Connection: Unknown

    +

    🖇️ Last Received Connection: None since start

    💾 Stored Blocks:

    📨 Blocks in queue:

    🔗 Outgoing Connections:

    diff --git a/onionr/static-data/www/shared/main/stats.js b/onionr/static-data/www/shared/main/stats.js index 3a445acb..3c8d2af6 100644 --- a/onionr/static-data/www/shared/main/stats.js +++ b/onionr/static-data/www/shared/main/stats.js @@ -1,6 +1,5 @@ /* - - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file loads stats to show on the main node web page @@ -36,7 +35,7 @@ function getStats(){ lastConnect = humanDate.toString() } else{ - lastConnect = 'Unknown' + lastConnect = 'None since start' } lastIncoming.innerText = lastConnect } From f5b34066849cc0f6c8043a4c99a6c834b1d6e871 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 14 Jun 2019 13:02:02 -0500 Subject: [PATCH 084/173] + added tor data deletion command * better support multiple tor versions in tor bootstrap (latest broke on manjaro before this) * reduced interval for upload block timer to 5 seconds * show message if inbox is empty in webui mail --- onionr/communicator.py | 2 +- onionr/netcontroller.py | 6 +++--- onionr/onionrcommands/README.md | 4 +++- onionr/onionrcommands/__init__.py | 9 ++++++--- onionr/onionrcommands/resettor.py | 29 +++++++++++++++++++++++++++++ onionr/static-data/www/mail/mail.js | 1 + 6 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 onionr/onionrcommands/resettor.py diff --git a/onionr/communicator.py b/onionr/communicator.py index e9eeb28f..b2788e0f 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -118,7 +118,7 @@ class OnionrCommunicatorDaemon: OnionrCommunicatorTimers(self, cooldownpeer.cooldown_peer, 30, myArgs=[self], requiresPeer=True) # Timer to read the upload queue and upload the entries to peers - OnionrCommunicatorTimers(self, self.uploadBlock, 10, requiresPeer=True, maxThreads=1) + OnionrCommunicatorTimers(self, self.uploadBlock, 5, requiresPeer=True, maxThreads=1) # Timer to process the daemon command queue OnionrCommunicatorTimers(self, self.daemonCommands, 6, maxThreads=3) diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py index 6edbd474..69a5f21e 100755 --- a/onionr/netcontroller.py +++ b/onionr/netcontroller.py @@ -144,15 +144,15 @@ HiddenServicePort 80 ''' + self.apiServerIP + ''':''' + str(self.hsPort) # wait for tor to get to 100% bootstrap try: for line in iter(tor.stdout.readline, b''): - if 'Bootstrapped 100%: Done' in line.decode(): + if 'bootstrapped 100' in line.decode().lower(): break - elif 'Opening Socks listener' in line.decode(): + elif 'opening socks listener' in line.decode().lower(): logger.debug(line.decode().replace('\n', '')) else: logger.fatal('Failed to start Tor. Maybe a stray instance of Tor used by Onionr is still running?') return False except KeyboardInterrupt: - logger.fatal('Got keyboard interrupt.', timestamp = False, level = logger.LEVEL_IMPORTANT) + logger.fatal('Got keyboard interrupt. Onionr will exit soon.', timestamp = False, level = logger.LEVEL_IMPORTANT) return False logger.debug('Finished starting Tor.', timestamp=True) diff --git a/onionr/onionrcommands/README.md b/onionr/onionrcommands/README.md index a817cc7f..fed35bb5 100644 --- a/onionr/onionrcommands/README.md +++ b/onionr/onionrcommands/README.md @@ -22,4 +22,6 @@ openwebinterface.py: command to open the web interface (useful because it requir plugincommands.py: commands to enable/disable/reload plugins -pubkeymanager.py: commands to generate a new onionr user id, change the active id, or add/remove/list friends \ No newline at end of file +pubkeymanager.py: commands to generate a new onionr user id, change the active id, or add/remove/list friends + +resettor.py: command to delete the Tor data directory \ No newline at end of file diff --git a/onionr/onionrcommands/__init__.py b/onionr/onionrcommands/__init__.py index e561292c..3a50d59d 100644 --- a/onionr/onionrcommands/__init__.py +++ b/onionr/onionrcommands/__init__.py @@ -21,7 +21,7 @@ import webbrowser, sys import logger from . import pubkeymanager, onionrstatistics, daemonlaunch, filecommands, plugincommands, keyadders -from . import banblocks, exportblocks, openwebinterface +from . import banblocks, exportblocks, openwebinterface, resettor def show_help(o_inst, command): @@ -128,7 +128,9 @@ def get_commands(onionr_inst): 'friend': onionr_inst.friendCmd, 'addid': onionr_inst.addID, 'add-id': onionr_inst.addID, - 'change-id': onionr_inst.changeID + 'change-id': onionr_inst.changeID, + + 'reset-tor': resettor.reset_tor } cmd_help = { @@ -159,5 +161,6 @@ cmd_help = { 'friend': '[add|remove] [public key/id]', 'add-id': 'Generate a new ID (key pair)', 'change-id': 'Change active ID', - 'open-home': 'Open your node\'s home/info screen' + 'open-home': 'Open your node\'s home/info screen', + 'reset-tor': 'Delete the Tor data directory. Only do this if Tor never starts.' } diff --git a/onionr/onionrcommands/resettor.py b/onionr/onionrcommands/resettor.py new file mode 100644 index 00000000..d49cacfe --- /dev/null +++ b/onionr/onionrcommands/resettor.py @@ -0,0 +1,29 @@ +''' + Onionr - Private P2P Communication + + Command to delete the Tor data directory if its safe to do so +''' +''' + 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, shutil +import logger, core +def reset_tor(): + c = core.Core() + tor_dir = c.dataDir + 'tordata' + if os.path.exists(tor_dir): + if c._utils.localCommand('/ping') == 'pong!': + logger.warn('Cannot delete Tor data while Onionr is running') + else: + shutil.rmtree(tor_dir) \ No newline at end of file diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index d7b62765..fb7b2b3a 100644 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -201,6 +201,7 @@ function getInbox(){ var requested = '' for(var i = 0; i < pms.length; i++) { if (pms[i].trim().length == 0){ + threadPart.innerText = 'No messages to show ¯\\_(ツ)_/¯' continue } else{ From 5e73f83c572aadb9f2d4604d951fa398a06cbcf0 Mon Sep 17 00:00:00 2001 From: KF Date: Fri, 14 Jun 2019 20:31:01 -0500 Subject: [PATCH 085/173] + added powchoice.py to use old pow on windows, temporary fix for subprocess method not working there * various bug fixes --- onionr/api.py | 3 ++- onionr/core.py | 10 ++++++--- onionr/{proofofmemory.py => etc/powchoice.py} | 22 +++++++++---------- onionr/netcontroller.py | 6 ++++- onionr/onionrcommands/daemonlaunch.py | 14 +++++++++--- onionr/onionrcommands/openwebinterface.py | 2 +- onionr/onionrproofs.py | 15 +++++++------ onionr/static-data/default_config.json | 1 + run-windows-dev.bat | 6 +++++ 9 files changed, 51 insertions(+), 28 deletions(-) rename onionr/{proofofmemory.py => etc/powchoice.py} (65%) create mode 100644 run-windows-dev.bat diff --git a/onionr/api.py b/onionr/api.py index daefd6ef..3a5f9f87 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -29,7 +29,7 @@ import httpapi from httpapi import friendsapi, profilesapi, configapi, miscpublicapi from onionrservices import httpheaders import onionr - +config.reload() class FDSafeHandler(WSGIHandler): '''Our WSGI handler. Doesn't do much non-default except timeouts''' def handle(self): @@ -57,6 +57,7 @@ def setBindIP(filePath=''): else: data = '127.0.0.1' if filePath != '': + print(filePath) with open(filePath, 'w') as bindFile: bindFile.write(data) return data diff --git a/onionr/core.py b/onionr/core.py index 53d834f6..e7cf8a3a 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -25,7 +25,7 @@ import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrex import onionrblacklist from onionrusers import onionrusers import dbcreator, onionrstorage, serializeddata, subprocesspow -from etc import onionrvalues +from etc import onionrvalues, powchoice if sys.version_info < (3, 6): try: @@ -65,7 +65,7 @@ class Core: self.dbCreate = dbcreator.DBCreator(self) self.forwardKeysFile = self.dataDir + 'forward-keys.db' self.keyStore = simplekv.DeadSimpleKV(self.dataDir + 'cachedstorage.dat', refresh_seconds=5) - + # Socket data, defined here because of multithreading constraints with gevent self.killSockets = False self.startSocket = {} @@ -104,6 +104,7 @@ class Core: else: logger.warn('Warning: address bootstrap file not found ' + self.bootstrapFileLocation) + self.use_subprocess = powchoice.use_subprocess(self) self._utils = onionrutils.OnionrUtils(self) # Initialize the crypto object self._crypto = onionrcrypto.OnionrCrypto(self) @@ -792,7 +793,10 @@ class Core: metadata['expire'] = expire # send block data (and metadata) to POW module to get tokenized block data - payload = subprocesspow.SubprocessPOW(data, metadata, self).start() + if self.use_subprocess: + payload = subprocesspow.SubprocessPOW(data, metadata, self).start() + else: + payload = onionrproofs.POW(metadata, data).waitForResult() if payload != False: try: retData = self.setData(payload) diff --git a/onionr/proofofmemory.py b/onionr/etc/powchoice.py similarity index 65% rename from onionr/proofofmemory.py rename to onionr/etc/powchoice.py index 4b0b0fa7..cf1c7990 100644 --- a/onionr/proofofmemory.py +++ b/onionr/etc/powchoice.py @@ -1,7 +1,7 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication - This file handles proof of memory functionality + This file does determinations for what proof of work module should be used ''' ''' This program is free software: you can redistribute it and/or modify @@ -17,13 +17,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' - -class ProofOfMemory: - def __init__(self, commInst): - self.communicator = commInst - return - - def checkRandomPeer(self): - return - def checkPeer(self, peer): - return \ No newline at end of file +import platform +def use_subprocess(core_inst): + use = True + if not core_inst.config.get('general.use_subprocess_pow_if_possible', True): + use = False + if 'Windows' == platform.system(): + use = False + return use \ No newline at end of file diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py index 69a5f21e..213c4ce7 100755 --- a/onionr/netcontroller.py +++ b/onionr/netcontroller.py @@ -189,7 +189,11 @@ HiddenServicePort 80 ''' + self.apiServerIP + ''':''' + str(self.hsPort) return try: - os.kill(int(pidN), signal.SIGTERM) + try: + os.kill(int(pidN), signal.SIGTERM) + except PermissionError: + # seems to happen on win 10 + pass os.remove(self.dataDir + 'torPid.txt') except ProcessLookupError: pass diff --git a/onionr/onionrcommands/daemonlaunch.py b/onionr/onionrcommands/daemonlaunch.py index 4b343ede..88e43fae 100644 --- a/onionr/onionrcommands/daemonlaunch.py +++ b/onionr/onionrcommands/daemonlaunch.py @@ -23,6 +23,11 @@ from threading import Thread import onionr, api, logger, communicator import onionrevents as events from netcontroller import NetController + +def _proper_shutdown(o_inst): + o_inst.onionrUtils.localCommand('shutdown') + sys.exit(1) + def daemon(o_inst): ''' Starts the Onionr communication daemon @@ -39,8 +44,7 @@ def daemon(o_inst): time.sleep(0) except KeyboardInterrupt: logger.debug('Got keyboard interrupt, shutting down...') - time.sleep(1) - o_inst.onionrUtils.localCommand('shutdown') + _proper_shutdown(o_inst) apiHost = '' while apiHost == '': @@ -64,7 +68,11 @@ def daemon(o_inst): else: logger.debug('.onion service disabled') logger.debug('Using public key: %s' % (logger.colors.underline + o_inst.onionrCore._crypto.pubKey)) - time.sleep(1) + + try: + time.sleep(1) + except KeyboardInterrupt: + _proper_shutdown(o_inst) o_inst.onionrCore.torPort = net.socksPort communicatorThread = Thread(target=communicator.startCommunicator, args=(o_inst, str(net.socksPort))) diff --git a/onionr/onionrcommands/openwebinterface.py b/onionr/onionrcommands/openwebinterface.py index 823703a0..c28f3217 100644 --- a/onionr/onionrcommands/openwebinterface.py +++ b/onionr/onionrcommands/openwebinterface.py @@ -26,5 +26,5 @@ def open_home(o_inst): logger.error('Onionr seems to not be running (could not get api host)') else: url = 'http://%s/#%s' % (url, o_inst.onionrCore.config.get('client.webpassword')) - print('If Onionr does not open automatically, use this URL:', url) + logger.info('If Onionr does not open automatically, use this URL:', url) webbrowser.open_new_tab(url) \ No newline at end of file diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index ac64575f..efd114f1 100755 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -205,18 +205,19 @@ class POW: else: myCore = coreInst - dataLen = len(data) + len(json.dumps(metadata)) + json_metadata = json.dumps(metadata).encode() - if forceDifficulty > 0: - self.difficulty = forceDifficulty - else: - # Calculate difficulty. Dumb for now, may use good algorithm in the future. - self.difficulty = getDifficultyForNewBlock(dataLen) - try: self.data = self.data.encode() except AttributeError: pass + + if forceDifficulty > 0: + self.difficulty = forceDifficulty + else: + # Calculate difficulty. Dumb for now, may use good algorithm in the future. + self.difficulty = getDifficultyForNewBlock(bytes(json_metadata + b'\n' + self.data)) + logger.info('Computing POW (difficulty: %s)...' % self.difficulty) diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index 2f1954f8..65cad2f5 100755 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -4,6 +4,7 @@ "display_header" : false, "minimum_block_pow": 4, "minimum_send_pow": 4, + "use_subprocess_pow_if_possible": true, "socket_servers": false, "security_level": 0, "hide_created_blocks": true, diff --git a/run-windows-dev.bat b/run-windows-dev.bat new file mode 100644 index 00000000..31ba8c88 --- /dev/null +++ b/run-windows-dev.bat @@ -0,0 +1,6 @@ +@echo off +set ONIONR_HOME=data%random% +echo Using %ONIONR_HOME% +setlocal +chdir onionr +python onionr.py %* From a6595fd8bcfb73f4c7fe84107e8ce8195d09fe75 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 14 Jun 2019 23:26:10 -0500 Subject: [PATCH 086/173] minor boilerplate adjustments --- README.md | 2 +- onionr/netcontroller.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 34491fad..1c621d26 100755 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

    - Private P2P Communication network 🕵️ + Private P2P Communication Network 🕵️

    (***pre-alpha & experimental, not well tested or easy to use yet***) diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py index 69a5f21e..633986d0 100755 --- a/onionr/netcontroller.py +++ b/onionr/netcontroller.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication Netcontroller library, used to control/work with Tor/I2P and send requests through them ''' From 7cb5f212ec5c7c07e58a83d736eb384fd24a05e0 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 15 Jun 2019 13:56:57 -0500 Subject: [PATCH 087/173] Started reducing unnecessary disk i/o: * Spawn core.Core() much less --- onionr/api.py | 7 +++---- onionr/communicator.py | 2 +- onionr/communicatorutils/daemonqueuehandler.py | 2 +- onionr/core.py | 13 ++++++------- onionr/onionrcrypto.py | 2 +- onionr/onionrpeers.py | 1 - onionr/onionrproofs.py | 6 +++--- onionr/onionrutils.py | 2 +- onionr/subprocesspow.py | 2 +- 9 files changed, 17 insertions(+), 20 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 3a5f9f87..854711f0 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -17,11 +17,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' +import random, threading, hmac, base64, time, os, json, socket from gevent.pywsgi import WSGIServer, WSGIHandler from gevent import Timeout -import flask, cgi, uuid +import flask from flask import request, Response, abort, send_from_directory -import sys, random, threading, hmac, base64, time, os, json, socket import core from onionrblockapi import Block import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config @@ -29,6 +29,7 @@ import httpapi from httpapi import friendsapi, profilesapi, configapi, miscpublicapi from onionrservices import httpheaders import onionr + config.reload() class FDSafeHandler(WSGIHandler): '''Our WSGI handler. Doesn't do much non-default except timeouts''' @@ -57,7 +58,6 @@ def setBindIP(filePath=''): else: data = '127.0.0.1' if filePath != '': - print(filePath) with open(filePath, 'w') as bindFile: bindFile.write(data) return data @@ -332,7 +332,6 @@ class API: if self._core._utils.validateHash(name): try: resp = Block(name, decrypt=True).bcontent - #resp = cgi.escape(Block(name, decrypt=True).bcontent, quote=True) except TypeError: pass else: diff --git a/onionr/communicator.py b/onionr/communicator.py index b2788e0f..52ea546a 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -371,7 +371,7 @@ class OnionrCommunicatorDaemon: time.sleep(1) else: # This executes if the api is NOT detected to be running - events.event('daemon_crash', onionr = None, data = {}) + events.event('daemon_crash', onionr = self._core.onionrInst, data = {}) logger.error('Daemon detected API crash (or otherwise unable to reach API after long time), stopping...') self.shutdown = True self.decrementThreadCount('detectAPICrash') diff --git a/onionr/communicatorutils/daemonqueuehandler.py b/onionr/communicatorutils/daemonqueuehandler.py index 5c906f64..ddbb0783 100644 --- a/onionr/communicatorutils/daemonqueuehandler.py +++ b/onionr/communicatorutils/daemonqueuehandler.py @@ -23,7 +23,7 @@ def handle_daemon_commands(comm_inst): cmd = comm_inst._core.daemonQueue() response = '' if cmd is not False: - events.event('daemon_command', onionr = None, data = {'cmd' : cmd}) + events.event('daemon_command', onionr = comm_inst._core.onionrInst, data = {'cmd' : cmd}) if cmd[0] == 'shutdown': comm_inst.shutdown = True elif cmd[0] == 'announceNode': diff --git a/onionr/core.py b/onionr/core.py index e7cf8a3a..33b310af 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -39,7 +39,6 @@ class Core: ''' Initialize Core Onionr library ''' - # set data dir self.dataDir = os.environ.get('ONIONR_HOME', os.environ.get('DATA_DIR', 'data/')) if not self.dataDir.endswith('/'): @@ -135,7 +134,7 @@ class Core: if not self._utils.validatePubKey(peerID): return False - events.event('pubkey_add', data = {'key': peerID}, onionr = None) + events.event('pubkey_add', data = {'key': peerID}, onionr = self.onionrInst) conn = sqlite3.connect(self.peerDB, timeout=30) hashID = self._crypto.pubKeyHashID(peerID) @@ -187,7 +186,7 @@ class Core: conn.commit() conn.close() - events.event('address_add', data = {'address': address}, onionr = None) + events.event('address_add', data = {'address': address}, onionr = self.onionrInst) return True else: @@ -207,7 +206,7 @@ class Core: conn.commit() conn.close() - events.event('address_remove', data = {'address': address}, onionr = None) + events.event('address_remove', data = {'address': address}, onionr = self.onionrInst) return True else: return False @@ -342,7 +341,7 @@ class Core: conn.commit() conn.close() - events.event('queue_pop', data = {'data': retData}, onionr = None) + events.event('queue_pop', data = {'data': retData}, onionr = self.onionrInst) return retData @@ -363,7 +362,7 @@ class Core: except sqlite3.OperationalError: retData = False self.daemonQueue() - events.event('queue_push', data = {'command': command, 'data': data}, onionr = None) + events.event('queue_push', data = {'command': command, 'data': data}, onionr = self.onionrInst) conn.close() return retData @@ -406,7 +405,7 @@ class Core: pass conn.close() - events.event('queue_clear', onionr = None) + events.event('queue_clear', onionr = self.onionrInst) return diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py index dab3e8b7..bab96092 100755 --- a/onionr/onionrcrypto.py +++ b/onionr/onionrcrypto.py @@ -246,7 +246,7 @@ class OnionrCrypto: except AttributeError: pass - difficulty = onionrproofs.getDifficultyForNewBlock(blockContent, ourBlock=False) + difficulty = onionrproofs.getDifficultyForNewBlock(blockContent, ourBlock=False, coreInst=self._core) if difficulty < int(config.get('general.minimum_block_pow')): difficulty = int(config.get('general.minimum_block_pow')) diff --git a/onionr/onionrpeers.py b/onionr/onionrpeers.py index ad60d543..aa0887f6 100755 --- a/onionr/onionrpeers.py +++ b/onionr/onionrpeers.py @@ -34,7 +34,6 @@ class PeerProfiles: if not isinstance(coreInst, core.Core): raise TypeError("coreInst must be a type of core.Core") self.coreInst = coreInst - assert isinstance(self.coreInst, core.Core) self.loadScore() self.getConnectTime() diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index efd114f1..10d08c66 100755 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -45,7 +45,7 @@ def getDifficultyModifier(coreOrUtilsInst=None): return retData -def getDifficultyForNewBlock(data, ourBlock=True): +def getDifficultyForNewBlock(data, ourBlock=True, coreInst=None): ''' Get difficulty for block. Accepts size in integer, Block instance, or str/bytes full block contents ''' @@ -61,7 +61,7 @@ def getDifficultyForNewBlock(data, ourBlock=True): else: minDifficulty = config.get('general.minimum_block_pow', 4) - retData = max(minDifficulty, math.floor(dataSize / 100000)) + getDifficultyModifier() + retData = max(minDifficulty, math.floor(dataSize / 100000)) + getDifficultyModifier(coreInst) return retData @@ -216,7 +216,7 @@ class POW: self.difficulty = forceDifficulty else: # Calculate difficulty. Dumb for now, may use good algorithm in the future. - self.difficulty = getDifficultyForNewBlock(bytes(json_metadata + b'\n' + self.data)) + self.difficulty = getDifficultyForNewBlock(bytes(json_metadata + b'\n' + self.data), coreInst=myCore) logger.info('Computing POW (difficulty: %s)...' % self.difficulty) diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index b196fc22..d857b6bf 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -185,7 +185,7 @@ class OnionrUtils: self._core.updateBlockInfo(blockHash, 'expire', expireTime) if not blockType is None: self._core.updateBlockInfo(blockHash, 'dataType', blockType) - onionrevents.event('processblocks', data = {'block': myBlock, 'type': blockType, 'signer': signer, 'validSig': valid}, onionr = None) + onionrevents.event('processblocks', data = {'block': myBlock, 'type': blockType, 'signer': signer, 'validSig': valid}, onionr = self._core.onionrInst) else: pass #logger.debug('Not processing metadata on encrypted block we cannot decrypt.') diff --git a/onionr/subprocesspow.py b/onionr/subprocesspow.py index e7b78d5f..52a90c64 100755 --- a/onionr/subprocesspow.py +++ b/onionr/subprocesspow.py @@ -53,7 +53,7 @@ class SubprocessPOW: self.data = onionrutils.OnionrUtils.strToBytes(data) # Calculate difficulty. Dumb for now, may use good algorithm in the future. - self.difficulty = onionrproofs.getDifficultyForNewBlock(bytes(json_metadata + b'\n' + self.data)) + self.difficulty = onionrproofs.getDifficultyForNewBlock(bytes(json_metadata + b'\n' + self.data), coreInst=self.core_inst) logger.info('Computing POW (difficulty: %s)...' % self.difficulty) From c07901ec06e58790c699b70de899d05bcdf5477a Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 16 Jun 2019 01:06:32 -0500 Subject: [PATCH 088/173] reduced config reloads by a lot --- onionr/communicator.py | 1 - onionr/config.py | 1 - onionr/netcontroller.py | 6 +----- onionr/onionrblacklist.py | 2 +- onionr/onionrcrypto.py | 2 +- onionr/onionrevents.py | 2 +- onionr/onionrpeers.py | 4 ++-- onionr/onionrpluginapi.py | 2 +- onionr/onionrplugins.py | 7 ++----- onionr/onionrproofs.py | 4 ++-- onionr/onionrstorage.py | 2 +- onionr/onionrutils.py | 4 +--- onionr/setupconfig.py | 20 +++++++++++++++++++- onionr/storagecounter.py | 2 +- 14 files changed, 33 insertions(+), 26 deletions(-) diff --git a/onionr/communicator.py b/onionr/communicator.py index 52ea546a..a75cd998 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -35,7 +35,6 @@ OnionrCommunicatorTimers = onionrcommunicatortimers.OnionrCommunicatorTimers config.reload() class OnionrCommunicatorDaemon: def __init__(self, onionrInst, proxyPort, developmentMode=config.get('general.dev_mode', False)): - config.reload() onionrInst.communicatorInst = self # configure logger and stuff onionr.Onionr.setupConfig('data/', self = self) diff --git a/onionr/config.py b/onionr/config.py index 6cb75050..47373f8b 100755 --- a/onionr/config.py +++ b/onionr/config.py @@ -115,7 +115,6 @@ def reload(): ''' Reloads the configuration data in memory from the file ''' - check() try: with open(get_config_file(), 'r', encoding="utf8") as configfile: diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py index 5f12115b..b1b1792b 100755 --- a/onionr/netcontroller.py +++ b/onionr/netcontroller.py @@ -21,7 +21,7 @@ import subprocess, os, sys, time, signal, base64, socket from shutil import which import logger, config from onionrblockapi import Block - +config.reload() def getOpenPort(): # taken from (but modified) https://stackoverflow.com/a/2838309 by https://stackoverflow.com/users/133374/albert ccy-by-sa-3 https://creativecommons.org/licenses/by-sa/3.0/ # changes from source: import moved to top of file, bind specifically to localhost @@ -65,10 +65,6 @@ class NetController: else: self.torBinary = 'tor' - config.reload() - - return - def generateTorrc(self): ''' Generate a torrc file for our tor instance diff --git a/onionr/onionrblacklist.py b/onionr/onionrblacklist.py index 63ecbb6b..9c4e99d7 100755 --- a/onionr/onionrblacklist.py +++ b/onionr/onionrblacklist.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file handles maintenence of a blacklist database, for blocks and peers ''' diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py index bab96092..75fe2138 100755 --- a/onionr/onionrcrypto.py +++ b/onionr/onionrcrypto.py @@ -22,10 +22,10 @@ import nacl.signing, nacl.encoding, nacl.public, nacl.hash, nacl.pwhash, nacl.ut import logger, onionrproofs import onionrexceptions, keymanager, core import config +config.reload() class OnionrCrypto: def __init__(self, coreInstance): - config.reload() self._core = coreInstance self._keyFile = self._core.dataDir + 'keys.txt' self.pubKey = None diff --git a/onionr/onionrevents.py b/onionr/onionrevents.py index c6ebcc40..beaeca01 100755 --- a/onionr/onionrevents.py +++ b/onionr/onionrevents.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication This file deals with configuration management. ''' diff --git a/onionr/onionrpeers.py b/onionr/onionrpeers.py index aa0887f6..8bf76289 100755 --- a/onionr/onionrpeers.py +++ b/onionr/onionrpeers.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file contains both the PeerProfiles class for network profiling of Onionr nodes ''' @@ -19,6 +19,7 @@ ''' import sqlite3 import core, config, logger +config.reload() class PeerProfiles: ''' PeerProfiles @@ -91,7 +92,6 @@ def peerCleanup(coreInst): raise TypeError('coreInst must be instance of core.Core') logger.info('Cleaning peers...') - config.reload() adders = getScoreSortedPeerList(coreInst) adders.reverse() diff --git a/onionr/onionrpluginapi.py b/onionr/onionrpluginapi.py index 0120dad7..78c8a008 100755 --- a/onionr/onionrpluginapi.py +++ b/onionr/onionrpluginapi.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication This file deals with the object that is passed with each event ''' diff --git a/onionr/onionrplugins.py b/onionr/onionrplugins.py index c334ea09..9698f5d1 100755 --- a/onionr/onionrplugins.py +++ b/onionr/onionrplugins.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication This file deals with management of modules/plugins. ''' @@ -27,6 +27,7 @@ if not dataDir.endswith('/'): _pluginsfolder = dataDir + 'plugins/' _instances = dict() +config.reload() def reload(onionr = None, stop_event = True): ''' @@ -211,8 +212,6 @@ def get_enabled_plugins(): check() - config.reload() - return list(config.get('plugins.enabled', list())) def is_enabled(name): @@ -253,8 +252,6 @@ def check(): Checks to make sure files exist ''' - config.reload() - if not config.is_set('plugins'): logger.debug('Generating plugin configuration data...') config.set('plugins', {'enabled': []}, True) diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index 10d08c66..5ff32964 100755 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -20,6 +20,8 @@ import multiprocessing, nacl.encoding, nacl.hash, nacl.utils, time, math, threading, binascii, sys, json import core, onionrutils, config, logger, onionrblockapi +config.reload() + def getDifficultyModifier(coreOrUtilsInst=None): '''Accepts a core or utils instance returns the difficulty modifier for block storage based @@ -82,7 +84,6 @@ def hashMeetsDifficulty(h): ''' Return bool for a hash string to see if it meets pow difficulty defined in config ''' - config.reload() hashDifficulty = getHashDifficulty(h) try: expected = int(config.get('general.minimum_block_pow')) @@ -100,7 +101,6 @@ class DataPOW: self.data = data self.threadCount = threadCount self.rounds = 0 - config.reload() if forceDifficulty == 0: dataLen = sys.getsizeof(data) diff --git a/onionr/onionrstorage.py b/onionr/onionrstorage.py index 65fe6757..5f44c4f3 100644 --- a/onionr/onionrstorage.py +++ b/onionr/onionrstorage.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file handles block storage, providing an abstraction for storing blocks between file system and database ''' diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index d857b6bf..c5546d09 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -34,7 +34,7 @@ if sys.version_info < (3, 6): except ModuleNotFoundError: logger.fatal('On Python 3 versions prior to 3.6.x, you need the sha3 module') sys.exit(1) - +config.reload() class OnionrUtils: ''' Various useful functions for validating things, etc functions, connectivity @@ -47,7 +47,6 @@ class OnionrUtils: self.avoidDupe = [] # list used to prevent duplicate requests per peer for certain actions self.peerProcessing = {} # dict of current peer actions: peer, actionList self.storageCounter = storagecounter.StorageCounter(self._core) # used to keep track of how much data onionr is using on disk - config.reload() # onionr config return def getTimeBypassToken(self): @@ -85,7 +84,6 @@ class OnionrUtils: ''' Send a command to the local http API server, securely. Intended for local clients, DO NOT USE for remote peers. ''' - config.reload() self.getTimeBypassToken() # TODO: URL encode parameters, just as an extra measure. May not be needed, but should be added regardless. hostname = '' diff --git a/onionr/setupconfig.py b/onionr/setupconfig.py index e439845a..e0752f74 100644 --- a/onionr/setupconfig.py +++ b/onionr/setupconfig.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + Initialize Onionr configuration +''' +''' + 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, json import config, logger @@ -6,7 +25,6 @@ def setup_config(dataDir, o_inst = None): if not data_exists: os.mkdir(dataDir) config.reload() - if not os.path.exists(config._configfile): if os.path.exists('static-data/default_config.json'): diff --git a/onionr/storagecounter.py b/onionr/storagecounter.py index 76ebe16b..9dbb8827 100755 --- a/onionr/storagecounter.py +++ b/onionr/storagecounter.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network. + Onionr - Private P2P Communication Keeps track of how much disk space we're using ''' From ea7c7a80cebf566849fff65b214453d3f103597b Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 16 Jun 2019 01:12:54 -0500 Subject: [PATCH 089/173] bumped deadsimplekv version --- requirements.in | 2 +- requirements.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.in b/requirements.in index 2a255e58..2b6ca2b3 100755 --- a/requirements.in +++ b/requirements.in @@ -6,5 +6,5 @@ defusedxml==0.5.0 Flask==1.0.2 PySocks==1.6.8 stem==1.7.1 -deadsimplekv==0.0.1 +deadsimplekv==0.1.1 jinja2==2.10.1 diff --git a/requirements.txt b/requirements.txt index 5facf81b..81bbcfe5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -46,8 +46,8 @@ click==7.0 \ --hash=sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13 \ --hash=sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7 \ # via flask -deadsimplekv==0.0.1 \ - --hash=sha256:1bb78e4feb01d975e89e81cac7b0141666a14ebefa06fffc1c2d86c3308e3930 +deadsimplekv==0.1.1 \ + --hash=sha256:4bf951e188c302006e37f95bde6117b1b938fb454153d583c6346090d9bead1a defusedxml==0.5.0 \ --hash=sha256:24d7f2f94f7f3cb6061acb215685e5125fbcdc40a857eff9de22518820b0a4f4 \ --hash=sha256:702a91ade2968a82beb0db1e0766a6a273f33d4616a6ce8cde475d8e09853b20 From 8bbf9b10abea720374f83deab12179e03751bc9a Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 16 Jun 2019 01:18:29 -0500 Subject: [PATCH 090/173] removed defunct defusedxml requirement --- requirements.in | 1 - requirements.txt | 3 --- 2 files changed, 4 deletions(-) diff --git a/requirements.in b/requirements.in index 2b6ca2b3..bbb9cdc3 100755 --- a/requirements.in +++ b/requirements.in @@ -2,7 +2,6 @@ urllib3==1.24.2 requests==2.21.0 PyNaCl==1.2.1 gevent==1.3.6 -defusedxml==0.5.0 Flask==1.0.2 PySocks==1.6.8 stem==1.7.1 diff --git a/requirements.txt b/requirements.txt index 81bbcfe5..06b3303d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -48,9 +48,6 @@ click==7.0 \ # via flask deadsimplekv==0.1.1 \ --hash=sha256:4bf951e188c302006e37f95bde6117b1b938fb454153d583c6346090d9bead1a -defusedxml==0.5.0 \ - --hash=sha256:24d7f2f94f7f3cb6061acb215685e5125fbcdc40a857eff9de22518820b0a4f4 \ - --hash=sha256:702a91ade2968a82beb0db1e0766a6a273f33d4616a6ce8cde475d8e09853b20 flask==1.0.2 \ --hash=sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48 \ --hash=sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05 From 934602ee0906783b09f870bfc641f70c054c4cec Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 16 Jun 2019 01:30:17 -0500 Subject: [PATCH 091/173] handle keyboardinterrupt, eof and blank file path in interactive ui --- onionr/static-data/default-plugins/cliui/main.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/onionr/static-data/default-plugins/cliui/main.py b/onionr/static-data/default-plugins/cliui/main.py index e1398a13..c4186b81 100755 --- a/onionr/static-data/default-plugins/cliui/main.py +++ b/onionr/static-data/default-plugins/cliui/main.py @@ -39,8 +39,6 @@ class OnionrCLIUI: def subCommand(self, command, args=None): try: - #subprocess.run(["./onionr.py", command]) - #subprocess.Popen(['./onionr.py', command], stdin=subprocess.STD, stdout=subprocess.STDOUT, stderr=subprocess.STDOUT) if args != None: subprocess.call(['./onionr.py', command, args]) else: @@ -89,8 +87,13 @@ class OnionrCLIUI: else: print('Plugin not enabled') elif choice in ("3", "file sharing", "file"): - filename = input("Enter full path to file: ").strip() - self.subCommand("addfile", filename) + try: + filename = input("Enter full path to file: ").strip() + except (EOFError, KeyboardInterrupt) as e: + pass + else: + if len(filename.strip()) > 0: + self.subCommand("addfile", filename) elif choice in ("4", "quit"): showMenu = False self.shutdown = True From 741150963f6bd543744f3e64a5d81810851cad52 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 16 Jun 2019 01:36:38 -0500 Subject: [PATCH 092/173] added security disclaimer to readme --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1c621d26..581f9510 100755 --- a/README.md +++ b/README.md @@ -125,7 +125,13 @@ Email: beardog [ at ] mailbox.org ## Disclaimers and legal -Onionr is published under the GNU GPL v3 license. +No matter how good Onionr and other software gets, there will always be ways for clever or well-funded adversaries to break your security. + +*Do not rely on Onionr or any other software to hold up if your life or liberty are at stake.* + +### Licenses and Branding + +Onionr is published under the GNU GPL v3 license, except for the logo. The Tor Project and I2P developers do not own, create, or endorse this project, and are not otherwise involved. From 901b275d40967f6f33a44eef123656c0957b4434 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 16 Jun 2019 02:04:32 -0500 Subject: [PATCH 093/173] fixed everything being executable permission --- CODE_OF_CONDUCT.md | 0 CONTRIBUTING.md | 0 Dockerfile | 0 ISSUE_TEMPLATE.md | 0 LICENSE.txt | 0 Makefile | 0 README.md | 0 onionr/__init__.py | 0 onionr/api.py | 0 onionr/blockimporter.py | 0 onionr/communicator.py | 0 onionr/config.py | 0 onionr/core.py | 0 onionr/dbcreator.py | 0 onionr/keymanager.py | 0 onionr/logger.py | 0 onionr/netcontroller.py | 0 onionr/onionrblacklist.py | 0 onionr/onionrblockapi.py | 0 onionr/onionrcrypto.py | 0 onionr/onionrevents.py | 0 onionr/onionrexceptions.py | 0 onionr/onionrpeers.py | 0 onionr/onionrpluginapi.py | 0 onionr/onionrplugins.py | 0 onionr/onionrproofs.py | 0 onionr/onionrutils.py | 0 onionr/storagecounter.py | 0 onionr/subprocesspow.py | 0 requirements.in | 0 run-windows.bat | 0 setprofile.sh | 0 32 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 CODE_OF_CONDUCT.md mode change 100755 => 100644 CONTRIBUTING.md mode change 100755 => 100644 Dockerfile mode change 100755 => 100644 ISSUE_TEMPLATE.md mode change 100755 => 100644 LICENSE.txt mode change 100755 => 100644 Makefile mode change 100755 => 100644 README.md mode change 100755 => 100644 onionr/__init__.py mode change 100755 => 100644 onionr/api.py mode change 100755 => 100644 onionr/blockimporter.py mode change 100755 => 100644 onionr/communicator.py mode change 100755 => 100644 onionr/config.py mode change 100755 => 100644 onionr/core.py mode change 100755 => 100644 onionr/dbcreator.py mode change 100755 => 100644 onionr/keymanager.py mode change 100755 => 100644 onionr/logger.py mode change 100755 => 100644 onionr/netcontroller.py mode change 100755 => 100644 onionr/onionrblacklist.py mode change 100755 => 100644 onionr/onionrblockapi.py mode change 100755 => 100644 onionr/onionrcrypto.py mode change 100755 => 100644 onionr/onionrevents.py mode change 100755 => 100644 onionr/onionrexceptions.py mode change 100755 => 100644 onionr/onionrpeers.py mode change 100755 => 100644 onionr/onionrpluginapi.py mode change 100755 => 100644 onionr/onionrplugins.py mode change 100755 => 100644 onionr/onionrproofs.py mode change 100755 => 100644 onionr/onionrutils.py mode change 100755 => 100644 onionr/storagecounter.py mode change 100755 => 100644 onionr/subprocesspow.py mode change 100755 => 100644 requirements.in mode change 100755 => 100644 run-windows.bat mode change 100755 => 100644 setprofile.sh diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md old mode 100755 new mode 100644 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md old mode 100755 new mode 100644 diff --git a/Dockerfile b/Dockerfile old mode 100755 new mode 100644 diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md old mode 100755 new mode 100644 diff --git a/LICENSE.txt b/LICENSE.txt old mode 100755 new mode 100644 diff --git a/Makefile b/Makefile old mode 100755 new mode 100644 diff --git a/README.md b/README.md old mode 100755 new mode 100644 diff --git a/onionr/__init__.py b/onionr/__init__.py old mode 100755 new mode 100644 diff --git a/onionr/api.py b/onionr/api.py old mode 100755 new mode 100644 diff --git a/onionr/blockimporter.py b/onionr/blockimporter.py old mode 100755 new mode 100644 diff --git a/onionr/communicator.py b/onionr/communicator.py old mode 100755 new mode 100644 diff --git a/onionr/config.py b/onionr/config.py old mode 100755 new mode 100644 diff --git a/onionr/core.py b/onionr/core.py old mode 100755 new mode 100644 diff --git a/onionr/dbcreator.py b/onionr/dbcreator.py old mode 100755 new mode 100644 diff --git a/onionr/keymanager.py b/onionr/keymanager.py old mode 100755 new mode 100644 diff --git a/onionr/logger.py b/onionr/logger.py old mode 100755 new mode 100644 diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py old mode 100755 new mode 100644 diff --git a/onionr/onionrblacklist.py b/onionr/onionrblacklist.py old mode 100755 new mode 100644 diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py old mode 100755 new mode 100644 diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py old mode 100755 new mode 100644 diff --git a/onionr/onionrevents.py b/onionr/onionrevents.py old mode 100755 new mode 100644 diff --git a/onionr/onionrexceptions.py b/onionr/onionrexceptions.py old mode 100755 new mode 100644 diff --git a/onionr/onionrpeers.py b/onionr/onionrpeers.py old mode 100755 new mode 100644 diff --git a/onionr/onionrpluginapi.py b/onionr/onionrpluginapi.py old mode 100755 new mode 100644 diff --git a/onionr/onionrplugins.py b/onionr/onionrplugins.py old mode 100755 new mode 100644 diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py old mode 100755 new mode 100644 diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py old mode 100755 new mode 100644 diff --git a/onionr/storagecounter.py b/onionr/storagecounter.py old mode 100755 new mode 100644 diff --git a/onionr/subprocesspow.py b/onionr/subprocesspow.py old mode 100755 new mode 100644 diff --git a/requirements.in b/requirements.in old mode 100755 new mode 100644 diff --git a/run-windows.bat b/run-windows.bat old mode 100755 new mode 100644 diff --git a/setprofile.sh b/setprofile.sh old mode 100755 new mode 100644 From 8a9a5abe2ec3e39789a9cd5ea8cd457e3e7ce2c3 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 16 Jun 2019 12:55:48 -0500 Subject: [PATCH 094/173] * phrase security.md better * display correct number of peers in stats * hopefully fix sigpipe issue --- SECURITY.md | 6 ++++-- onionr/logger.py | 5 ++++- onionr/onionrcommands/onionrstatistics.py | 2 +- onionr/onionrutils.py | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index ce612cca..47a7f806 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,5 +1,7 @@ # Security Policy +We welcome responsible and constructive security review. + # Scope The Onionr software and any nodes you control are within scope. @@ -14,7 +16,7 @@ The following exploits are of particular interest: * Easily associating public keys with server addresses * Discovering true server IPs when behind Tor/I2P (aside from Tor/i2p-level attacks) * Easily discovering which nodes are the block creator -* XSS, CSRF, clickjacking +* XSS, CSRF, clickjacking, DNS rebinding * Timing attacks against the local http server ([see blog post](https://www.chaoswebs.net/blog/timebleed-breaking-privacy-with-a-simple-timing-attack.html)) * Discovering direct connection servers as a non participant. * Cryptography/protocol issues @@ -26,7 +28,7 @@ We do not consider non-network based same-machine attacks to be very significant Onionr is a student-owned hobby project, resources are not available for large rewards. -Stickers or other reasonable & negotiable rewards are available. We reserve the right to refuse rewards for any reason. +Stickers or other small rewards are available. We reserve the right to refuse rewards for any reason. Public recognition can be given upon request. diff --git a/onionr/logger.py b/onionr/logger.py index 34dc4d06..a7abf715 100644 --- a/onionr/logger.py +++ b/onionr/logger.py @@ -132,7 +132,10 @@ def raw(data, fd = sys.stdout, sensitive = False): ''' if get_settings() & OUTPUT_TO_CONSOLE: - ts = fd.write('%s\n' % data) + try: + ts = fd.write('%s\n' % data) + except OSError: + pass if get_settings() & OUTPUT_TO_FILE and not sensitive: try: with open(_outputfile, "a+") as f: diff --git a/onionr/onionrcommands/onionrstatistics.py b/onionr/onionrcommands/onionrstatistics.py index 0974b290..88643b0c 100644 --- a/onionr/onionrcommands/onionrstatistics.py +++ b/onionr/onionrcommands/onionrstatistics.py @@ -39,7 +39,7 @@ def show_stats(o_inst): # count stats 'div2' : True, - 'Known Peers' : str(len(o_inst.onionrCore.listPeers()) - 1), + 'Known Peers' : str(len(o_inst.onionrCore.listPeers())), 'Enabled Plugins' : str(len(o_inst.onionrCore.config.get('plugins.enabled', list()))) + ' / ' + str(len(os.listdir(o_inst.dataDir + 'plugins/'))), 'Stored Blocks' : str(totalBlocks), 'Percent Blocks Signed' : str(round(100 * signedBlocks / max(totalBlocks, 1), 2)) + '%' diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index c5546d09..e2a5964d 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -438,7 +438,7 @@ class OnionrUtils: else: logger.warn('Failed to verify hash for %s' % block) if not exist: - print('No blocks found to import') + logger.info('No blocks found to import') def progressBar(self, value = 0, endvalue = 100, width = None): ''' From 71805811cf3f44dd2e5c555e72b98c52a0e62a90 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 16 Jun 2019 13:20:54 -0500 Subject: [PATCH 095/173] corrected permissions again --- .github/FUNDING.yml | 0 Makefile | 0 docs/onionr-1.png | Bin docs/onionr-2.png | Bin docs/onionr-3.png | Bin docs/onionr-icon.png | Bin docs/onionr-logo.png | Bin docs/specs/block-spec.md | 0 docs/tor-stinks-02.png | Bin install/onionr.service | 0 onionr/__init__.py | 0 onionr/api.py | 0 onionr/blockimporter.py | 0 onionr/communicator.py | 0 onionr/communicatorutils/README.md | 0 onionr/communicatorutils/__init__.py | 0 onionr/communicatorutils/announcenode.py | 0 onionr/communicatorutils/connectnewpeers.py | 0 onionr/communicatorutils/cooldownpeer.py | 0 onionr/communicatorutils/daemonqueuehandler.py | 0 onionr/communicatorutils/deniableinserts.py | 0 onionr/communicatorutils/downloadblocks.py | 0 onionr/communicatorutils/housekeeping.py | 0 onionr/communicatorutils/lookupadders.py | 0 onionr/communicatorutils/lookupblocks.py | 0 onionr/communicatorutils/netcheck.py | 0 onionr/communicatorutils/proxypicker.py | 0 onionr/communicatorutils/servicecreator.py | 0 onionr/communicatorutils/uploadblocks.py | 0 onionr/config.py | 0 onionr/core.py | 0 onionr/dbcreator.py | 0 onionr/etc/README.md | 0 onionr/etc/humanreadabletime.py | 0 onionr/etc/powchoice.py | 0 onionr/httpapi/README.md | 0 onionr/httpapi/configapi/__init__.py | 0 onionr/httpapi/miscpublicapi/__init__.py | 0 onionr/httpapi/miscpublicapi/announce.py | 0 onionr/httpapi/miscpublicapi/getblocks.py | 0 onionr/httpapi/miscpublicapi/upload.py | 0 onionr/httpapi/profilesapi/__init__.py | 0 onionr/httpapi/profilesapi/profiles.py | 0 onionr/keymanager.py | 0 onionr/logger.py | 0 onionr/netcontroller.py | 2 +- onionr/onionrblacklist.py | 0 onionr/onionrblockapi.py | 0 onionr/onionrcommands/README.md | 0 onionr/onionrcommands/__init__.py | 0 onionr/onionrcommands/banblocks.py | 0 onionr/onionrcommands/daemonlaunch.py | 0 onionr/onionrcommands/exportblocks.py | 0 onionr/onionrcommands/filecommands.py | 0 onionr/onionrcommands/keyadders.py | 0 onionr/onionrcommands/onionrstatistics.py | 0 onionr/onionrcommands/openwebinterface.py | 0 onionr/onionrcommands/plugincommands.py | 0 onionr/onionrcommands/pubkeymanager.py | 0 onionr/onionrcommands/resettor.py | 0 onionr/onionrcrypto.py | 0 onionr/onionrevents.py | 0 onionr/onionrexceptions.py | 0 onionr/onionrpeers.py | 0 onionr/onionrpluginapi.py | 0 onionr/onionrplugins.py | 0 onionr/onionrproofs.py | 0 onionr/onionrservices/README.md | 0 onionr/onionrservices/__init__.py | 0 onionr/onionrservices/bootstrapservice.py | 0 onionr/onionrservices/connectionserver.py | 0 onionr/onionrservices/httpheaders.py | 0 onionr/onionrstorage.py | 0 onionr/onionrusers/README.md | 0 onionr/onionrusers/__init__.py | 0 onionr/onionrutils.py | 0 onionr/serializeddata.py | 0 onionr/setupconfig.py | 0 onionr/static-data/README.md | 0 .../default-plugins/clandestine/controlapi.py | 0 .../default-plugins/clandestine/peerserver.py | 0 onionr/static-data/default-plugins/flow/flowapi.py | 0 onionr/static-data/default-plugins/pms/loadinbox.py | 0 onionr/static-data/default-plugins/pms/mailapi.py | 0 onionr/static-data/www/board/board.js | 0 onionr/static-data/www/board/index.html | 0 onionr/static-data/www/board/theme.css | 0 onionr/static-data/www/clandestine/index.html | 0 onionr/static-data/www/clandestine/js/main.js | 0 onionr/static-data/www/mail/index.html | 0 onionr/static-data/www/mail/mail.css | 0 onionr/static-data/www/mail/mail.js | 0 onionr/static-data/www/private/index.html | 0 onionr/static-data/www/private/main.css | 0 onionr/static-data/www/profiles/index.html | 0 onionr/static-data/www/profiles/profiles.js | 0 onionr/static-data/www/shared/configeditor.js | 0 onionr/static-data/www/shared/main/stats.js | 0 onionr/static-data/www/shared/main/style.css | 0 onionr/static-data/www/shared/misc.js | 0 onionr/static-data/www/shared/onionr-icon.png | Bin onionr/static-data/www/shared/onionrblocks.js | 0 onionr/static-data/www/shared/panel.js | 0 onionr/static-data/www/shared/sites.js | 0 onionr/static-data/www/ui/dist/js/main.js | 0 onionr/storagecounter.py | 0 onionr/subprocesspow.py | 0 run-windows-dev.bat | 0 run-windows.bat | 0 109 files changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 .github/FUNDING.yml mode change 100644 => 100755 Makefile mode change 100644 => 100755 docs/onionr-1.png mode change 100644 => 100755 docs/onionr-2.png mode change 100644 => 100755 docs/onionr-3.png mode change 100644 => 100755 docs/onionr-icon.png mode change 100644 => 100755 docs/onionr-logo.png mode change 100644 => 100755 docs/specs/block-spec.md mode change 100644 => 100755 docs/tor-stinks-02.png mode change 100644 => 100755 install/onionr.service mode change 100644 => 100755 onionr/__init__.py mode change 100644 => 100755 onionr/api.py mode change 100644 => 100755 onionr/blockimporter.py mode change 100644 => 100755 onionr/communicator.py mode change 100644 => 100755 onionr/communicatorutils/README.md mode change 100644 => 100755 onionr/communicatorutils/__init__.py mode change 100644 => 100755 onionr/communicatorutils/announcenode.py mode change 100644 => 100755 onionr/communicatorutils/connectnewpeers.py mode change 100644 => 100755 onionr/communicatorutils/cooldownpeer.py mode change 100644 => 100755 onionr/communicatorutils/daemonqueuehandler.py mode change 100644 => 100755 onionr/communicatorutils/deniableinserts.py mode change 100644 => 100755 onionr/communicatorutils/downloadblocks.py mode change 100644 => 100755 onionr/communicatorutils/housekeeping.py mode change 100644 => 100755 onionr/communicatorutils/lookupadders.py mode change 100644 => 100755 onionr/communicatorutils/lookupblocks.py mode change 100644 => 100755 onionr/communicatorutils/netcheck.py mode change 100644 => 100755 onionr/communicatorutils/proxypicker.py mode change 100644 => 100755 onionr/communicatorutils/servicecreator.py mode change 100644 => 100755 onionr/communicatorutils/uploadblocks.py mode change 100644 => 100755 onionr/config.py mode change 100644 => 100755 onionr/core.py mode change 100644 => 100755 onionr/dbcreator.py mode change 100644 => 100755 onionr/etc/README.md mode change 100644 => 100755 onionr/etc/humanreadabletime.py mode change 100644 => 100755 onionr/etc/powchoice.py mode change 100644 => 100755 onionr/httpapi/README.md mode change 100644 => 100755 onionr/httpapi/configapi/__init__.py mode change 100644 => 100755 onionr/httpapi/miscpublicapi/__init__.py mode change 100644 => 100755 onionr/httpapi/miscpublicapi/announce.py mode change 100644 => 100755 onionr/httpapi/miscpublicapi/getblocks.py mode change 100644 => 100755 onionr/httpapi/miscpublicapi/upload.py mode change 100644 => 100755 onionr/httpapi/profilesapi/__init__.py mode change 100644 => 100755 onionr/httpapi/profilesapi/profiles.py mode change 100644 => 100755 onionr/keymanager.py mode change 100644 => 100755 onionr/logger.py mode change 100644 => 100755 onionr/netcontroller.py mode change 100644 => 100755 onionr/onionrblacklist.py mode change 100644 => 100755 onionr/onionrblockapi.py mode change 100644 => 100755 onionr/onionrcommands/README.md mode change 100644 => 100755 onionr/onionrcommands/__init__.py mode change 100644 => 100755 onionr/onionrcommands/banblocks.py mode change 100644 => 100755 onionr/onionrcommands/daemonlaunch.py mode change 100644 => 100755 onionr/onionrcommands/exportblocks.py mode change 100644 => 100755 onionr/onionrcommands/filecommands.py mode change 100644 => 100755 onionr/onionrcommands/keyadders.py mode change 100644 => 100755 onionr/onionrcommands/onionrstatistics.py mode change 100644 => 100755 onionr/onionrcommands/openwebinterface.py mode change 100644 => 100755 onionr/onionrcommands/plugincommands.py mode change 100644 => 100755 onionr/onionrcommands/pubkeymanager.py mode change 100644 => 100755 onionr/onionrcommands/resettor.py mode change 100644 => 100755 onionr/onionrcrypto.py mode change 100644 => 100755 onionr/onionrevents.py mode change 100644 => 100755 onionr/onionrexceptions.py mode change 100644 => 100755 onionr/onionrpeers.py mode change 100644 => 100755 onionr/onionrpluginapi.py mode change 100644 => 100755 onionr/onionrplugins.py mode change 100644 => 100755 onionr/onionrproofs.py mode change 100644 => 100755 onionr/onionrservices/README.md mode change 100644 => 100755 onionr/onionrservices/__init__.py mode change 100644 => 100755 onionr/onionrservices/bootstrapservice.py mode change 100644 => 100755 onionr/onionrservices/connectionserver.py mode change 100644 => 100755 onionr/onionrservices/httpheaders.py mode change 100644 => 100755 onionr/onionrstorage.py mode change 100644 => 100755 onionr/onionrusers/README.md mode change 100644 => 100755 onionr/onionrusers/__init__.py mode change 100644 => 100755 onionr/onionrutils.py mode change 100644 => 100755 onionr/serializeddata.py mode change 100644 => 100755 onionr/setupconfig.py mode change 100644 => 100755 onionr/static-data/README.md mode change 100644 => 100755 onionr/static-data/default-plugins/clandestine/controlapi.py mode change 100644 => 100755 onionr/static-data/default-plugins/clandestine/peerserver.py mode change 100644 => 100755 onionr/static-data/default-plugins/flow/flowapi.py mode change 100644 => 100755 onionr/static-data/default-plugins/pms/loadinbox.py mode change 100644 => 100755 onionr/static-data/default-plugins/pms/mailapi.py mode change 100644 => 100755 onionr/static-data/www/board/board.js mode change 100644 => 100755 onionr/static-data/www/board/index.html mode change 100644 => 100755 onionr/static-data/www/board/theme.css mode change 100644 => 100755 onionr/static-data/www/clandestine/index.html mode change 100644 => 100755 onionr/static-data/www/clandestine/js/main.js mode change 100644 => 100755 onionr/static-data/www/mail/index.html mode change 100644 => 100755 onionr/static-data/www/mail/mail.css mode change 100644 => 100755 onionr/static-data/www/mail/mail.js mode change 100644 => 100755 onionr/static-data/www/private/index.html mode change 100644 => 100755 onionr/static-data/www/private/main.css mode change 100644 => 100755 onionr/static-data/www/profiles/index.html mode change 100644 => 100755 onionr/static-data/www/profiles/profiles.js mode change 100644 => 100755 onionr/static-data/www/shared/configeditor.js mode change 100644 => 100755 onionr/static-data/www/shared/main/stats.js mode change 100644 => 100755 onionr/static-data/www/shared/main/style.css mode change 100644 => 100755 onionr/static-data/www/shared/misc.js mode change 100644 => 100755 onionr/static-data/www/shared/onionr-icon.png mode change 100644 => 100755 onionr/static-data/www/shared/onionrblocks.js mode change 100644 => 100755 onionr/static-data/www/shared/panel.js mode change 100644 => 100755 onionr/static-data/www/shared/sites.js mode change 100644 => 100755 onionr/static-data/www/ui/dist/js/main.js mode change 100644 => 100755 onionr/storagecounter.py mode change 100644 => 100755 onionr/subprocesspow.py mode change 100644 => 100755 run-windows-dev.bat mode change 100644 => 100755 run-windows.bat diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml old mode 100644 new mode 100755 diff --git a/Makefile b/Makefile old mode 100644 new mode 100755 diff --git a/docs/onionr-1.png b/docs/onionr-1.png old mode 100644 new mode 100755 diff --git a/docs/onionr-2.png b/docs/onionr-2.png old mode 100644 new mode 100755 diff --git a/docs/onionr-3.png b/docs/onionr-3.png old mode 100644 new mode 100755 diff --git a/docs/onionr-icon.png b/docs/onionr-icon.png old mode 100644 new mode 100755 diff --git a/docs/onionr-logo.png b/docs/onionr-logo.png old mode 100644 new mode 100755 diff --git a/docs/specs/block-spec.md b/docs/specs/block-spec.md old mode 100644 new mode 100755 diff --git a/docs/tor-stinks-02.png b/docs/tor-stinks-02.png old mode 100644 new mode 100755 diff --git a/install/onionr.service b/install/onionr.service old mode 100644 new mode 100755 diff --git a/onionr/__init__.py b/onionr/__init__.py old mode 100644 new mode 100755 diff --git a/onionr/api.py b/onionr/api.py old mode 100644 new mode 100755 diff --git a/onionr/blockimporter.py b/onionr/blockimporter.py old mode 100644 new mode 100755 diff --git a/onionr/communicator.py b/onionr/communicator.py old mode 100644 new mode 100755 diff --git a/onionr/communicatorutils/README.md b/onionr/communicatorutils/README.md old mode 100644 new mode 100755 diff --git a/onionr/communicatorutils/__init__.py b/onionr/communicatorutils/__init__.py old mode 100644 new mode 100755 diff --git a/onionr/communicatorutils/announcenode.py b/onionr/communicatorutils/announcenode.py old mode 100644 new mode 100755 diff --git a/onionr/communicatorutils/connectnewpeers.py b/onionr/communicatorutils/connectnewpeers.py old mode 100644 new mode 100755 diff --git a/onionr/communicatorutils/cooldownpeer.py b/onionr/communicatorutils/cooldownpeer.py old mode 100644 new mode 100755 diff --git a/onionr/communicatorutils/daemonqueuehandler.py b/onionr/communicatorutils/daemonqueuehandler.py old mode 100644 new mode 100755 diff --git a/onionr/communicatorutils/deniableinserts.py b/onionr/communicatorutils/deniableinserts.py old mode 100644 new mode 100755 diff --git a/onionr/communicatorutils/downloadblocks.py b/onionr/communicatorutils/downloadblocks.py old mode 100644 new mode 100755 diff --git a/onionr/communicatorutils/housekeeping.py b/onionr/communicatorutils/housekeeping.py old mode 100644 new mode 100755 diff --git a/onionr/communicatorutils/lookupadders.py b/onionr/communicatorutils/lookupadders.py old mode 100644 new mode 100755 diff --git a/onionr/communicatorutils/lookupblocks.py b/onionr/communicatorutils/lookupblocks.py old mode 100644 new mode 100755 diff --git a/onionr/communicatorutils/netcheck.py b/onionr/communicatorutils/netcheck.py old mode 100644 new mode 100755 diff --git a/onionr/communicatorutils/proxypicker.py b/onionr/communicatorutils/proxypicker.py old mode 100644 new mode 100755 diff --git a/onionr/communicatorutils/servicecreator.py b/onionr/communicatorutils/servicecreator.py old mode 100644 new mode 100755 diff --git a/onionr/communicatorutils/uploadblocks.py b/onionr/communicatorutils/uploadblocks.py old mode 100644 new mode 100755 diff --git a/onionr/config.py b/onionr/config.py old mode 100644 new mode 100755 diff --git a/onionr/core.py b/onionr/core.py old mode 100644 new mode 100755 diff --git a/onionr/dbcreator.py b/onionr/dbcreator.py old mode 100644 new mode 100755 diff --git a/onionr/etc/README.md b/onionr/etc/README.md old mode 100644 new mode 100755 diff --git a/onionr/etc/humanreadabletime.py b/onionr/etc/humanreadabletime.py old mode 100644 new mode 100755 diff --git a/onionr/etc/powchoice.py b/onionr/etc/powchoice.py old mode 100644 new mode 100755 diff --git a/onionr/httpapi/README.md b/onionr/httpapi/README.md old mode 100644 new mode 100755 diff --git a/onionr/httpapi/configapi/__init__.py b/onionr/httpapi/configapi/__init__.py old mode 100644 new mode 100755 diff --git a/onionr/httpapi/miscpublicapi/__init__.py b/onionr/httpapi/miscpublicapi/__init__.py old mode 100644 new mode 100755 diff --git a/onionr/httpapi/miscpublicapi/announce.py b/onionr/httpapi/miscpublicapi/announce.py old mode 100644 new mode 100755 diff --git a/onionr/httpapi/miscpublicapi/getblocks.py b/onionr/httpapi/miscpublicapi/getblocks.py old mode 100644 new mode 100755 diff --git a/onionr/httpapi/miscpublicapi/upload.py b/onionr/httpapi/miscpublicapi/upload.py old mode 100644 new mode 100755 diff --git a/onionr/httpapi/profilesapi/__init__.py b/onionr/httpapi/profilesapi/__init__.py old mode 100644 new mode 100755 diff --git a/onionr/httpapi/profilesapi/profiles.py b/onionr/httpapi/profilesapi/profiles.py old mode 100644 new mode 100755 diff --git a/onionr/keymanager.py b/onionr/keymanager.py old mode 100644 new mode 100755 diff --git a/onionr/logger.py b/onionr/logger.py old mode 100644 new mode 100755 diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py old mode 100644 new mode 100755 index b1b1792b..79f77692 --- a/onionr/netcontroller.py +++ b/onionr/netcontroller.py @@ -145,7 +145,7 @@ HiddenServicePort 80 ''' + self.apiServerIP + ''':''' + str(self.hsPort) elif 'opening socks listener' in line.decode().lower(): logger.debug(line.decode().replace('\n', '')) else: - logger.fatal('Failed to start Tor. Maybe a stray instance of Tor used by Onionr is still running?') + logger.fatal('Failed to start Tor. Maybe a stray instance of Tor used by Onionr is still running? This can also be a result of file permissions being too open') return False except KeyboardInterrupt: logger.fatal('Got keyboard interrupt. Onionr will exit soon.', timestamp = False, level = logger.LEVEL_IMPORTANT) diff --git a/onionr/onionrblacklist.py b/onionr/onionrblacklist.py old mode 100644 new mode 100755 diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py old mode 100644 new mode 100755 diff --git a/onionr/onionrcommands/README.md b/onionr/onionrcommands/README.md old mode 100644 new mode 100755 diff --git a/onionr/onionrcommands/__init__.py b/onionr/onionrcommands/__init__.py old mode 100644 new mode 100755 diff --git a/onionr/onionrcommands/banblocks.py b/onionr/onionrcommands/banblocks.py old mode 100644 new mode 100755 diff --git a/onionr/onionrcommands/daemonlaunch.py b/onionr/onionrcommands/daemonlaunch.py old mode 100644 new mode 100755 diff --git a/onionr/onionrcommands/exportblocks.py b/onionr/onionrcommands/exportblocks.py old mode 100644 new mode 100755 diff --git a/onionr/onionrcommands/filecommands.py b/onionr/onionrcommands/filecommands.py old mode 100644 new mode 100755 diff --git a/onionr/onionrcommands/keyadders.py b/onionr/onionrcommands/keyadders.py old mode 100644 new mode 100755 diff --git a/onionr/onionrcommands/onionrstatistics.py b/onionr/onionrcommands/onionrstatistics.py old mode 100644 new mode 100755 diff --git a/onionr/onionrcommands/openwebinterface.py b/onionr/onionrcommands/openwebinterface.py old mode 100644 new mode 100755 diff --git a/onionr/onionrcommands/plugincommands.py b/onionr/onionrcommands/plugincommands.py old mode 100644 new mode 100755 diff --git a/onionr/onionrcommands/pubkeymanager.py b/onionr/onionrcommands/pubkeymanager.py old mode 100644 new mode 100755 diff --git a/onionr/onionrcommands/resettor.py b/onionr/onionrcommands/resettor.py old mode 100644 new mode 100755 diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py old mode 100644 new mode 100755 diff --git a/onionr/onionrevents.py b/onionr/onionrevents.py old mode 100644 new mode 100755 diff --git a/onionr/onionrexceptions.py b/onionr/onionrexceptions.py old mode 100644 new mode 100755 diff --git a/onionr/onionrpeers.py b/onionr/onionrpeers.py old mode 100644 new mode 100755 diff --git a/onionr/onionrpluginapi.py b/onionr/onionrpluginapi.py old mode 100644 new mode 100755 diff --git a/onionr/onionrplugins.py b/onionr/onionrplugins.py old mode 100644 new mode 100755 diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py old mode 100644 new mode 100755 diff --git a/onionr/onionrservices/README.md b/onionr/onionrservices/README.md old mode 100644 new mode 100755 diff --git a/onionr/onionrservices/__init__.py b/onionr/onionrservices/__init__.py old mode 100644 new mode 100755 diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py old mode 100644 new mode 100755 diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py old mode 100644 new mode 100755 diff --git a/onionr/onionrservices/httpheaders.py b/onionr/onionrservices/httpheaders.py old mode 100644 new mode 100755 diff --git a/onionr/onionrstorage.py b/onionr/onionrstorage.py old mode 100644 new mode 100755 diff --git a/onionr/onionrusers/README.md b/onionr/onionrusers/README.md old mode 100644 new mode 100755 diff --git a/onionr/onionrusers/__init__.py b/onionr/onionrusers/__init__.py old mode 100644 new mode 100755 diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py old mode 100644 new mode 100755 diff --git a/onionr/serializeddata.py b/onionr/serializeddata.py old mode 100644 new mode 100755 diff --git a/onionr/setupconfig.py b/onionr/setupconfig.py old mode 100644 new mode 100755 diff --git a/onionr/static-data/README.md b/onionr/static-data/README.md old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/clandestine/controlapi.py b/onionr/static-data/default-plugins/clandestine/controlapi.py old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/clandestine/peerserver.py b/onionr/static-data/default-plugins/clandestine/peerserver.py old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/flow/flowapi.py b/onionr/static-data/default-plugins/flow/flowapi.py old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/pms/loadinbox.py b/onionr/static-data/default-plugins/pms/loadinbox.py old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/pms/mailapi.py b/onionr/static-data/default-plugins/pms/mailapi.py old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/board/board.js b/onionr/static-data/www/board/board.js old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/board/index.html b/onionr/static-data/www/board/index.html old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/board/theme.css b/onionr/static-data/www/board/theme.css old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/clandestine/index.html b/onionr/static-data/www/clandestine/index.html old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/clandestine/js/main.js b/onionr/static-data/www/clandestine/js/main.js old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/mail/mail.css b/onionr/static-data/www/mail/mail.css old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/private/main.css b/onionr/static-data/www/private/main.css old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/profiles/index.html b/onionr/static-data/www/profiles/index.html old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/profiles/profiles.js b/onionr/static-data/www/profiles/profiles.js old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/shared/configeditor.js b/onionr/static-data/www/shared/configeditor.js old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/shared/main/stats.js b/onionr/static-data/www/shared/main/stats.js old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/shared/main/style.css b/onionr/static-data/www/shared/main/style.css old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/shared/misc.js b/onionr/static-data/www/shared/misc.js old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/shared/onionr-icon.png b/onionr/static-data/www/shared/onionr-icon.png old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/shared/onionrblocks.js b/onionr/static-data/www/shared/onionrblocks.js old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/shared/panel.js b/onionr/static-data/www/shared/panel.js old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/shared/sites.js b/onionr/static-data/www/shared/sites.js old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/dist/js/main.js b/onionr/static-data/www/ui/dist/js/main.js old mode 100644 new mode 100755 diff --git a/onionr/storagecounter.py b/onionr/storagecounter.py old mode 100644 new mode 100755 diff --git a/onionr/subprocesspow.py b/onionr/subprocesspow.py old mode 100644 new mode 100755 diff --git a/run-windows-dev.bat b/run-windows-dev.bat old mode 100644 new mode 100755 diff --git a/run-windows.bat b/run-windows.bat old mode 100644 new mode 100755 From 3825d3857c9cf3a1dd9b1bd8a6e5955cf1370e57 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 16 Jun 2019 15:56:22 -0500 Subject: [PATCH 096/173] - removed defunct config option * changed bootstrap node * improved web interface styles * start-daemon script hopefully less prone to crash * fixed openwebinterface url output * added defaults for timer configs incase config can't be read or the values are missing --- onionr/communicator.py | 4 ++-- onionr/onionr.py | 2 -- onionr/onionrcommands/openwebinterface.py | 2 +- onionr/static-data/bootstrap-nodes.txt | 2 +- onionr/static-data/www/friends/friends.js | 5 ++++- onionr/static-data/www/friends/index.html | 6 ++++-- onionr/static-data/www/private/index.html | 2 +- onionr/static-data/www/shared/main/style.css | 6 +++++- start-daemon.sh | 2 +- 9 files changed, 19 insertions(+), 12 deletions(-) diff --git a/onionr/communicator.py b/onionr/communicator.py index a75cd998..b245b4de 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -101,8 +101,8 @@ class OnionrCommunicatorDaemon: OnionrCommunicatorTimers(self, self.runCheck, 2, maxThreads=1) # Timers to periodically lookup new blocks and download them - OnionrCommunicatorTimers(self, self.lookupBlocks, self._core.config.get('timers.lookupBlocks'), requiresPeer=True, maxThreads=1) - OnionrCommunicatorTimers(self, self.getBlocks, self._core.config.get('timers.getBlocks'), requiresPeer=True, maxThreads=2) + OnionrCommunicatorTimers(self, self.lookupBlocks, self._core.config.get('timers.lookupBlocks', 25), requiresPeer=True, maxThreads=1) + OnionrCommunicatorTimers(self, self.getBlocks, self._core.config.get('timers.getBlocks', 30), requiresPeer=True, maxThreads=2) # Timer to reset the longest offline peer so contact can be attempted again OnionrCommunicatorTimers(self, self.clearOfflinePeer, 58) diff --git a/onionr/onionr.py b/onionr/onionr.py index c73edb4b..16ec1839 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -121,8 +121,6 @@ class Onionr: if type(config.get('client.public.port')) is type(None): randomPort = netcontroller.getOpenPort() 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) diff --git a/onionr/onionrcommands/openwebinterface.py b/onionr/onionrcommands/openwebinterface.py index c28f3217..2bd0cdd3 100755 --- a/onionr/onionrcommands/openwebinterface.py +++ b/onionr/onionrcommands/openwebinterface.py @@ -26,5 +26,5 @@ def open_home(o_inst): logger.error('Onionr seems to not be running (could not get api host)') else: url = 'http://%s/#%s' % (url, o_inst.onionrCore.config.get('client.webpassword')) - logger.info('If Onionr does not open automatically, use this URL:', url) + logger.info('If Onionr does not open automatically, use this URL: ' + url) webbrowser.open_new_tab(url) \ No newline at end of file diff --git a/onionr/static-data/bootstrap-nodes.txt b/onionr/static-data/bootstrap-nodes.txt index faec3008..885591e6 100755 --- a/onionr/static-data/bootstrap-nodes.txt +++ b/onionr/static-data/bootstrap-nodes.txt @@ -1 +1 @@ -7abghj4dhb4p7ytet4wm4hsejuf2c5ufaznypoifkcacvlzs6yhc6gyd.onion \ No newline at end of file +6i2apk7llawfcqy4y4xumqiedo7nltczmdk2cbvdztaqpkjnygwwz6qd.onion \ No newline at end of file diff --git a/onionr/static-data/www/friends/friends.js b/onionr/static-data/www/friends/friends.js index 28e2a2da..cd17bbc8 100755 --- a/onionr/static-data/www/friends/friends.js +++ b/onionr/static-data/www/friends/friends.js @@ -54,7 +54,10 @@ fetch('/friends/list', { var keys = []; for(var k in resp) keys.push(k); console.log(keys) - friendListDisplay.innerHTML = 'Click name to view info

    ' + + if (keys.length == 0){ + friendListDisplay.innerText = "None yet :(" + } for (var i = 0; i < keys.length; i++){ var peer = keys[i] var name = resp[keys[i]]['name'] diff --git a/onionr/static-data/www/friends/index.html b/onionr/static-data/www/friends/index.html index 8e38ac39..d7673d70 100755 --- a/onionr/static-data/www/friends/index.html +++ b/onionr/static-data/www/friends/index.html @@ -21,7 +21,7 @@

    - Onionr Web Control Panel + Onionr Friends

    Home

    Friend Manager

    @@ -31,7 +31,9 @@

    Friend List:

    -
    None Yet :(
    + Click name to view info

    +
    +
    diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index 2bb74000..633bda94 100755 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -17,7 +17,7 @@
    - Onionr Web Control Panel + Onionr

    🕵️‍♂️ Current Used Identity:

    diff --git a/onionr/static-data/www/shared/main/style.css b/onionr/static-data/www/shared/main/style.css index a673ea7b..c895696b 100755 --- a/onionr/static-data/www/shared/main/style.css +++ b/onionr/static-data/www/shared/main/style.css @@ -41,11 +41,15 @@ body{ vertical-align: middle; } .logoText{ - font-family: sans-serif; font-size: 2em; margin-top: 1em; margin-left: 1%; } + + .logoText, h1, h2{ + font-family: Verdana, Geneva, Tahoma, sans-serif; + } + .main{ min-height: 500px; } diff --git a/start-daemon.sh b/start-daemon.sh index 1b713100..4b2e3abb 100755 --- a/start-daemon.sh +++ b/start-daemon.sh @@ -2,4 +2,4 @@ cd "$(dirname "$0")" echo "starting Onionr daemon..." echo "run onionr.sh stop to stop the daemon, or onionr.sh start to get output" -nohup ./onionr.sh start & disown > /dev/null +exec nohup ./onionr.sh start > /dev/null 2>&1 & disown \ No newline at end of file From ce2095d7f8802b161acae1f9cb289ca9c0209d7f Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 16 Jun 2019 17:34:43 -0500 Subject: [PATCH 097/173] + added hit count stat - removed bypass_tor_check as defunct config option * actually fixed connect stat in cli for real this time * improved ui stats more --- docs/http-api.md | 3 +++ onionr/api.py | 6 ++++++ onionr/netcontroller.py | 1 - onionr/onionrcommands/onionrstatistics.py | 2 +- .../default-plugins/pluginmanager/main.py | 2 -- .../static-data/default-plugins/pms/main.py | 5 +++-- onionr/static-data/default_config.json | 21 +++++++++---------- onionr/static-data/www/private/index.html | 13 +++++++----- onionr/static-data/www/shared/main/stats.js | 5 ++++- onionr/static-data/www/shared/main/style.css | 2 +- 10 files changed, 36 insertions(+), 24 deletions(-) diff --git a/docs/http-api.md b/docs/http-api.md index 241320ae..74745e1a 100755 --- a/docs/http-api.md +++ b/docs/http-api.md @@ -37,6 +37,9 @@ Please note: endpoints that simply provide static web app files are not document * /getblockheader/hash - Methods: GET - Returns the header (metadata section) of a block. +* /hitcount + - Methods: GET + - Return the amount of requests the public api server has received this session * /lastconnect - Methods: GET - Returns the epoch timestamp of when the last incoming connection to the public API server was logged diff --git a/onionr/api.py b/onionr/api.py index 854711f0..ab1eeb59 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -76,6 +76,7 @@ class PublicAPI: self.i2pAdder = clientAPI._core.i2pAddress self.bindPort = config.get('client.public.port') self.lastRequest = 0 + self.hitCount = 0 # total rec requests to public api since server started logger.info('Running public api on %s:%s' % (self.host, self.bindPort)) @app.before_request @@ -90,6 +91,7 @@ class PublicAPI: if request.host not in (self.i2pAdder, self.torAdder): # Disallow connection if wrong HTTP hostname, in order to prevent DNS rebinding attacks abort(403) + self.hitCount += 1 # raise hit count for valid requests @app.after_request def sendHeaders(resp): @@ -295,6 +297,10 @@ class API: abort(403) return send_from_directory(config.get('www.private.path', 'static-data/www/private/'), path) + @app.route('/hitcount') + def get_hit_count(): + return Response(str(self.publicAPI.hitCount)) + @app.route('/queueResponseAdd/', methods=['post']) def queueResponseAdd(name): # Responses from the daemon. TODO: change to direct var access instead of http endpoint diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py index 79f77692..74087500 100755 --- a/onionr/netcontroller.py +++ b/onionr/netcontroller.py @@ -72,7 +72,6 @@ class NetController: hsVer = '# v2 onions' if config.get('tor.v3onions'): hsVer = 'HiddenServiceVersion 3' - logger.debug('Using v3 onions') if os.path.exists(self.torConfigLocation): os.remove(self.torConfigLocation) diff --git a/onionr/onionrcommands/onionrstatistics.py b/onionr/onionrcommands/onionrstatistics.py index 88643b0c..918d8445 100755 --- a/onionr/onionrcommands/onionrstatistics.py +++ b/onionr/onionrcommands/onionrstatistics.py @@ -39,7 +39,7 @@ def show_stats(o_inst): # count stats 'div2' : True, - 'Known Peers' : str(len(o_inst.onionrCore.listPeers())), + 'Known Peers' : str(max(len(o_inst.onionrCore.listPeers()) - 1, 0)), 'Enabled Plugins' : str(len(o_inst.onionrCore.config.get('plugins.enabled', list()))) + ' / ' + str(len(os.listdir(o_inst.dataDir + 'plugins/'))), 'Stored Blocks' : str(totalBlocks), 'Percent Blocks Signed' : str(round(100 * signedBlocks / max(totalBlocks, 1), 2)) + '%' diff --git a/onionr/static-data/default-plugins/pluginmanager/main.py b/onionr/static-data/default-plugins/pluginmanager/main.py index aa5c2887..979b1133 100755 --- a/onionr/static-data/default-plugins/pluginmanager/main.py +++ b/onionr/static-data/default-plugins/pluginmanager/main.py @@ -389,8 +389,6 @@ def commandInstallPlugin(): pkobh = distributor except Exception as e: logger.warn('Failed to lookup plugin in repositories.', timestamp = False) - logger.error('asdf', error = e, timestamp = False) - return True if pkobh is None: diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py index ce58c58e..9acb99f2 100755 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -303,9 +303,10 @@ def add_deleted(keyStore, bHash): keyStore.put('deleted_mail', existing.append(bHash)) def on_insertblock(api, data={}): - sentboxTools = sentboxdb.SentBox(api.get_core()) meta = json.loads(data['meta']) - sentboxTools.addToSent(data['hash'], data['peer'], data['content'], meta['subject']) + if meta['type'] == 'pm': + sentboxTools = sentboxdb.SentBox(api.get_core()) + sentboxTools.addToSent(data['hash'], data['peer'], data['content'], meta['subject']) def on_init(api, data = None): ''' diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index 65cad2f5..cd68ede8 100755 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -2,17 +2,16 @@ "general" : { "dev_mode" : true, "display_header" : false, - "minimum_block_pow": 4, - "minimum_send_pow": 4, - "use_subprocess_pow_if_possible": true, - "socket_servers": false, - "security_level": 0, - "hide_created_blocks": true, - "insert_deniable_blocks": true, - "max_block_age": 2678400, - "bypass_tor_check": false, - "public_key": "", - "random_bind_ip": false + "minimum_block_pow" : 4, + "minimum_send_pow" : 4, + "use_subprocess_pow_if_possible" : true, + "socket_servers" : false, + "security_level" : 0, + "hide_created_blocks" : true, + "insert_deniable_blocks" : true, + "max_block_age" : 2678400, + "public_key" : "", + "random_bind_ip" : false }, "www" : { diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index 633bda94..07247011 100755 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -23,7 +23,7 @@


    -

    Onionr Services

    +

    Onionr Services




    Mail - Friend Manager - Boards - @@ -38,13 +38,16 @@
    -

    Stats

    +

    Statistics

    🕰️ Uptime:

    -

    🖇️ Last Received Connection: None since start

    -

    💾 Stored Blocks:

    -

    📨 Blocks in queue:

    +

    Connections

    +

    🖇️ Last Received Request: None since start

    +

    ⬇️ Total Requests Received: None since start

    🔗 Outgoing Connections:

    
    +            

    Blocks

    +

    💾 Stored Blocks:

    +

    📨 Blocks in queue:

    diff --git a/onionr/static-data/www/shared/main/stats.js b/onionr/static-data/www/shared/main/stats.js index 3c8d2af6..86b7f451 100755 --- a/onionr/static-data/www/shared/main/stats.js +++ b/onionr/static-data/www/shared/main/stats.js @@ -21,6 +21,7 @@ connectedDisplay = document.getElementById('connectedNodes') storedBlockDisplay = document.getElementById('storedBlocks') queuedBlockDisplay = document.getElementById('blockQueue') lastIncoming = document.getElementById('lastIncoming') +totalRec = document.getElementById('totalRec') function getStats(){ stats = JSON.parse(httpGet('getstats', webpass)) @@ -28,11 +29,13 @@ function getStats(){ connectedDisplay.innerText = stats['connectedNodes'] storedBlockDisplay.innerText = stats['blockCount'] queuedBlockDisplay.innerText = stats['blockQueueCount'] + totalRec.innerText = httpGet('/hitcount') var lastConnect = httpGet('/lastconnect') if (lastConnect > 0){ var humanDate = new Date(0) humanDate.setUTCSeconds(httpGet('/lastconnect')) - lastConnect = humanDate.toString() + humanDate = humanDate.toString() + lastConnect = humanDate.substring(0, humanDate.indexOf('(')); } else{ lastConnect = 'None since start' diff --git a/onionr/static-data/www/shared/main/style.css b/onionr/static-data/www/shared/main/style.css index c895696b..18388845 100755 --- a/onionr/static-data/www/shared/main/style.css +++ b/onionr/static-data/www/shared/main/style.css @@ -46,7 +46,7 @@ body{ margin-left: 1%; } - .logoText, h1, h2{ + .logoText, h1, h2, h3{ font-family: Verdana, Geneva, Tahoma, sans-serif; } From b377890a791c9011bb5f66d99f9b7b144bd3efa7 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 16 Jun 2019 22:02:06 -0500 Subject: [PATCH 098/173] removed another defunct config value --- onionr/static-data/default_config.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index cd68ede8..bf757242 100755 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -17,14 +17,12 @@ "www" : { "public" : { "run" : true, - "path" : "static-data/www/public/", - "guess_mime" : true + "path" : "static-data/www/public/" }, "private" : { "run" : true, - "path" : "static-data/www/private/", - "guess_mime" : true + "path" : "static-data/www/private/" }, "ui" : { From 6c69d72d34e9163ba1c90a930e70e1dd6d5af303 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 16 Jun 2019 23:24:12 -0500 Subject: [PATCH 099/173] + added add to contact button in box message viewer * fixed/improved name setting for friends/contacts --- .../default-plugins/pms/mailapi.py | 2 + onionr/static-data/www/mail/index.html | 4 +- onionr/static-data/www/mail/mail.css | 5 ++- onionr/static-data/www/mail/mail.js | 38 ++++++++++++++++--- 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/onionr/static-data/default-plugins/pms/mailapi.py b/onionr/static-data/default-plugins/pms/mailapi.py index c9df3a52..2ba99d08 100755 --- a/onionr/static-data/default-plugins/pms/mailapi.py +++ b/onionr/static-data/default-plugins/pms/mailapi.py @@ -60,4 +60,6 @@ def list_sentbox(): for x in list_copy: if x['hash'] in deleted: sentbox_list.remove(x) + continue + x['name'] = contactmanager.ContactManager(c, x['peer'], saveUser=False).get_info('name') return json.dumps(sentbox_list) diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index 81a0128e..4a671ab5 100755 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -22,7 +22,7 @@ API server either shutdown, has disabled mail, or has experienced a bug.

    -
    Current Used Identity:
    +
    🕵️‍♂️ Current Used Identity:


    @@ -34,7 +34,7 @@
    - From: Signature: + From: Signature:
    Subject: diff --git a/onionr/static-data/www/mail/mail.css b/onionr/static-data/www/mail/mail.css index 88afc0b9..32999440 100755 --- a/onionr/static-data/www/mail/mail.css +++ b/onionr/static-data/www/mail/mail.css @@ -54,10 +54,13 @@ input{ } .mailPing{ - display: none; color: orange; } + #addUnknownContact, .mailPing{ + display: none; + } + .danger{ color: red; } diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index fb7b2b3a..cc4895a1 100755 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -24,6 +24,19 @@ threadPlaceholder = document.getElementById('threadPlaceholder') tabBtns = document.getElementById('tabBtns') threadContent = {} replyBtn = document.getElementById('replyBtn') +addUnknownContact = document.getElementById('addUnknownContact') + +function addContact(pubkey, friendName){ + fetch('/friends/add/' + pubkey, { + method: 'POST', + headers: { + "token": webpass + }}).then(function(data) { + if (friendName.trim().length > 0){ + post_to_url('/friends/setinfo/' + pubkey + '/name', {'data': friendName, 'token': webpass}) + } + }) +} function openReply(bHash, quote, subject){ var inbox = document.getElementsByClassName('threadEntry') @@ -53,6 +66,7 @@ function openReply(bHash, quote, subject){ } function openThread(bHash, sender, date, sigBool, pubkey, subjectLine){ + addUnknownContact.style.display = 'none' var messageDisplay = document.getElementById('threadDisplay') var blockContent = httpGet('/getblockbody/' + bHash) @@ -62,6 +76,11 @@ function openThread(bHash, sender, date, sigBool, pubkey, subjectLine){ var sigEl = document.getElementById('sigValid') var sigMsg = 'signature' + // show add unknown contact button if peer is unknown but still has pubkey + if (sender == pubkey){ + addUnknownContact.style.display = 'inline' + } + if (sigBool){ sigMsg = 'Good ' + sigMsg sigEl.classList.remove('danger') @@ -76,6 +95,13 @@ function openThread(bHash, sender, date, sigBool, pubkey, subjectLine){ replyBtn.onclick = function(){ openReply(bHash, messageDisplay.innerText, subjectLine) } + addUnknownContact.onclick = function(){ + var friendName = prompt("Enter an alias for this contact:") + if (friendName === null || friendName.length == 0){ + return + } + addContact(pubkey, friendName) + } } function setActiveTab(tabName){ @@ -144,6 +170,7 @@ function loadInboxEntries(bHash){ var humanDate = new Date(0) var metadata = resp['metadata'] humanDate.setUTCSeconds(resp['meta']['time']) + humanDate = humanDate.toString() validSig.style.display = 'none' if (resp['meta']['signer'] != ''){ senderInput.value = httpGet('/friends/getinfo/' + resp['meta']['signer'] + '/name') @@ -162,7 +189,7 @@ function loadInboxEntries(bHash){ entry.setAttribute('data-hash', bHash) entry.setAttribute('data-pubkey', resp['meta']['signer']) senderInput.readOnly = true - dateStr.innerText = humanDate.toString() + dateStr.innerText = humanDate.substring(0, humanDate.indexOf('(')) deleteBtn.innerText = 'X' deleteBtn.classList.add('dangerBtn', 'deleteBtn') if (metadata['subject'] === undefined || metadata['subject'] === null) { @@ -237,14 +264,15 @@ function getSentbox(){ var sentDate = document.createElement('span') var humanDate = new Date(0) humanDate.setUTCSeconds(resp[i]['date']) + humanDate = humanDate.toString() var preview = document.createElement('span') var deleteBtn = document.createElement('button') var message = resp[i]['message'] deleteBtn.classList.add('deleteBtn', 'dangerBtn') deleteBtn.innerText = 'X' toEl.readOnly = true - sentDate.innerText = humanDate - if (resp[i]['name'] == null){ + sentDate.innerText = humanDate.substring(0, humanDate.indexOf('(')) + if (resp[i]['name'] == null || resp[i]['name'].toLowerCase() == 'anonymous'){ toEl.value = resp[i]['peer'] } else{ @@ -266,7 +294,7 @@ function getSentbox(){ e.target.parentNode.parentNode.removeChild(e.target.parentNode) return } - showSentboxWindow(resp[i]['peer'], message) + showSentboxWindow(toEl.value, message) } })(i, resp) threadPart.appendChild(entry) @@ -319,7 +347,7 @@ fetch('/friends/list', { friendSelectParent.appendChild(document.createElement('option')) for (var i = 0; i < keys.length; i++) { var option = document.createElement("option") - var name = resp[keys[i]]['name'] + var name = resp[keys[i]]['name'] || "" option.value = keys[i] if (name.length == 0){ option.text = keys[i] From ba7de585f535ef49864831bffe8a909824e21ec4 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 17 Jun 2019 02:40:32 -0500 Subject: [PATCH 100/173] updated readme --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 581f9510..536e323b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ (***pre-alpha & experimental, not well tested or easy to use yet***) [![Open Source Love](https://badges.frapsoft.com/os/v3/open-source.png?v=103)](https://github.com/ellerbrock/open-source-badges/) - + - [Onionr.net](https://onionr.net/)
    @@ -48,7 +48,7 @@ Currently usable: * Mail * Public anonymous chat -* Simple webpage hosting (Will be greatly extended) +* Simple webpage hosting - Will be greatly extended * File sharing (Work in progress) Not yet usable: @@ -107,7 +107,7 @@ Everyone is welcome to contribute. Help is wanted for the following: * Security review/audit * Automatic I2P setup -Contribute money: +## Contribute money: Donating at least $5 gets you cool Onionr stickers. Get in touch if you want them. @@ -119,10 +119,12 @@ USD (Card/Paypal): [Ko-Fi](https://www.ko-fi.com/beardogkf) Note: probably not tax deductible -## Contact +# Contact Email: beardog [ at ] mailbox.org +Onionr Mail: TRH763JURNY47QPBTTQ4LLPYCYQK6Q5YA33R6GANKZK5C5DKCIGQ==== + ## Disclaimers and legal No matter how good Onionr and other software gets, there will always be ways for clever or well-funded adversaries to break your security. @@ -139,7 +141,7 @@ Tor is a trademark for the Tor Project. We do not own it. The 'open source badge' is by Maik Ellerbrock and is licensed under a Creative Commons Attribution 4.0 International License. -## Logo +## Onionr Logo The Onionr logo was created by [Anhar Ismail](https://github.com/anharismail) under the [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/). From 1daf410f8b8342fd10f75245601bff5cfc9d9619 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 17 Jun 2019 15:51:00 -0500 Subject: [PATCH 101/173] bumped api crash timeout again --- onionr/communicator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionr/communicator.py b/onionr/communicator.py index b245b4de..acd749f0 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -364,7 +364,7 @@ class OnionrCommunicatorDaemon: def detectAPICrash(self): '''exit if the api server crashes/stops''' if self._core._utils.localCommand('ping', silent=False) not in ('pong', 'pong!'): - for i in range(20): + for i in range(300): if self._core._utils.localCommand('ping') in ('pong', 'pong!') or self.shutdown: break # break for loop time.sleep(1) From a03d0268a8427f5310d41800d332c4159a45a54e Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 17 Jun 2019 16:02:45 -0500 Subject: [PATCH 102/173] correct start-daemon shebang --- start-daemon.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/start-daemon.sh b/start-daemon.sh index 4b2e3abb..6b478dae 100755 --- a/start-daemon.sh +++ b/start-daemon.sh @@ -1,5 +1,5 @@ -#!/usr/bin/bash +#!/bin/bash cd "$(dirname "$0")" echo "starting Onionr daemon..." echo "run onionr.sh stop to stop the daemon, or onionr.sh start to get output" -exec nohup ./onionr.sh start > /dev/null 2>&1 & disown \ No newline at end of file +exec nohup ./onionr.sh start > /dev/null 2>&1 & disown From 3855aa544f99a94fdf43c933bc2c29e848521dfb Mon Sep 17 00:00:00 2001 From: Travis Date: Mon, 17 Jun 2019 22:36:19 -0500 Subject: [PATCH 103/173] Updated favicon Moved onionr-icon.png into shared/images --- onionr/static-data/www/clandestine/index.html | 4 ++-- onionr/static-data/www/friends/index.html | 4 ++-- onionr/static-data/www/mail/index.html | 4 ++-- onionr/static-data/www/private/index.html | 8 ++++---- onionr/static-data/www/profiles/index.html | 2 +- onionr/static-data/www/shared/images/favicon.ico | Bin 0 -> 3568 bytes .../www/shared/{ => images}/onionr-icon.png | Bin 7 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 onionr/static-data/www/shared/images/favicon.ico rename onionr/static-data/www/shared/{ => images}/onionr-icon.png (100%) diff --git a/onionr/static-data/www/clandestine/index.html b/onionr/static-data/www/clandestine/index.html index 2badec8d..91bc2e07 100755 --- a/onionr/static-data/www/clandestine/index.html +++ b/onionr/static-data/www/clandestine/index.html @@ -5,13 +5,13 @@ Clandestine - +
    - + Clandestine
      diff --git a/onionr/static-data/www/friends/index.html b/onionr/static-data/www/friends/index.html index d7673d70..78590d55 100755 --- a/onionr/static-data/www/friends/index.html +++ b/onionr/static-data/www/friends/index.html @@ -5,7 +5,7 @@ Onionr - + @@ -20,7 +20,7 @@
      - + Onionr Friends

      Home diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index 4a671ab5..309cdc88 100755 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -5,7 +5,7 @@ Onionr Mail - + @@ -14,7 +14,7 @@
      - + Onionr Mail ✉️

      Home
      diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index 07247011..151c3704 100755 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -5,7 +5,7 @@ Onionr - + @@ -16,7 +16,7 @@
      - + Onionr

      🕵️‍♂️ Current Used Identity:
      @@ -52,7 +52,7 @@ - - + + \ No newline at end of file diff --git a/onionr/static-data/www/profiles/index.html b/onionr/static-data/www/profiles/index.html index a4dad9b7..7134b943 100755 --- a/onionr/static-data/www/profiles/index.html +++ b/onionr/static-data/www/profiles/index.html @@ -5,7 +5,7 @@ Onionr Profiles - + diff --git a/onionr/static-data/www/shared/images/favicon.ico b/onionr/static-data/www/shared/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3c11d7e278a51daabb68f484a727c6d564bbbbfa GIT binary patch literal 3568 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 qs`wXub+!$E(U+&& Date: Mon, 17 Jun 2019 23:33:22 -0500 Subject: [PATCH 104/173] added announce node config --- onionr/communicator.py | 2 +- onionr/static-data/default_config.json | 1 + start-daemon.sh | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/onionr/communicator.py b/onionr/communicator.py index acd749f0..2c0bf4c7 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -143,7 +143,7 @@ class OnionrCommunicatorDaemon: netCheckTimer = OnionrCommunicatorTimers(self, netcheck.net_check, 600, myArgs=[self]) # Announce the public API server transport address to other nodes if security level allows - if config.get('general.security_level', 1) == 0: + if config.get('general.security_level', 1) == 0 and config.get('general.announce_node', True): # Default to high security level incase config breaks announceTimer = OnionrCommunicatorTimers(self, announcenode.announce_node, 3600, myArgs=[self], requiresPeer=True, maxThreads=1) announceTimer.count = (announceTimer.frequency - 120) diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index bf757242..3fb3b3a8 100755 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -1,6 +1,7 @@ { "general" : { "dev_mode" : true, + "announce_node" : true, "display_header" : false, "minimum_block_pow" : 4, "minimum_send_pow" : 4, diff --git a/start-daemon.sh b/start-daemon.sh index 6b478dae..d427d8e8 100755 --- a/start-daemon.sh +++ b/start-daemon.sh @@ -1,5 +1,5 @@ #!/bin/bash cd "$(dirname "$0")" echo "starting Onionr daemon..." -echo "run onionr.sh stop to stop the daemon, or onionr.sh start to get output" +echo "run onionr.sh stop to stop the daemon" exec nohup ./onionr.sh start > /dev/null 2>&1 & disown From 1b897ebe1dbc3c0063bc03ab4908a38dc2c78101 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 18 Jun 2019 10:02:54 -0500 Subject: [PATCH 105/173] disabled api crash detector to see if it is the thing causing crashes --- onionr/communicator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionr/communicator.py b/onionr/communicator.py index 2c0bf4c7..de49e601 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -123,7 +123,7 @@ class OnionrCommunicatorDaemon: OnionrCommunicatorTimers(self, self.daemonCommands, 6, maxThreads=3) # Timer that kills Onionr if the API server crashes - OnionrCommunicatorTimers(self, self.detectAPICrash, 30, maxThreads=1) + #OnionrCommunicatorTimers(self, self.detectAPICrash, 30, maxThreads=1) # Setup direct connections if config.get('general.socket_servers', False): From cb2e803ae81f7fce9ab0f4c539168a811a0549bc Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 18 Jun 2019 11:55:14 -0500 Subject: [PATCH 106/173] use contact module for friend command list names instead of getName() --- onionr/onionrcommands/pubkeymanager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/onionr/onionrcommands/pubkeymanager.py b/onionr/onionrcommands/pubkeymanager.py index a294396b..e12fc670 100755 --- a/onionr/onionrcommands/pubkeymanager.py +++ b/onionr/onionrcommands/pubkeymanager.py @@ -20,7 +20,7 @@ import sys, getpass import logger, onionrexceptions -from onionrusers import onionrusers +from onionrusers import onionrusers, contactmanager def add_ID(o_inst): try: sys.argv[2] @@ -75,8 +75,8 @@ def friend_command(o_inst): action = action.lower() if action == 'list': # List out peers marked as our friend - for friend in onionrusers.OnionrUser.list_friends(o_inst.onionrCore): - logger.info(friend.publicKey + ' - ' + friend.getName()) + for friend in contactmanager.ContactManager.list_friends(o_inst.onionrCore): + logger.info(friend.publicKey + ' - ' + friend.get_info('name')) elif action in ('add', 'remove'): try: friend = sys.argv[3] From 8082570b7fc78470d601b0d57171a7b8b723805e Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 19 Jun 2019 01:57:13 -0500 Subject: [PATCH 107/173] * bumped nacl and unpaddedbase32 verison * added/improved support for unpaddedbase32 keys * greatly improved home UI and mail * deniable blocks shouldnt use forward secrecy anymore * dont add yourself as a contact --- onionr/communicatorutils/deniableinserts.py | 2 +- onionr/core.py | 9 +++- onionr/httpapi/friendsapi/__init__.py | 3 +- onionr/onionrcommands/pubkeymanager.py | 2 + onionr/onionrcrypto.py | 6 ++- onionr/onionrusers/contactmanager.py | 2 + onionr/onionrusers/onionrusers.py | 5 +- onionr/onionrutils.py | 6 ++- onionr/static-data/www/mail/mail.css | 4 ++ onionr/static-data/www/mail/mail.js | 6 +-- onionr/static-data/www/mail/sendmail.js | 7 ++- onionr/static-data/www/private/index.html | 10 ++-- onionr/static-data/www/private/main.css | 10 ++++ onionr/static-data/www/shared/main/stats.js | 25 +++++++++ onionr/static-data/www/shared/main/style.css | 8 +++ onionr/tests/test_stringvalidations.py | 8 +-- requirements.in | 3 +- requirements.txt | 56 ++++++++------------ 18 files changed, 117 insertions(+), 55 deletions(-) diff --git a/onionr/communicatorutils/deniableinserts.py b/onionr/communicatorutils/deniableinserts.py index f993e6d0..571a6093 100755 --- a/onionr/communicatorutils/deniableinserts.py +++ b/onionr/communicatorutils/deniableinserts.py @@ -27,5 +27,5 @@ def insert_deniable_block(comm_inst): # This assumes on the libsodium primitives to have key-privacy fakePeer = onionrvalues.DENIABLE_PEER_ADDRESS data = secrets.token_hex(secrets.randbelow(1024) + 1) - comm_inst._core.insertBlock(data, header='pm', encryptType='asym', asymPeer=fakePeer, meta={'subject': 'foo'}) + comm_inst._core.insertBlock(data, header='pm', encryptType='asym', asymPeer=fakePeer, disableForward=True, meta={'subject': 'foo'}) comm_inst.decrementThreadCount('insert_deniable_block') \ No newline at end of file diff --git a/onionr/core.py b/onionr/core.py index 33b310af..be309309 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -128,7 +128,8 @@ class Core: ''' Adds a public key to the key database (misleading function name) ''' - assert peerID not in self.listPeers() + if peerID in self.listPeers() or peerID == self._crypto.pubKey: + raise ValueError("specified id is already known") # This function simply adds a peer to the DB if not self._utils.validatePubKey(peerID): @@ -776,7 +777,11 @@ class Core: data = self._crypto.pubKeyEncrypt(data, asymPeer, encodedData=True).decode() signature = self._crypto.pubKeyEncrypt(signature, asymPeer, encodedData=True).decode() signer = self._crypto.pubKeyEncrypt(signer, asymPeer, encodedData=True).decode() - onionrusers.OnionrUser(self, asymPeer, saveUser=True) + try: + onionrusers.OnionrUser(self, asymPeer, saveUser=True) + except ValueError: + # if peer is already known + pass else: raise onionrexceptions.InvalidPubkey(asymPeer + ' is not a valid base32 encoded ed25519 key') diff --git a/onionr/httpapi/friendsapi/__init__.py b/onionr/httpapi/friendsapi/__init__.py index 8d6b4122..5b4a883b 100755 --- a/onionr/httpapi/friendsapi/__init__.py +++ b/onionr/httpapi/friendsapi/__init__.py @@ -26,7 +26,8 @@ friends = Blueprint('friends', __name__) @friends.route('/friends/list') def list_friends(): pubkey_list = {} - friend_list = contactmanager.ContactManager.list_friends(core.Core()) + c = core.Core() + friend_list = contactmanager.ContactManager.list_friends(c) for friend in friend_list: pubkey_list[friend.publicKey] = {'name': friend.get_info('name')} return json.dumps(pubkey_list) diff --git a/onionr/onionrcommands/pubkeymanager.py b/onionr/onionrcommands/pubkeymanager.py index e12fc670..2a6370f9 100755 --- a/onionr/onionrcommands/pubkeymanager.py +++ b/onionr/onionrcommands/pubkeymanager.py @@ -21,6 +21,7 @@ import sys, getpass import logger, onionrexceptions from onionrusers import onionrusers, contactmanager +import unpaddedbase32 def add_ID(o_inst): try: sys.argv[2] @@ -50,6 +51,7 @@ def add_ID(o_inst): def change_ID(o_inst): try: key = sys.argv[2] + key = unpaddedbase32.repad(key.encode()).decode() except IndexError: logger.warn('Specify pubkey to use') else: diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py index 75fe2138..3c77b3ce 100755 --- a/onionr/onionrcrypto.py +++ b/onionr/onionrcrypto.py @@ -19,6 +19,7 @@ ''' import os, binascii, base64, hashlib, time, sys, hmac, secrets import nacl.signing, nacl.encoding, nacl.public, nacl.hash, nacl.pwhash, nacl.utils, nacl.secret +import unpaddedbase32 import logger, onionrproofs import onionrexceptions, keymanager, core import config @@ -93,6 +94,7 @@ class OnionrCrypto: def pubKeyEncrypt(self, data, pubkey, encodedData=False): '''Encrypt to a public key (Curve25519, taken from base32 Ed25519 pubkey)''' + pubkey = unpaddedbase32.repad(self._core._utils.strToBytes(pubkey)) retVal = '' box = None data = self._core._utils.strToBytes(data) @@ -129,7 +131,7 @@ class OnionrCrypto: return decrypted def symmetricEncrypt(self, data, key, encodedKey=False, returnEncoded=True): - '''Encrypt data to a 32-byte key (Salsa20-Poly1305 MAC)''' + '''Encrypt data with a 32-byte key (Salsa20-Poly1305 MAC)''' if encodedKey: encoding = nacl.encoding.Base64Encoder else: @@ -199,7 +201,7 @@ class OnionrCrypto: if pubkey == '': pubkey = self.pubKey prev = '' - pubkey = pubkey.encode() + pubkey = self._core._utils.strToBytes(pubkey) for i in range(self.HASH_ID_ROUNDS): try: prev = prev.encode() diff --git a/onionr/onionrusers/contactmanager.py b/onionr/onionrusers/contactmanager.py index 0524ed64..098cba65 100755 --- a/onionr/onionrusers/contactmanager.py +++ b/onionr/onionrusers/contactmanager.py @@ -18,10 +18,12 @@ along with this program. If not, see . ''' import os, json, onionrexceptions +import unpaddedbase32 from onionrusers import onionrusers class ContactManager(onionrusers.OnionrUser): def __init__(self, coreInst, publicKey, saveUser=False, recordExpireSeconds=5): + publicKey = unpaddedbase32.repad(coreInst._utils.strToBytes(publicKey)).decode() super(ContactManager, self).__init__(coreInst, publicKey, saveUser=saveUser) self.dataDir = coreInst.dataDir + '/contacts/' self.dataFile = '%s/contacts/%s.json' % (coreInst.dataDir, publicKey) diff --git a/onionr/onionrusers/onionrusers.py b/onionr/onionrusers/onionrusers.py index 9e26d047..51977b39 100755 --- a/onionr/onionrusers/onionrusers.py +++ b/onionr/onionrusers/onionrusers.py @@ -18,6 +18,7 @@ along with this program. If not, see . ''' import onionrblockapi, logger, onionrexceptions, json, sqlite3, time +import unpaddedbase32 import nacl.exceptions def deleteExpiredKeys(coreInst): @@ -55,8 +56,7 @@ class OnionrUser: Takes an instance of onionr core, a base32 encoded ed25519 public key, and a bool saveUser saveUser determines if we should add a user to our peer database or not. ''' - if ' ' in coreInst._utils.bytesToStr(publicKey).strip(): - publicKey = coreInst._utils.convertHumanReadableID(publicKey) + publicKey = unpaddedbase32.repad(coreInst._utils.strToBytes(publicKey)).decode() self.trust = 0 self._core = coreInst @@ -190,6 +190,7 @@ class OnionrUser: return list(keyList) def addForwardKey(self, newKey, expire=DEFAULT_KEY_EXPIRE): + newKey = self._core._utils.bytesToStr(unpaddedbase32.repad(self._core._utils.strToBytes(newKey))) if not self._core._utils.validatePubKey(newKey): # Do not add if something went wrong with the key raise onionrexceptions.InvalidPubkey(newKey) diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index e2a5964d..887c421a 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -21,6 +21,7 @@ import sys, os, sqlite3, binascii, time, base64, json, glob, shutil, math, re, urllib.parse, string import requests import nacl.signing, nacl.encoding +import unpaddedbase32 from onionrblockapi import Block import onionrexceptions, config, logger from onionr import API_VERSION @@ -319,9 +320,12 @@ class OnionrUtils: ''' Validate if a string is a valid base32 encoded Ed25519 key ''' - retVal = False if type(key) is type(None): return False + # Accept keys that have no = padding + key = unpaddedbase32.repad(self.strToBytes(key)) + + retVal = False try: nacl.signing.SigningKey(seed=key, encoder=nacl.encoding.Base32Encoder) except nacl.exceptions.ValueError: diff --git a/onionr/static-data/www/mail/mail.css b/onionr/static-data/www/mail/mail.css index 32999440..f2c31086 100755 --- a/onionr/static-data/www/mail/mail.css +++ b/onionr/static-data/www/mail/mail.css @@ -113,4 +113,8 @@ input{ color: black; font-size: 1.5em; width: 10%; +} + +.content{ + min-height: 1000px; } \ No newline at end of file diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index cc4895a1..27180173 100755 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -58,9 +58,9 @@ function openReply(bHash, quote, subject){ // Add quoted reply var splitQuotes = quote.split('\n') for (var x = 0; x < splitQuotes.length; x++){ - splitQuotes[x] = '>' + splitQuotes[x] + splitQuotes[x] = '> ' + splitQuotes[x] } - quote = '\n' + splitQuotes.join('\n') + quote = '\n' + key.substring(0, 12) + ' wrote:' + '\n' + splitQuotes.join('\n') document.getElementById('draftText').value = quote setActiveTab('send message') } @@ -77,7 +77,7 @@ function openThread(bHash, sender, date, sigBool, pubkey, subjectLine){ var sigMsg = 'signature' // show add unknown contact button if peer is unknown but still has pubkey - if (sender == pubkey){ + if (sender === pubkey && sender !== myPub){ addUnknownContact.style.display = 'inline' } diff --git a/onionr/static-data/www/mail/sendmail.js b/onionr/static-data/www/mail/sendmail.js index f8ff49bc..e2d34c1f 100755 --- a/onionr/static-data/www/mail/sendmail.js +++ b/onionr/static-data/www/mail/sendmail.js @@ -54,6 +54,11 @@ sendForm.onsubmit = function(){ return false } } - sendMail(to.value, messageContent.value, subject.value) + if (to.value.length !== 56 && to.value.length !== 52){ + alert('Public key is not valid') + } + else{ + sendMail(to.value, messageContent.value, subject.value) + } return false } diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index 151c3704..a2d3d3c9 100755 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -21,26 +21,26 @@

      🕵️‍♂️ Current Used Identity:

      - +

      -

      Onionr Services


      -

      Mail - Friend Manager - Boards - +

      Mail - Friend Manager - Circle - Clandestine


      Edit Configuration
      -

      Warning: Some values can be dangerous to change. Use caution.

      +

      Warning: Some values can be dangerous to change.

      Configuration contains sensitive information.



      -

      Statistics

      +

      🔒 Security Level:

      🕰️ Uptime:

      Connections

      +

      🖇️ Last Received Request: None since start

      ⬇️ Total Requests Received: None since start

      🔗 Outgoing Connections:

      diff --git a/onionr/static-data/www/private/main.css b/onionr/static-data/www/private/main.css index 91a7b588..896cd2f5 100755 --- a/onionr/static-data/www/private/main.css +++ b/onionr/static-data/www/private/main.css @@ -5,4 +5,14 @@ .saveConfig{ margin-top: 1em; +} + + +.idLink{ + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently + supported by Chrome and Opera */ } \ No newline at end of file diff --git a/onionr/static-data/www/shared/main/stats.js b/onionr/static-data/www/shared/main/stats.js index 86b7f451..9b50185e 100755 --- a/onionr/static-data/www/shared/main/stats.js +++ b/onionr/static-data/www/shared/main/stats.js @@ -18,10 +18,34 @@ */ uptimeDisplay = document.getElementById('uptime') connectedDisplay = document.getElementById('connectedNodes') +connectedDisplay.style.maxHeight = '300px' +connectedDisplay.style.overflowY = 'scroll' storedBlockDisplay = document.getElementById('storedBlocks') queuedBlockDisplay = document.getElementById('blockQueue') lastIncoming = document.getElementById('lastIncoming') totalRec = document.getElementById('totalRec') +securityLevel = document.getElementById('securityLevel') +sec_description_str = 'unknown' + +function showSecStatNotice(){ + var secWarnEls = document.getElementsByClassName('secRequestNotice') + for (el = 0; el < secWarnEls.length; el++){ + secWarnEls[el].style.display = 'block' + } +} + +switch (httpGet('/config/get/general.security_level')){ + case "0": + sec_description_str = 'normal' + break; + case "1": + sec_description_str = 'high' + break; +} + +if (sec_description_str !== 'normal'){ + showSecStatNotice() +} function getStats(){ stats = JSON.parse(httpGet('getstats', webpass)) @@ -29,6 +53,7 @@ function getStats(){ connectedDisplay.innerText = stats['connectedNodes'] storedBlockDisplay.innerText = stats['blockCount'] queuedBlockDisplay.innerText = stats['blockQueueCount'] + securityLevel.innerText = sec_description_str totalRec.innerText = httpGet('/hitcount') var lastConnect = httpGet('/lastconnect') if (lastConnect > 0){ diff --git a/onionr/static-data/www/shared/main/style.css b/onionr/static-data/www/shared/main/style.css index 18388845..8181df8d 100755 --- a/onionr/static-data/www/shared/main/style.css +++ b/onionr/static-data/www/shared/main/style.css @@ -178,8 +178,16 @@ body{ background-color:#396BAC; } +.btn:hover{ + opacity: 0.6; +} + .openSiteBtn{ padding: 5px; border: 1px solid black; border-radius: 5px; } + +.hidden{ + display: none; +} diff --git a/onionr/tests/test_stringvalidations.py b/onionr/tests/test_stringvalidations.py index 0cc45eae..caac34df 100755 --- a/onionr/tests/test_stringvalidations.py +++ b/onionr/tests/test_stringvalidations.py @@ -29,11 +29,13 @@ class OnionrValidations(unittest.TestCase): def test_pubkey_validator(self): # Test ed25519 public key validity - valid = 'JZ5VE72GUS3C7BOHDRIYZX4B5U5EJMCMLKHLYCVBQQF3UKHYIRRQ====' + valids = ['JZ5VE72GUS3C7BOHDRIYZX4B5U5EJMCMLKHLYCVBQQF3UKHYIRRQ====', 'JZ5VE72GUS3C7BOHDRIYZX4B5U5EJMCMLKHLYCVBQQF3UKHYIRRQ'] invalid = [None, '', ' ', 'dfsg', '\n', 'JZ5VE72GUS3C7BOHDRIYZX4B5U5EJMCMLKHLYCVBQQF3UKHYIR$Q===='] c = core.Core() - print('testing', valid) - self.assertTrue(c._utils.validatePubKey(valid)) + + for valid in valids: + print('testing', valid) + self.assertTrue(c._utils.validatePubKey(valid)) for x in invalid: #print('testing', x) diff --git a/requirements.in b/requirements.in index bbb9cdc3..c285834a 100644 --- a/requirements.in +++ b/requirements.in @@ -1,9 +1,10 @@ urllib3==1.24.2 requests==2.21.0 -PyNaCl==1.2.1 +PyNaCl==1.3.0 gevent==1.3.6 Flask==1.0.2 PySocks==1.6.8 stem==1.7.1 deadsimplekv==0.1.1 +unpaddedbase32==0.1.0 jinja2==2.10.1 diff --git a/requirements.txt b/requirements.txt index 06b3303d..261a9996 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --generate-hashes --output-file requirements.txt requirements.in +# pip-compile --generate-hashes --output-file=requirements.txt requirements.in # certifi==2018.11.29 \ --hash=sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7 \ @@ -140,38 +140,26 @@ markupsafe==1.1.1 \ pycparser==2.19 \ --hash=sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3 \ # via cffi -pynacl==1.2.1 \ - --hash=sha256:04e30e5bdeeb2d5b34107f28cd2f5bbfdc6c616f3be88fc6f53582ff1669eeca \ - --hash=sha256:0bfa0d94d2be6874e40f896e0a67e290749151e7de767c5aefbad1121cad7512 \ - --hash=sha256:11aa4e141b2456ce5cecc19c130e970793fa3a2c2e6fbb8ad65b28f35aa9e6b6 \ - --hash=sha256:13bdc1fe084ff9ac7653ae5a924cae03bf4bb07c6667c9eb5b6eb3c570220776 \ - --hash=sha256:14339dc233e7a9dda80a3800e64e7ff89d0878ba23360eea24f1af1b13772cac \ - --hash=sha256:1d33e775fab3f383167afb20b9927aaf4961b953d76eeb271a5703a6d756b65b \ - --hash=sha256:2a42b2399d0428619e58dac7734838102d35f6dcdee149e0088823629bf99fbb \ - --hash=sha256:2dce05ac8b3c37b9e2f65eab56c544885607394753e9613fd159d5e2045c2d98 \ - --hash=sha256:63cfccdc6217edcaa48369191ae4dca0c390af3c74f23c619e954973035948cd \ - --hash=sha256:6453b0dae593163ffc6db6f9c9c1597d35c650598e2c39c0590d1757207a1ac2 \ - --hash=sha256:73a5a96fb5fbf2215beee2353a128d382dbca83f5341f0d3c750877a236569ef \ - --hash=sha256:8abb4ef79161a5f58848b30ab6fb98d8c466da21fdd65558ce1d7afc02c70b5f \ - --hash=sha256:8ac1167195b32a8755de06efd5b2d2fe76fc864517dab66aaf65662cc59e1988 \ - --hash=sha256:8f505f42f659012794414fa57c498404e64db78f1d98dfd40e318c569f3c783b \ - --hash=sha256:9c8a06556918ee8e3ab48c65574f318f5a0a4d31437fc135da7ee9d4f9080415 \ - --hash=sha256:a1e25fc5650cf64f01c9e435033e53a4aca9de30eb9929d099f3bb078e18f8f2 \ - --hash=sha256:be71cd5fce04061e1f3d39597f93619c80cdd3558a6c9ba99a546f144a8d8101 \ - --hash=sha256:c5b1a7a680218dee9da0f1b5e24072c46b3c275d35712bc1d505b85bb03441c0 \ - --hash=sha256:cb785db1a9468841a1265c9215c60fe5d7af2fb1b209e3316a152704607fc582 \ - --hash=sha256:cf6877124ae6a0698404e169b3ba534542cfbc43f939d46b927d956daf0a373a \ - --hash=sha256:d0eb5b2795b7ee2cbcfcadacbe95a13afbda048a262bd369da9904fecb568975 \ - --hash=sha256:d3a934e2b9f20abac009d5b6951067cfb5486889cb913192b4d8288b216842f1 \ - --hash=sha256:d795f506bcc9463efb5ebb0f65ed77921dcc9e0a50499dedd89f208445de9ecb \ - --hash=sha256:d8aaf7e5d6b0e0ef7d6dbf7abeb75085713d0100b4eb1a4e4e857de76d77ac45 \ - --hash=sha256:de2aaca8386cf4d70f1796352f2346f48ddb0bed61dc43a3ce773ba12e064031 \ - --hash=sha256:e0d38fa0a75f65f556fb912f2c6790d1fa29b7dd27a1d9cc5591b281321eaaa9 \ - --hash=sha256:eb2acabbd487a46b38540a819ef67e477a674481f84a82a7ba2234b9ba46f752 \ - --hash=sha256:eeee629828d0eb4f6d98ac41e9a3a6461d114d1d0aa111a8931c049359298da0 \ - --hash=sha256:f5836463a3c0cca300295b229b6c7003c415a9d11f8f9288ddbd728e2746524c \ - --hash=sha256:f5ce9e26d25eb0b2d96f3ef0ad70e1d3ae89b5d60255c462252a3e456a48c053 \ - --hash=sha256:fabf73d5d0286f9e078774f3435601d2735c94ce9e514ac4fb945701edead7e4 +pynacl==1.3.0 \ + --hash=sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255 \ + --hash=sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c \ + --hash=sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e \ + --hash=sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae \ + --hash=sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621 \ + --hash=sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56 \ + --hash=sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39 \ + --hash=sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310 \ + --hash=sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1 \ + --hash=sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a \ + --hash=sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786 \ + --hash=sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b \ + --hash=sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b \ + --hash=sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f \ + --hash=sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20 \ + --hash=sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415 \ + --hash=sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715 \ + --hash=sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1 \ + --hash=sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0 pysocks==1.6.8 \ --hash=sha256:3fe52c55890a248676fd69dc9e3c4e811718b777834bcaab7a8125cf9deac672 requests==2.21.0 \ @@ -183,6 +171,8 @@ six==1.12.0 \ # via pynacl stem==1.7.1 \ --hash=sha256:c9eaf3116cb60c15995cbd3dec3a5cbc50e9bb6e062c4d6d42201e566f498ca2 +unpaddedbase32==0.1.0 \ + --hash=sha256:5e4143fcaf77c9c6b4f60d18301c7570f0dac561dcf9b9aed8b5ba6ead7f218c urllib3==1.24.2 \ --hash=sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0 \ --hash=sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3 From 065e97ab118718515715ee22aa3038fb86d778b3 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 19 Jun 2019 15:29:27 -0500 Subject: [PATCH 108/173] improved logging messages to be less spammy --- README.md | 2 +- onionr/communicator.py | 16 +-- onionr/communicatorutils/connectnewpeers.py | 2 +- onionr/communicatorutils/downloadblocks.py | 1 + onionr/communicatorutils/lookupblocks.py | 116 +++++++++--------- onionr/logger.py | 32 ++--- onionr/netcontroller.py | 11 +- onionr/onionr.py | 10 +- onionr/onionrcommands/banblocks.py | 8 +- onionr/onionrcommands/daemonlaunch.py | 32 ++--- onionr/onionrcommands/onionrstatistics.py | 16 +-- onionr/onionrcommands/openwebinterface.py | 2 +- onionr/onionrutils.py | 2 +- .../static-data/default-plugins/flow/main.py | 8 +- 14 files changed, 129 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index 536e323b..4417e31f 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ The following applies to Ubuntu Bionic. Other distros may have different package `$ sudo apt install python3-pip python3-dev tor` -* Have python3.6+, python3-pip, Tor (daemon, not browser) installed (python3-dev recommended) +* Have python3.6+, python3-pip, Tor (daemon, not browser) installed. python3-dev is recommended. * Clone the git repo: `$ git clone https://gitlab.com/beardog/onionr` * cd into install direction: `$ cd onionr/` * Install the Python dependencies ([virtualenv strongly recommended](https://virtualenv.pypa.io/en/stable/userguide/)): `$ pip3 install --require-hashes -r requirements.txt` diff --git a/onionr/communicator.py b/onionr/communicator.py index de49e601..370a4981 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -176,7 +176,7 @@ class OnionrCommunicatorDaemon: self.shutdown = True pass - logger.info('Goodbye. (Onionr is cleaning up, and will exit)') + logger.info('Goodbye. (Onionr is cleaning up, and will exit)', terminal=True) try: self.service_greenlets except AttributeError: @@ -252,7 +252,7 @@ class OnionrCommunicatorDaemon: break else: if len(self.onlinePeers) == 0: - logger.debug('Couldn\'t connect to any peers.' + (' Last node seen %s ago.' % humanreadabletime.human_readable_time(time.time() - self.lastNodeSeen) if not self.lastNodeSeen is None else '')) + logger.debug('Couldn\'t connect to any peers.' + (' Last node seen %s ago.' % humanreadabletime.human_readable_time(time.time() - self.lastNodeSeen) if not self.lastNodeSeen is None else ''), terminal=True) else: self.lastNodeSeen = time.time() self.decrementThreadCount('getOnlinePeers') @@ -293,12 +293,12 @@ class OnionrCommunicatorDaemon: def printOnlinePeers(self): '''logs online peer list''' if len(self.onlinePeers) == 0: - logger.warn('No online peers') + logger.warn('No online peers', terminal=True) else: - logger.info('Online peers:') + logger.info('Online peers:', terminal=True) for i in self.onlinePeers: score = str(self.getPeerProfileInstance(i).score) - logger.info(i + ', score: ' + score) + logger.info(i + ', score: ' + score, terminal=True) def peerAction(self, peer, action, data='', returnHeaders=False): '''Perform a get request to a peer''' @@ -318,6 +318,7 @@ class OnionrCommunicatorDaemon: self.getPeerProfileInstance(peer).addScore(-10) self.removeOnlinePeer(peer) if action != 'ping': + logger.warn('Lost connection to ' + peer, terminal=True) self.getOnlinePeers() # Will only add a new peer to pool if needed except ValueError: pass @@ -359,7 +360,7 @@ class OnionrCommunicatorDaemon: def announce(self, peer): '''Announce to peers our address''' if announcenode.announce_node(self) == False: - logger.warn('Could not introduce node.') + logger.warn('Could not introduce node.', terminal=True) def detectAPICrash(self): '''exit if the api server crashes/stops''' @@ -371,7 +372,7 @@ class OnionrCommunicatorDaemon: else: # This executes if the api is NOT detected to be running events.event('daemon_crash', onionr = self._core.onionrInst, data = {}) - logger.error('Daemon detected API crash (or otherwise unable to reach API after long time), stopping...') + logger.fatal('Daemon detected API crash (or otherwise unable to reach API after long time), stopping...', terminal=True) self.shutdown = True self.decrementThreadCount('detectAPICrash') @@ -388,5 +389,4 @@ def run_file_exists(daemon): if os.path.isfile(daemon._core.dataDir + '.runcheck'): os.remove(daemon._core.dataDir + '.runcheck') return True - return False \ No newline at end of file diff --git a/onionr/communicatorutils/connectnewpeers.py b/onionr/communicatorutils/connectnewpeers.py index ee1eb468..96e4ecac 100755 --- a/onionr/communicatorutils/connectnewpeers.py +++ b/onionr/communicatorutils/connectnewpeers.py @@ -72,7 +72,7 @@ def connect_new_peer_to_communicator(comm_inst, peer='', useBootstrap=False): # Add a peer to our list if it isn't already since it successfully connected networkmerger.mergeAdders(address, comm_inst._core) if address not in comm_inst.onlinePeers: - logger.info('Connected to ' + address) + logger.info('Connected to ' + address, terminal=True) comm_inst.onlinePeers.append(address) comm_inst.connectTimes[address] = comm_inst._core._utils.getEpoch() retData = address diff --git a/onionr/communicatorutils/downloadblocks.py b/onionr/communicatorutils/downloadblocks.py index 720e0eae..aad5f884 100755 --- a/onionr/communicatorutils/downloadblocks.py +++ b/onionr/communicatorutils/downloadblocks.py @@ -110,6 +110,7 @@ def download_blocks_from_communicator(comm_inst): if removeFromQueue: try: del comm_inst.blockQueue[blockHash] # remove from block queue both if success or false + logger.info('%s blocks remaining in queue' % [len(comm_inst.blockQueue)]) except KeyError: pass comm_inst.currentDownloading.remove(blockHash) diff --git a/onionr/communicatorutils/lookupblocks.py b/onionr/communicatorutils/lookupblocks.py index 0994d15c..e39e3c68 100755 --- a/onionr/communicatorutils/lookupblocks.py +++ b/onionr/communicatorutils/lookupblocks.py @@ -19,61 +19,65 @@ ''' import logger, onionrproofs def lookup_blocks_from_communicator(comm_inst): - logger.info('Looking up new blocks...') - tryAmount = 2 - newBlocks = '' - existingBlocks = comm_inst._core.getBlockList() - triedPeers = [] # list of peers we've tried this time around - maxBacklog = 1560 # Max amount of *new* block hashes to have already in queue, to avoid memory exhaustion - lastLookupTime = 0 # Last time we looked up a particular peer's list - for i in range(tryAmount): - listLookupCommand = 'getblocklist' # This is defined here to reset it each time - if len(comm_inst.blockQueue) >= maxBacklog: + logger.info('Looking up new blocks...') + tryAmount = 2 + newBlocks = '' + existingBlocks = comm_inst._core.getBlockList() + triedPeers = [] # list of peers we've tried this time around + maxBacklog = 1560 # Max amount of *new* block hashes to have already in queue, to avoid memory exhaustion + lastLookupTime = 0 # Last time we looked up a particular peer's list + new_block_count = 0 + for i in range(tryAmount): + listLookupCommand = 'getblocklist' # This is defined here to reset it each time + if len(comm_inst.blockQueue) >= maxBacklog: + break + if not comm_inst.isOnline: + break + # check if disk allocation is used + if comm_inst._core._utils.storageCounter.isFull(): + logger.debug('Not looking up new blocks due to maximum amount of allowed disk space used') + break + peer = comm_inst.pickOnlinePeer() # select random online peer + # if we've already tried all the online peers this time around, stop + if peer in triedPeers: + if len(comm_inst.onlinePeers) == len(triedPeers): break - if not comm_inst.isOnline: - break - # check if disk allocation is used - if comm_inst._core._utils.storageCounter.isFull(): - logger.debug('Not looking up new blocks due to maximum amount of allowed disk space used') - break - peer = comm_inst.pickOnlinePeer() # select random online peer - # if we've already tried all the online peers this time around, stop - if peer in triedPeers: - if len(comm_inst.onlinePeers) == len(triedPeers): - break - else: - continue - triedPeers.append(peer) + else: + continue + triedPeers.append(peer) - # Get the last time we looked up a peer's stamp to only fetch blocks since then. - # Saved in memory only for privacy reasons - try: - lastLookupTime = comm_inst.dbTimestamps[peer] - except KeyError: - lastLookupTime = 0 - else: - listLookupCommand += '?date=%s' % (lastLookupTime,) - try: - newBlocks = comm_inst.peerAction(peer, listLookupCommand) # get list of new block hashes - except Exception as error: - logger.warn('Could not get new blocks from %s.' % peer, error = error) - newBlocks = False - else: - comm_inst.dbTimestamps[peer] = comm_inst._core._utils.getRoundedEpoch(roundS=60) - if newBlocks != False: - # if request was a success - for i in newBlocks.split('\n'): - if comm_inst._core._utils.validateHash(i): - # if newline seperated string is valid hash - if not i in existingBlocks: - # if block does not exist on disk and is not already in block queue - if i not in comm_inst.blockQueue: - if onionrproofs.hashMeetsDifficulty(i) and not comm_inst._core._blacklist.inBlacklist(i): - if len(comm_inst.blockQueue) <= 1000000: - comm_inst.blockQueue[i] = [peer] # add blocks to download queue - else: - if peer not in comm_inst.blockQueue[i]: - if len(comm_inst.blockQueue[i]) < 10: - comm_inst.blockQueue[i].append(peer) - comm_inst.decrementThreadCount('lookupBlocks') - return \ No newline at end of file + # Get the last time we looked up a peer's stamp to only fetch blocks since then. + # Saved in memory only for privacy reasons + try: + lastLookupTime = comm_inst.dbTimestamps[peer] + except KeyError: + lastLookupTime = 0 + else: + listLookupCommand += '?date=%s' % (lastLookupTime,) + try: + newBlocks = comm_inst.peerAction(peer, listLookupCommand) # get list of new block hashes + except Exception as error: + logger.warn('Could not get new blocks from %s.' % peer, error = error) + newBlocks = False + else: + comm_inst.dbTimestamps[peer] = comm_inst._core._utils.getRoundedEpoch(roundS=60) + if newBlocks != False: + # if request was a success + for i in newBlocks.split('\n'): + if comm_inst._core._utils.validateHash(i): + # if newline seperated string is valid hash + if not i in existingBlocks: + # if block does not exist on disk and is not already in block queue + if i not in comm_inst.blockQueue: + if onionrproofs.hashMeetsDifficulty(i) and not comm_inst._core._blacklist.inBlacklist(i): + if len(comm_inst.blockQueue) <= 1000000: + comm_inst.blockQueue[i] = [peer] # add blocks to download queue + new_block_count += 1 + else: + if peer not in comm_inst.blockQueue[i]: + if len(comm_inst.blockQueue[i]) < 10: + comm_inst.blockQueue[i].append(peer) + if new_block_count > 0: + logger.info('Discovered %s new blocks' % (new_block_count,), terminal=True) + comm_inst.decrementThreadCount('lookupBlocks') + return \ No newline at end of file diff --git a/onionr/logger.py b/onionr/logger.py index a7abf715..01ada8ce 100755 --- a/onionr/logger.py +++ b/onionr/logger.py @@ -126,24 +126,24 @@ def get_file(): return _outputfile -def raw(data, fd = sys.stdout, sensitive = False): +def raw(data, fd = sys.stdout, terminal = False): ''' Outputs raw data to console without formatting ''' - if get_settings() & OUTPUT_TO_CONSOLE: + if terminal and (get_settings() & OUTPUT_TO_CONSOLE): try: ts = fd.write('%s\n' % data) except OSError: pass - if get_settings() & OUTPUT_TO_FILE and not sensitive: + if get_settings() & OUTPUT_TO_FILE: try: with open(_outputfile, "a+") as f: f.write(colors.filter(data) + '\n') except OSError: pass -def log(prefix, data, color = '', timestamp=True, fd = sys.stdout, prompt = True, sensitive = False): +def log(prefix, data, color = '', timestamp=True, fd = sys.stdout, prompt = True, terminal = False): ''' Logs the data prefix : The prefix to the output @@ -158,7 +158,7 @@ def log(prefix, data, color = '', timestamp=True, fd = sys.stdout, prompt = True if not get_settings() & USE_ANSI: output = colors.filter(output) - raw(output, fd = fd, sensitive = sensitive) + raw(output, fd = fd, terminal = terminal) def readline(message = ''): ''' @@ -210,37 +210,37 @@ def confirm(default = 'y', message = 'Are you sure %s? '): return default == 'y' # debug: when there is info that could be useful for debugging purposes only -def debug(data, error = None, timestamp = True, prompt = True, sensitive = False, level = LEVEL_DEBUG): +def debug(data, error = None, timestamp = True, prompt = True, terminal = False, level = LEVEL_DEBUG): if get_level() <= level: - log('/', data, timestamp = timestamp, prompt = prompt, sensitive = sensitive) + log('/', data, timestamp = timestamp, prompt = prompt, terminal = terminal) if not error is None: debug('Error: ' + str(error) + parse_error()) # info: when there is something to notify the user of, such as the success of a process -def info(data, timestamp = False, prompt = True, sensitive = False, level = LEVEL_INFO): +def info(data, timestamp = False, prompt = True, terminal = False, level = LEVEL_INFO): if get_level() <= level: - log('+', data, colors.fg.green, timestamp = timestamp, prompt = prompt, sensitive = sensitive) + log('+', data, colors.fg.green, timestamp = timestamp, prompt = prompt, terminal = terminal) # warn: when there is a potential for something bad to happen -def warn(data, error = None, timestamp = True, prompt = True, sensitive = False, level = LEVEL_WARN): +def warn(data, error = None, timestamp = True, prompt = True, terminal = False, level = LEVEL_WARN): if not error is None: debug('Error: ' + str(error) + parse_error()) if get_level() <= level: - log('!', data, colors.fg.orange, timestamp = timestamp, prompt = prompt, sensitive = sensitive) + log('!', data, colors.fg.orange, timestamp = timestamp, prompt = prompt, terminal = terminal) # error: when only one function, module, or process of the program encountered a problem and must stop -def error(data, error = None, timestamp = True, prompt = True, sensitive = False, level = LEVEL_ERROR): +def error(data, error = None, timestamp = True, prompt = True, terminal = False, level = LEVEL_ERROR): if get_level() <= level: - log('-', data, colors.fg.red, timestamp = timestamp, fd = sys.stderr, prompt = prompt, sensitive = sensitive) + log('-', data, colors.fg.red, timestamp = timestamp, fd = sys.stderr, prompt = prompt, terminal = terminal) if not error is None: debug('Error: ' + str(error) + parse_error()) # fatal: when the something so bad has happened that the program must stop -def fatal(data, error = None, timestamp=True, prompt = True, sensitive = False, level = LEVEL_FATAL): +def fatal(data, error = None, timestamp=True, prompt = True, terminal = False, level = LEVEL_FATAL): if not error is None: - debug('Error: ' + str(error) + parse_error(), sensitive = sensitive) + debug('Error: ' + str(error) + parse_error(), terminal = terminal) if get_level() <= level: - log('#', data, colors.bg.red + colors.fg.green + colors.bold, timestamp = timestamp, fd = sys.stderr, prompt = prompt, sensitive = sensitive) + log('#', data, colors.bg.red + colors.fg.green + colors.bold, timestamp = timestamp, fd = sys.stderr, prompt = prompt, terminal = terminal) # returns a formatted error message def parse_error(): diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py index 74087500..b2d8237f 100755 --- a/onionr/netcontroller.py +++ b/onionr/netcontroller.py @@ -124,14 +124,14 @@ HiddenServicePort 80 ''' + self.apiServerIP + ''':''' + str(self.hsPort) try: tor = subprocess.Popen([self.torBinary, '-f', self.torConfigLocation], stdout=subprocess.PIPE, stderr=subprocess.PIPE) except FileNotFoundError: - logger.fatal("Tor was not found in your path or the Onionr directory. Please install Tor and try again.") + logger.fatal("Tor was not found in your path or the Onionr directory. Please install Tor and try again.", terminal=True) sys.exit(1) else: # Test Tor Version torVersion = subprocess.Popen([self.torBinary, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) for line in iter(torVersion.stdout.readline, b''): if 'Tor 0.2.' in line.decode(): - logger.error('Tor 0.3+ required') + logger.fatal('Tor 0.3+ required', terminal=True) sys.exit(1) break torVersion.kill() @@ -140,17 +140,18 @@ HiddenServicePort 80 ''' + self.apiServerIP + ''':''' + str(self.hsPort) try: for line in iter(tor.stdout.readline, b''): if 'bootstrapped 100' in line.decode().lower(): + logger.info(line.decode()) break elif 'opening socks listener' in line.decode().lower(): logger.debug(line.decode().replace('\n', '')) else: - logger.fatal('Failed to start Tor. Maybe a stray instance of Tor used by Onionr is still running? This can also be a result of file permissions being too open') + logger.fatal('Failed to start Tor. Maybe a stray instance of Tor used by Onionr is still running? This can also be a result of file permissions being too open', terminal=True) return False except KeyboardInterrupt: - logger.fatal('Got keyboard interrupt. Onionr will exit soon.', timestamp = False, level = logger.LEVEL_IMPORTANT) + logger.fatal('Got keyboard interrupt. Onionr will exit soon.', timestamp = False, level = logger.LEVEL_IMPORTANT, terminal=True) return False - logger.debug('Finished starting Tor.', timestamp=True) + logger.info('Finished starting Tor.', terminal=True) self.readyState = True try: diff --git a/onionr/onionr.py b/onionr/onionr.py index 16ec1839..6306acca 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -159,7 +159,7 @@ class Onionr: 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)) if not message is None: - logger.info(logger.colors.fg.lightgreen + '-> ' + str(message) + logger.colors.reset + logger.colors.fg.lightgreen + ' <-\n', sensitive=True) + logger.info(logger.colors.fg.lightgreen + '-> ' + str(message) + logger.colors.reset + logger.colors.fg.lightgreen + ' <-\n', terminal=True) def deleteRunFiles(self): try: @@ -238,7 +238,7 @@ class Onionr: return config.get('client.webpassword') def printWebPassword(self): - logger.info(self.getWebPassword(), sensitive = True) + logger.info(self.getWebPassword(), term_only = True) def getHelp(self): return self.cmdhelp @@ -289,11 +289,11 @@ class Onionr: Displays the Onionr version ''' - function('Onionr v%s (%s) (API v%s)' % (ONIONR_VERSION, platform.machine(), API_VERSION)) + function('Onionr v%s (%s) (API v%s)' % (ONIONR_VERSION, platform.machine(), API_VERSION), terminal=True) if verbosity >= 1: - function(ONIONR_TAGLINE) + function(ONIONR_TAGLINE, terminal=True) if verbosity >= 2: - function('Running on %s %s' % (platform.platform(), platform.release())) + function('Running on %s %s' % (platform.platform(), platform.release()), terminal=True) def doPEX(self): '''make communicator do pex''' diff --git a/onionr/onionrcommands/banblocks.py b/onionr/onionrcommands/banblocks.py index a9caa867..fe50d16a 100755 --- a/onionr/onionrcommands/banblocks.py +++ b/onionr/onionrcommands/banblocks.py @@ -30,10 +30,10 @@ def ban_block(o_inst): o_inst.onionrCore._blacklist.addToDB(ban) o_inst.onionrCore.removeBlock(ban) except Exception as error: - logger.error('Could not blacklist block', error=error) + logger.error('Could not blacklist block', error=error, terminal=True) else: - logger.info('Block blacklisted') + logger.info('Block blacklisted', terminal=True) else: - logger.warn('That block is already blacklisted') + logger.warn('That block is already blacklisted', terminal=True) else: - logger.error('Invalid block hash') \ No newline at end of file + logger.error('Invalid block hash', terminal=True) \ No newline at end of file diff --git a/onionr/onionrcommands/daemonlaunch.py b/onionr/onionrcommands/daemonlaunch.py index 88e43fae..d65a33d6 100755 --- a/onionr/onionrcommands/daemonlaunch.py +++ b/onionr/onionrcommands/daemonlaunch.py @@ -40,11 +40,6 @@ def daemon(o_inst): Thread(target=api.API, args=(o_inst, o_inst.debug, onionr.API_VERSION)).start() Thread(target=api.PublicAPI, args=[o_inst.getClientApi()]).start() - try: - time.sleep(0) - except KeyboardInterrupt: - logger.debug('Got keyboard interrupt, shutting down...') - _proper_shutdown(o_inst) apiHost = '' while apiHost == '': @@ -56,10 +51,17 @@ def daemon(o_inst): time.sleep(0.5) #onionr.Onionr.setupConfig('data/', self = o_inst) + logger.raw('', terminal=True) + # print nice header thing :) + if o_inst.onionrCore.config.get('general.display_header', True): + o_inst.header() + o_inst.version(verbosity = 5, function = logger.info) + logger.debug('Python version %s' % platform.python_version()) + if o_inst._developmentMode: - logger.warn('DEVELOPMENT MODE ENABLED (NOT RECOMMENDED)', timestamp = False) + logger.warn('DEVELOPMENT MODE ENABLED', timestamp = False, terminal=True) net = NetController(o_inst.onionrCore.config.get('client.public.port', 59497), apiServerIP=apiHost) - logger.debug('Tor is starting...') + logger.info('Tor is starting...', terminal=True) if not net.startTor(): o_inst.onionrUtils.localCommand('shutdown') sys.exit(1) @@ -67,7 +69,7 @@ def daemon(o_inst): logger.debug('Started .onion service: %s' % (logger.colors.underline + net.myID)) else: logger.debug('.onion service disabled') - logger.debug('Using public key: %s' % (logger.colors.underline + o_inst.onionrCore._crypto.pubKey)) + logger.info('Using public key: %s' % (logger.colors.underline + o_inst.onionrCore._crypto.pubKey[:52]), terminal=True) try: time.sleep(1) @@ -81,14 +83,6 @@ def daemon(o_inst): while o_inst.communicatorInst is None: time.sleep(0.1) - # print nice header thing :) - if o_inst.onionrCore.config.get('general.display_header', True): - o_inst.header() - - # print out debug info - o_inst.version(verbosity = 5, function = logger.debug) - logger.debug('Python version %s' % platform.python_version()) - logger.debug('Started communicator.') events.event('daemon_start', onionr = o_inst) @@ -124,7 +118,7 @@ def kill_daemon(o_inst): Shutdown the Onionr daemon ''' - logger.warn('Stopping the running daemon...', timestamp = False) + logger.warn('Stopping the running daemon...', timestamp = False, terminal=True) try: events.event('daemon_stop', onionr = o_inst) net = NetController(o_inst.onionrCore.config.get('client.port', 59496)) @@ -135,12 +129,12 @@ def kill_daemon(o_inst): net.killTor() except Exception as e: - logger.error('Failed to shutdown daemon.', error = e, timestamp = False) + logger.error('Failed to shutdown daemon.', error = e, timestamp = False, terminal=True) return def start(o_inst, input = False, override = False): 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).') + 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).', terminal=True) else: if not o_inst.debug and not o_inst._developmentMode: lockFile = open('.onionr-lock', 'w') diff --git a/onionr/onionrcommands/onionrstatistics.py b/onionr/onionrcommands/onionrstatistics.py index 918d8445..a28fb7db 100755 --- a/onionr/onionrcommands/onionrstatistics.py +++ b/onionr/onionrcommands/onionrstatistics.py @@ -65,21 +65,21 @@ def show_stats(o_inst): groupsize = width - prewidth - len('[+] ') # generate stats table - logger.info(colors['title'] + 'Onionr v%s Statistics' % onionr.ONIONR_VERSION + colors['reset']) - logger.info(colors['border'] + '-' * (maxlength + 1) + '+' + colors['reset']) + logger.info(colors['title'] + 'Onionr v%s Statistics' % onionr.ONIONR_VERSION + colors['reset'], terminal=True) + logger.info(colors['border'] + '-' * (maxlength + 1) + '+' + colors['reset'], terminal=True) for key, val in messages.items(): if not (type(val) is bool and val is True): val = [str(val)[i:i + groupsize] for i in range(0, len(str(val)), groupsize)] - logger.info(colors['key'] + str(key).rjust(maxlength) + colors['reset'] + colors['border'] + ' | ' + colors['reset'] + colors['val'] + str(val.pop(0)) + colors['reset']) + logger.info(colors['key'] + str(key).rjust(maxlength) + colors['reset'] + colors['border'] + ' | ' + colors['reset'] + colors['val'] + str(val.pop(0)) + colors['reset'], terminal=True) for value in val: - logger.info(' ' * maxlength + colors['border'] + ' | ' + colors['reset'] + colors['val'] + str(value) + colors['reset']) + logger.info(' ' * maxlength + colors['border'] + ' | ' + colors['reset'] + colors['val'] + str(value) + colors['reset'], terminal=True) else: - logger.info(colors['border'] + '-' * (maxlength + 1) + '+' + colors['reset']) - logger.info(colors['border'] + '-' * (maxlength + 1) + '+' + colors['reset']) + logger.info(colors['border'] + '-' * (maxlength + 1) + '+' + colors['reset'], terminal=True) + logger.info(colors['border'] + '-' * (maxlength + 1) + '+' + colors['reset'], terminal=True) except Exception as e: - logger.error('Failed to generate statistics table.', error = e, timestamp = False) + logger.error('Failed to generate statistics table.', error = e, timestamp = False, terminal=True) def show_details(o_inst): details = { @@ -90,7 +90,7 @@ def show_details(o_inst): } for detail in details: - logger.info('%s%s: \n%s%s\n' % (logger.colors.fg.lightgreen, detail, logger.colors.fg.green, details[detail]), sensitive = True) + logger.info('%s%s: \n%s%s\n' % (logger.colors.fg.lightgreen, detail, logger.colors.fg.green, details[detail]), terminal = True) def show_peers(o_inst): randID = str(uuid.uuid4()) diff --git a/onionr/onionrcommands/openwebinterface.py b/onionr/onionrcommands/openwebinterface.py index 2bd0cdd3..3e68e3ea 100755 --- a/onionr/onionrcommands/openwebinterface.py +++ b/onionr/onionrcommands/openwebinterface.py @@ -26,5 +26,5 @@ def open_home(o_inst): logger.error('Onionr seems to not be running (could not get api host)') else: url = 'http://%s/#%s' % (url, o_inst.onionrCore.config.get('client.webpassword')) - logger.info('If Onionr does not open automatically, use this URL: ' + url) + logger.info('If Onionr does not open automatically, use this URL: ' + url, terminal=True) webbrowser.open_new_tab(url) \ No newline at end of file diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 887c421a..4aa02d76 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -519,7 +519,7 @@ class OnionrUtils: except KeyboardInterrupt: raise KeyboardInterrupt except ValueError as e: - logger.debug('Failed to make GET request to %s' % url, error = e, sensitive = True) + pass except onionrexceptions.InvalidAPIVersion: if 'X-API' in response_headers: logger.debug('Using API version %s. Cannot communicate with node\'s API version of %s.' % (API_VERSION, response_headers['X-API'])) diff --git a/onionr/static-data/default-plugins/flow/main.py b/onionr/static-data/default-plugins/flow/main.py index de406207..0162019f 100755 --- a/onionr/static-data/default-plugins/flow/main.py +++ b/onionr/static-data/default-plugins/flow/main.py @@ -40,7 +40,7 @@ class OnionrFlow: return def start(self): - logger.warn("Please note: everything said here is public, even if a random channel name is used.") + logger.warn("Please note: everything said here is public, even if a random channel name is used.", terminal=True) message = "" self.flowRunning = True newThread = threading.Thread(target=self.showOutput) @@ -63,7 +63,7 @@ class OnionrFlow: if len(message) > 0: self.myCore.insertBlock(message, header='txt', expire=expireTime, meta={'ch': self.channel}) - logger.info("Flow is exiting, goodbye") + logger.info("Flow is exiting, goodbye", terminal=True) return def showOutput(self): @@ -81,11 +81,11 @@ class OnionrFlow: continue if not self.flowRunning: break - logger.info('\n------------------------', prompt = False) + logger.info('\n------------------------', prompt = False, terminal=True) content = block.getContent() # Escape new lines, remove trailing whitespace, and escape ansi sequences content = self.myCore._utils.escapeAnsi(content.replace('\n', '\\n').replace('\r', '\\r').strip()) - logger.info(block.getDate().strftime("%m/%d %H:%M") + ' - ' + logger.colors.reset + content, prompt = False) + logger.info(block.getDate().strftime("%m/%d %H:%M") + ' - ' + logger.colors.reset + content, prompt = False, terminal=True) self.alreadyOutputed.append(block.getHash()) time.sleep(5) except KeyboardInterrupt: From 6630071802eb64eb385e8d27242522b612c4de6c Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 19 Jun 2019 19:59:05 -0500 Subject: [PATCH 109/173] log to terminal again in places where it should --- onionr/communicatorutils/lookupblocks.py | 5 +- onionr/onionr.py | 4 +- onionr/onionrcommands/exportblocks.py | 9 +- onionr/onionrcommands/filecommands.py | 18 +-- onionr/onionrcommands/keyadders.py | 14 +-- onionr/onionrcommands/openwebinterface.py | 2 +- onionr/onionrcommands/plugincommands.py | 22 ++-- onionr/onionrcommands/pubkeymanager.py | 36 +++--- onionr/onionrcommands/resettor.py | 2 +- .../static-data/default-plugins/cliui/main.py | 2 +- .../default-plugins/encrypt/main.py | 22 ++-- .../default-plugins/pluginmanager/main.py | 108 +++++++++--------- .../static-data/default-plugins/pms/main.py | 40 +++---- 13 files changed, 144 insertions(+), 140 deletions(-) diff --git a/onionr/communicatorutils/lookupblocks.py b/onionr/communicatorutils/lookupblocks.py index e39e3c68..490a051e 100755 --- a/onionr/communicatorutils/lookupblocks.py +++ b/onionr/communicatorutils/lookupblocks.py @@ -78,6 +78,9 @@ def lookup_blocks_from_communicator(comm_inst): if len(comm_inst.blockQueue[i]) < 10: comm_inst.blockQueue[i].append(peer) if new_block_count > 0: - logger.info('Discovered %s new blocks' % (new_block_count,), terminal=True) + block_string = "" + if new_block_count > 1: + block_string = "s" + logger.info('Discovered %s new block%s' % (new_block_count, block_string), terminal=True) comm_inst.decrementThreadCount('lookupBlocks') return \ No newline at end of file diff --git a/onionr/onionr.py b/onionr/onionr.py index 6306acca..e94e35b8 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -200,7 +200,7 @@ class Onionr: ''' def exportBlock(self): - commands.exportblocks(self) + commands.exportblocks.export_block(self) def showDetails(self): commands.onionrstatistics.show_details(self) @@ -347,7 +347,7 @@ class Onionr: Displays a "command not found" message ''' - logger.error('Command not found.', timestamp = False) + logger.error('Command not found.', timestamp = False, terminal=True) def showHelpSuggestion(self): ''' diff --git a/onionr/onionrcommands/exportblocks.py b/onionr/onionrcommands/exportblocks.py index 243ee752..daa70f35 100755 --- a/onionr/onionrcommands/exportblocks.py +++ b/onionr/onionrcommands/exportblocks.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import sys +import sys, os import logger, onionrstorage def doExport(o_inst, bHash): exportDir = o_inst.dataDir + 'block-export/' @@ -25,18 +25,19 @@ def doExport(o_inst, bHash): if os.path.exists(o_inst.dataDir): os.mkdir(exportDir) else: - logger.error('Onionr Not initialized') + logger.error('Onionr Not initialized', terminal=True) data = onionrstorage.getData(o_inst.onionrCore, bHash) with open('%s/%s.dat' % (exportDir, bHash), 'wb') as exportFile: exportFile.write(data) + logger.info('Block exported as file', terminal=True) def export_block(o_inst): exportDir = o_inst.dataDir + 'block-export/' try: assert o_inst.onionrUtils.validateHash(sys.argv[2]) except (IndexError, AssertionError): - logger.error('No valid block hash specified.') + logger.error('No valid block hash specified.', terminal=True) sys.exit(1) else: bHash = sys.argv[2] - o_inst.doExport(bHash) \ No newline at end of file + doExport(o_inst, bHash) \ No newline at end of file diff --git a/onionr/onionrcommands/filecommands.py b/onionr/onionrcommands/filecommands.py index 444fd147..8f97f90a 100755 --- a/onionr/onionrcommands/filecommands.py +++ b/onionr/onionrcommands/filecommands.py @@ -31,18 +31,18 @@ def add_file(o_inst, singleBlock=False, blockType='bin'): contents = None if not os.path.exists(filename): - logger.error('That file does not exist. Improper path (specify full path)?') + logger.error('That file does not exist. Improper path (specify full path)?', terminal=True) return - logger.info('Adding file... this might take a long time.') + logger.info('Adding file... this might take a long time.', terminal=True) try: with open(filename, 'rb') as singleFile: blockhash = o_inst.onionrCore.insertBlock(base64.b64encode(singleFile.read()), header=blockType) if len(blockhash) > 0: - logger.info('File %s saved in block %s' % (filename, blockhash)) + logger.info('File %s saved in block %s' % (filename, blockhash), terminal=True) except: - logger.error('Failed to save file in block.', timestamp = False) + logger.error('Failed to save file in block.', timestamp = False, terminal=True) else: - logger.error('%s add-file ' % sys.argv[0], timestamp = False) + logger.error('%s add-file ' % sys.argv[0], timestamp = False, terminal=True) def getFile(o_inst): ''' @@ -52,16 +52,16 @@ def getFile(o_inst): fileName = sys.argv[2] bHash = sys.argv[3] except IndexError: - logger.error("Syntax %s %s" % (sys.argv[0], '/path/to/filename ')) + logger.error("Syntax %s %s" % (sys.argv[0], '/path/to/filename '), terminal=True) else: - logger.info(fileName) + logger.info(fileName, terminal=True) contents = None if os.path.exists(fileName): - logger.error("File already exists") + logger.error("File already exists", terminal=True) return if not o_inst.onionrUtils.validateHash(bHash): - logger.error('Block hash is invalid') + logger.error('Block hash is invalid', terminal=True) return with open(fileName, 'wb') as myFile: diff --git a/onionr/onionrcommands/keyadders.py b/onionr/onionrcommands/keyadders.py index 0edda6b0..363dfe28 100755 --- a/onionr/onionrcommands/keyadders.py +++ b/onionr/onionrcommands/keyadders.py @@ -26,14 +26,14 @@ def add_peer(o_inst): pass else: if o_inst.onionrUtils.hasKey(newPeer): - logger.info('We already have that key') + logger.info('We already have that key', terminal=True) return - logger.info("Adding peer: " + logger.colors.underline + newPeer) + logger.info("Adding peer: " + logger.colors.underline + newPeer, terminal=True) try: if o_inst.onionrCore.addPeer(newPeer): - logger.info('Successfully added key') + logger.info('Successfully added key', terminal=True) except AssertionError: - logger.error('Failed to add key') + logger.error('Failed to add key', terminal=True) def add_address(o_inst): try: @@ -42,8 +42,8 @@ def add_address(o_inst): except IndexError: pass else: - logger.info("Adding address: " + logger.colors.underline + newAddress) + logger.info("Adding address: " + logger.colors.underline + newAddress, terminal=True) if o_inst.onionrCore.addAddress(newAddress): - logger.info("Successfully added address.") + logger.info("Successfully added address.", terminal=True) else: - logger.warn("Unable to add address.") \ No newline at end of file + logger.warn("Unable to add address.", terminal=True) \ No newline at end of file diff --git a/onionr/onionrcommands/openwebinterface.py b/onionr/onionrcommands/openwebinterface.py index 3e68e3ea..8ce71744 100755 --- a/onionr/onionrcommands/openwebinterface.py +++ b/onionr/onionrcommands/openwebinterface.py @@ -23,7 +23,7 @@ def open_home(o_inst): try: url = o_inst.onionrUtils.getClientAPIServer() except FileNotFoundError: - logger.error('Onionr seems to not be running (could not get api host)') + logger.error('Onionr seems to not be running (could not get api host)', terminal=True) else: url = 'http://%s/#%s' % (url, o_inst.onionrCore.config.get('client.webpassword')) logger.info('If Onionr does not open automatically, use this URL: ' + url, terminal=True) diff --git a/onionr/onionrcommands/plugincommands.py b/onionr/onionrcommands/plugincommands.py index e4f88d96..154bb6cf 100755 --- a/onionr/onionrcommands/plugincommands.py +++ b/onionr/onionrcommands/plugincommands.py @@ -24,18 +24,18 @@ import logger, onionrplugins as plugins def enable_plugin(o_inst): if len(sys.argv) >= 3: plugin_name = sys.argv[2] - logger.info('Enabling plugin "%s"...' % plugin_name) + logger.info('Enabling plugin "%s"...' % plugin_name, terminal=True) plugins.enable(plugin_name, o_inst) else: - logger.info('%s %s ' % (sys.argv[0], sys.argv[1])) + logger.info('%s %s ' % (sys.argv[0], sys.argv[1]), terminal=True) def disable_plugin(o_inst): if len(sys.argv) >= 3: plugin_name = sys.argv[2] - logger.info('Disabling plugin "%s"...' % plugin_name) + logger.info('Disabling plugin "%s"...' % plugin_name, terminal=True) plugins.disable(plugin_name, o_inst) else: - logger.info('%s %s ' % (sys.argv[0], sys.argv[1])) + logger.info('%s %s ' % (sys.argv[0], sys.argv[1]), terminal=True) def reload_plugin(o_inst): ''' @@ -44,11 +44,11 @@ def reload_plugin(o_inst): if len(sys.argv) >= 3: plugin_name = sys.argv[2] - logger.info('Reloading plugin "%s"...' % plugin_name) + logger.info('Reloading plugin "%s"...' % plugin_name, terminal=True) plugins.stop(plugin_name, o_inst) plugins.start(plugin_name, o_inst) else: - logger.info('Reloading all plugins...') + logger.info('Reloading all plugins...', terminal=True) plugins.reload(o_inst) @@ -62,7 +62,7 @@ def create_plugin(o_inst): 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) + logger.info('Creating plugin "%s"...' % plugin_name, terminal=True) os.makedirs(plugins.get_plugins_folder(plugin_name)) with open(plugins.get_plugins_folder(plugin_name) + '/main.py', 'a') as main: @@ -76,12 +76,12 @@ def create_plugin(o_inst): 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) + logger.info('Enabling plugin "%s"...' % plugin_name, terminal=True) plugins.enable(plugin_name, o_inst) else: - logger.warn('Cannot create plugin directory structure; plugin "%s" exists.' % plugin_name) + logger.warn('Cannot create plugin directory structure; plugin "%s" exists.' % plugin_name, terminal=True) except Exception as e: - logger.error('Failed to create plugin directory structure.', e) + logger.error('Failed to create plugin directory structure.', e, terminal=True) else: - logger.info('%s %s ' % (sys.argv[0], sys.argv[1])) \ No newline at end of file + logger.info('%s %s ' % (sys.argv[0], sys.argv[1]), terminal=True) \ No newline at end of file diff --git a/onionr/onionrcommands/pubkeymanager.py b/onionr/onionrcommands/pubkeymanager.py index 2a6370f9..e8f29b2a 100755 --- a/onionr/onionrcommands/pubkeymanager.py +++ b/onionr/onionrcommands/pubkeymanager.py @@ -29,42 +29,42 @@ def add_ID(o_inst): except (IndexError, AssertionError) as e: newID = o_inst.onionrCore._crypto.keyManager.addKey()[0] else: - logger.warn('Deterministic keys require random and long passphrases.') - logger.warn('If a good passphrase is not used, your key can be easily stolen.') - logger.warn('You should use a series of hard to guess words, see this for reference: https://www.xkcd.com/936/') + logger.warn('Deterministic keys require random and long passphrases.', terminal=True) + logger.warn('If a good passphrase is not used, your key can be easily stolen.', terminal=True) + logger.warn('You should use a series of hard to guess words, see this for reference: https://www.xkcd.com/936/', terminal=True) pass1 = getpass.getpass(prompt='Enter at least %s characters: ' % (o_inst.onionrCore._crypto.deterministicRequirement,)) pass2 = getpass.getpass(prompt='Confirm entry: ') if o_inst.onionrCore._crypto.safeCompare(pass1, pass2): try: - logger.info('Generating deterministic key. This can take a while.') + logger.info('Generating deterministic key. This can take a while.', terminal=True) newID, privKey = o_inst.onionrCore._crypto.generateDeterministic(pass1) except onionrexceptions.PasswordStrengthError: - logger.error('Passphrase must use at least %s characters.' % (o_inst.onionrCore._crypto.deterministicRequirement,)) + logger.error('Passphrase must use at least %s characters.' % (o_inst.onionrCore._crypto.deterministicRequirement,), terminal=True) sys.exit(1) else: - logger.error('Passwords do not match.') + logger.error('Passwords do not match.', terminal=True) sys.exit(1) o_inst.onionrCore._crypto.keyManager.addKey(pubKey=newID, privKey=privKey) - logger.info('Added ID: %s' % (o_inst.onionrUtils.bytesToStr(newID),)) + logger.info('Added ID: %s' % (o_inst.onionrUtils.bytesToStr(newID),), terminal=True) def change_ID(o_inst): try: key = sys.argv[2] key = unpaddedbase32.repad(key.encode()).decode() except IndexError: - logger.warn('Specify pubkey to use') + logger.warn('Specify pubkey to use', terminal=True) else: if o_inst.onionrUtils.validatePubKey(key): if key in o_inst.onionrCore._crypto.keyManager.getPubkeyList(): o_inst.onionrCore.config.set('general.public_key', key) o_inst.onionrCore.config.save() - logger.info('Set active key to: %s' % (key,)) - logger.info('Restart Onionr if it is running.') + logger.info('Set active key to: %s' % (key,), terminal=True) + logger.info('Restart Onionr if it is running.', terminal=True) else: - logger.warn('That key does not exist') + logger.warn('That key does not exist', terminal=True) else: - logger.warn('Invalid key %s' % (key,)) + logger.warn('Invalid key %s' % (key,), terminal=True) def friend_command(o_inst): friend = '' @@ -72,13 +72,13 @@ def friend_command(o_inst): # Get the friend command action = sys.argv[2] except IndexError: - logger.info('Syntax: friend add/remove/list [address]') + logger.info('Syntax: friend add/remove/list [address]', terminal=True) else: action = action.lower() if action == 'list': # List out peers marked as our friend for friend in contactmanager.ContactManager.list_friends(o_inst.onionrCore): - logger.info(friend.publicKey + ' - ' + friend.get_info('name')) + logger.info(friend.publicKey + ' - ' + friend.get_info('name'), terminal=True) elif action in ('add', 'remove'): try: friend = sys.argv[3] @@ -88,7 +88,7 @@ def friend_command(o_inst): raise onionrexceptions.KeyNotKnown friend = onionrusers.OnionrUser(o_inst.onionrCore, friend) except IndexError: - logger.warn('Friend ID is required.') + logger.warn('Friend ID is required.', terminal=True) action = 'error' # set to 'error' so that the finally block does not process anything except onionrexceptions.KeyNotKnown: o_inst.onionrCore.addPeer(friend) @@ -96,9 +96,9 @@ def friend_command(o_inst): finally: if action == 'add': friend.setTrust(1) - logger.info('Added %s as friend.' % (friend.publicKey,)) + logger.info('Added %s as friend.' % (friend.publicKey,), terminal=True) elif action == 'remove': friend.setTrust(0) - logger.info('Removed %s as friend.' % (friend.publicKey,)) + logger.info('Removed %s as friend.' % (friend.publicKey,), terminal=True) else: - logger.info('Syntax: friend add/remove/list [address]') \ No newline at end of file + logger.info('Syntax: friend add/remove/list [address]', terminal=True) \ No newline at end of file diff --git a/onionr/onionrcommands/resettor.py b/onionr/onionrcommands/resettor.py index d49cacfe..cd7c51d2 100755 --- a/onionr/onionrcommands/resettor.py +++ b/onionr/onionrcommands/resettor.py @@ -24,6 +24,6 @@ def reset_tor(): tor_dir = c.dataDir + 'tordata' if os.path.exists(tor_dir): if c._utils.localCommand('/ping') == 'pong!': - logger.warn('Cannot delete Tor data while Onionr is running') + logger.warn('Cannot delete Tor data while Onionr is running', terminal=True) else: shutil.rmtree(tor_dir) \ No newline at end of file diff --git a/onionr/static-data/default-plugins/cliui/main.py b/onionr/static-data/default-plugins/cliui/main.py index c4186b81..5f47c455 100755 --- a/onionr/static-data/default-plugins/cliui/main.py +++ b/onionr/static-data/default-plugins/cliui/main.py @@ -100,7 +100,7 @@ class OnionrCLIUI: elif choice == "": pass else: - logger.error("Invalid choice") + logger.error("Invalid choice", terminal=True) return def on_init(api, data = None): diff --git a/onionr/static-data/default-plugins/encrypt/main.py b/onionr/static-data/default-plugins/encrypt/main.py index 15697ac2..8c874c99 100755 --- a/onionr/static-data/default-plugins/encrypt/main.py +++ b/onionr/static-data/default-plugins/encrypt/main.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication This default plugin allows users to encrypt/decrypt messages without using blocks ''' @@ -46,13 +46,13 @@ class PlainEncryption: if not self.api.get_core()._utils.validatePubKey(sys.argv[2]): raise onionrexceptions.InvalidPubkey except (ValueError, IndexError) as e: - logger.error("Peer public key not specified") + logger.error("Peer public key not specified", terminal=True) except onionrexceptions.InvalidPubkey: - logger.error("Invalid public key") + logger.error("Invalid public key", terminal=True) else: pubkey = sys.argv[2] # Encrypt if public key is valid - logger.info("Please enter your message (ctrl-d or -q to stop):") + logger.info("Please enter your message (ctrl-d or -q to stop):", terminal=True) try: for line in sys.stdin: if line == '-q\n': @@ -72,12 +72,12 @@ class PlainEncryption: plaintext = data encrypted = self.api.get_core()._crypto.pubKeyEncrypt(plaintext, pubkey, encodedData=True) encrypted = self.api.get_core()._utils.bytesToStr(encrypted) - logger.info('Encrypted Message: \n\nONIONR ENCRYPTED DATA %s END ENCRYPTED DATA' % (encrypted,)) + logger.info('Encrypted Message: \n\nONIONR ENCRYPTED DATA %s END ENCRYPTED DATA' % (encrypted,), terminal=True) def decrypt(self): plaintext = "" data = "" - logger.info("Please enter your message (ctrl-d or -q to stop):") + logger.info("Please enter your message (ctrl-d or -q to stop):", terminal=True) try: for line in sys.stdin: if line == '-q\n': @@ -91,17 +91,17 @@ class PlainEncryption: myPub = self.api.get_core()._crypto.pubKey decrypted = self.api.get_core()._crypto.pubKeyDecrypt(encrypted, privkey=self.api.get_core()._crypto.privKey, encodedData=True) if decrypted == False: - logger.error("Decryption failed") + logger.error("Decryption failed", terminal=True) else: data = json.loads(decrypted) - logger.info('Decrypted Message: \n\n%s' % data['data']) + logger.info('Decrypted Message: \n\n%s' % data['data'], terminal=True) try: - logger.info("Signing public key: %s" % (data['signer'],)) + logger.info("Signing public key: %s" % (data['signer'],), terminal=True) assert self.api.get_core()._crypto.edVerify(data['data'], data['signer'], data['sig']) != False except (AssertionError, KeyError) as e: - logger.warn("WARNING: THIS MESSAGE HAS A MISSING OR INVALID SIGNATURE") + logger.warn("WARNING: THIS MESSAGE HAS A MISSING OR INVALID SIGNATURE", terminal=True) else: - logger.info("Message has good signature.") + logger.info("Message has good signature.", terminal=True) return def on_init(api, data = None): diff --git a/onionr/static-data/default-plugins/pluginmanager/main.py b/onionr/static-data/default-plugins/pluginmanager/main.py index 979b1133..d4feb69d 100755 --- a/onionr/static-data/default-plugins/pluginmanager/main.py +++ b/onionr/static-data/default-plugins/pluginmanager/main.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network. + Onionr - Private P2P Communication This plugin acts as a plugin manager, and allows the user to install other plugins distributed over Onionr. ''' @@ -180,11 +180,11 @@ def blockToPlugin(block): shutil.unpack_archive(source, destination) pluginapi.plugins.enable(name) - logger.info('Installation of %s complete.' % 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) + logger.error('Failed to install plugin.', error = e, timestamp = False, terminal=True) return False @@ -240,9 +240,9 @@ def pluginToBlock(plugin, import_block = True): return hash else: - logger.error('Plugin %s does not exist.' % plugin) + 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) + logger.error('Failed to convert plugin to block.', error = e, timestamp = False, terminal=True) return False @@ -261,7 +261,7 @@ def installBlock(block): install = False - logger.info(('Will install %s' + (' v' + version if not version is None else '') + ' (%s), by %s') % (name, date, author)) + 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)): @@ -273,12 +273,12 @@ def installBlock(block): blockToPlugin(block.getHash()) addPlugin(name) else: - logger.info('Installation cancelled.') + logger.info('Installation cancelled.', terminal=True) return False return True except Exception as e: - logger.error('Failed to install plugin.', error = e, timestamp = False) + logger.error('Failed to install plugin.', error = e, timestamp = False, terminal=True) return False def uninstallPlugin(plugin): @@ -291,12 +291,12 @@ def uninstallPlugin(plugin): remove = False if not exists: - logger.warn('Plugin %s does not exist.' % plugin, timestamp = False) + 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) + 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) @@ -306,20 +306,20 @@ def uninstallPlugin(plugin): pluginapi.plugins.disable(plugin) shutil.rmtree(pluginFolder) - logger.info('Uninstallation of %s complete.' % plugin) + 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) + 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]') - logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [public key/block hash]') + 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: @@ -345,20 +345,20 @@ def commandInstallPlugin(): if pkobh is None: # still nothing found, try searching repositories - logger.info('Searching for public key in 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)) + 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]) + logger.info('Found distributor: %s' % distributors[0], terminal=True) distributor = distributors[0] else: distributors_message = '' @@ -368,11 +368,11 @@ def commandInstallPlugin(): 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)) + 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)) + 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) @@ -380,7 +380,7 @@ def commandInstallPlugin(): distributor = distributors[int(choice)] valid = True except KeyboardInterrupt: - logger.info('Installation cancelled.') + logger.info('Installation cancelled.', terminal=True) return True except: pass @@ -388,11 +388,11 @@ def commandInstallPlugin(): if not distributor is None: pkobh = distributor except Exception as e: - logger.warn('Failed to lookup plugin in repositories.', timestamp = False) + 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) + logger.error('No key for this plugin found in keystore or repositories, please specify.', timestamp = False, terminal=True) return True @@ -409,21 +409,21 @@ def commandInstallPlugin(): blockhash = None if valid_hash and not real_block: - logger.error('Block hash not found. Perhaps it has not been synced yet?', timestamp = False) - logger.debug('Is valid hash, but does not belong to a known 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) + 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) - logger.debug('Is valid key, but the key is not a known one.') + 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) + logger.debug('Using public key %s...' % publickey, terminal=True) saveKey(pluginname, pkobh) @@ -455,14 +455,14 @@ def commandInstallPlugin(): 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) - logger.debug('Most recent block matching parameters is %s' % mostRecentVersionBlock) + 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) + 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]') + logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [public key/block hash]', terminal=True) return True @@ -470,12 +470,12 @@ def commandUninstallPlugin(): if len(sys.argv) >= 3: uninstallPlugin(sys.argv[2]) else: - logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' ') + 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.') + logger.info('This feature has not been created yet. Please check back later.', terminal=True) return True def commandAddRepository(): @@ -495,22 +495,22 @@ def commandAddRepository(): if pluginapi.get_utils().validatePubKey(distributor): pluginslist[pluginname] = distributor - logger.debug('Found %s records in repository.' % len(pluginslist)) + logger.debug('Found %s records in repository.' % len(pluginslist), terminal=True) if len(pluginslist) != 0: addRepository(blockhash, pluginslist) - logger.info('Successfully added repository.') + logger.info('Successfully added repository.', terminal=True) else: - logger.error('Repository contains no records, not importing.', timestamp = False) + logger.error('Repository contains no records, not importing.', timestamp = False, terminal=True) except Exception as e: - logger.error('Failed to parse block.', error = 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) + 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) + 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]') + logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [block hash]', terminal=True) return True @@ -524,15 +524,15 @@ def commandRemoveRepository(): if blockhash in getRepositories(): try: removeRepository(blockhash) - logger.info('Successfully removed repository.') + logger.info('Successfully removed repository.', terminal=True) except Exception as e: - logger.error('Failed to parse block.', error = e) + logger.error('Failed to parse block.', error = e, terminal=True) else: - logger.error('Repository has not been imported, nothing to remove.', timestamp = False) + 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)) + logger.error('Unknown data "%s"; must be block hash.' % str(pkobh), terminal=True) else: - logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [block hash]') + logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [block hash]', terminal=True) return True @@ -545,11 +545,11 @@ def commandPublishPlugin(): if os.path.exists(pluginfolder) and not os.path.isfile(pluginfolder): block = pluginToBlock(pluginname) - logger.info('Plugin saved in block %s.' % block) + logger.info('Plugin saved in block %s.' % block, terminal=True) else: - logger.error('Plugin %s does not exist.' % pluginname, timestamp = False) + logger.error('Plugin %s does not exist.' % pluginname, timestamp = False, terminal=True) else: - logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' ') + logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' ', terminal=True) def commandCreateRepository(): if len(sys.argv) >= 3: @@ -573,22 +573,22 @@ def commandCreateRepository(): 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) + 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.') + 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)) + 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.') + logger.error('Failed to create repository, an unknown error occurred.', terminal=True) else: - logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [plugins...]') + logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [plugins...]', terminal=True) return True diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py index 9acb99f2..e91a153c 100755 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -73,7 +73,7 @@ class OnionrMail: blockCount = 0 pmBlockMap = {} pmBlocks = {} - logger.info('Decrypting messages...') + logger.info('Decrypting messages...', terminal=True) choice = '' displayList = [] subject = '' @@ -108,7 +108,7 @@ class OnionrMail: displayList.append('%s. %s - %s - <%s>: %s' % (blockCount, blockDate, senderDisplay[:12], subject[:10], blockHash)) while choice not in ('-q', 'q', 'quit'): for i in displayList: - logger.info(i) + logger.info(i, terminal=True) try: choice = logger.readline('Enter a block number, -r to refresh, or -q to stop: ').strip().lower() except (EOFError, KeyboardInterrupt): @@ -138,18 +138,18 @@ class OnionrMail: senderDisplay = self.myCore._utils.bytesToStr(readBlock.signer) if len(senderDisplay.strip()) == 0: senderDisplay = 'Anonymous' - logger.info('Message received from %s' % (senderDisplay,)) - logger.info('Valid signature: %s' % readBlock.validSig) + logger.info('Message received from %s' % (senderDisplay,), terminal=True) + logger.info('Valid signature: %s' % readBlock.validSig, terminal=True) if not readBlock.validSig: - logger.warn('This message has an INVALID/NO signature. ANYONE could have sent this message.') + logger.warn('This message has an INVALID/NO signature. ANYONE could have sent this message.', terminal=True) cancel = logger.readline('Press enter to continue to message, or -q to not open the message (recommended).') print('') if cancel != '-q': try: print(draw_border(self.myCore._utils.escapeAnsi(readBlock.bcontent.decode().strip()))) except ValueError: - logger.warn('Error presenting message. This is usually due to a malformed or blank message.') + logger.warn('Error presenting message. This is usually due to a malformed or blank message.', terminal=True) pass if readBlock.validSig: reply = logger.readline("Press enter to continue, or enter %s to reply" % ("-r",)) @@ -168,7 +168,7 @@ class OnionrMail: entering = True while entering: self.get_sent_list() - logger.info('Enter a block number or -q to return') + logger.info('Enter a block number or -q to return', terminal=True) try: choice = input('>') except (EOFError, KeyboardInterrupt) as e: @@ -182,11 +182,11 @@ class OnionrMail: try: self.sentboxList[int(choice)] except (IndexError, ValueError) as e: - logger.warn('Invalid block.') + logger.warn('Invalid block.', terminal=True) else: - logger.info('Sent to: ' + self.sentMessages[self.sentboxList[int(choice)]][1]) + logger.info('Sent to: ' + self.sentMessages[self.sentboxList[int(choice)]][1], terminal=True) # Print ansi escaped sent message - logger.info(self.myCore._utils.escapeAnsi(self.sentMessages[self.sentboxList[int(choice)]][0])) + logger.info(self.myCore._utils.escapeAnsi(self.sentMessages[self.sentboxList[int(choice)]][0]), terminal=True) input('Press enter to continue...') finally: if choice == '-q': @@ -201,7 +201,7 @@ class OnionrMail: self.sentboxList.append(i['hash']) self.sentMessages[i['hash']] = (self.myCore._utils.bytesToStr(i['message']), i['peer'], i['subject']) if display: - logger.info('%s. %s - %s - (%s) - %s' % (count, i['hash'], i['peer'][:12], i['subject'], i['date'])) + logger.info('%s. %s - %s - (%s) - %s' % (count, i['hash'], i['peer'][:12], i['subject'], i['date']), terminal=True) count += 1 return json.dumps(self.sentMessages) @@ -220,7 +220,7 @@ class OnionrMail: if not self.myCore._utils.validatePubKey(recip): raise onionrexceptions.InvalidPubkey('Must be a valid ed25519 base32 encoded public key') except onionrexceptions.InvalidPubkey: - logger.warn('Invalid public key') + logger.warn('Invalid public key', terminal=True) except (KeyboardInterrupt, EOFError): entering = False else: @@ -234,7 +234,7 @@ class OnionrMail: pass cancelEnter = False - logger.info('Enter your message, stop by entering -q on a new line. -c to cancel') + logger.info('Enter your message, stop by entering -q on a new line. -c to cancel', terminal=True) while newLine != '-q': try: newLine = input() @@ -249,7 +249,7 @@ class OnionrMail: message += newLine if not cancelEnter: - logger.info('Inserting encrypted message as Onionr block....') + logger.info('Inserting encrypted message as Onionr block....', terminal=True) blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=self.doSigs, meta={'subject': subject}) @@ -261,16 +261,16 @@ class OnionrMail: while True: sigMsg = 'Message Signing: %s' - logger.info(self.strings.programTag + '\n\nUser ID: ' + self.myCore._crypto.pubKey) + logger.info(self.strings.programTag + '\n\nUser ID: ' + self.myCore._crypto.pubKey, terminal=True) if self.doSigs: sigMsg = sigMsg % ('enabled',) else: sigMsg = sigMsg % ('disabled (Your messages cannot be trusted)',) if self.doSigs: - logger.info(sigMsg) + logger.info(sigMsg, terminal=True) else: - logger.warn(sigMsg) - logger.info(self.strings.mainMenu.title()) # print out main menu + logger.warn(sigMsg, terminal=True) + logger.info(self.strings.mainMenu.title(), terminal=True) # print out main menu try: choice = logger.readline('Enter 1-%s:\n' % (len(self.strings.mainMenuChoices))).lower().strip() except (KeyboardInterrupt, EOFError): @@ -285,12 +285,12 @@ class OnionrMail: elif choice in (self.strings.mainMenuChoices[3], '4'): self.toggle_signing() elif choice in (self.strings.mainMenuChoices[4], '5'): - logger.info('Goodbye.') + logger.info('Goodbye.', terminal=True) break elif choice == '': pass else: - logger.warn('Invalid choice.') + logger.warn('Invalid choice.', terminal=True) return def add_deleted(keyStore, bHash): From 12227b6bcbd591c33fae02662894cc1ebad54178 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 20 Jun 2019 02:59:32 -0500 Subject: [PATCH 110/173] a little work on clandestine UI --- .../default-plugins/clandestine/controlapi.py | 2 +- .../default-plugins/clandestine/main.py | 2 +- .../default-plugins/clandestine/peerserver.py | 2 +- .../static-data/default-plugins/cliui/main.py | 2 +- .../default-plugins/contactmanager/main.py | 2 +- .../default-plugins/flow/flowapi.py | 2 +- .../static-data/default-plugins/flow/main.py | 2 +- .../default-plugins/metadataprocessor/main.py | 2 +- .../default-plugins/pms/loadinbox.py | 19 +++++++++++++++++++ .../default-plugins/pms/mailapi.py | 2 +- .../static-data/default-plugins/pms/main.py | 2 +- .../default-plugins/pms/sentboxdb.py | 2 +- onionr/static-data/www/clandestine/index.html | 1 + onionr/static-data/www/clandestine/js/main.js | 12 +++++++----- onionr/static-data/www/private/index.html | 2 +- 15 files changed, 39 insertions(+), 17 deletions(-) diff --git a/onionr/static-data/default-plugins/clandestine/controlapi.py b/onionr/static-data/default-plugins/clandestine/controlapi.py index 773e9c36..e40de9b6 100755 --- a/onionr/static-data/default-plugins/clandestine/controlapi.py +++ b/onionr/static-data/default-plugins/clandestine/controlapi.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication HTTP endpoints for controlling IMs ''' diff --git a/onionr/static-data/default-plugins/clandestine/main.py b/onionr/static-data/default-plugins/clandestine/main.py index 17007f0e..29afa3e1 100755 --- a/onionr/static-data/default-plugins/clandestine/main.py +++ b/onionr/static-data/default-plugins/clandestine/main.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication Instant message conversations with Onionr peers ''' diff --git a/onionr/static-data/default-plugins/clandestine/peerserver.py b/onionr/static-data/default-plugins/clandestine/peerserver.py index 5fe92fe9..5e4fba25 100755 --- a/onionr/static-data/default-plugins/clandestine/peerserver.py +++ b/onionr/static-data/default-plugins/clandestine/peerserver.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication HTTP endpoints for communicating with peers ''' diff --git a/onionr/static-data/default-plugins/cliui/main.py b/onionr/static-data/default-plugins/cliui/main.py index 5f47c455..f6a0b906 100755 --- a/onionr/static-data/default-plugins/cliui/main.py +++ b/onionr/static-data/default-plugins/cliui/main.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This is an interactive menu-driven CLI interface for Onionr ''' diff --git a/onionr/static-data/default-plugins/contactmanager/main.py b/onionr/static-data/default-plugins/contactmanager/main.py index bdf6ac5b..d6c4499c 100755 --- a/onionr/static-data/default-plugins/contactmanager/main.py +++ b/onionr/static-data/default-plugins/contactmanager/main.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This is an interactive menu-driven CLI interface for Onionr ''' diff --git a/onionr/static-data/default-plugins/flow/flowapi.py b/onionr/static-data/default-plugins/flow/flowapi.py index 95d5fb4f..09b54629 100755 --- a/onionr/static-data/default-plugins/flow/flowapi.py +++ b/onionr/static-data/default-plugins/flow/flowapi.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication This file primarily serves to allow specific fetching of flow board messages ''' diff --git a/onionr/static-data/default-plugins/flow/main.py b/onionr/static-data/default-plugins/flow/main.py index 0162019f..63c86cbc 100755 --- a/onionr/static-data/default-plugins/flow/main.py +++ b/onionr/static-data/default-plugins/flow/main.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication This default plugin handles "flow" messages (global chatroom style communication) ''' diff --git a/onionr/static-data/default-plugins/metadataprocessor/main.py b/onionr/static-data/default-plugins/metadataprocessor/main.py index 5feb4305..333c68aa 100755 --- a/onionr/static-data/default-plugins/metadataprocessor/main.py +++ b/onionr/static-data/default-plugins/metadataprocessor/main.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This processes metadata for Onionr blocks ''' diff --git a/onionr/static-data/default-plugins/pms/loadinbox.py b/onionr/static-data/default-plugins/pms/loadinbox.py index 996d8f06..b4011b00 100755 --- a/onionr/static-data/default-plugins/pms/loadinbox.py +++ b/onionr/static-data/default-plugins/pms/loadinbox.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + Load the user's inbox and return it as a list +''' +''' + 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 onionrblockapi def load_inbox(myCore): inbox_list = [] diff --git a/onionr/static-data/default-plugins/pms/mailapi.py b/onionr/static-data/default-plugins/pms/mailapi.py index 2ba99d08..af59bd93 100755 --- a/onionr/static-data/default-plugins/pms/mailapi.py +++ b/onionr/static-data/default-plugins/pms/mailapi.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication HTTP endpoints for mail plugin. ''' diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py index e91a153c..e594c368 100755 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This default plugin handles private messages in an email like fashion ''' diff --git a/onionr/static-data/default-plugins/pms/sentboxdb.py b/onionr/static-data/default-plugins/pms/sentboxdb.py index 28a5de6f..7090e214 100755 --- a/onionr/static-data/default-plugins/pms/sentboxdb.py +++ b/onionr/static-data/default-plugins/pms/sentboxdb.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication This file handles the sentbox for the mail plugin ''' diff --git a/onionr/static-data/www/clandestine/index.html b/onionr/static-data/www/clandestine/index.html index 91bc2e07..7fc0cab0 100755 --- a/onionr/static-data/www/clandestine/index.html +++ b/onionr/static-data/www/clandestine/index.html @@ -14,6 +14,7 @@ Clandestine +
      Current Used ID:
        diff --git a/onionr/static-data/www/clandestine/js/main.js b/onionr/static-data/www/clandestine/js/main.js index 0e7071a0..a757f713 100755 --- a/onionr/static-data/www/clandestine/js/main.js +++ b/onionr/static-data/www/clandestine/js/main.js @@ -1,12 +1,14 @@ -friendList = [] +friendList = {} convoListElement = document.getElementsByClassName('conversationList')[0] function createConvoList(){ - for (var x = 0; x < friendList.length; x++){ + console.log(friendList) + + for (friend in friendList){ var convoEntry = document.createElement('div') convoEntry.classList.add('convoEntry') - convoEntry.setAttribute('data-pubkey', friendList[x]) - convoEntry.innerText = friendList[x] + convoEntry.setAttribute('data-pubkey', friend) + convoEntry.innerText = friendList[friend] convoListElement.append(convoEntry) } } @@ -20,7 +22,7 @@ fetch('/friends/list', { var keys = [] for(var k in resp) keys.push(k) for (var i = 0; i < keys.length; i++){ - friendList.push(keys[i]) + friendList[keys[i]] = resp[keys[i]]['name'] } createConvoList() }) \ No newline at end of file diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index a2d3d3c9..9a548ebd 100755 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -23,7 +23,7 @@


        - +


        Mail - Friend Manager - Circle - Clandestine From 5cee375b028906b133c4519d0cdaff0ac914c297 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 20 Jun 2019 16:59:36 -0500 Subject: [PATCH 111/173] fixed small cli logging bugs --- onionr/communicator.py | 2 +- onionr/onionr.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/onionr/communicator.py b/onionr/communicator.py index 370a4981..d59b76da 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -317,7 +317,7 @@ class OnionrCommunicatorDaemon: try: self.getPeerProfileInstance(peer).addScore(-10) self.removeOnlinePeer(peer) - if action != 'ping': + if action != 'ping' and not self.shutdown: logger.warn('Lost connection to ' + peer, terminal=True) self.getOnlinePeers() # Will only add a new peer to pool if needed except ValueError: diff --git a/onionr/onionr.py b/onionr/onionr.py index e94e35b8..4030aae1 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -354,7 +354,7 @@ class Onionr: 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.') + logger.info('Do ' + logger.colors.bold + sys.argv[0] + ' --help' + logger.colors.reset + logger.colors.fg.green + ' for Onionr help.', terminal=True) def start(self, input = False, override = False): ''' From 50e93f46e40ba95f3535cbfb70f6c20256613c51 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 22 Jun 2019 16:16:12 -0500 Subject: [PATCH 112/173] * Major core refactoring * renamed clandestine to esoteric --- onionr/communicatorutils/downloadblocks.py | 2 +- onionr/core.py | 375 ++---------------- onionr/coredb/__init__.py | 1 + onionr/coredb/blockmetadb/__init__.py | 58 +++ onionr/coredb/blockmetadb/expiredblocks.py | 15 + onionr/coredb/blockmetadb/updateblockinfo.py | 13 + onionr/coredb/daemonqueue/__init__.py | 75 ++++ onionr/coredb/keydb/__init__.py | 1 + onionr/coredb/keydb/addkeys.py | 70 ++++ onionr/coredb/keydb/listkeys.py | 63 +++ onionr/coredb/keydb/removekeys.py | 19 + onionr/coredb/keydb/transportinfo.py | 53 +++ onionr/coredb/keydb/userinfo.py | 50 +++ onionr/onionrstorage.py | 4 +- .../{clandestine => esoteric}/controlapi.py | 12 +- .../{clandestine => esoteric}/info.json | 2 +- .../{clandestine => esoteric}/main.py | 10 +- .../{clandestine => esoteric}/peerserver.py | 12 +- .../static-data/default-plugins/flow/main.py | 1 + onionr/static-data/www/private/index.html | 2 +- 20 files changed, 472 insertions(+), 366 deletions(-) create mode 100644 onionr/coredb/__init__.py create mode 100644 onionr/coredb/blockmetadb/__init__.py create mode 100644 onionr/coredb/blockmetadb/expiredblocks.py create mode 100644 onionr/coredb/blockmetadb/updateblockinfo.py create mode 100644 onionr/coredb/daemonqueue/__init__.py create mode 100644 onionr/coredb/keydb/__init__.py create mode 100644 onionr/coredb/keydb/addkeys.py create mode 100644 onionr/coredb/keydb/listkeys.py create mode 100644 onionr/coredb/keydb/removekeys.py create mode 100644 onionr/coredb/keydb/transportinfo.py create mode 100644 onionr/coredb/keydb/userinfo.py rename onionr/static-data/default-plugins/{clandestine => esoteric}/controlapi.py (83%) rename onionr/static-data/default-plugins/{clandestine => esoteric}/info.json (64%) rename onionr/static-data/default-plugins/{clandestine => esoteric}/main.py (94%) rename onionr/static-data/default-plugins/{clandestine => esoteric}/peerserver.py (78%) diff --git a/onionr/communicatorutils/downloadblocks.py b/onionr/communicatorutils/downloadblocks.py index aad5f884..cb72bd12 100755 --- a/onionr/communicatorutils/downloadblocks.py +++ b/onionr/communicatorutils/downloadblocks.py @@ -110,7 +110,7 @@ def download_blocks_from_communicator(comm_inst): if removeFromQueue: try: del comm_inst.blockQueue[blockHash] # remove from block queue both if success or false - logger.info('%s blocks remaining in queue' % [len(comm_inst.blockQueue)]) + logger.info('%s blocks remaining in queue' % [len(comm_inst.blockQueue)], terminal=True) except KeyError: pass comm_inst.currentDownloading.remove(blockHash) diff --git a/onionr/core.py b/onionr/core.py index be309309..735358e5 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -20,6 +20,7 @@ import sqlite3, os, sys, time, json, uuid import logger, netcontroller, config from onionrblockapi import Block +import coredb import deadsimplekv as simplekv import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions import onionrblacklist @@ -128,89 +129,19 @@ class Core: ''' Adds a public key to the key database (misleading function name) ''' - if peerID in self.listPeers() or peerID == self._crypto.pubKey: - raise ValueError("specified id is already known") - - # This function simply adds a peer to the DB - if not self._utils.validatePubKey(peerID): - return False - - events.event('pubkey_add', data = {'key': peerID}, onionr = self.onionrInst) - - conn = sqlite3.connect(self.peerDB, timeout=30) - hashID = self._crypto.pubKeyHashID(peerID) - c = conn.cursor() - t = (peerID, name, 'unknown', hashID, 0) - - for i in c.execute("SELECT * FROM peers WHERE id = ?;", (peerID,)): - try: - if i[0] == peerID: - conn.close() - return False - except ValueError: - pass - except IndexError: - pass - c.execute('INSERT INTO peers (id, name, dateSeen, hashID, trust) VALUES(?, ?, ?, ?, ?);', t) - conn.commit() - conn.close() - - return True + return coredb.keydb.addkeys.add_peer(self, peerID, name) def addAddress(self, address): ''' Add an address to the address database (only tor currently) ''' - - if type(address) is None or len(address) == 0: - return False - if self._utils.validateID(address): - if address == config.get('i2p.ownAddr', None) or address == self.hsAddress: - return False - conn = sqlite3.connect(self.addressDB, timeout=30) - c = conn.cursor() - # check if address is in database - # this is safe to do because the address is validated above, but we strip some chars here too just in case - address = address.replace('\'', '').replace(';', '').replace('"', '').replace('\\', '') - for i in c.execute("SELECT * FROM adders WHERE address = ?;", (address,)): - try: - if i[0] == address: - conn.close() - return False - except ValueError: - pass - except IndexError: - pass - - t = (address, 1) - c.execute('INSERT INTO adders (address, type) VALUES(?, ?);', t) - conn.commit() - conn.close() - - events.event('address_add', data = {'address': address}, onionr = self.onionrInst) - - return True - else: - #logger.debug('Invalid ID: %s' % address) - return False + return coredb.keydb.addkeys.add_address(self, address) def removeAddress(self, address): ''' Remove an address from the address database ''' - - if self._utils.validateID(address): - conn = sqlite3.connect(self.addressDB, timeout=30) - c = conn.cursor() - t = (address,) - c.execute('Delete from adders where address=?;', t) - conn.commit() - conn.close() - - events.event('address_remove', data = {'address': address}, onionr = self.onionrInst) - return True - else: - return False + return coredb.keydb.removekeys.remove_address(self, address) def removeBlock(self, block): ''' @@ -272,17 +203,6 @@ class Core: conn.commit() conn.close() - return - - def getData(self, hash): - ''' - Simply return the data associated to a hash - ''' - - data = onionrstorage.getData(self, hash) - - return data - def setData(self, data): ''' Set the data assciated with a hash @@ -299,10 +219,9 @@ class Core: if type(dataHash) is bytes: dataHash = dataHash.decode() blockFileName = self.blockDataLocation + dataHash + '.dat' - if os.path.exists(blockFileName): - pass # TODO: properly check if block is already saved elsewhere - #raise Exception("Data is already set for " + dataHash) - else: + try: + onionrstorage.getData(self, dataHash) + except onionrexceptions.NoDataAvailable: if self._utils.storageCounter.addBytes(dataSize) != False: onionrstorage.store(self, data, blockHash=dataHash) conn = sqlite3.connect(self.blockDB, timeout=30) @@ -314,126 +233,48 @@ class Core: nonceFile.write(dataHash + '\n') else: raise onionrexceptions.DiskAllocationReached + else: + raise Exception("Data is already set for " + dataHash) return dataHash + def getData(self, hash): + ''' + Simply return the data associated to a hash + ''' + return onionrstorage.getData(self, hash) + def daemonQueue(self): ''' Gives commands to the communication proccess/daemon by reading an sqlite3 database This function intended to be used by the client. Queue to exchange data between "client" and server. ''' - - retData = False - if not os.path.exists(self.queueDB): - self.dbCreate.createDaemonDB() - else: - conn = sqlite3.connect(self.queueDB, timeout=30) - c = conn.cursor() - try: - for row in c.execute('SELECT command, data, date, min(ID), responseID FROM commands group by id'): - retData = row - break - except sqlite3.OperationalError: - self.dbCreate.createDaemonDB() - else: - if retData != False: - c.execute('DELETE FROM commands WHERE id=?;', (retData[3],)) - conn.commit() - conn.close() - - events.event('queue_pop', data = {'data': retData}, onionr = self.onionrInst) - - return retData + return coredb.daemonqueue.daemon_queue(self) def daemonQueueAdd(self, command, data='', responseID=''): ''' Add a command to the daemon queue, used by the communication daemon (communicator.py) ''' - - retData = True - - date = self._utils.getEpoch() - conn = sqlite3.connect(self.queueDB, timeout=30) - c = conn.cursor() - t = (command, data, date, responseID) - try: - c.execute('INSERT INTO commands (command, data, date, responseID) VALUES(?, ?, ?, ?)', t) - conn.commit() - except sqlite3.OperationalError: - retData = False - self.daemonQueue() - events.event('queue_push', data = {'command': command, 'data': data}, onionr = self.onionrInst) - conn.close() - return retData + return coredb.daemonqueue.daemon_queue_add(self, command, data, responseID) def daemonQueueGetResponse(self, responseID=''): ''' Get a response sent by communicator to the API, by requesting to the API ''' - assert len(responseID) > 0 - resp = self._utils.localCommand('queueResponse/' + responseID) - return resp - - def daemonQueueWaitForResponse(self, responseID='', checkFreqSecs=1): - resp = 'failure' - while resp == 'failure': - resp = self.daemonQueueGetResponse(responseID) - time.sleep(1) - return resp - - def daemonQueueSimple(self, command, data='', checkFreqSecs=1): - ''' - A simplified way to use the daemon queue. Will register a command (with optional data) and wait, return the data - Not always useful, but saves time + LOC in some cases. - This is a blocking function, so be careful. - ''' - responseID = str(uuid.uuid4()) # generate unique response ID - self.daemonQueueAdd(command, data=data, responseID=responseID) - return self.daemonQueueWaitForResponse(responseID, checkFreqSecs) + return coredb.daemonqueue.daemon_queue_get_response(responseID) def clearDaemonQueue(self): ''' Clear the daemon queue (somewhat dangerous) ''' - conn = sqlite3.connect(self.queueDB, timeout=30) - c = conn.cursor() - - try: - c.execute('DELETE FROM commands;') - conn.commit() - except: - pass - - conn.close() - events.event('queue_clear', onionr = self.onionrInst) - - return + return coredb.daemonqueue.clear_daemon_queue(self) def listAdders(self, randomOrder=True, i2p=True, recent=0): ''' Return a list of addresses ''' - conn = sqlite3.connect(self.addressDB, timeout=30) - c = conn.cursor() - if randomOrder: - addresses = c.execute('SELECT * FROM adders ORDER BY RANDOM();') - else: - addresses = c.execute('SELECT * FROM adders;') - addressList = [] - for i in addresses: - if len(i[0].strip()) == 0: - continue - addressList.append(i[0]) - conn.close() - testList = list(addressList) # create new list to iterate - for address in testList: - try: - if recent > 0 and (self._utils.getEpoch() - self.getAddressInfo(address, 'lastConnect')) > recent: - raise TypeError # If there is no last-connected date or it was too long ago, don't add peer to list if recent is not 0 - except TypeError: - addressList.remove(address) - return addressList + return coredb.keydb.listkeys.list_adders(self, randomOrder, i2p, recent) def listPeers(self, randomOrder=True, getPow=False, trust=0): ''' @@ -442,35 +283,7 @@ class Core: randomOrder determines if the list should be in a random order trust sets the minimum trust to list ''' - conn = sqlite3.connect(self.peerDB, timeout=30) - c = conn.cursor() - - payload = '' - - if trust not in (0, 1, 2): - logger.error('Tried to select invalid trust.') - return - - if randomOrder: - payload = 'SELECT * FROM peers WHERE trust >= ? ORDER BY RANDOM();' - else: - payload = 'SELECT * FROM peers WHERE trust >= ?;' - - peerList = [] - - for i in c.execute(payload, (trust,)): - try: - if len(i[0]) != 0: - if getPow: - peerList.append(i[0] + '-' + i[1]) - else: - peerList.append(i[0]) - except TypeError: - pass - - conn.close() - - return peerList + return coredb.keydb.listkeys.list_peers(self, randomOrder, getPow, trust) def getPeerInfo(self, peer, info): ''' @@ -483,46 +296,13 @@ class Core: trust int 4 hashID text 5 ''' - conn = sqlite3.connect(self.peerDB, timeout=30) - c = conn.cursor() - - command = (peer,) - infoNumbers = {'id': 0, 'name': 1, 'adders': 2, 'dateSeen': 3, 'trust': 4, 'hashID': 5} - info = infoNumbers[info] - iterCount = 0 - retVal = '' - - for row in c.execute('SELECT * FROM peers WHERE id=?;', command): - for i in row: - if iterCount == info: - retVal = i - break - else: - iterCount += 1 - - conn.close() - - return retVal + return coredb.keydb.userinfo.get_user_info(self, peer, info) def setPeerInfo(self, peer, key, data): ''' Update a peer for a key ''' - - conn = sqlite3.connect(self.peerDB, timeout=30) - c = conn.cursor() - - command = (data, peer) - - # TODO: validate key on whitelist - if key not in ('id', 'name', 'pubkey', 'forwardKey', 'dateSeen', 'trust'): - raise Exception("Got invalid database key when setting peer info") - - c.execute('UPDATE peers SET ' + key + ' = ? WHERE id=?', command) - conn.commit() - conn.close() - - return + return coredb.keydb.userinfo.set_peer_info(self, peer, key, data) def getAddressInfo(self, address, info): ''' @@ -539,117 +319,35 @@ class Core: trust 8 introduced 9 ''' - - conn = sqlite3.connect(self.addressDB, timeout=30) - c = conn.cursor() - - command = (address,) - infoNumbers = {'address': 0, 'type': 1, 'knownPeer': 2, 'speed': 3, 'success': 4, 'powValue': 5, 'failure': 6, 'lastConnect': 7, 'trust': 8, 'introduced': 9} - info = infoNumbers[info] - iterCount = 0 - retVal = '' - - for row in c.execute('SELECT * FROM adders WHERE address=?;', command): - for i in row: - if iterCount == info: - retVal = i - break - else: - iterCount += 1 - conn.close() - - return retVal + return coredb.keydb.transportinfo.get_address_info(self, address, info) def setAddressInfo(self, address, key, data): ''' Update an address for a key ''' - - conn = sqlite3.connect(self.addressDB, timeout=30) - c = conn.cursor() - - command = (data, address) - - if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'failure', 'powValue', 'lastConnect', 'lastConnectAttempt', 'trust', 'introduced'): - raise Exception("Got invalid database key when setting address info") - else: - c.execute('UPDATE adders SET ' + key + ' = ? WHERE address=?', command) - conn.commit() - conn.close() - - return + return coredb.keydb.transportinfo.set_address_info(self, address, key, data) def getBlockList(self, dateRec = None, unsaved = False): ''' Get list of our blocks ''' - if dateRec == None: - dateRec = 0 - - conn = sqlite3.connect(self.blockDB, timeout=30) - c = conn.cursor() - - execute = 'SELECT hash FROM hashes WHERE dateReceived >= ? ORDER BY dateReceived ASC;' - args = (dateRec,) - rows = list() - for row in c.execute(execute, args): - for i in row: - rows.append(i) - conn.close() - return rows + return coredb.blockmetadb.get_block_list(self, dateRec, unsaved) def getBlockDate(self, blockHash): ''' Returns the date a block was received ''' - - conn = sqlite3.connect(self.blockDB, timeout=30) - c = conn.cursor() - - execute = 'SELECT dateReceived FROM hashes WHERE hash=?;' - args = (blockHash,) - for row in c.execute(execute, args): - for i in row: - return int(i) - conn.close() - return None + return coredb.blockmetadb.get_block_date(self, blockHash) def getBlocksByType(self, blockType, orderDate=True): ''' Returns a list of blocks by the type ''' - - conn = sqlite3.connect(self.blockDB, timeout=30) - c = conn.cursor() - - if orderDate: - execute = 'SELECT hash FROM hashes WHERE dataType=? ORDER BY dateReceived;' - else: - execute = 'SELECT hash FROM hashes WHERE dataType=?;' - - args = (blockType,) - rows = list() - - for row in c.execute(execute, args): - for i in row: - rows.append(i) - conn.close() - return rows + return coredb.blockmetadb.get_blocks_by_type(self, blockType, orderDate) def getExpiredBlocks(self): '''Returns a list of expired blocks''' - conn = sqlite3.connect(self.blockDB, timeout=30) - c = conn.cursor() - date = int(self._utils.getEpoch()) - - execute = 'SELECT hash FROM hashes WHERE expire <= %s ORDER BY dateReceived;' % (date,) - - rows = list() - for row in c.execute(execute): - for i in row: - rows.append(i) - conn.close() - return rows + return coredb.blockmetadb.expiredblocks.get_expired_blocks(self) def updateBlockInfo(self, hash, key, data): ''' @@ -666,18 +364,7 @@ class Core: dateClaimed - timestamp claimed inside the block, only as trustworthy as the block author is expire - expire date for a block ''' - - if key not in ('dateReceived', 'decrypted', 'dataType', 'dataFound', 'dataSaved', 'sig', 'author', 'dateClaimed', 'expire'): - return False - - conn = sqlite3.connect(self.blockDB, timeout=30) - c = conn.cursor() - args = (data, hash) - c.execute("UPDATE hashes SET " + key + " = ? where hash = ?;", args) - conn.commit() - conn.close() - - return True + return coredb.blockmetadb.updateblockinfo def insertBlock(self, data, header='txt', sign=False, encryptType='', symKey='', asymPeer='', meta = {}, expire=None, disableForward=False): ''' @@ -695,8 +382,6 @@ class Core: createTime = self._utils.getRoundedEpoch() - # check nonce - #print(data) dataNonce = self._utils.bytesToStr(self._crypto.sha3Hash(data)) try: with open(self.dataNonceFile, 'r') as nonces: diff --git a/onionr/coredb/__init__.py b/onionr/coredb/__init__.py new file mode 100644 index 00000000..39c909ba --- /dev/null +++ b/onionr/coredb/__init__.py @@ -0,0 +1 @@ +from . import keydb, blockmetadb, daemonqueue \ No newline at end of file diff --git a/onionr/coredb/blockmetadb/__init__.py b/onionr/coredb/blockmetadb/__init__.py new file mode 100644 index 00000000..fd408d35 --- /dev/null +++ b/onionr/coredb/blockmetadb/__init__.py @@ -0,0 +1,58 @@ +import sqlite3 +from . import expiredblocks, updateblockinfo +def get_block_list(core_inst, dateRec = None, unsaved = False): + ''' + Get list of our blocks + ''' + if dateRec == None: + dateRec = 0 + + conn = sqlite3.connect(core_inst.blockDB, timeout=30) + c = conn.cursor() + + execute = 'SELECT hash FROM hashes WHERE dateReceived >= ? ORDER BY dateReceived ASC;' + args = (dateRec,) + rows = list() + for row in c.execute(execute, args): + for i in row: + rows.append(i) + conn.close() + return rows + +def get_block_date(core_inst, blockHash): + ''' + Returns the date a block was received + ''' + + conn = sqlite3.connect(core_inst.blockDB, timeout=30) + c = conn.cursor() + + execute = 'SELECT dateReceived FROM hashes WHERE hash=?;' + args = (blockHash,) + for row in c.execute(execute, args): + for i in row: + return int(i) + conn.close() + return None + +def get_blocks_by_type(core_inst, blockType, orderDate=True): + ''' + Returns a list of blocks by the type + ''' + + conn = sqlite3.connect(core_inst.blockDB, timeout=30) + c = conn.cursor() + + if orderDate: + execute = 'SELECT hash FROM hashes WHERE dataType=? ORDER BY dateReceived;' + else: + execute = 'SELECT hash FROM hashes WHERE dataType=?;' + + args = (blockType,) + rows = list() + + for row in c.execute(execute, args): + for i in row: + rows.append(i) + conn.close() + return rows \ No newline at end of file diff --git a/onionr/coredb/blockmetadb/expiredblocks.py b/onionr/coredb/blockmetadb/expiredblocks.py new file mode 100644 index 00000000..8debbe26 --- /dev/null +++ b/onionr/coredb/blockmetadb/expiredblocks.py @@ -0,0 +1,15 @@ +import sqlite3 +def get_expired_blocks(core_inst): + '''Returns a list of expired blocks''' + conn = sqlite3.connect(core_inst.blockDB, timeout=30) + c = conn.cursor() + date = int(core_inst._utils.getEpoch()) + + execute = 'SELECT hash FROM hashes WHERE expire <= %s ORDER BY dateReceived;' % (date,) + + rows = list() + for row in c.execute(execute): + for i in row: + rows.append(i) + conn.close() + return rows \ No newline at end of file diff --git a/onionr/coredb/blockmetadb/updateblockinfo.py b/onionr/coredb/blockmetadb/updateblockinfo.py new file mode 100644 index 00000000..05f0fc8a --- /dev/null +++ b/onionr/coredb/blockmetadb/updateblockinfo.py @@ -0,0 +1,13 @@ +import sqlite3 +def update_block_info(core_inst, hash, key, data): + if key not in ('dateReceived', 'decrypted', 'dataType', 'dataFound', 'dataSaved', 'sig', 'author', 'dateClaimed', 'expire'): + return False + + conn = sqlite3.connect(core_inst.blockDB, timeout=30) + c = conn.cursor() + args = (data, hash) + c.execute("UPDATE hashes SET " + key + " = ? where hash = ?;", args) + conn.commit() + conn.close() + + return True \ No newline at end of file diff --git a/onionr/coredb/daemonqueue/__init__.py b/onionr/coredb/daemonqueue/__init__.py new file mode 100644 index 00000000..8bd21cfd --- /dev/null +++ b/onionr/coredb/daemonqueue/__init__.py @@ -0,0 +1,75 @@ +import sqlite3, os +import onionrevents as events +def daemon_queue(core_inst): + ''' + Gives commands to the communication proccess/daemon by reading an sqlite3 database + + This function intended to be used by the client. Queue to exchange data between "client" and server. + ''' + + retData = False + if not os.path.exists(core_inst.queueDB): + core_inst.dbCreate.createDaemonDB() + else: + conn = sqlite3.connect(core_inst.queueDB, timeout=30) + c = conn.cursor() + try: + for row in c.execute('SELECT command, data, date, min(ID), responseID FROM commands group by id'): + retData = row + break + except sqlite3.OperationalError: + core_inst.dbCreate.createDaemonDB() + else: + if retData != False: + c.execute('DELETE FROM commands WHERE id=?;', (retData[3],)) + conn.commit() + conn.close() + + events.event('queue_pop', data = {'data': retData}, onionr = core_inst.onionrInst) + + return retData + +def daemon_queue_add(core_inst, command, data='', responseID=''): + ''' + Add a command to the daemon queue, used by the communication daemon (communicator.py) + ''' + + retData = True + + date = core_inst._utils.getEpoch() + conn = sqlite3.connect(core_inst.queueDB, timeout=30) + c = conn.cursor() + t = (command, data, date, responseID) + try: + c.execute('INSERT INTO commands (command, data, date, responseID) VALUES(?, ?, ?, ?)', t) + conn.commit() + except sqlite3.OperationalError: + retData = False + core_inst.daemonQueue() + events.event('queue_push', data = {'command': command, 'data': data}, onionr = core_inst.onionrInst) + conn.close() + return retData + +def daemon_queue_get_response(core_inst, responseID=''): + ''' + Get a response sent by communicator to the API, by requesting to the API + ''' + assert len(responseID) > 0 + resp = core_inst._utils.localCommand('queueResponse/' + responseID) + return resp + +def clear_daemon_queue(core_inst): + ''' + Clear the daemon queue (somewhat dangerous) + ''' + conn = sqlite3.connect(core_inst.queueDB, timeout=30) + c = conn.cursor() + + try: + c.execute('DELETE FROM commands;') + conn.commit() + except: + pass + + conn.close() + events.event('queue_clear', onionr = core_inst.onionrInst) \ No newline at end of file diff --git a/onionr/coredb/keydb/__init__.py b/onionr/coredb/keydb/__init__.py new file mode 100644 index 00000000..16d602d5 --- /dev/null +++ b/onionr/coredb/keydb/__init__.py @@ -0,0 +1 @@ +from . import addkeys, listkeys, removekeys, userinfo, transportinfo \ No newline at end of file diff --git a/onionr/coredb/keydb/addkeys.py b/onionr/coredb/keydb/addkeys.py new file mode 100644 index 00000000..ac219b1a --- /dev/null +++ b/onionr/coredb/keydb/addkeys.py @@ -0,0 +1,70 @@ +import sqlite3 +import onionrevents as events, config +def add_peer(core_inst, peerID, name=''): + ''' + Adds a public key to the key database (misleading function name) + ''' + if peerID in core_inst.listPeers() or peerID == core_inst._crypto.pubKey: + raise ValueError("specified id is already known") + + # This function simply adds a peer to the DB + if not core_inst._utils.validatePubKey(peerID): + return False + + events.event('pubkey_add', data = {'key': peerID}, onionr = core_inst.onionrInst) + + conn = sqlite3.connect(core_inst.peerDB, timeout=30) + hashID = core_inst._crypto.pubKeyHashID(peerID) + c = conn.cursor() + t = (peerID, name, 'unknown', hashID, 0) + + for i in c.execute("SELECT * FROM peers WHERE id = ?;", (peerID,)): + try: + if i[0] == peerID: + conn.close() + return False + except ValueError: + pass + except IndexError: + pass + c.execute('INSERT INTO peers (id, name, dateSeen, hashID, trust) VALUES(?, ?, ?, ?, ?);', t) + conn.commit() + conn.close() + + return True + +def add_address(core_inst, address): + ''' + Add an address to the address database (only tor currently) + ''' + + if type(address) is None or len(address) == 0: + return False + if core_inst._utils.validateID(address): + if address == config.get('i2p.ownAddr', None) or address == core_inst.hsAddress: + return False + conn = sqlite3.connect(core_inst.addressDB, timeout=30) + c = conn.cursor() + # check if address is in database + # this is safe to do because the address is validated above, but we strip some chars here too just in case + address = address.replace('\'', '').replace(';', '').replace('"', '').replace('\\', '') + for i in c.execute("SELECT * FROM adders WHERE address = ?;", (address,)): + try: + if i[0] == address: + conn.close() + return False + except ValueError: + pass + except IndexError: + pass + + t = (address, 1) + c.execute('INSERT INTO adders (address, type) VALUES(?, ?);', t) + conn.commit() + conn.close() + + events.event('address_add', data = {'address': address}, onionr = core_inst.onionrInst) + + return True + else: + return False diff --git a/onionr/coredb/keydb/listkeys.py b/onionr/coredb/keydb/listkeys.py new file mode 100644 index 00000000..5ab44fb9 --- /dev/null +++ b/onionr/coredb/keydb/listkeys.py @@ -0,0 +1,63 @@ +import sqlite3 +import logger +def list_peers(core_inst, randomOrder=True, getPow=False, trust=0): + ''' + Return a list of public keys (misleading function name) + + randomOrder determines if the list should be in a random order + trust sets the minimum trust to list + ''' + conn = sqlite3.connect(core_inst.peerDB, timeout=30) + c = conn.cursor() + + payload = '' + + if trust not in (0, 1, 2): + logger.error('Tried to select invalid trust.') + return + + if randomOrder: + payload = 'SELECT * FROM peers WHERE trust >= ? ORDER BY RANDOM();' + else: + payload = 'SELECT * FROM peers WHERE trust >= ?;' + + peerList = [] + + for i in c.execute(payload, (trust,)): + try: + if len(i[0]) != 0: + if getPow: + peerList.append(i[0] + '-' + i[1]) + else: + peerList.append(i[0]) + except TypeError: + pass + + conn.close() + + return peerList + +def list_adders(core_inst, randomOrder=True, i2p=True, recent=0): + ''' + Return a list of transport addresses + ''' + conn = sqlite3.connect(core_inst.addressDB, timeout=30) + c = conn.cursor() + if randomOrder: + addresses = c.execute('SELECT * FROM adders ORDER BY RANDOM();') + else: + addresses = c.execute('SELECT * FROM adders;') + addressList = [] + for i in addresses: + if len(i[0].strip()) == 0: + continue + addressList.append(i[0]) + conn.close() + testList = list(addressList) # create new list to iterate + for address in testList: + try: + if recent > 0 and (core_inst._utils.getEpoch() - core_inst.getAddressInfo(address, 'lastConnect')) > recent: + raise TypeError # If there is no last-connected date or it was too long ago, don't add peer to list if recent is not 0 + except TypeError: + addressList.remove(address) + return addressList \ No newline at end of file diff --git a/onionr/coredb/keydb/removekeys.py b/onionr/coredb/keydb/removekeys.py new file mode 100644 index 00000000..10f44a1b --- /dev/null +++ b/onionr/coredb/keydb/removekeys.py @@ -0,0 +1,19 @@ +import sqlite3 +import onionrevents as events +def remove_address(core_inst, address): + ''' + Remove an address from the address database + ''' + + if core_inst._utils.validateID(address): + conn = sqlite3.connect(core_inst.addressDB, timeout=30) + c = conn.cursor() + t = (address,) + c.execute('Delete from adders where address=?;', t) + conn.commit() + conn.close() + + events.event('address_remove', data = {'address': address}, onionr = core_inst.onionrInst) + return True + else: + return False \ No newline at end of file diff --git a/onionr/coredb/keydb/transportinfo.py b/onionr/coredb/keydb/transportinfo.py new file mode 100644 index 00000000..9fd642ec --- /dev/null +++ b/onionr/coredb/keydb/transportinfo.py @@ -0,0 +1,53 @@ +import sqlite3 +def get_address_info(core_inst, address, info): + ''' + Get info about an address from its database entry + + address text, 0 + type int, 1 + knownPeer text, 2 + speed int, 3 + success int, 4 + powValue 5 + failure int 6 + lastConnect 7 + trust 8 + introduced 9 + ''' + + conn = sqlite3.connect(core_inst.addressDB, timeout=30) + c = conn.cursor() + + command = (address,) + infoNumbers = {'address': 0, 'type': 1, 'knownPeer': 2, 'speed': 3, 'success': 4, 'powValue': 5, 'failure': 6, 'lastConnect': 7, 'trust': 8, 'introduced': 9} + info = infoNumbers[info] + iterCount = 0 + retVal = '' + + for row in c.execute('SELECT * FROM adders WHERE address=?;', command): + for i in row: + if iterCount == info: + retVal = i + break + else: + iterCount += 1 + conn.close() + + return retVal + +def set_address_info(core_inst, address, key, data): + ''' + Update an address for a key + ''' + + conn = sqlite3.connect(core_inst.addressDB, timeout=30) + c = conn.cursor() + + command = (data, address) + + if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'failure', 'powValue', 'lastConnect', 'lastConnectAttempt', 'trust', 'introduced'): + raise Exception("Got invalid database key when setting address info") + else: + c.execute('UPDATE adders SET ' + key + ' = ? WHERE address=?', command) + conn.commit() + conn.close() \ No newline at end of file diff --git a/onionr/coredb/keydb/userinfo.py b/onionr/coredb/keydb/userinfo.py new file mode 100644 index 00000000..84a737d6 --- /dev/null +++ b/onionr/coredb/keydb/userinfo.py @@ -0,0 +1,50 @@ +import sqlite3 +def get_user_info(core_inst, peer, info): + ''' + Get info about a peer from their database entry + + id text 0 + name text, 1 + adders text, 2 + dateSeen not null, 3 + trust int 4 + hashID text 5 + ''' + conn = sqlite3.connect(core_inst.peerDB, timeout=30) + c = conn.cursor() + + command = (peer,) + infoNumbers = {'id': 0, 'name': 1, 'adders': 2, 'dateSeen': 3, 'trust': 4, 'hashID': 5} + info = infoNumbers[info] + iterCount = 0 + retVal = '' + + for row in c.execute('SELECT * FROM peers WHERE id=?;', command): + for i in row: + if iterCount == info: + retVal = i + break + else: + iterCount += 1 + + conn.close() + + return retVal + +def set_peer_info(core_inst, peer, key, data): + ''' + Update a peer for a key + ''' + + conn = sqlite3.connect(core_inst.peerDB, timeout=30) + c = conn.cursor() + + command = (data, peer) + + # TODO: validate key on whitelist + if key not in ('id', 'name', 'pubkey', 'forwardKey', 'dateSeen', 'trust'): + raise Exception("Got invalid database key when setting peer info") + + c.execute('UPDATE peers SET ' + key + ' = ? WHERE id=?', command) + conn.commit() + conn.close() \ No newline at end of file diff --git a/onionr/onionrstorage.py b/onionr/onionrstorage.py index 5f44c4f3..b859662d 100755 --- a/onionr/onionrstorage.py +++ b/onionr/onionrstorage.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import core, sys, sqlite3, os, dbcreator +import core, sys, sqlite3, os, dbcreator, onionrexceptions DB_ENTRY_SIZE_LIMIT = 10000 # Will be a config option @@ -94,4 +94,6 @@ def getData(coreInst, bHash): retData = block.read() else: retData = _dbFetch(coreInst, bHash) + if retData is None: + raise onionrexceptions.NoDataAvailable("Block data for %s is not available" % [bHash]) return retData \ No newline at end of file diff --git a/onionr/static-data/default-plugins/clandestine/controlapi.py b/onionr/static-data/default-plugins/esoteric/controlapi.py similarity index 83% rename from onionr/static-data/default-plugins/clandestine/controlapi.py rename to onionr/static-data/default-plugins/esoteric/controlapi.py index e40de9b6..7c298063 100755 --- a/onionr/static-data/default-plugins/clandestine/controlapi.py +++ b/onionr/static-data/default-plugins/esoteric/controlapi.py @@ -22,13 +22,13 @@ from flask import Response, request, redirect, Blueprint, send_from_directory import core core_inst = core.Core() -flask_blueprint = Blueprint('clandestine_control', __name__) +flask_blueprint = Blueprint('esoteric_control', __name__) -@flask_blueprint.route('/clandestine/ping') +@flask_blueprint.route('/esoteric/ping') def ping(): return 'pong!' -@flask_blueprint.route('/clandestine/send/', methods=['POST']) +@flask_blueprint.route('/esoteric/send/', methods=['POST']) def send_message(peer): data = request.get_json(force=True) core_inst.keyStore.refresh() @@ -40,14 +40,14 @@ def send_message(peer): core_inst.keyStore.flush() return Response('success') -@flask_blueprint.route('/clandestine/gets/') +@flask_blueprint.route('/esoteric/gets/') def get_sent(peer): sent = core_inst.keyStore.get('s' + peer) if sent is None: sent = [] return Response(json.dumps(sent)) -@flask_blueprint.route('/clandestine/addrec/', methods=['POST']) +@flask_blueprint.route('/esoteric/addrec/', methods=['POST']) def add_rec(peer): data = request.get_json(force=True) core_inst.keyStore.refresh() @@ -59,7 +59,7 @@ def add_rec(peer): core_inst.keyStore.flush() return Response('success') -@flask_blueprint.route('/clandestine/getrec/') +@flask_blueprint.route('/esoteric/getrec/') def get_messages(peer): core_inst.keyStore.refresh() existing = core_inst.keyStore.get('r' + peer) diff --git a/onionr/static-data/default-plugins/clandestine/info.json b/onionr/static-data/default-plugins/esoteric/info.json similarity index 64% rename from onionr/static-data/default-plugins/clandestine/info.json rename to onionr/static-data/default-plugins/esoteric/info.json index 7c5a143c..6fcc6e0e 100755 --- a/onionr/static-data/default-plugins/clandestine/info.json +++ b/onionr/static-data/default-plugins/esoteric/info.json @@ -1,5 +1,5 @@ { - "name" : "clandestine", + "name" : "esoteric", "version" : "1.0", "author" : "onionr" } diff --git a/onionr/static-data/default-plugins/clandestine/main.py b/onionr/static-data/default-plugins/esoteric/main.py similarity index 94% rename from onionr/static-data/default-plugins/clandestine/main.py rename to onionr/static-data/default-plugins/esoteric/main.py index 29afa3e1..1f4cdd32 100755 --- a/onionr/static-data/default-plugins/clandestine/main.py +++ b/onionr/static-data/default-plugins/esoteric/main.py @@ -24,7 +24,7 @@ locale.setlocale(locale.LC_ALL, '') import onionrservices, logger from onionrservices import bootstrapservice -plugin_name = 'clandestine' +plugin_name = 'esoteric' PLUGIN_VERSION = '0.0.0' sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) import controlapi, peerserver @@ -36,7 +36,7 @@ def exit_with_error(text=''): logger.error(text) sys.exit(1) -class Clandestine: +class Esoteric: def __init__(self, pluginapi): self.myCore = pluginapi.get_core() self.peer = None @@ -58,7 +58,7 @@ class Clandestine: message += '\n' except EOFError: message = json.dumps({'m': message, 't': self.myCore._utils.getEpoch()}) - print(self.myCore._utils.doPostRequest('http://%s/clandestine/sendto' % (self.transport,), port=self.socks, data=message)) + print(self.myCore._utils.doPostRequest('http://%s/esoteric/sendto' % (self.transport,), port=self.socks, data=message)) message = '' except KeyboardInterrupt: self.shutdown = True @@ -89,6 +89,6 @@ def on_init(api, data = None): ''' pluginapi = api - chat = Clandestine(pluginapi) - api.commands.register(['clandestine'], chat.create) + chat = Esoteric(pluginapi) + api.commands.register(['esoteric'], chat.create) return diff --git a/onionr/static-data/default-plugins/clandestine/peerserver.py b/onionr/static-data/default-plugins/esoteric/peerserver.py similarity index 78% rename from onionr/static-data/default-plugins/clandestine/peerserver.py rename to onionr/static-data/default-plugins/esoteric/peerserver.py index 5e4fba25..b3ae423e 100755 --- a/onionr/static-data/default-plugins/clandestine/peerserver.py +++ b/onionr/static-data/default-plugins/esoteric/peerserver.py @@ -21,7 +21,7 @@ import sys, os, json import core from flask import Response, request, redirect, Blueprint, abort, g sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) -direct_blueprint = Blueprint('clandestine', __name__) +direct_blueprint = Blueprint('esoteric', __name__) core_inst = core.Core() storage_dir = core_inst.dataDir @@ -35,11 +35,11 @@ def request_setup(): g.host = host g.peer = core_inst.keyStore.get('dc-' + g.host) -@direct_blueprint.route('/clandestine/ping') +@direct_blueprint.route('/esoteric/ping') def pingdirect(): return 'pong!' -@direct_blueprint.route('/clandestine/sendto', methods=['POST', 'GET']) +@direct_blueprint.route('/esoteric/sendto', methods=['POST', 'GET']) def sendto(): try: msg = request.get_json(force=True) @@ -47,9 +47,9 @@ def sendto(): msg = '' else: msg = json.dumps(msg) - core_inst._utils.localCommand('/clandestine/addrec/%s' % (g.peer,), post=True, postData=msg) + core_inst._utils.localCommand('/esoteric/addrec/%s' % (g.peer,), post=True, postData=msg) return Response('success') -@direct_blueprint.route('/clandestine/poll') +@direct_blueprint.route('/esoteric/poll') def poll_chat(): - return Response(core_inst._utils.localCommand('/clandestine/gets/%s' % (g.peer,))) \ No newline at end of file + return Response(core_inst._utils.localCommand('/esoteric/gets/%s' % (g.peer,))) \ No newline at end of file diff --git a/onionr/static-data/default-plugins/flow/main.py b/onionr/static-data/default-plugins/flow/main.py index 63c86cbc..d1c2c87d 100755 --- a/onionr/static-data/default-plugins/flow/main.py +++ b/onionr/static-data/default-plugins/flow/main.py @@ -61,6 +61,7 @@ class OnionrFlow: self.flowRunning = False expireTime = self.myCore._utils.getEpoch() + 43200 if len(message) > 0: + logger.info('Inserting message as block...', terminal=True) self.myCore.insertBlock(message, header='txt', expire=expireTime, meta={'ch': self.channel}) logger.info("Flow is exiting, goodbye", terminal=True) diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index 9a548ebd..a1650883 100755 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -26,7 +26,7 @@


        Mail - Friend Manager - Circle - - Clandestine + Esoteric


        Edit Configuration From 4e00bdb348a32caf1be23335ec78e77bc4af56e0 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 22 Jun 2019 17:54:41 -0500 Subject: [PATCH 113/173] fixed small cli logging bugs --- onionr/core.py | 73 ++----------------- onionr/coredb/blockmetadb/add.py | 23 ++++++ .../__init__.py} | 0 onionr/onionrstorage/removeblock.py | 20 +++++ onionr/onionrstorage/setdata.py | 36 +++++++++ 5 files changed, 85 insertions(+), 67 deletions(-) create mode 100644 onionr/coredb/blockmetadb/add.py rename onionr/{onionrstorage.py => onionrstorage/__init__.py} (100%) create mode 100644 onionr/onionrstorage/removeblock.py create mode 100644 onionr/onionrstorage/setdata.py diff --git a/onionr/core.py b/onionr/core.py index 735358e5..7fd4b3ce 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -25,16 +25,10 @@ import deadsimplekv as simplekv import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions import onionrblacklist from onionrusers import onionrusers +from onionrstorage import removeblock, setdata import dbcreator, onionrstorage, serializeddata, subprocesspow from etc import onionrvalues, powchoice -if sys.version_info < (3, 6): - try: - import sha3 - except ModuleNotFoundError: - logger.fatal('On Python 3 versions prior to 3.6.x, you need the sha3 module') - sys.exit(1) - class Core: def __init__(self, torPort=0): ''' @@ -149,18 +143,7 @@ class Core: **You may want blacklist.addToDB(blockHash) ''' - - if self._utils.validateHash(block): - conn = sqlite3.connect(self.blockDB, timeout=30) - c = conn.cursor() - t = (block,) - c.execute('Delete from hashes where hash=?;', t) - conn.commit() - conn.close() - dataSize = sys.getsizeof(onionrstorage.getData(self, block)) - self._utils.storageCounter.removeBytes(dataSize) - else: - raise onionrexceptions.InvalidHexHash + removeblock.remove_block(self, block) def createAddressDB(self): ''' @@ -186,57 +169,13 @@ class Core: Should be in hex format! ''' - - if not os.path.exists(self.blockDB): - raise Exception('Block db does not exist') - if self._utils.hasBlock(newHash): - return - conn = sqlite3.connect(self.blockDB, timeout=30) - c = conn.cursor() - currentTime = self._utils.getEpoch() + self._crypto.secrets.randbelow(301) - if selfInsert or dataSaved: - selfInsert = 1 - else: - selfInsert = 0 - data = (newHash, currentTime, '', selfInsert) - c.execute('INSERT INTO hashes (hash, dateReceived, dataType, dataSaved) VALUES(?, ?, ?, ?);', data) - conn.commit() - conn.close() + coredb.blockmetadb.add(self, newHash, selfInsert, dataSaved) def setData(self, data): ''' Set the data assciated with a hash ''' - - data = data - dataSize = sys.getsizeof(data) - - if not type(data) is bytes: - data = data.encode() - - dataHash = self._crypto.sha3Hash(data) - - if type(dataHash) is bytes: - dataHash = dataHash.decode() - blockFileName = self.blockDataLocation + dataHash + '.dat' - try: - onionrstorage.getData(self, dataHash) - except onionrexceptions.NoDataAvailable: - if self._utils.storageCounter.addBytes(dataSize) != False: - onionrstorage.store(self, data, blockHash=dataHash) - conn = sqlite3.connect(self.blockDB, timeout=30) - c = conn.cursor() - c.execute("UPDATE hashes SET dataSaved=1 WHERE hash = ?;", (dataHash,)) - conn.commit() - conn.close() - with open(self.dataNonceFile, 'a') as nonceFile: - nonceFile.write(dataHash + '\n') - else: - raise onionrexceptions.DiskAllocationReached - else: - raise Exception("Data is already set for " + dataHash) - - return dataHash + return onionrstorage.setdata.set_data(self, data) def getData(self, hash): ''' @@ -513,6 +452,6 @@ class Core: ''' if self._utils.localCommand('/ping', maxWait=10) == 'pong!': self.daemonQueueAdd('announceNode') - logger.info('Introduction command will be processed.') + logger.info('Introduction command will be processed.', terminal=True) else: - logger.warn('No running node detected. Cannot introduce.') \ No newline at end of file + logger.warn('No running node detected. Cannot introduce.', terminal=True) \ No newline at end of file diff --git a/onionr/coredb/blockmetadb/add.py b/onionr/coredb/blockmetadb/add.py new file mode 100644 index 00000000..d307d351 --- /dev/null +++ b/onionr/coredb/blockmetadb/add.py @@ -0,0 +1,23 @@ +import os, sqlite3 +def add_to_block_DB(core_inst, newHash, selfInsert=False, dataSaved=False): + ''' + Add a hash value to the block db + + Should be in hex format! + ''' + + if not os.path.exists(core_inst.blockDB): + raise Exception('Block db does not exist') + if core_inst._utils.hasBlock(newHash): + return + conn = sqlite3.connect(core_inst.blockDB, timeout=30) + c = conn.cursor() + currentTime = core_inst._utils.getEpoch() + core_inst._crypto.secrets.randbelow(301) + if selfInsert or dataSaved: + selfInsert = 1 + else: + selfInsert = 0 + data = (newHash, currentTime, '', selfInsert) + c.execute('INSERT INTO hashes (hash, dateReceived, dataType, dataSaved) VALUES(?, ?, ?, ?);', data) + conn.commit() + conn.close() \ No newline at end of file diff --git a/onionr/onionrstorage.py b/onionr/onionrstorage/__init__.py similarity index 100% rename from onionr/onionrstorage.py rename to onionr/onionrstorage/__init__.py diff --git a/onionr/onionrstorage/removeblock.py b/onionr/onionrstorage/removeblock.py new file mode 100644 index 00000000..23e574b6 --- /dev/null +++ b/onionr/onionrstorage/removeblock.py @@ -0,0 +1,20 @@ +import sys, sqlite3 +import onionrexceptions, onionrstorage +def remove_block(core_inst, block): + ''' + remove a block from this node (does not automatically blacklist) + + **You may want blacklist.addToDB(blockHash) + ''' + + if core_inst._utils.validateHash(block): + conn = sqlite3.connect(core_inst.blockDB, timeout=30) + c = conn.cursor() + t = (block,) + c.execute('Delete from hashes where hash=?;', t) + conn.commit() + conn.close() + dataSize = sys.getsizeof(onionrstorage.getData(core_inst, block)) + core_inst._utils.storageCounter.removeBytes(dataSize) + else: + raise onionrexceptions.InvalidHexHash \ No newline at end of file diff --git a/onionr/onionrstorage/setdata.py b/onionr/onionrstorage/setdata.py new file mode 100644 index 00000000..60378112 --- /dev/null +++ b/onionr/onionrstorage/setdata.py @@ -0,0 +1,36 @@ +import sys, sqlite3 +import onionrstorage, onionrexceptions +def set_data(core_inst, data): + ''' + Set the data assciated with a hash + ''' + + data = data + dataSize = sys.getsizeof(data) + + if not type(data) is bytes: + data = data.encode() + + dataHash = core_inst._crypto.sha3Hash(data) + + if type(dataHash) is bytes: + dataHash = dataHash.decode() + blockFileName = core_inst.blockDataLocation + dataHash + '.dat' + try: + onionrstorage.getData(core_inst, dataHash) + except onionrexceptions.NoDataAvailable: + if core_inst._utils.storageCounter.addBytes(dataSize) != False: + onionrstorage.store(core_inst, data, blockHash=dataHash) + conn = sqlite3.connect(core_inst.blockDB, timeout=30) + c = conn.cursor() + c.execute("UPDATE hashes SET dataSaved=1 WHERE hash = ?;", (dataHash,)) + conn.commit() + conn.close() + with open(core_inst.dataNonceFile, 'a') as nonceFile: + nonceFile.write(dataHash + '\n') + else: + raise onionrexceptions.DiskAllocationReached + else: + raise Exception("Data is already set for " + dataHash) + + return dataHash \ No newline at end of file From 2b64cc9ba4ba71b5533a3209d5e3db413ea05b97 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 22 Jun 2019 18:01:55 -0500 Subject: [PATCH 114/173] correct add function name --- onionr/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionr/core.py b/onionr/core.py index 7fd4b3ce..1a0a465d 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -169,7 +169,7 @@ class Core: Should be in hex format! ''' - coredb.blockmetadb.add(self, newHash, selfInsert, dataSaved) + coredb.blockmetadb.add.add_to_block_DB(self, newHash, selfInsert, dataSaved) def setData(self, data): ''' From e4ec850b60ff33a13716e816f5ad3e612c5b59cd Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 22 Jun 2019 18:06:16 -0500 Subject: [PATCH 115/173] correctly import add in blockmetadb --- onionr/core.py | 2 +- onionr/coredb/blockmetadb/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/onionr/core.py b/onionr/core.py index 1a0a465d..5ab70e7d 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import sqlite3, os, sys, time, json, uuid +import os, sys, time, json, uuid import logger, netcontroller, config from onionrblockapi import Block import coredb diff --git a/onionr/coredb/blockmetadb/__init__.py b/onionr/coredb/blockmetadb/__init__.py index fd408d35..9aa991a5 100644 --- a/onionr/coredb/blockmetadb/__init__.py +++ b/onionr/coredb/blockmetadb/__init__.py @@ -1,5 +1,5 @@ import sqlite3 -from . import expiredblocks, updateblockinfo +from . import expiredblocks, updateblockinfo, add def get_block_list(core_inst, dateRec = None, unsaved = False): ''' Get list of our blocks From 7830484760ddaf78c2d9d5bcc80a3ca5578a7c47 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 23 Jun 2019 02:00:27 -0500 Subject: [PATCH 116/173] * started refactoring onionrusers into a full module instead of a class * now use daemon threads to prevent process hanging --- .../onionrcommunicatortimers.py | 2 +- onionr/core.py | 2 +- onionr/onionrcommands/daemonlaunch.py | 8 +- .../__init__.py} | 177 +----------------- onionr/onionrutils/blockmetadata.py | 68 +++++++ onionr/onionrutils/localcommand.py | 31 +++ onionr/onionrutils/validatemetadata.py | 77 ++++++++ .../static-data/default-plugins/flow/main.py | 2 - 8 files changed, 188 insertions(+), 179 deletions(-) rename onionr/{onionrutils.py => onionrutils/__init__.py} (65%) create mode 100644 onionr/onionrutils/blockmetadata.py create mode 100644 onionr/onionrutils/localcommand.py create mode 100644 onionr/onionrutils/validatemetadata.py diff --git a/onionr/communicatorutils/onionrcommunicatortimers.py b/onionr/communicatorutils/onionrcommunicatortimers.py index b7255da4..e765fd66 100755 --- a/onionr/communicatorutils/onionrcommunicatortimers.py +++ b/onionr/communicatorutils/onionrcommunicatortimers.py @@ -55,7 +55,7 @@ class OnionrCommunicatorTimers: logger.debug('%s is currently using the maximum number of threads, not starting another.' % self.timerFunction.__name__) else: self.daemonInstance.threadCounts[self.timerFunction.__name__] += 1 - newThread = threading.Thread(target=self.timerFunction, args=self.args) + newThread = threading.Thread(target=self.timerFunction, args=self.args, daemon=True) newThread.start() else: self.timerFunction() diff --git a/onionr/core.py b/onionr/core.py index 5ab70e7d..ebc35e51 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -201,7 +201,7 @@ class Core: ''' Get a response sent by communicator to the API, by requesting to the API ''' - return coredb.daemonqueue.daemon_queue_get_response(responseID) + return coredb.daemonqueue.daemon_queue_get_response(self, responseID) def clearDaemonQueue(self): ''' diff --git a/onionr/onionrcommands/daemonlaunch.py b/onionr/onionrcommands/daemonlaunch.py index d65a33d6..1aaf7230 100755 --- a/onionr/onionrcommands/daemonlaunch.py +++ b/onionr/onionrcommands/daemonlaunch.py @@ -38,8 +38,8 @@ def daemon(o_inst): logger.debug('Runcheck file found on daemon start, deleting in advance.') os.remove('%s/.runcheck' % (o_inst.onionrCore.dataDir,)) - Thread(target=api.API, args=(o_inst, o_inst.debug, onionr.API_VERSION)).start() - Thread(target=api.PublicAPI, args=[o_inst.getClientApi()]).start() + Thread(target=api.API, args=(o_inst, o_inst.debug, onionr.API_VERSION), daemon=True).start() + Thread(target=api.PublicAPI, args=[o_inst.getClientApi()], daemon=True).start() apiHost = '' while apiHost == '': @@ -77,7 +77,7 @@ def daemon(o_inst): _proper_shutdown(o_inst) o_inst.onionrCore.torPort = net.socksPort - communicatorThread = Thread(target=communicator.startCommunicator, args=(o_inst, str(net.socksPort))) + communicatorThread = Thread(target=communicator.startCommunicator, args=(o_inst, str(net.socksPort)), daemon=True) communicatorThread.start() while o_inst.communicatorInst is None: @@ -106,7 +106,7 @@ def daemon(o_inst): o_inst.onionrUtils.localCommand('shutdown') net.killTor() - time.sleep(3) + time.sleep(8) # Time to allow threads to finish, if not any "daemon" threads will be slaughtered http://docs.python.org/library/threading.html#threading.Thread.daemon o_inst.deleteRunFiles() return diff --git a/onionr/onionrutils.py b/onionr/onionrutils/__init__.py similarity index 65% rename from onionr/onionrutils.py rename to onionr/onionrutils/__init__.py index 4aa02d76..67d7f8f0 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils/__init__.py @@ -29,12 +29,8 @@ import onionrevents import storagecounter from etc import pgpwords, onionrvalues from onionrusers import onionrusers -if sys.version_info < (3, 6): - try: - import sha3 - except ModuleNotFoundError: - logger.fatal('On Python 3 versions prior to 3.6.x, you need the sha3 module') - sys.exit(1) +from . import localcommand, blockmetadata, validatemetadata + config.reload() class OnionrUtils: ''' @@ -44,25 +40,11 @@ class OnionrUtils: #self.fingerprintFile = 'data/own-fingerprint.txt' #TODO Remove since probably not needed self._core = coreInstance # onionr core instance - self.timingToken = '' # for when we make local connections to our http api, to bypass timing attack defense mechanism self.avoidDupe = [] # list used to prevent duplicate requests per peer for certain actions self.peerProcessing = {} # dict of current peer actions: peer, actionList self.storageCounter = storagecounter.StorageCounter(self._core) # used to keep track of how much data onionr is using on disk return - def getTimeBypassToken(self): - ''' - Load our timingToken from disk for faster local HTTP API - ''' - try: - if os.path.exists(self._core.dataDir + 'time-bypass.txt'): - with open(self._core.dataDir + 'time-bypass.txt', 'r') as bypass: - self.timingToken = bypass.read() - except Exception as error: - logger.error('Failed to fetch time bypass token.', error = error) - - return self.timingToken - def getRoundedEpoch(self, roundS=60): ''' Returns the epoch, rounded down to given seconds (Default 60) @@ -85,32 +67,7 @@ class OnionrUtils: ''' Send a command to the local http API server, securely. Intended for local clients, DO NOT USE for remote peers. ''' - self.getTimeBypassToken() - # TODO: URL encode parameters, just as an extra measure. May not be needed, but should be added regardless. - hostname = '' - waited = 0 - while hostname == '': - try: - hostname = self.getClientAPIServer() - except FileNotFoundError: - time.sleep(1) - waited += 1 - if waited == maxWait: - return False - if data != '': - data = '&data=' + urllib.parse.quote_plus(data) - payload = 'http://%s/%s%s' % (hostname, command, data) - try: - if post: - retData = requests.post(payload, data=postData, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, maxWait)).text - else: - retData = requests.get(payload, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, maxWait)).text - except Exception as error: - if not silent: - logger.error('Failed to make local request (command: %s):%s' % (command, error)) - retData = False - - return retData + return localcommand.local_command(self, command, data, silent, post, postData, maxWait) def getHumanReadableID(self, pub=''): '''gets a human readable ID from a public key''' @@ -130,64 +87,13 @@ class OnionrUtils: metadata, meta (meta being internal metadata, which will be returned as an encrypted base64 string if it is encrypted, dict if not). ''' - meta = {} - metadata = {} - data = blockData - try: - blockData = blockData.encode() - except AttributeError: - pass - - try: - metadata = json.loads(blockData[:blockData.find(b'\n')].decode()) - except json.decoder.JSONDecodeError: - pass - else: - data = blockData[blockData.find(b'\n'):].decode() - - if not metadata['encryptType'] in ('asym', 'sym'): - try: - meta = json.loads(metadata['meta']) - except KeyError: - pass - meta = metadata['meta'] - return (metadata, meta, data) + return blockmetadata.get_block_metadata_from_data(self, blockData) def processBlockMetadata(self, blockHash): ''' Read metadata from a block and cache it to the block database ''' - curTime = self.getRoundedEpoch(roundS=60) - myBlock = Block(blockHash, self._core) - if myBlock.isEncrypted: - myBlock.decrypt() - if (myBlock.isEncrypted and myBlock.decrypted) or (not myBlock.isEncrypted): - blockType = myBlock.getMetadata('type') # we would use myBlock.getType() here, but it is bugged with encrypted blocks - signer = self.bytesToStr(myBlock.signer) - valid = myBlock.verifySig() - if myBlock.getMetadata('newFSKey') is not None: - onionrusers.OnionrUser(self._core, signer).addForwardKey(myBlock.getMetadata('newFSKey')) - - try: - if len(blockType) <= 10: - self._core.updateBlockInfo(blockHash, 'dataType', blockType) - except TypeError: - logger.warn("Missing block information") - pass - # Set block expire time if specified - try: - expireTime = myBlock.getHeader('expire') - assert len(str(int(expireTime))) < 20 # test that expire time is an integer of sane length (for epoch) - except (AssertionError, ValueError, TypeError) as e: - expireTime = onionrvalues.OnionrValues().default_expire + curTime - finally: - self._core.updateBlockInfo(blockHash, 'expire', expireTime) - if not blockType is None: - self._core.updateBlockInfo(blockHash, 'dataType', blockType) - onionrevents.event('processblocks', data = {'block': myBlock, 'type': blockType, 'signer': signer, 'validSig': valid}, onionr = self._core.onionrInst) - else: - pass - #logger.debug('Not processing metadata on encrypted block we cannot decrypt.') + return blockmetadata.process_block_metadata(self, blockHash) def escapeAnsi(self, line): ''' @@ -243,78 +149,7 @@ class OnionrUtils: def validateMetadata(self, metadata, blockData): '''Validate metadata meets onionr spec (does not validate proof value computation), take in either dictionary or json string''' - # TODO, make this check sane sizes - retData = False - maxClockDifference = 120 - - # convert to dict if it is json string - if type(metadata) is str: - try: - metadata = json.loads(metadata) - except json.JSONDecodeError: - pass - - # Validate metadata dict for invalid keys to sizes that are too large - maxAge = config.get("general.max_block_age", onionrvalues.OnionrValues().default_expire) - if type(metadata) is dict: - for i in metadata: - try: - self._core.requirements.blockMetadataLengths[i] - except KeyError: - logger.warn('Block has invalid metadata key ' + i) - break - else: - testData = metadata[i] - try: - testData = len(testData) - except (TypeError, AttributeError) as e: - testData = len(str(testData)) - if self._core.requirements.blockMetadataLengths[i] < testData: - logger.warn('Block metadata key ' + i + ' exceeded maximum size') - break - if i == 'time': - if not self.isIntegerString(metadata[i]): - logger.warn('Block metadata time stamp is not integer string or int') - break - isFuture = (metadata[i] - self.getEpoch()) - if isFuture > maxClockDifference: - logger.warn('Block timestamp is skewed to the future over the max %s: %s' (maxClockDifference, isFuture)) - break - if (self.getEpoch() - metadata[i]) > maxAge: - logger.warn('Block is outdated: %s' % (metadata[i],)) - break - elif i == 'expire': - try: - assert int(metadata[i]) > self.getEpoch() - except AssertionError: - logger.warn('Block is expired: %s less than %s' % (metadata[i], self.getEpoch())) - break - elif i == 'encryptType': - try: - assert metadata[i] in ('asym', 'sym', '') - except AssertionError: - logger.warn('Invalid encryption mode') - break - else: - # if metadata loop gets no errors, it does not break, therefore metadata is valid - # make sure we do not have another block with the same data content (prevent data duplication and replay attacks) - nonce = self._core._utils.bytesToStr(self._core._crypto.sha3Hash(blockData)) - try: - with open(self._core.dataNonceFile, 'r') as nonceFile: - if nonce in nonceFile.read(): - retData = False # we've seen that nonce before, so we can't pass metadata - raise onionrexceptions.DataExists - except FileNotFoundError: - retData = True - except onionrexceptions.DataExists: - # do not set retData to True, because nonce has been seen before - pass - else: - retData = True - else: - logger.warn('In call to utils.validateMetadata, metadata must be JSON string or a dictionary object') - - return retData + return validatemetadata.validate_metadata(self, metadata, blockData) def validatePubKey(self, key): ''' diff --git a/onionr/onionrutils/blockmetadata.py b/onionr/onionrutils/blockmetadata.py new file mode 100644 index 00000000..0515ffc7 --- /dev/null +++ b/onionr/onionrutils/blockmetadata.py @@ -0,0 +1,68 @@ +import json +import logger, onionrevents +from onionrusers import onionrusers +from etc import onionrvalues +from onionrblockapi import Block +def get_block_metadata_from_data(utils_inst, blockData): + ''' + accepts block contents as string, returns a tuple of + metadata, meta (meta being internal metadata, which will be + returned as an encrypted base64 string if it is encrypted, dict if not). + ''' + meta = {} + metadata = {} + data = blockData + try: + blockData = blockData.encode() + except AttributeError: + pass + + try: + metadata = json.loads(blockData[:blockData.find(b'\n')].decode()) + except json.decoder.JSONDecodeError: + pass + else: + data = blockData[blockData.find(b'\n'):].decode() + + if not metadata['encryptType'] in ('asym', 'sym'): + try: + meta = json.loads(metadata['meta']) + except KeyError: + pass + meta = metadata['meta'] + return (metadata, meta, data) + +def process_block_metadata(utils_inst, blockHash): + ''' + Read metadata from a block and cache it to the block database + ''' + curTime = utils_inst.getRoundedEpoch(roundS=60) + myBlock = Block(blockHash, utils_inst._core) + if myBlock.isEncrypted: + myBlock.decrypt() + if (myBlock.isEncrypted and myBlock.decrypted) or (not myBlock.isEncrypted): + blockType = myBlock.getMetadata('type') # we would use myBlock.getType() here, but it is bugged with encrypted blocks + signer = utils_inst.bytesToStr(myBlock.signer) + valid = myBlock.verifySig() + if myBlock.getMetadata('newFSKey') is not None: + onionrusers.OnionrUser(utils_inst._core, signer).addForwardKey(myBlock.getMetadata('newFSKey')) + + try: + if len(blockType) <= 10: + utils_inst._core.updateBlockInfo(blockHash, 'dataType', blockType) + except TypeError: + logger.warn("Missing block information") + pass + # Set block expire time if specified + try: + expireTime = myBlock.getHeader('expire') + assert len(str(int(expireTime))) < 20 # test that expire time is an integer of sane length (for epoch) + except (AssertionError, ValueError, TypeError) as e: + expireTime = onionrvalues.OnionrValues().default_expire + curTime + finally: + utils_inst._core.updateBlockInfo(blockHash, 'expire', expireTime) + if not blockType is None: + utils_inst._core.updateBlockInfo(blockHash, 'dataType', blockType) + onionrevents.event('processblocks', data = {'block': myBlock, 'type': blockType, 'signer': signer, 'validSig': valid}, onionr = utils_inst._core.onionrInst) + else: + pass \ No newline at end of file diff --git a/onionr/onionrutils/localcommand.py b/onionr/onionrutils/localcommand.py new file mode 100644 index 00000000..fff050c8 --- /dev/null +++ b/onionr/onionrutils/localcommand.py @@ -0,0 +1,31 @@ +import urllib, requests, time +import logger +def local_command(utils_inst, command, data='', silent = True, post=False, postData = {}, maxWait=20): + ''' + Send a command to the local http API server, securely. Intended for local clients, DO NOT USE for remote peers. + ''' + # TODO: URL encode parameters, just as an extra measure. May not be needed, but should be added regardless. + hostname = '' + waited = 0 + while hostname == '': + try: + hostname = utils_inst.getClientAPIServer() + except FileNotFoundError: + time.sleep(1) + waited += 1 + if waited == maxWait: + return False + if data != '': + data = '&data=' + urllib.parse.quote_plus(data) + payload = 'http://%s/%s%s' % (hostname, command, data) + try: + if post: + retData = requests.post(payload, data=postData, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, maxWait)).text + else: + retData = requests.get(payload, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, maxWait)).text + except Exception as error: + if not silent: + logger.error('Failed to make local request (command: %s):%s' % (command, error), terminal=True) + retData = False + + return retData \ No newline at end of file diff --git a/onionr/onionrutils/validatemetadata.py b/onionr/onionrutils/validatemetadata.py new file mode 100644 index 00000000..c4bcc4a8 --- /dev/null +++ b/onionr/onionrutils/validatemetadata.py @@ -0,0 +1,77 @@ +import json +import logger, onionrexceptions +from etc import onionrvalues +def validate_metadata(utils_inst, metadata, blockData): + '''Validate metadata meets onionr spec (does not validate proof value computation), take in either dictionary or json string''' + # TODO, make this check sane sizes + retData = False + maxClockDifference = 120 + + # convert to dict if it is json string + if type(metadata) is str: + try: + metadata = json.loads(metadata) + except json.JSONDecodeError: + pass + + # Validate metadata dict for invalid keys to sizes that are too large + maxAge = utils_inst._coreconfig.get("general.max_block_age", onionrvalues.OnionrValues().default_expire) + if type(metadata) is dict: + for i in metadata: + try: + utils_inst._core.requirements.blockMetadataLengths[i] + except KeyError: + logger.warn('Block has invalid metadata key ' + i) + break + else: + testData = metadata[i] + try: + testData = len(testData) + except (TypeError, AttributeError) as e: + testData = len(str(testData)) + if utils_inst._core.requirements.blockMetadataLengths[i] < testData: + logger.warn('Block metadata key ' + i + ' exceeded maximum size') + break + if i == 'time': + if not utils_inst.isIntegerString(metadata[i]): + logger.warn('Block metadata time stamp is not integer string or int') + break + isFuture = (metadata[i] - utils_inst.getEpoch()) + if isFuture > maxClockDifference: + logger.warn('Block timestamp is skewed to the future over the max %s: %s' (maxClockDifference, isFuture)) + break + if (utils_inst.getEpoch() - metadata[i]) > maxAge: + logger.warn('Block is outdated: %s' % (metadata[i],)) + break + elif i == 'expire': + try: + assert int(metadata[i]) > utils_inst.getEpoch() + except AssertionError: + logger.warn('Block is expired: %s less than %s' % (metadata[i], utils_inst.getEpoch())) + break + elif i == 'encryptType': + try: + assert metadata[i] in ('asym', 'sym', '') + except AssertionError: + logger.warn('Invalid encryption mode') + break + else: + # if metadata loop gets no errors, it does not break, therefore metadata is valid + # make sure we do not have another block with the same data content (prevent data duplication and replay attacks) + nonce = utils_inst._core._utils.bytesToStr(utils_inst._core._crypto.sha3Hash(blockData)) + try: + with open(utils_inst._core.dataNonceFile, 'r') as nonceFile: + if nonce in nonceFile.read(): + retData = False # we've seen that nonce before, so we can't pass metadata + raise onionrexceptions.DataExists + except FileNotFoundError: + retData = True + except onionrexceptions.DataExists: + # do not set retData to True, because nonce has been seen before + pass + else: + retData = True + else: + logger.warn('In call to utils.validateMetadata, metadata must be JSON string or a dictionary object') + + return retData \ No newline at end of file diff --git a/onionr/static-data/default-plugins/flow/main.py b/onionr/static-data/default-plugins/flow/main.py index d1c2c87d..5f61aa38 100755 --- a/onionr/static-data/default-plugins/flow/main.py +++ b/onionr/static-data/default-plugins/flow/main.py @@ -75,10 +75,8 @@ class OnionrFlow: for block in self.myCore.getBlocksByType('txt'): block = Block(block) if block.getMetadata('ch') != self.channel: - #print('not chan', block.getMetadata('ch')) continue if block.getHash() in self.alreadyOutputed: - #print('already') continue if not self.flowRunning: break From 0d258e1a16b42d746a4d86594456ed57cd3f16ab Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 23 Jun 2019 02:25:40 -0500 Subject: [PATCH 117/173] * more utils refactoring, requests this time --- onionr/onionrutils/__init__.py | 81 ++--------------------------- onionr/onionrutils/basicrequests.py | 71 +++++++++++++++++++++++++ run-windows-dev.bat | 3 +- 3 files changed, 76 insertions(+), 79 deletions(-) create mode 100644 onionr/onionrutils/basicrequests.py diff --git a/onionr/onionrutils/__init__.py b/onionr/onionrutils/__init__.py index 67d7f8f0..a7efbae7 100755 --- a/onionr/onionrutils/__init__.py +++ b/onionr/onionrutils/__init__.py @@ -24,12 +24,11 @@ import nacl.signing, nacl.encoding import unpaddedbase32 from onionrblockapi import Block import onionrexceptions, config, logger -from onionr import API_VERSION import onionrevents import storagecounter from etc import pgpwords, onionrvalues from onionrusers import onionrusers -from . import localcommand, blockmetadata, validatemetadata +from . import localcommand, blockmetadata, validatemetadata, basicrequests config.reload() class OnionrUtils: @@ -279,23 +278,6 @@ class OnionrUtils: if not exist: logger.info('No blocks found to import') - def progressBar(self, value = 0, endvalue = 100, width = None): - ''' - Outputs a progress bar with a percentage. Write \n after use. - ''' - - if width is None or height is None: - width, height = shutil.get_terminal_size((80, 24)) - - bar_length = width - 6 - - percent = float(value) / endvalue - arrow = '─' * int(round(percent * bar_length)-1) + '>' - spaces = ' ' * (bar_length - len(arrow)) - - sys.stdout.write("\r┣{0}┫ {1}%".format(arrow + spaces, int(round(percent * 100)))) - sys.stdout.flush() - def getEpoch(self): '''returns epoch''' return math.floor(time.time()) @@ -304,70 +286,13 @@ class OnionrUtils: ''' Do a POST request through a local tor or i2p instance ''' - if proxyType == 'tor': - if port == 0: - port = self._core.torPort - proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} - elif proxyType == 'i2p': - proxies = {'http': 'http://127.0.0.1:4444'} - else: - return - headers = {'user-agent': 'PyOnionr', 'Connection':'close'} - try: - proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} - r = requests.post(url, data=data, headers=headers, proxies=proxies, allow_redirects=False, timeout=(15, 30)) - retData = r.text - except KeyboardInterrupt: - raise KeyboardInterrupt - except requests.exceptions.RequestException as e: - logger.debug('Error: %s' % str(e)) - retData = False - return retData + return basicrequests.do_post_request(self, url, data, port, proxyType) def doGetRequest(self, url, port=0, proxyType='tor', ignoreAPI=False, returnHeaders=False): ''' Do a get request through a local tor or i2p instance ''' - retData = False - if proxyType == 'tor': - if port == 0: - raise onionrexceptions.MissingPort('Socks port required for Tor HTTP get request') - proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} - elif proxyType == 'i2p': - proxies = {'http': 'http://127.0.0.1:4444'} - else: - return - headers = {'user-agent': 'PyOnionr', 'Connection':'close'} - response_headers = dict() - try: - proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} - r = requests.get(url, headers=headers, proxies=proxies, allow_redirects=False, timeout=(15, 30), ) - # Check server is using same API version as us - if not ignoreAPI: - try: - response_headers = r.headers - if r.headers['X-API'] != str(API_VERSION): - raise onionrexceptions.InvalidAPIVersion - except KeyError: - raise onionrexceptions.InvalidAPIVersion - retData = r.text - except KeyboardInterrupt: - raise KeyboardInterrupt - except ValueError as e: - pass - except onionrexceptions.InvalidAPIVersion: - if 'X-API' in response_headers: - logger.debug('Using API version %s. Cannot communicate with node\'s API version of %s.' % (API_VERSION, response_headers['X-API'])) - else: - logger.debug('Using API version %s. API version was not sent with the request.' % API_VERSION) - except requests.exceptions.RequestException as e: - if not 'ConnectTimeoutError' in str(e) and not 'Request rejected or failed' in str(e): - logger.debug('Error: %s' % str(e)) - retData = False - if returnHeaders: - return (retData, response_headers) - else: - return retData + return basicrequests.do_get_request(self, url, port, proxyType, ignoreAPI, returnHeaders) @staticmethod def strToBytes(data): diff --git a/onionr/onionrutils/basicrequests.py b/onionr/onionrutils/basicrequests.py new file mode 100644 index 00000000..f9432928 --- /dev/null +++ b/onionr/onionrutils/basicrequests.py @@ -0,0 +1,71 @@ +import requests +import logger, onionrexceptions +from onionr import API_VERSION +def do_post_request(utils_inst, url, data={}, port=0, proxyType='tor'): + ''' + Do a POST request through a local tor or i2p instance + ''' + if proxyType == 'tor': + if port == 0: + port = utils_inst._core.torPort + proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} + elif proxyType == 'i2p': + proxies = {'http': 'http://127.0.0.1:4444'} + else: + return + headers = {'user-agent': 'PyOnionr', 'Connection':'close'} + try: + proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} + r = requests.post(url, data=data, headers=headers, proxies=proxies, allow_redirects=False, timeout=(15, 30)) + retData = r.text + except KeyboardInterrupt: + raise KeyboardInterrupt + except requests.exceptions.RequestException as e: + logger.debug('Error: %s' % str(e)) + retData = False + return retData + +def do_get_request(utils_inst, url, port=0, proxyType='tor', ignoreAPI=False, returnHeaders=False): + ''' + Do a get request through a local tor or i2p instance + ''' + retData = False + if proxyType == 'tor': + if port == 0: + raise onionrexceptions.MissingPort('Socks port required for Tor HTTP get request') + proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} + elif proxyType == 'i2p': + proxies = {'http': 'http://127.0.0.1:4444'} + else: + return + headers = {'user-agent': 'PyOnionr', 'Connection':'close'} + response_headers = dict() + try: + proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} + r = requests.get(url, headers=headers, proxies=proxies, allow_redirects=False, timeout=(15, 30), ) + # Check server is using same API version as us + if not ignoreAPI: + try: + response_headers = r.headers + if r.headers['X-API'] != str(API_VERSION): + raise onionrexceptions.InvalidAPIVersion + except KeyError: + raise onionrexceptions.InvalidAPIVersion + retData = r.text + except KeyboardInterrupt: + raise KeyboardInterrupt + except ValueError as e: + pass + except onionrexceptions.InvalidAPIVersion: + if 'X-API' in response_headers: + logger.debug('Using API version %s. Cannot communicate with node\'s API version of %s.' % (API_VERSION, response_headers['X-API'])) + else: + logger.debug('Using API version %s. API version was not sent with the request.' % API_VERSION) + except requests.exceptions.RequestException as e: + if not 'ConnectTimeoutError' in str(e) and not 'Request rejected or failed' in str(e): + logger.debug('Error: %s' % str(e)) + retData = False + if returnHeaders: + return (retData, response_headers) + else: + return retData \ No newline at end of file diff --git a/run-windows-dev.bat b/run-windows-dev.bat index 31ba8c88..50de1152 100755 --- a/run-windows-dev.bat +++ b/run-windows-dev.bat @@ -1,6 +1,7 @@ @echo off +echo This script is only intended for use in Onionr development, as it uses a random profile. set ONIONR_HOME=data%random% -echo Using %ONIONR_HOME% +echo Using profile: %ONIONR_HOME% setlocal chdir onionr python onionr.py %* From d3783400991f1e6199ce3ad2c547317005c8219b Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 23 Jun 2019 12:41:07 -0500 Subject: [PATCH 118/173] * further splitting onionrutils into a module --- onionr/communicator.py | 7 +- onionr/communicatorutils/connectnewpeers.py | 3 +- .../communicatorutils/daemonqueuehandler.py | 5 +- onionr/communicatorutils/lookupadders.py | 3 +- onionr/communicatorutils/netcheck.py | 6 +- onionr/communicatorutils/servicecreator.py | 4 +- onionr/communicatorutils/uploadblocks.py | 3 +- onionr/core.py | 7 +- onionr/coredb/daemonqueue/__init__.py | 4 +- onionr/coredb/keydb/addkeys.py | 8 +- onionr/coredb/keydb/removekeys.py | 4 +- onionr/httpapi/miscpublicapi/announce.py | 4 +- onionr/onionrcommands/__init__.py | 5 +- onionr/onionrcommands/daemonlaunch.py | 9 +- onionr/onionrcommands/onionrstatistics.py | 3 +- onionr/onionrcommands/openwebinterface.py | 3 +- onionr/onionrcommands/resettor.py | 4 +- onionr/onionrpluginapi.py | 3 +- onionr/onionrservices/__init__.py | 3 +- onionr/onionrservices/bootstrapservice.py | 3 +- onionr/onionrutils/__init__.py | 156 +----------------- onionr/onionrutils/checkcommunicator.py | 19 +++ onionr/onionrutils/getclientapiserver.py | 10 ++ onionr/onionrutils/importnewblocks.py | 28 ++++ onionr/onionrutils/localcommand.py | 5 +- onionr/onionrutils/stringvalidators.py | 97 +++++++++++ onionr/onionrutils/validatemetadata.py | 5 +- .../static-data/default-plugins/cliui/main.py | 3 +- .../default-plugins/esoteric/peerserver.py | 5 +- .../default-plugins/pluginmanager/main.py | 3 +- onionr/tests/test_stringvalidations.py | 11 +- 31 files changed, 234 insertions(+), 199 deletions(-) create mode 100644 onionr/onionrutils/checkcommunicator.py create mode 100644 onionr/onionrutils/getclientapiserver.py create mode 100644 onionr/onionrutils/importnewblocks.py create mode 100644 onionr/onionrutils/stringvalidators.py diff --git a/onionr/communicator.py b/onionr/communicator.py index d59b76da..57917b9a 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -27,6 +27,7 @@ from communicatorutils import downloadblocks, lookupblocks, lookupadders from communicatorutils import servicecreator, connectnewpeers, uploadblocks from communicatorutils import daemonqueuehandler, announcenode, deniableinserts from communicatorutils import cooldownpeer, housekeeping, netcheck +from onionrutils import localcommand from etc import humanreadabletime import onionrservices, onionr, onionrproofs @@ -184,7 +185,7 @@ class OnionrCommunicatorDaemon: else: for server in self.service_greenlets: server.stop() - self._core._utils.localCommand('shutdown') # shutdown the api + localcommand.local_command(self._core, 'shutdown') # shutdown the api time.sleep(0.5) def lookupAdders(self): @@ -364,9 +365,9 @@ class OnionrCommunicatorDaemon: def detectAPICrash(self): '''exit if the api server crashes/stops''' - if self._core._utils.localCommand('ping', silent=False) not in ('pong', 'pong!'): + if localcommand.local_command(self._core, 'ping', silent=False) not in ('pong', 'pong!'): for i in range(300): - if self._core._utils.localCommand('ping') in ('pong', 'pong!') or self.shutdown: + if localcommand.local_command(self._core, 'ping') in ('pong', 'pong!') or self.shutdown: break # break for loop time.sleep(1) else: diff --git a/onionr/communicatorutils/connectnewpeers.py b/onionr/communicatorutils/connectnewpeers.py index 96e4ecac..25929c24 100755 --- a/onionr/communicatorutils/connectnewpeers.py +++ b/onionr/communicatorutils/connectnewpeers.py @@ -20,6 +20,7 @@ import time, sys import onionrexceptions, logger, onionrpeers from utils import networkmerger +from onionrutils import stringvalidators # secrets module was added into standard lib in 3.6+ if sys.version_info[0] == 3 and sys.version_info[1] < 6: from dependencies import secrets @@ -30,7 +31,7 @@ def connect_new_peer_to_communicator(comm_inst, peer='', useBootstrap=False): retData = False tried = comm_inst.offlinePeers if peer != '': - if comm_inst._core._utils.validateID(peer): + if stringvalidators.validate_transport(peer): peerList = [peer] else: raise onionrexceptions.InvalidAddress('Will not attempt connection test to invalid address') diff --git a/onionr/communicatorutils/daemonqueuehandler.py b/onionr/communicatorutils/daemonqueuehandler.py index ddbb0783..c11cc929 100755 --- a/onionr/communicatorutils/daemonqueuehandler.py +++ b/onionr/communicatorutils/daemonqueuehandler.py @@ -19,6 +19,7 @@ ''' import logger import onionrevents as events +from onionrutils import localcommand def handle_daemon_commands(comm_inst): cmd = comm_inst._core.daemonQueue() response = '' @@ -39,7 +40,7 @@ def handle_daemon_commands(comm_inst): if response == '': response = 'none' elif cmd[0] == 'localCommand': - response = comm_inst._core._utils.localCommand(cmd[1]) + response = localcommand.local_command(comm_inst._core, cmd[1]) elif cmd[0] == 'pex': for i in comm_inst.timers: if i.timerFunction.__name__ == 'lookupAdders': @@ -49,7 +50,7 @@ def handle_daemon_commands(comm_inst): if cmd[0] not in ('', None): if response != '': - comm_inst._core._utils.localCommand('queueResponseAdd/' + cmd[4], post=True, postData={'data': response}) + localcommand.local_command(comm_inst._core, 'queueResponseAdd/' + cmd[4], post=True, postData={'data': response}) response = '' comm_inst.decrementThreadCount('daemonCommands') \ No newline at end of file diff --git a/onionr/communicatorutils/lookupadders.py b/onionr/communicatorutils/lookupadders.py index 4f861682..fc4527dc 100755 --- a/onionr/communicatorutils/lookupadders.py +++ b/onionr/communicatorutils/lookupadders.py @@ -18,6 +18,7 @@ along with this program. If not, see . ''' import logger +from onionrutils import stringvalidators def lookup_new_peer_transports_with_communicator(comm_inst): logger.info('Looking up new addresses...') @@ -39,7 +40,7 @@ def lookup_new_peer_transports_with_communicator(comm_inst): invalid = [] for x in newPeers: x = x.strip() - if not comm_inst._core._utils.validateID(x) or x in comm_inst.newPeers or x == comm_inst._core.hsAddress: + if not stringvalidators.validate_transport(x) or x in comm_inst.newPeers or x == comm_inst._core.hsAddress: # avoid adding if its our address invalid.append(x) for x in invalid: diff --git a/onionr/communicatorutils/netcheck.py b/onionr/communicatorutils/netcheck.py index c5e95906..688feeea 100755 --- a/onionr/communicatorutils/netcheck.py +++ b/onionr/communicatorutils/netcheck.py @@ -20,17 +20,19 @@ ''' import logger from utils import netutils +from onionrutils import localcommand def net_check(comm_inst): '''Check if we are connected to the internet or not when we can't connect to any peers''' rec = False # for detecting if we have received incoming connections recently + c = comm_inst._core if len(comm_inst.onlinePeers) == 0: try: - if (comm_inst._core._utils.getEpoch() - int(comm_inst._core._utils.localCommand('/lastconnect'))) <= 60: + if (c._utils.getEpoch() - int(localcommand.local_command(c, '/lastconnect'))) <= 60: comm_inst.isOnline = True rec = True except ValueError: pass - if not rec and not netutils.checkNetwork(comm_inst._core._utils, torPort=comm_inst.proxyPort): + if not rec and not netutils.checkNetwork(c._utils, torPort=comm_inst.proxyPort): if not comm_inst.shutdown: logger.warn('Network check failed, are you connected to the Internet, and is Tor working?') comm_inst.isOnline = False diff --git a/onionr/communicatorutils/servicecreator.py b/onionr/communicatorutils/servicecreator.py index c1f2c7e2..94fc51c8 100755 --- a/onionr/communicatorutils/servicecreator.py +++ b/onionr/communicatorutils/servicecreator.py @@ -18,6 +18,8 @@ along with this program. If not, see . ''' import communicator, onionrblockapi +from onionrutils import stringvalidators + def service_creator(daemon): assert isinstance(daemon, communicator.OnionrCommunicatorDaemon) core = daemon._core @@ -30,7 +32,7 @@ def service_creator(daemon): if not b in daemon.active_services: bl = onionrblockapi.Block(b, core=core, decrypt=True) bs = utils.bytesToStr(bl.bcontent) + '.onion' - if utils.validatePubKey(bl.signer) and utils.validateID(bs): + if utils.validatePubKey(bl.signer) and stringvalidators.validate_transport(bs): signer = utils.bytesToStr(bl.signer) daemon.active_services.append(b) daemon.active_services.append(signer) diff --git a/onionr/communicatorutils/uploadblocks.py b/onionr/communicatorutils/uploadblocks.py index 69d0353d..497e07d5 100755 --- a/onionr/communicatorutils/uploadblocks.py +++ b/onionr/communicatorutils/uploadblocks.py @@ -20,6 +20,7 @@ import logger from communicatorutils import proxypicker import onionrblockapi as block +from onionrutils import localcommand def upload_blocks_from_communicator(comm_inst): # when inserting a block, we try to upload it to a few peers to add some deniability @@ -42,7 +43,7 @@ def upload_blocks_from_communicator(comm_inst): proxyType = proxypicker.pick_proxy(peer) logger.info("Uploading block to " + peer) if not comm_inst._core._utils.doPostRequest(url, data=data, proxyType=proxyType) == False: - comm_inst._core._utils.localCommand('waitforshare/' + bl, post=True) + localcommand.local_command(comm_inst._core, 'waitforshare/' + bl, post=True) finishedUploads.append(bl) for x in finishedUploads: try: diff --git a/onionr/core.py b/onionr/core.py index ebc35e51..53bf0b4c 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -28,6 +28,7 @@ from onionrusers import onionrusers from onionrstorage import removeblock, setdata import dbcreator, onionrstorage, serializeddata, subprocesspow from etc import onionrvalues, powchoice +from onionrutils import localcommand class Core: def __init__(self, torPort=0): @@ -433,8 +434,8 @@ class Core: retData = False else: # Tell the api server through localCommand to wait for the daemon to upload this block to make statistical analysis more difficult - if self._utils.localCommand('/ping', maxWait=10) == 'pong!': - self._utils.localCommand('/waitforshare/' + retData, post=True, maxWait=5) + if localcommand.local_command(self, '/ping', maxWait=10) == 'pong!': + localcommand.local_command(self, '/waitforshare/' + retData, post=True, maxWait=5) self.daemonQueueAdd('uploadBlock', retData) self.addToBlockDB(retData, selfInsert=True, dataSaved=True) self._utils.processBlockMetadata(retData) @@ -450,7 +451,7 @@ class Core: ''' Introduces our node into the network by telling X many nodes our HS address ''' - if self._utils.localCommand('/ping', maxWait=10) == 'pong!': + if localcommand.local_command(self, '/ping', maxWait=10) == 'pong!': self.daemonQueueAdd('announceNode') logger.info('Introduction command will be processed.', terminal=True) else: diff --git a/onionr/coredb/daemonqueue/__init__.py b/onionr/coredb/daemonqueue/__init__.py index 8bd21cfd..a2f7a483 100644 --- a/onionr/coredb/daemonqueue/__init__.py +++ b/onionr/coredb/daemonqueue/__init__.py @@ -1,5 +1,7 @@ import sqlite3, os import onionrevents as events +from onionrutils import localcommand + def daemon_queue(core_inst): ''' Gives commands to the communication proccess/daemon by reading an sqlite3 database @@ -55,7 +57,7 @@ def daemon_queue_get_response(core_inst, responseID=''): Get a response sent by communicator to the API, by requesting to the API ''' assert len(responseID) > 0 - resp = core_inst._utils.localCommand('queueResponse/' + responseID) + resp = localcommand.local_command(core_inst, 'queueResponse/' + responseID) return resp def clear_daemon_queue(core_inst): diff --git a/onionr/coredb/keydb/addkeys.py b/onionr/coredb/keydb/addkeys.py index ac219b1a..716581cd 100644 --- a/onionr/coredb/keydb/addkeys.py +++ b/onionr/coredb/keydb/addkeys.py @@ -1,5 +1,7 @@ import sqlite3 -import onionrevents as events, config +import onionrevents as events +from onionrutils import stringvalidators + def add_peer(core_inst, peerID, name=''): ''' Adds a public key to the key database (misleading function name) @@ -40,8 +42,8 @@ def add_address(core_inst, address): if type(address) is None or len(address) == 0: return False - if core_inst._utils.validateID(address): - if address == config.get('i2p.ownAddr', None) or address == core_inst.hsAddress: + if stringvalidators.validate_transport(address): + if address == core_inst.config.get('i2p.ownAddr', None) or address == core_inst.hsAddress: return False conn = sqlite3.connect(core_inst.addressDB, timeout=30) c = conn.cursor() diff --git a/onionr/coredb/keydb/removekeys.py b/onionr/coredb/keydb/removekeys.py index 10f44a1b..ce4ba5fc 100644 --- a/onionr/coredb/keydb/removekeys.py +++ b/onionr/coredb/keydb/removekeys.py @@ -1,11 +1,13 @@ import sqlite3 import onionrevents as events +from onionrutils import stringvalidators + def remove_address(core_inst, address): ''' Remove an address from the address database ''' - if core_inst._utils.validateID(address): + if stringvalidators.validate_transport(address): conn = sqlite3.connect(core_inst.addressDB, timeout=30) c = conn.cursor() t = (address,) diff --git a/onionr/httpapi/miscpublicapi/announce.py b/onionr/httpapi/miscpublicapi/announce.py index 8a25b635..f17c8d83 100755 --- a/onionr/httpapi/miscpublicapi/announce.py +++ b/onionr/httpapi/miscpublicapi/announce.py @@ -21,6 +21,8 @@ import base64 from flask import Response import logger from etc import onionrvalues +from onionrutils import stringvalidators + def handle_announce(clientAPI, request): ''' accept announcement posts, validating POW @@ -52,7 +54,7 @@ def handle_announce(clientAPI, request): pass if powHash.startswith('0' * onionrvalues.OnionrValues().announce_pow): newNode = clientAPI._core._utils.bytesToStr(newNode) - if clientAPI._core._utils.validateID(newNode) and not newNode in clientAPI._core.onionrInst.communicatorInst.newPeers: + if stringvalidators.validate_transport(newNode) and not newNode in clientAPI._core.onionrInst.communicatorInst.newPeers: clientAPI._core.onionrInst.communicatorInst.newPeers.append(newNode) resp = 'Success' else: diff --git a/onionr/onionrcommands/__init__.py b/onionr/onionrcommands/__init__.py index 3a50d59d..59c9d6bc 100755 --- a/onionr/onionrcommands/__init__.py +++ b/onionr/onionrcommands/__init__.py @@ -22,6 +22,7 @@ import webbrowser, sys import logger from . import pubkeymanager, onionrstatistics, daemonlaunch, filecommands, plugincommands, keyadders from . import banblocks, exportblocks, openwebinterface, resettor +from onionrutils import importnewblocks def show_help(o_inst, command): @@ -110,8 +111,8 @@ def get_commands(onionr_inst): 'listconn': onionr_inst.listConn, 'list-conn': onionr_inst.listConn, - 'import-blocks': onionr_inst.onionrUtils.importNewBlocks, - 'importblocks': onionr_inst.onionrUtils.importNewBlocks, + 'import-blocks': importnewblocks.import_new_blocks, + 'importblocks': importnewblocks.import_new_blocks, 'introduce': onionr_inst.onionrCore.introduceNode, 'pex': onionr_inst.doPEX, diff --git a/onionr/onionrcommands/daemonlaunch.py b/onionr/onionrcommands/daemonlaunch.py index 1aaf7230..28202c10 100755 --- a/onionr/onionrcommands/daemonlaunch.py +++ b/onionr/onionrcommands/daemonlaunch.py @@ -23,9 +23,10 @@ from threading import Thread import onionr, api, logger, communicator import onionrevents as events from netcontroller import NetController +from onionrutils import localcommand def _proper_shutdown(o_inst): - o_inst.onionrUtils.localCommand('shutdown') + localcommand.local_command(o_inst.onionrCore, 'shutdown') sys.exit(1) def daemon(o_inst): @@ -63,7 +64,7 @@ def daemon(o_inst): net = NetController(o_inst.onionrCore.config.get('client.public.port', 59497), apiServerIP=apiHost) logger.info('Tor is starting...', terminal=True) if not net.startTor(): - o_inst.onionrUtils.localCommand('shutdown') + localcommand.local_command(o_inst.onionrCore, 'shutdown') sys.exit(1) if len(net.myID) > 0 and o_inst.onionrCore.config.get('general.security_level', 1) == 0: logger.debug('Started .onion service: %s' % (logger.colors.underline + net.myID)) @@ -103,10 +104,10 @@ def daemon(o_inst): signal.signal(signal.SIGINT, _ignore_sigint) o_inst.onionrCore.daemonQueueAdd('shutdown') - o_inst.onionrUtils.localCommand('shutdown') + localcommand.local_command(o_inst.onionrCore, 'shutdown') net.killTor() - time.sleep(8) # Time to allow threads to finish, if not any "daemon" threads will be slaughtered http://docs.python.org/library/threading.html#threading.Thread.daemon + time.sleep(5) # Time to allow threads to finish, if not any "daemon" threads will be slaughtered http://docs.python.org/library/threading.html#threading.Thread.daemon o_inst.deleteRunFiles() return diff --git a/onionr/onionrcommands/onionrstatistics.py b/onionr/onionrcommands/onionrstatistics.py index a28fb7db..2f50c6e7 100755 --- a/onionr/onionrcommands/onionrstatistics.py +++ b/onionr/onionrcommands/onionrstatistics.py @@ -21,6 +21,7 @@ import os, uuid, time import logger, onionrutils from onionrblockapi import Block import onionr +from onionrutils import checkcommunicator def show_stats(o_inst): try: @@ -29,7 +30,7 @@ def show_stats(o_inst): signedBlocks = len(Block.getBlocks(signed = True)) messages = { # info about local client - 'Onionr Daemon Status' : ((logger.colors.fg.green + 'Online') if o_inst.onionrUtils.isCommunicatorRunning(timeout = 9) else logger.colors.fg.red + 'Offline'), + 'Onionr Daemon Status' : ((logger.colors.fg.green + 'Online') if checkcommunicator.is_communicator_running(o_inst.onionrCore, timeout = 9) else logger.colors.fg.red + 'Offline'), # file and folder size stats 'div1' : True, # this creates a solid line across the screen, a div diff --git a/onionr/onionrcommands/openwebinterface.py b/onionr/onionrcommands/openwebinterface.py index 8ce71744..42c80b57 100755 --- a/onionr/onionrcommands/openwebinterface.py +++ b/onionr/onionrcommands/openwebinterface.py @@ -19,9 +19,10 @@ ''' import webbrowser import logger +from onionrutils import getclientapiserver def open_home(o_inst): try: - url = o_inst.onionrUtils.getClientAPIServer() + url = getclientapiserver.get_client_API_server(o_inst.onionrCore) except FileNotFoundError: logger.error('Onionr seems to not be running (could not get api host)', terminal=True) else: diff --git a/onionr/onionrcommands/resettor.py b/onionr/onionrcommands/resettor.py index cd7c51d2..e327bccc 100755 --- a/onionr/onionrcommands/resettor.py +++ b/onionr/onionrcommands/resettor.py @@ -19,11 +19,13 @@ ''' import os, shutil import logger, core +from onionrutils import localcommand + def reset_tor(): c = core.Core() tor_dir = c.dataDir + 'tordata' if os.path.exists(tor_dir): - if c._utils.localCommand('/ping') == 'pong!': + if localcommand.local_command(c, '/ping') == 'pong!': logger.warn('Cannot delete Tor data while Onionr is running', terminal=True) else: shutil.rmtree(tor_dir) \ No newline at end of file diff --git a/onionr/onionrpluginapi.py b/onionr/onionrpluginapi.py index 78c8a008..24ba4f12 100755 --- a/onionr/onionrpluginapi.py +++ b/onionr/onionrpluginapi.py @@ -19,6 +19,7 @@ ''' import onionrplugins, core as onionrcore, logger +from onionrutils import localcommand class DaemonAPI: def __init__(self, pluginapi): @@ -40,7 +41,7 @@ class DaemonAPI: return def local_command(self, command): - return self.pluginapi.get_utils().localCommand(self, command) + return localcommand.local_command(self.pluginapi.get_core(), command) def queue_pop(self): return self.get_core().daemonQueue() diff --git a/onionr/onionrservices/__init__.py b/onionr/onionrservices/__init__.py index 2792f7e3..1c23e5fb 100755 --- a/onionr/onionrservices/__init__.py +++ b/onionr/onionrservices/__init__.py @@ -21,6 +21,7 @@ import time import stem import core from . import connectionserver, bootstrapservice +from onionrutils import stringvalidators class OnionrServices: ''' @@ -39,7 +40,7 @@ class OnionrServices: When a client wants to connect, contact their bootstrap address and tell them our ephemeral address for our service by creating a new ConnectionServer instance ''' - assert self._core._utils.validateID(address) + assert stringvalidators.validate_transport(address) BOOTSTRAP_TRIES = 10 # How many times to attempt contacting the bootstrap server TRY_WAIT = 3 # Seconds to wait before trying bootstrap again # HTTP is fine because .onion/i2p is encrypted/authenticated diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py index 077b4657..e06300e6 100755 --- a/onionr/onionrservices/bootstrapservice.py +++ b/onionr/onionrservices/bootstrapservice.py @@ -24,6 +24,7 @@ from flask import Flask, Response import core from netcontroller import getOpenPort from . import httpheaders +from onionrutils import stringvalidators def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): ''' @@ -61,7 +62,7 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): @bootstrap_app.route('/bs/
        ', methods=['POST']) def get_bootstrap(address): - if core_inst._utils.validateID(address + '.onion'): + if stringvalidators.validate_transport(address + '.onion'): # Set the bootstrap address then close the server bootstrap_address = address + '.onion' core_inst.keyStore.put(bs_id, bootstrap_address) diff --git a/onionr/onionrutils/__init__.py b/onionr/onionrutils/__init__.py index a7efbae7..23907a00 100755 --- a/onionr/onionrutils/__init__.py +++ b/onionr/onionrutils/__init__.py @@ -29,6 +29,7 @@ import storagecounter from etc import pgpwords, onionrvalues from onionrusers import onionrusers from . import localcommand, blockmetadata, validatemetadata, basicrequests +from . import stringvalidators config.reload() class OnionrUtils: @@ -50,23 +51,6 @@ class OnionrUtils: ''' epoch = self.getEpoch() return epoch - (epoch % roundS) - - def getClientAPIServer(self): - retData = '' - try: - with open(self._core.privateApiHostFile, 'r') as host: - hostname = host.read() - except FileNotFoundError: - raise FileNotFoundError - else: - retData += '%s:%s' % (hostname, config.get('client.client.port')) - return retData - - def localCommand(self, command, data='', silent = True, post=False, postData = {}, maxWait=20): - ''' - Send a command to the local http API server, securely. Intended for local clients, DO NOT USE for remote peers. - ''' - return localcommand.local_command(self, command, data, silent, post, postData, maxWait) def getHumanReadableID(self, pub=''): '''gets a human readable ID from a public key''' @@ -132,19 +116,7 @@ class OnionrUtils: ''' Validate if a string is a valid hash hex digest (does not compare, just checks length and charset) ''' - retVal = True - if data == False or data == True: - return False - data = data.strip() - if len(data) != length: - retVal = False - else: - try: - int(data, 16) - except ValueError: - retVal = False - - return retVal + return stringvalidators.validate_hash(self, data, length) def validateMetadata(self, metadata, blockData): '''Validate metadata meets onionr spec (does not validate proof value computation), take in either dictionary or json string''' @@ -154,129 +126,7 @@ class OnionrUtils: ''' Validate if a string is a valid base32 encoded Ed25519 key ''' - if type(key) is type(None): - return False - # Accept keys that have no = padding - key = unpaddedbase32.repad(self.strToBytes(key)) - - retVal = False - try: - nacl.signing.SigningKey(seed=key, encoder=nacl.encoding.Base32Encoder) - except nacl.exceptions.ValueError: - pass - except base64.binascii.Error as err: - pass - else: - retVal = True - return retVal - - @staticmethod - def validateID(id): - ''' - Validate if an address is a valid tor or i2p hidden service - ''' - try: - idLength = len(id) - retVal = True - idNoDomain = '' - peerType = '' - # i2p b32 addresses are 60 characters long (including .b32.i2p) - if idLength == 60: - peerType = 'i2p' - if not id.endswith('.b32.i2p'): - retVal = False - else: - idNoDomain = id.split('.b32.i2p')[0] - # Onion v2's are 22 (including .onion), v3's are 62 with .onion - elif idLength == 22 or idLength == 62: - peerType = 'onion' - if not id.endswith('.onion'): - retVal = False - else: - idNoDomain = id.split('.onion')[0] - else: - retVal = False - if retVal: - if peerType == 'i2p': - try: - id.split('.b32.i2p')[2] - except: - pass - else: - retVal = False - elif peerType == 'onion': - try: - id.split('.onion')[2] - except: - pass - else: - retVal = False - if not idNoDomain.isalnum(): - retVal = False - - # Validate address is valid base32 (when capitalized and minus extension); v2/v3 onions and .b32.i2p use base32 - for x in idNoDomain.upper(): - if x not in string.ascii_uppercase and x not in '234567': - retVal = False - - return retVal - except: - return False - - @staticmethod - def isIntegerString(data): - '''Check if a string is a valid base10 integer (also returns true if already an int)''' - try: - int(data) - except (ValueError, TypeError) as e: - return False - else: - return True - - def isCommunicatorRunning(self, timeout = 5, interval = 0.1): - try: - runcheck_file = self._core.dataDir + '.runcheck' - - if not os.path.isfile(runcheck_file): - open(runcheck_file, 'w+').close() - - # self._core.daemonQueueAdd('runCheck') # deprecated - starttime = time.time() - - while True: - time.sleep(interval) - - if not os.path.isfile(runcheck_file): - return True - elif time.time() - starttime >= timeout: - return False - except: - return False - - def importNewBlocks(self, scanDir=''): - ''' - This function is intended to scan for new blocks ON THE DISK and import them - ''' - blockList = self._core.getBlockList() - exist = False - if scanDir == '': - scanDir = self._core.blockDataLocation - if not scanDir.endswith('/'): - scanDir += '/' - for block in glob.glob(scanDir + "*.dat"): - if block.replace(scanDir, '').replace('.dat', '') not in blockList: - exist = True - logger.info('Found new block on dist %s' % block) - with open(block, 'rb') as newBlock: - block = block.replace(scanDir, '').replace('.dat', '') - if self._core._crypto.sha3Hash(newBlock.read()) == block.replace('.dat', ''): - self._core.addToBlockDB(block.replace('.dat', ''), dataSaved=True) - logger.info('Imported block %s.' % block) - self._core._utils.processBlockMetadata(block) - else: - logger.warn('Failed to verify hash for %s' % block) - if not exist: - logger.info('No blocks found to import') + return stringvalidators.validate_pub_key(self, key) def getEpoch(self): '''returns epoch''' diff --git a/onionr/onionrutils/checkcommunicator.py b/onionr/onionrutils/checkcommunicator.py new file mode 100644 index 00000000..07f8dc1a --- /dev/null +++ b/onionr/onionrutils/checkcommunicator.py @@ -0,0 +1,19 @@ +import time, os +def is_communicator_running(core_inst, timeout = 5, interval = 0.1): + try: + runcheck_file = core_inst.dataDir + '.runcheck' + + if not os.path.isfile(runcheck_file): + open(runcheck_file, 'w+').close() + + starttime = time.time() + + while True: + time.sleep(interval) + + if not os.path.isfile(runcheck_file): + return True + elif time.time() - starttime >= timeout: + return False + except: + return False \ No newline at end of file diff --git a/onionr/onionrutils/getclientapiserver.py b/onionr/onionrutils/getclientapiserver.py new file mode 100644 index 00000000..23020f66 --- /dev/null +++ b/onionr/onionrutils/getclientapiserver.py @@ -0,0 +1,10 @@ +def get_client_API_server(core_inst): + retData = '' + try: + with open(core_inst.privateApiHostFile, 'r') as host: + hostname = host.read() + except FileNotFoundError: + raise FileNotFoundError + else: + retData += '%s:%s' % (hostname, core_inst.config.get('client.client.port')) + return retData \ No newline at end of file diff --git a/onionr/onionrutils/importnewblocks.py b/onionr/onionrutils/importnewblocks.py new file mode 100644 index 00000000..a2c1e518 --- /dev/null +++ b/onionr/onionrutils/importnewblocks.py @@ -0,0 +1,28 @@ +import glob +import logger, core +def import_new_blocks(core_inst=None, scanDir=''): + ''' + This function is intended to scan for new blocks ON THE DISK and import them + ''' + if core_inst is None: + core_inst = core.Core() + blockList = core_inst.getBlockList() + exist = False + if scanDir == '': + scanDir = core_inst.blockDataLocation + if not scanDir.endswith('/'): + scanDir += '/' + for block in glob.glob(scanDir + "*.dat"): + if block.replace(scanDir, '').replace('.dat', '') not in blockList: + exist = True + logger.info('Found new block on dist %s' % block) + with open(block, 'rb') as newBlock: + block = block.replace(scanDir, '').replace('.dat', '') + if core_inst._crypto.sha3Hash(newBlock.read()) == block.replace('.dat', ''): + core_inst.addToBlockDB(block.replace('.dat', ''), dataSaved=True) + logger.info('Imported block %s.' % block) + core_inst._utils.processBlockMetadata(block) + else: + logger.warn('Failed to verify hash for %s' % block) + if not exist: + logger.info('No blocks found to import') \ No newline at end of file diff --git a/onionr/onionrutils/localcommand.py b/onionr/onionrutils/localcommand.py index fff050c8..90b0d7ac 100644 --- a/onionr/onionrutils/localcommand.py +++ b/onionr/onionrutils/localcommand.py @@ -1,6 +1,7 @@ import urllib, requests, time import logger -def local_command(utils_inst, command, data='', silent = True, post=False, postData = {}, maxWait=20): +from onionrutils import getclientapiserver +def local_command(core_inst, command, data='', silent = True, post=False, postData = {}, maxWait=20): ''' Send a command to the local http API server, securely. Intended for local clients, DO NOT USE for remote peers. ''' @@ -9,7 +10,7 @@ def local_command(utils_inst, command, data='', silent = True, post=False, postD waited = 0 while hostname == '': try: - hostname = utils_inst.getClientAPIServer() + hostname = getclientapiserver.get_client_API_server(core_inst) except FileNotFoundError: time.sleep(1) waited += 1 diff --git a/onionr/onionrutils/stringvalidators.py b/onionr/onionrutils/stringvalidators.py new file mode 100644 index 00000000..49f2c33a --- /dev/null +++ b/onionr/onionrutils/stringvalidators.py @@ -0,0 +1,97 @@ +import base64, string +import unpaddedbase32, nacl.signing, nacl.encoding +def validate_hash(utils_inst, data, length=64): + ''' + Validate if a string is a valid hash hex digest (does not compare, just checks length and charset) + ''' + retVal = True + if data == False or data == True: + return False + data = data.strip() + if len(data) != length: + retVal = False + else: + try: + int(data, 16) + except ValueError: + retVal = False + + return retVal + +def validate_pub_key(utils_inst, key): + ''' + Validate if a string is a valid base32 encoded Ed25519 key + ''' + if type(key) is type(None): + return False + # Accept keys that have no = padding + key = unpaddedbase32.repad(utils_inst.strToBytes(key)) + + retVal = False + try: + nacl.signing.SigningKey(seed=key, encoder=nacl.encoding.Base32Encoder) + except nacl.exceptions.ValueError: + pass + except base64.binascii.Error as err: + pass + else: + retVal = True + return retVal + +def validate_transport(id): + try: + idLength = len(id) + retVal = True + idNoDomain = '' + peerType = '' + # i2p b32 addresses are 60 characters long (including .b32.i2p) + if idLength == 60: + peerType = 'i2p' + if not id.endswith('.b32.i2p'): + retVal = False + else: + idNoDomain = id.split('.b32.i2p')[0] + # Onion v2's are 22 (including .onion), v3's are 62 with .onion + elif idLength == 22 or idLength == 62: + peerType = 'onion' + if not id.endswith('.onion'): + retVal = False + else: + idNoDomain = id.split('.onion')[0] + else: + retVal = False + if retVal: + if peerType == 'i2p': + try: + id.split('.b32.i2p')[2] + except: + pass + else: + retVal = False + elif peerType == 'onion': + try: + id.split('.onion')[2] + except: + pass + else: + retVal = False + if not idNoDomain.isalnum(): + retVal = False + + # Validate address is valid base32 (when capitalized and minus extension); v2/v3 onions and .b32.i2p use base32 + for x in idNoDomain.upper(): + if x not in string.ascii_uppercase and x not in '234567': + retVal = False + + return retVal + except Exception as e: + return False + +def is_integer_string(data): + '''Check if a string is a valid base10 integer (also returns true if already an int)''' + try: + int(data) + except (ValueError, TypeError) as e: + return False + else: + return True diff --git a/onionr/onionrutils/validatemetadata.py b/onionr/onionrutils/validatemetadata.py index c4bcc4a8..8feb9859 100644 --- a/onionr/onionrutils/validatemetadata.py +++ b/onionr/onionrutils/validatemetadata.py @@ -1,6 +1,7 @@ import json import logger, onionrexceptions from etc import onionrvalues +from onionrutils import stringvalidators def validate_metadata(utils_inst, metadata, blockData): '''Validate metadata meets onionr spec (does not validate proof value computation), take in either dictionary or json string''' # TODO, make this check sane sizes @@ -15,7 +16,7 @@ def validate_metadata(utils_inst, metadata, blockData): pass # Validate metadata dict for invalid keys to sizes that are too large - maxAge = utils_inst._coreconfig.get("general.max_block_age", onionrvalues.OnionrValues().default_expire) + maxAge = utils_inst._core.config.get("general.max_block_age", onionrvalues.OnionrValues().default_expire) if type(metadata) is dict: for i in metadata: try: @@ -33,7 +34,7 @@ def validate_metadata(utils_inst, metadata, blockData): logger.warn('Block metadata key ' + i + ' exceeded maximum size') break if i == 'time': - if not utils_inst.isIntegerString(metadata[i]): + if not stringvalidators.is_integer_string(metadata[i]): logger.warn('Block metadata time stamp is not integer string or int') break isFuture = (metadata[i] - utils_inst.getEpoch()) diff --git a/onionr/static-data/default-plugins/cliui/main.py b/onionr/static-data/default-plugins/cliui/main.py index f6a0b906..c65917c8 100755 --- a/onionr/static-data/default-plugins/cliui/main.py +++ b/onionr/static-data/default-plugins/cliui/main.py @@ -23,6 +23,7 @@ import threading, time, uuid, subprocess, sys import config, logger from onionrblockapi import Block import onionrplugins +from onionrutils import localcommand plugin_name = 'cliui' PLUGIN_VERSION = '0.0.1' @@ -48,7 +49,7 @@ class OnionrCLIUI: def isRunning(self): while not self.shutdown: - if self.myCore._utils.localCommand('ping', maxWait=5) == 'pong!': + if localcommand.local_command(self.myCore, 'ping', maxWait=5) == 'pong!': self.running = 'Yes' else: self.running = 'No' diff --git a/onionr/static-data/default-plugins/esoteric/peerserver.py b/onionr/static-data/default-plugins/esoteric/peerserver.py index b3ae423e..f48ba0be 100755 --- a/onionr/static-data/default-plugins/esoteric/peerserver.py +++ b/onionr/static-data/default-plugins/esoteric/peerserver.py @@ -19,6 +19,7 @@ ''' import sys, os, json import core +from onionrutils import localcommand from flask import Response, request, redirect, Blueprint, abort, g sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) direct_blueprint = Blueprint('esoteric', __name__) @@ -47,9 +48,9 @@ def sendto(): msg = '' else: msg = json.dumps(msg) - core_inst._utils.localCommand('/esoteric/addrec/%s' % (g.peer,), post=True, postData=msg) + localcommand.local_command(core_inst, '/esoteric/addrec/%s' % (g.peer,), post=True, postData=msg) return Response('success') @direct_blueprint.route('/esoteric/poll') def poll_chat(): - return Response(core_inst._utils.localCommand('/esoteric/gets/%s' % (g.peer,))) \ No newline at end of file + return Response(localcommand.local_command(core_inst, '/esoteric/gets/%s' % (g.peer,))) \ No newline at end of file diff --git a/onionr/static-data/default-plugins/pluginmanager/main.py b/onionr/static-data/default-plugins/pluginmanager/main.py index d4feb69d..2f261bbf 100755 --- a/onionr/static-data/default-plugins/pluginmanager/main.py +++ b/onionr/static-data/default-plugins/pluginmanager/main.py @@ -22,6 +22,7 @@ import logger, config import os, sys, json, time, random, shutil, base64, getpass, datetime, re from onionrblockapi import Block +from onionrutils import importnewblocks plugin_name = 'pluginmanager' @@ -236,7 +237,7 @@ def pluginToBlock(plugin, import_block = True): # hash = pluginapi.get_core().insertBlock(, header = 'plugin', sign = True) if import_block: - pluginapi.get_utils().importNewBlocks() + importnewblocks.import_new_blocks(pluginapi.get_core()) return hash else: diff --git a/onionr/tests/test_stringvalidations.py b/onionr/tests/test_stringvalidations.py index caac34df..4eb7f113 100755 --- a/onionr/tests/test_stringvalidations.py +++ b/onionr/tests/test_stringvalidations.py @@ -6,6 +6,7 @@ TEST_DIR = 'testdata/%s-%s' % (uuid.uuid4(), os.path.basename(__file__)) + '/' print("Test directory:", TEST_DIR) os.environ["ONIONR_HOME"] = TEST_DIR import core, onionr +from onionrutils import stringvalidators core.Core() @@ -13,7 +14,6 @@ class OnionrValidations(unittest.TestCase): def test_peer_validator(self): # Test hidden service domain validities - c = core.Core() valid = ['facebookcorewwwi.onion', 'vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd.onion', '5bvb5ncnfr4dlsfriwczpzcvo65kn7fnnlnt2ln7qvhzna2xaldq.b32.i2p'] @@ -21,11 +21,11 @@ class OnionrValidations(unittest.TestCase): for x in valid: print('testing', x) - self.assertTrue(c._utils.validateID(x)) + self.assertTrue(stringvalidators.validate_transport(x)) for x in invalid: print('testing', x) - self.assertFalse(c._utils.validateID(x)) + self.assertFalse(stringvalidators.validate_transport(x)) def test_pubkey_validator(self): # Test ed25519 public key validity @@ -44,14 +44,13 @@ class OnionrValidations(unittest.TestCase): def test_integer_string(self): valid = ["1", "100", 100, "-5", -5] invalid = ['test', "1d3434", "1e100", None] - c = core.Core() for x in valid: #print('testing', x) - self.assertTrue(c._utils.isIntegerString(x)) + self.assertTrue(stringvalidators.is_integer_string(x)) for x in invalid: #print('testing', x) - self.assertFalse(c._utils.isIntegerString(x)) + self.assertFalse(stringvalidators.is_integer_string(x)) unittest.main() \ No newline at end of file From 909c002dc4b38f87690b4d070b30a2a6a4ccb32c Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 25 Jun 2019 03:21:36 -0500 Subject: [PATCH 119/173] more progress in removing onionrutils class --- onionr/api.py | 13 +-- onionr/blockimporter.py | 9 +- onionr/communicatorutils/downloadblocks.py | 5 +- onionr/communicatorutils/servicecreator.py | 2 +- onionr/core.py | 15 +-- onionr/coredb/blockmetadb/add.py | 3 +- onionr/coredb/keydb/addkeys.py | 2 +- onionr/httpapi/miscpublicapi/getblocks.py | 8 +- onionr/netcontroller.py | 1 - onionr/onionr.py | 11 +- onionr/onionrblockapi.py | 3 +- onionr/onionrcommands/keyadders.py | 2 +- onionr/onionrcommands/onionrstatistics.py | 4 +- onionr/onionrcommands/pubkeymanager.py | 5 +- onionr/onionrcrypto.py | 21 ++-- onionr/onionrproofs.py | 7 +- onionr/onionrservices/bootstrapservice.py | 2 +- onionr/onionrservices/connectionserver.py | 3 +- onionr/onionrstorage/__init__.py | 3 +- onionr/onionrusers/contactmanager.py | 3 +- onionr/onionrusers/onionrusers.py | 12 +- onionr/onionrutils/__init__.py | 106 ++++-------------- onionr/onionrutils/basicrequests.py | 2 +- onionr/onionrutils/blockmetadata.py | 9 +- onionr/onionrutils/bytesconverter.py | 13 +++ onionr/onionrutils/epoch.py | 11 ++ onionr/onionrutils/importnewblocks.py | 3 +- onionr/onionrutils/mnemonickeys.py | 8 ++ onionr/onionrutils/stringvalidators.py | 6 +- onionr/onionrutils/validatemetadata.py | 20 ++-- .../default-plugins/encrypt/main.py | 3 +- .../default-plugins/esoteric/main.py | 3 +- .../default-plugins/metadataprocessor/main.py | 3 +- .../default-plugins/pluginmanager/main.py | 8 +- .../static-data/default-plugins/pms/main.py | 3 +- onionr/subprocesspow.py | 5 +- onionr/tests/test_highlevelcrypto.py | 16 +-- 37 files changed, 164 insertions(+), 189 deletions(-) mode change 100755 => 100644 onionr/onionrutils/__init__.py create mode 100644 onionr/onionrutils/bytesconverter.py create mode 100644 onionr/onionrutils/epoch.py create mode 100644 onionr/onionrutils/mnemonickeys.py diff --git a/onionr/api.py b/onionr/api.py index ab1eeb59..7d8bf671 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -23,8 +23,7 @@ from gevent import Timeout import flask from flask import request, Response, abort, send_from_directory import core -from onionrblockapi import Block -import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config +import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config, onionrblockapi import httpapi from httpapi import friendsapi, profilesapi, configapi, miscpublicapi from onionrservices import httpheaders @@ -337,7 +336,7 @@ class API: resp = '' if self._core._utils.validateHash(name): try: - resp = Block(name, decrypt=True).bcontent + resp = onionrblockapi.Block(name, decrypt=True).bcontent except TypeError: pass else: @@ -374,7 +373,7 @@ class API: resp = 'Not Found' if self._core._utils.validateHash(bHash): try: - resp = Block(bHash).bcontent + resp = onionrblockapi.Block(bHash).bcontent except onionrexceptions.NoDataAvailable: abort(404) except TypeError: @@ -505,7 +504,7 @@ class API: def getBlockData(self, bHash, decrypt=False, raw=False, headerOnly=False): assert self._core._utils.validateHash(bHash) - bl = Block(bHash, core=self._core) + bl = onionrblockapi.Block(bHash, core=self._core) if decrypt: bl.decrypt() if bl.isEncrypted and not bl.decrypted: @@ -521,8 +520,8 @@ class API: pass else: validSig = False - signer = self._core._utils.bytesToStr(bl.signer) - if bl.isSigned() and self._core._utils.validatePubKey(signer) and bl.isSigner(signer): + signer = onionrutils.bytes_to_str(bl.signer) + if bl.isSigned() and onionrutils.stringvalidators.validate_pub_key(signer) and bl.isSigner(signer): validSig = True bl.bheader['validSig'] = validSig bl.bheader['meta'] = '' diff --git a/onionr/blockimporter.py b/onionr/blockimporter.py index ccfc77db..d5e02e7d 100755 --- a/onionr/blockimporter.py +++ b/onionr/blockimporter.py @@ -18,6 +18,7 @@ along with this program. If not, see . ''' import core, onionrexceptions, logger +from onionrutils import validatemetadata, blockmetadata def importBlockFromData(content, coreInst): retData = False @@ -34,17 +35,17 @@ def importBlockFromData(content, coreInst): except AttributeError: pass - metas = coreInst._utils.getBlockMetadataFromData(content) # returns tuple(metadata, meta), meta is also in metadata + metas = blockmetadata.get_block_metadata_from_data(content) # returns tuple(metadata, meta), meta is also in metadata metadata = metas[0] - if coreInst._utils.validateMetadata(metadata, metas[2]): # check if metadata is valid + if validatemetadata(metadata, metas[2]): # check if metadata is valid if coreInst._crypto.verifyPow(content): # check if POW is enough/correct - logger.info('Block passed proof, saving.') + logger.info('Block passed proof, saving.', terminal=True) try: blockHash = coreInst.setData(content) except onionrexceptions.DiskAllocationReached: pass else: coreInst.addToBlockDB(blockHash, dataSaved=True) - coreInst._utils.processBlockMetadata(blockHash) # caches block metadata values to block database + blockmetadata.process_block_metadata(blockHash) # caches block metadata values to block database retData = True return retData \ No newline at end of file diff --git a/onionr/communicatorutils/downloadblocks.py b/onionr/communicatorutils/downloadblocks.py index cb72bd12..ccf121c5 100755 --- a/onionr/communicatorutils/downloadblocks.py +++ b/onionr/communicatorutils/downloadblocks.py @@ -19,6 +19,7 @@ ''' import communicator, onionrexceptions import logger, onionrpeers +from onionrutils import blockmetadata def download_blocks_from_communicator(comm_inst): assert isinstance(comm_inst, communicator.OnionrCommunicatorDaemon) @@ -72,7 +73,7 @@ def download_blocks_from_communicator(comm_inst): pass if realHash == blockHash: content = content.decode() # decode here because sha3Hash needs bytes above - metas = comm_inst._core._utils.getBlockMetadataFromData(content) # returns tuple(metadata, meta), meta is also in metadata + metas = blockmetadata.get_block_metadata_from_data(content) # returns tuple(metadata, meta), meta is also in metadata metadata = metas[0] if comm_inst._core._utils.validateMetadata(metadata, metas[2]): # check if metadata is valid, and verify nonce if comm_inst._core._crypto.verifyPow(content): # check if POW is enough/correct @@ -84,7 +85,7 @@ def download_blocks_from_communicator(comm_inst): removeFromQueue = False else: comm_inst._core.addToBlockDB(blockHash, dataSaved=True) - comm_inst._core._utils.processBlockMetadata(blockHash) # caches block metadata values to block database + blockmetadata.process_block_metadata(blockHash) # caches block metadata values to block database else: logger.warn('POW failed for block %s.' % blockHash) else: diff --git a/onionr/communicatorutils/servicecreator.py b/onionr/communicatorutils/servicecreator.py index 94fc51c8..07d3adfb 100755 --- a/onionr/communicatorutils/servicecreator.py +++ b/onionr/communicatorutils/servicecreator.py @@ -32,7 +32,7 @@ def service_creator(daemon): if not b in daemon.active_services: bl = onionrblockapi.Block(b, core=core, decrypt=True) bs = utils.bytesToStr(bl.bcontent) + '.onion' - if utils.validatePubKey(bl.signer) and stringvalidators.validate_transport(bs): + if stringvalidators.validate_pub_key(bl.signer) and stringvalidators.validate_transport(bs): signer = utils.bytesToStr(bl.signer) daemon.active_services.append(b) daemon.active_services.append(signer) diff --git a/onionr/core.py b/onionr/core.py index 53bf0b4c..96068a66 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -28,7 +28,8 @@ from onionrusers import onionrusers from onionrstorage import removeblock, setdata import dbcreator, onionrstorage, serializeddata, subprocesspow from etc import onionrvalues, powchoice -from onionrutils import localcommand +from onionrutils import localcommand, stringvalidators, bytesconverter, epoch +from onionrutils import blockmetadata class Core: def __init__(self, torPort=0): @@ -320,9 +321,9 @@ class Core: if type(data) is None: raise ValueError('Data cannot be none') - createTime = self._utils.getRoundedEpoch() + createTime = epoch.get_epoch() - dataNonce = self._utils.bytesToStr(self._crypto.sha3Hash(data)) + dataNonce = bytesconverter.bytes_to_str(self._crypto.sha3Hash(data)) try: with open(self.dataNonceFile, 'r') as nonces: if dataNonce in nonces: @@ -395,7 +396,7 @@ class Core: signature = self._crypto.symmetricEncrypt(signature, key=symKey, returnEncoded=True).decode() signer = self._crypto.symmetricEncrypt(signer, key=symKey, returnEncoded=True).decode() elif encryptType == 'asym': - if self._utils.validatePubKey(asymPeer): + if stringvalidators.validate_pub_key(asymPeer): # Encrypt block data with forward secrecy key first, but not meta jsonMeta = json.dumps(meta) jsonMeta = self._crypto.pubKeyEncrypt(jsonMeta, asymPeer, encodedData=True).decode() @@ -438,13 +439,13 @@ class Core: localcommand.local_command(self, '/waitforshare/' + retData, post=True, maxWait=5) self.daemonQueueAdd('uploadBlock', retData) self.addToBlockDB(retData, selfInsert=True, dataSaved=True) - self._utils.processBlockMetadata(retData) + blockmetadata.process_block_metadata(retData) if retData != False: if plaintextPeer == onionrvalues.DENIABLE_PEER_ADDRESS: - events.event('insertdeniable', {'content': plaintext, 'meta': plaintextMeta, 'hash': retData, 'peer': self._utils.bytesToStr(asymPeer)}, onionr = self.onionrInst, threaded = True) + events.event('insertdeniable', {'content': plaintext, 'meta': plaintextMeta, 'hash': retData, 'peer': bytesconverter.bytes_to_str(asymPeer)}, onionr = self.onionrInst, threaded = True) else: - events.event('insertblock', {'content': plaintext, 'meta': plaintextMeta, 'hash': retData, 'peer': self._utils.bytesToStr(asymPeer)}, onionr = self.onionrInst, threaded = True) + events.event('insertblock', {'content': plaintext, 'meta': plaintextMeta, 'hash': retData, 'peer': bytesconverter.bytes_to_str(asymPeer)}, onionr = self.onionrInst, threaded = True) return retData def introduceNode(self): diff --git a/onionr/coredb/blockmetadb/add.py b/onionr/coredb/blockmetadb/add.py index d307d351..69ac9b27 100644 --- a/onionr/coredb/blockmetadb/add.py +++ b/onionr/coredb/blockmetadb/add.py @@ -1,4 +1,5 @@ import os, sqlite3 +import onionrutils def add_to_block_DB(core_inst, newHash, selfInsert=False, dataSaved=False): ''' Add a hash value to the block db @@ -8,7 +9,7 @@ def add_to_block_DB(core_inst, newHash, selfInsert=False, dataSaved=False): if not os.path.exists(core_inst.blockDB): raise Exception('Block db does not exist') - if core_inst._utils.hasBlock(newHash): + if onionrutils.has_block(core_inst, newHash): return conn = sqlite3.connect(core_inst.blockDB, timeout=30) c = conn.cursor() diff --git a/onionr/coredb/keydb/addkeys.py b/onionr/coredb/keydb/addkeys.py index 716581cd..20d7500a 100644 --- a/onionr/coredb/keydb/addkeys.py +++ b/onionr/coredb/keydb/addkeys.py @@ -10,7 +10,7 @@ def add_peer(core_inst, peerID, name=''): raise ValueError("specified id is already known") # This function simply adds a peer to the DB - if not core_inst._utils.validatePubKey(peerID): + if not stringvalidators.validate_pub_key(peerID): return False events.event('pubkey_add', data = {'key': peerID}, onionr = core_inst.onionrInst) diff --git a/onionr/httpapi/miscpublicapi/getblocks.py b/onionr/httpapi/miscpublicapi/getblocks.py index fa9dde14..c1e58e4f 100755 --- a/onionr/httpapi/miscpublicapi/getblocks.py +++ b/onionr/httpapi/miscpublicapi/getblocks.py @@ -18,12 +18,12 @@ along with this program. If not, see . ''' from flask import Response, abort -import config +import config, onionrutils def get_public_block_list(clientAPI, publicAPI, request): # Provide a list of our blocks, with a date offset dateAdjust = request.args.get('date') bList = clientAPI._core.getBlockList(dateRec=dateAdjust) - if config.get('general.hide_created_blocks', True): + if clientAPI._core.config.get('general.hide_created_blocks', True): for b in publicAPI.hideBlocks: if b in bList: # Don't share blocks we created if they haven't been *uploaded* yet, makes it harder to find who created a block @@ -34,14 +34,14 @@ def get_block_data(clientAPI, publicAPI, data): '''data is the block hash in hex''' resp = '' if clientAPI._utils.validateHash(data): - if not config.get('general.hide_created_blocks', True) or data not in publicAPI.hideBlocks: + if not clientAPI._core.config.get('general.hide_created_blocks', True) or data not in publicAPI.hideBlocks: if data in clientAPI._core.getBlockList(): block = clientAPI.getBlockData(data, raw=True) try: block = block.encode() # Encode in case data is binary except AttributeError: abort(404) - block = clientAPI._core._utils.strToBytes(block) + block = onionrutils.str_to_bytes(block) resp = block if len(resp) == 0: abort(404) diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py index b2d8237f..3d43db8c 100755 --- a/onionr/netcontroller.py +++ b/onionr/netcontroller.py @@ -20,7 +20,6 @@ import subprocess, os, sys, time, signal, base64, socket from shutil import which import logger, config -from onionrblockapi import Block config.reload() def getOpenPort(): # taken from (but modified) https://stackoverflow.com/a/2838309 by https://stackoverflow.com/users/133374/albert ccy-by-sa-3 https://creativecommons.org/licenses/by-sa/3.0/ diff --git a/onionr/onionr.py b/onionr/onionr.py index 4030aae1..74c0c335 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -21,6 +21,10 @@ along with this program. If not, see . ''' import sys +ONIONR_TAGLINE = 'Private P2P Communication - 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. MIN_PY_VERSION = 6 if sys.version_info[0] == 2 or sys.version_info[1] < MIN_PY_VERSION: sys.stderr.write('Error, Onionr requires Python 3.%s+' % (MIN_PY_VERSION,)) @@ -40,10 +44,6 @@ try: except ImportError: raise Exception("You need the PySocks module (for use with socks5 proxy to use Tor)") -ONIONR_TAGLINE = 'Private P2P Communication - 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): @@ -72,7 +72,7 @@ class Onionr: data_exists = Onionr.setupConfig(self.dataDir, self) if netcontroller.torBinary() is None: - logger.error('Tor is not installed') + logger.error('Tor is not installed', terminal=True) sys.exit(1) # If block data folder does not exist @@ -101,7 +101,6 @@ class Onionr: 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 diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py index df17aa40..cc823a1f 100755 --- a/onionr/onionrblockapi.py +++ b/onionr/onionrblockapi.py @@ -21,6 +21,7 @@ import core as onionrcore, logger, config, onionrexceptions, nacl.exceptions import json, os, sys, datetime, base64, onionrstorage from onionrusers import onionrusers +from onionrutils import stringvalidators class Block: blockCacheOrder = list() # NEVER write your own code that writes to this! @@ -441,7 +442,7 @@ class Block: ''' try: - if (not self.isSigned()) or (not self.getCore()._utils.validatePubKey(signer)): + if (not self.isSigned()) or (not stringvalidators.validate_pub_key(signer)): return False return bool(self.getCore()._crypto.edVerify(self.getSignedData(), signer, self.getSignature(), encodedData = encodedData)) diff --git a/onionr/onionrcommands/keyadders.py b/onionr/onionrcommands/keyadders.py index 363dfe28..b9401d60 100755 --- a/onionr/onionrcommands/keyadders.py +++ b/onionr/onionrcommands/keyadders.py @@ -25,7 +25,7 @@ def add_peer(o_inst): except IndexError: pass else: - if o_inst.onionrUtils.hasKey(newPeer): + if newPeer in o_inst.onionrCore.listPeers(): logger.info('We already have that key', terminal=True) return logger.info("Adding peer: " + logger.colors.underline + newPeer, terminal=True) diff --git a/onionr/onionrcommands/onionrstatistics.py b/onionr/onionrcommands/onionrstatistics.py index 2f50c6e7..83d62baa 100755 --- a/onionr/onionrcommands/onionrstatistics.py +++ b/onionr/onionrcommands/onionrstatistics.py @@ -21,7 +21,7 @@ import os, uuid, time import logger, onionrutils from onionrblockapi import Block import onionr -from onionrutils import checkcommunicator +from onionrutils import checkcommunicator, mnemonickeys def show_stats(o_inst): try: @@ -87,7 +87,7 @@ def show_details(o_inst): 'Node Address' : o_inst.get_hostname(), 'Web Password' : o_inst.getWebPassword(), 'Public Key' : o_inst.onionrCore._crypto.pubKey, - 'Human-readable Public Key' : o_inst.onionrCore._utils.getHumanReadableID() + 'Human-readable Public Key' : mnemonickeys.get_human_readable_ID(o_inst.onionrCore) } for detail in details: diff --git a/onionr/onionrcommands/pubkeymanager.py b/onionr/onionrcommands/pubkeymanager.py index e8f29b2a..a6b067bc 100755 --- a/onionr/onionrcommands/pubkeymanager.py +++ b/onionr/onionrcommands/pubkeymanager.py @@ -20,6 +20,7 @@ import sys, getpass import logger, onionrexceptions +from onionrutils import stringvalidators from onionrusers import onionrusers, contactmanager import unpaddedbase32 def add_ID(o_inst): @@ -55,7 +56,7 @@ def change_ID(o_inst): except IndexError: logger.warn('Specify pubkey to use', terminal=True) else: - if o_inst.onionrUtils.validatePubKey(key): + if stringvalidators.validate_pub_key(key): if key in o_inst.onionrCore._crypto.keyManager.getPubkeyList(): o_inst.onionrCore.config.set('general.public_key', key) o_inst.onionrCore.config.save() @@ -82,7 +83,7 @@ def friend_command(o_inst): elif action in ('add', 'remove'): try: friend = sys.argv[3] - if not o_inst.onionrUtils.validatePubKey(friend): + if not stringvalidators.validate_pub_key(friend): raise onionrexceptions.InvalidPubkey('Public key is invalid') if friend not in o_inst.onionrCore.listPeers(): raise onionrexceptions.KeyNotKnown diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py index 3c77b3ce..c6dfe300 100755 --- a/onionr/onionrcrypto.py +++ b/onionr/onionrcrypto.py @@ -21,7 +21,8 @@ import os, binascii, base64, hashlib, time, sys, hmac, secrets import nacl.signing, nacl.encoding, nacl.public, nacl.hash, nacl.pwhash, nacl.utils, nacl.secret import unpaddedbase32 import logger, onionrproofs -import onionrexceptions, keymanager, core +from onionrutils import stringvalidators +import onionrexceptions, keymanager, core, onionrutils import config config.reload() @@ -38,8 +39,8 @@ class OnionrCrypto: # Load our own pub/priv Ed25519 keys, gen & save them if they don't exist if os.path.exists(self._keyFile): - if len(config.get('general.public_key', '')) > 0: - self.pubKey = config.get('general.public_key') + if len(self._core.config.get('general.public_key', '')) > 0: + self.pubKey = self._core.config.get('general.public_key') else: self.pubKey = self.keyManager.getPubkeyList()[0] self.privKey = self.keyManager.getPrivkey(self.pubKey) @@ -94,10 +95,10 @@ class OnionrCrypto: def pubKeyEncrypt(self, data, pubkey, encodedData=False): '''Encrypt to a public key (Curve25519, taken from base32 Ed25519 pubkey)''' - pubkey = unpaddedbase32.repad(self._core._utils.strToBytes(pubkey)) + pubkey = unpaddedbase32.repad(onionrutils.str_to_bytes(pubkey)) retVal = '' box = None - data = self._core._utils.strToBytes(data) + data = onionrutils.str_to_bytes(data) pubkey = nacl.signing.VerifyKey(pubkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_public_key() @@ -122,7 +123,7 @@ class OnionrCrypto: privkey = self.privKey ownKey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key() - if self._core._utils.validatePubKey(privkey): + if stringvalidators.validate_pub_key(privkey): privkey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key() anonBox = nacl.public.SealedBox(privkey) else: @@ -181,7 +182,7 @@ class OnionrCrypto: def generateDeterministic(self, passphrase, bypassCheck=False): '''Generate a Ed25519 public key pair from a password''' passStrength = self.deterministicRequirement - passphrase = self._core._utils.strToBytes(passphrase) # Convert to bytes if not already + passphrase = onionrutils.str_to_bytes(passphrase) # Convert to bytes if not already # Validate passphrase length if not bypassCheck: if len(passphrase) < passStrength: @@ -201,7 +202,7 @@ class OnionrCrypto: if pubkey == '': pubkey = self.pubKey prev = '' - pubkey = self._core._utils.strToBytes(pubkey) + pubkey = onionrutils.str_to_bytes(pubkey) for i in range(self.HASH_ID_ROUNDS): try: prev = prev.encode() @@ -250,8 +251,8 @@ class OnionrCrypto: difficulty = onionrproofs.getDifficultyForNewBlock(blockContent, ourBlock=False, coreInst=self._core) - if difficulty < int(config.get('general.minimum_block_pow')): - difficulty = int(config.get('general.minimum_block_pow')) + if difficulty < int(self._core.config.get('general.minimum_block_pow')): + difficulty = int(self._core.config.get('general.minimum_block_pow')) mainHash = '0000000000000000000000000000000000000000000000000000000000000000'#nacl.hash.blake2b(nacl.utils.random()).decode() puzzle = mainHash[:difficulty] diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index 5ff32964..7b8aa91a 100755 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -18,7 +18,8 @@ along with this program. If not, see . ''' import multiprocessing, nacl.encoding, nacl.hash, nacl.utils, time, math, threading, binascii, sys, json -import core, onionrutils, config, logger, onionrblockapi +import core, config, logger, onionrblockapi +from onionrutils import bytesconverter config.reload() @@ -31,8 +32,6 @@ def getDifficultyModifier(coreOrUtilsInst=None): retData = 0 if isinstance(classInst, core.Core): useFunc = classInst._utils.storageCounter.getPercent - elif isinstance(classInst, onionrutils.OnionrUtils): - useFunc = classInst.storageCounter.getPercent else: useFunc = core.Core()._utils.storageCounter.getPercent @@ -56,7 +55,7 @@ def getDifficultyForNewBlock(data, ourBlock=True, coreInst=None): if isinstance(data, onionrblockapi.Block): dataSize = len(data.getRaw().encode('utf-8')) else: - dataSize = len(onionrutils.OnionrUtils.strToBytes(data)) + dataSize = len(bytesconverter.str_to_bytes(data)) if ourBlock: minDifficulty = config.get('general.minimum_send_pow', 4) diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py index e06300e6..777801dd 100755 --- a/onionr/onionrservices/bootstrapservice.py +++ b/onionr/onionrservices/bootstrapservice.py @@ -33,7 +33,7 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): if core_inst is None: core_inst = core.Core() - if not core_inst._utils.validatePubKey(peer): + if not stringvalidators.validate_pub_key(peer): raise ValueError('Peer must be valid base32 ed25519 public key') bootstrap_port = getOpenPort() diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index e676e496..3c4238a7 100755 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -24,6 +24,7 @@ import core, logger, httpapi import onionrexceptions from netcontroller import getOpenPort import api +from onionrutils import stringvalidators from . import httpheaders class ConnectionServer: @@ -33,7 +34,7 @@ class ConnectionServer: else: self.core_inst = core_inst - if not core_inst._utils.validatePubKey(peer): + if not stringvalidators.validate_pub_key(peer): raise ValueError('Peer must be valid base32 ed25519 public key') socks = core_inst.config.get('tor.socksport') # Load config for Tor socks port for proxy diff --git a/onionr/onionrstorage/__init__.py b/onionr/onionrstorage/__init__.py index b859662d..2af2661b 100755 --- a/onionr/onionrstorage/__init__.py +++ b/onionr/onionrstorage/__init__.py @@ -18,6 +18,7 @@ along with this program. If not, see . ''' import core, sys, sqlite3, os, dbcreator, onionrexceptions +from onionrutils import bytesconverter DB_ENTRY_SIZE_LIMIT = 10000 # Will be a config option @@ -82,7 +83,7 @@ def getData(coreInst, bHash): assert isinstance(coreInst, core.Core) assert coreInst._utils.validateHash(bHash) - bHash = coreInst._utils.bytesToStr(bHash) + bHash = bytesconverter.bytes_to_str(bHash) # First check DB for data entry by hash # if no entry, check disk diff --git a/onionr/onionrusers/contactmanager.py b/onionr/onionrusers/contactmanager.py index 098cba65..1cac6953 100755 --- a/onionr/onionrusers/contactmanager.py +++ b/onionr/onionrusers/contactmanager.py @@ -20,10 +20,11 @@ import os, json, onionrexceptions import unpaddedbase32 from onionrusers import onionrusers +from onionrutils import bytesconverter class ContactManager(onionrusers.OnionrUser): def __init__(self, coreInst, publicKey, saveUser=False, recordExpireSeconds=5): - publicKey = unpaddedbase32.repad(coreInst._utils.strToBytes(publicKey)).decode() + publicKey = unpaddedbase32.repad(bytesconverter.str_to_bytes(publicKey)).decode() super(ContactManager, self).__init__(coreInst, publicKey, saveUser=saveUser) self.dataDir = coreInst.dataDir + '/contacts/' self.dataFile = '%s/contacts/%s.json' % (coreInst.dataDir, publicKey) diff --git a/onionr/onionrusers/onionrusers.py b/onionr/onionrusers/onionrusers.py index 51977b39..5bb2eac8 100755 --- a/onionr/onionrusers/onionrusers.py +++ b/onionr/onionrusers/onionrusers.py @@ -17,7 +17,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import onionrblockapi, logger, onionrexceptions, json, sqlite3, time +import logger, onionrexceptions, json, sqlite3, time +from onionrutils import stringvalidators, bytesconverter + import unpaddedbase32 import nacl.exceptions @@ -56,7 +58,7 @@ class OnionrUser: Takes an instance of onionr core, a base32 encoded ed25519 public key, and a bool saveUser saveUser determines if we should add a user to our peer database or not. ''' - publicKey = unpaddedbase32.repad(coreInst._utils.strToBytes(publicKey)).decode() + publicKey = unpaddedbase32.repad(bytesconverter.str_to_bytes(publicKey)).decode() self.trust = 0 self._core = coreInst @@ -103,7 +105,7 @@ class OnionrUser: deleteExpiredKeys(self._core) retData = '' forwardKey = self._getLatestForwardKey() - if self._core._utils.validatePubKey(forwardKey[0]): + if stringvalidators.validate_pub_key(forwardKey[0]): retData = self._core._crypto.pubKeyEncrypt(data, forwardKey[0], encodedData=True) else: raise onionrexceptions.InvalidPubkey("No valid forward secrecy key available for this user") @@ -190,8 +192,8 @@ class OnionrUser: return list(keyList) def addForwardKey(self, newKey, expire=DEFAULT_KEY_EXPIRE): - newKey = self._core._utils.bytesToStr(unpaddedbase32.repad(self._core._utils.strToBytes(newKey))) - if not self._core._utils.validatePubKey(newKey): + newKey = self._core._utils.bytesToStr(unpaddedbase32.repad(bytesconverter.str_to_bytes(newKey))) + if not stringvalidators.validate_pub_key(newKey): # Do not add if something went wrong with the key raise onionrexceptions.InvalidPubkey(newKey) diff --git a/onionr/onionrutils/__init__.py b/onionr/onionrutils/__init__.py old mode 100755 new mode 100644 index 23907a00..e430a7b1 --- a/onionr/onionrutils/__init__.py +++ b/onionr/onionrutils/__init__.py @@ -22,13 +22,11 @@ import sys, os, sqlite3, binascii, time, base64, json, glob, shutil, math, re, u import requests import nacl.signing, nacl.encoding import unpaddedbase32 -from onionrblockapi import Block import onionrexceptions, config, logger import onionrevents import storagecounter from etc import pgpwords, onionrvalues -from onionrusers import onionrusers -from . import localcommand, blockmetadata, validatemetadata, basicrequests +from . import localcommand, blockmetadata, basicrequests, validatemetadata from . import stringvalidators config.reload() @@ -45,39 +43,6 @@ class OnionrUtils: self.storageCounter = storagecounter.StorageCounter(self._core) # used to keep track of how much data onionr is using on disk return - def getRoundedEpoch(self, roundS=60): - ''' - Returns the epoch, rounded down to given seconds (Default 60) - ''' - epoch = self.getEpoch() - return epoch - (epoch % roundS) - - def getHumanReadableID(self, pub=''): - '''gets a human readable ID from a public key''' - if pub == '': - pub = self._core._crypto.pubKey - pub = base64.b16encode(base64.b32decode(pub)).decode() - return ' '.join(pgpwords.wordify(pub)) - - def convertHumanReadableID(self, pub): - '''Convert a human readable pubkey id to base32''' - pub = pub.lower() - return self.bytesToStr(base64.b32encode(binascii.unhexlify(pgpwords.hexify(pub.strip())))) - - def getBlockMetadataFromData(self, blockData): - ''' - accepts block contents as string, returns a tuple of - metadata, meta (meta being internal metadata, which will be - returned as an encrypted base64 string if it is encrypted, dict if not). - ''' - return blockmetadata.get_block_metadata_from_data(self, blockData) - - def processBlockMetadata(self, blockHash): - ''' - Read metadata from a block and cache it to the block database - ''' - return blockmetadata.process_block_metadata(self, blockHash) - def escapeAnsi(self, line): ''' Remove ANSI escape codes from a string with regex @@ -88,46 +53,12 @@ class OnionrUtils: ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]') return ansi_escape.sub('', line) - def hasBlock(self, hash): - ''' - Check for new block in the list - ''' - conn = sqlite3.connect(self._core.blockDB) - c = conn.cursor() - if not self.validateHash(hash): - raise Exception("Invalid hash") - for result in c.execute("SELECT COUNT() FROM hashes WHERE hash = ?", (hash,)): - if result[0] >= 1: - conn.commit() - conn.close() - return True - else: - conn.commit() - conn.close() - return False - - def hasKey(self, key): - ''' - Check for key in list of public keys - ''' - return key in self._core.listPeers() - def validateHash(self, data, length=64): ''' Validate if a string is a valid hash hex digest (does not compare, just checks length and charset) ''' return stringvalidators.validate_hash(self, data, length) - def validateMetadata(self, metadata, blockData): - '''Validate metadata meets onionr spec (does not validate proof value computation), take in either dictionary or json string''' - return validatemetadata.validate_metadata(self, metadata, blockData) - - def validatePubKey(self, key): - ''' - Validate if a string is a valid base32 encoded Ed25519 key - ''' - return stringvalidators.validate_pub_key(self, key) - def getEpoch(self): '''returns epoch''' return math.floor(time.time()) @@ -144,21 +75,6 @@ class OnionrUtils: ''' return basicrequests.do_get_request(self, url, port, proxyType, ignoreAPI, returnHeaders) - @staticmethod - def strToBytes(data): - try: - data = data.encode() - except AttributeError: - pass - return data - @staticmethod - def bytesToStr(data): - try: - data = data.decode() - except AttributeError: - pass - return data - def size(path='.'): ''' Returns the size of a folder's contents in bytes @@ -183,4 +99,22 @@ def humanSize(num, suffix='B'): if abs(num) < 1024.0: return "%.1f %s%s" % (num, unit, suffix) num /= 1024.0 - return "%.1f %s%s" % (num, 'Yi', suffix) \ No newline at end of file + return "%.1f %s%s" % (num, 'Yi', suffix) + +def has_block(core_inst, hash): + ''' + Check for new block in the list + ''' + conn = sqlite3.connect(core_inst.blockDB) + c = conn.cursor() + if not stringvalidators.validate_hash(hash): + raise Exception("Invalid hash") + for result in c.execute("SELECT COUNT() FROM hashes WHERE hash = ?", (hash,)): + if result[0] >= 1: + conn.commit() + conn.close() + return True + else: + conn.commit() + conn.close() + return False \ No newline at end of file diff --git a/onionr/onionrutils/basicrequests.py b/onionr/onionrutils/basicrequests.py index f9432928..e889b887 100644 --- a/onionr/onionrutils/basicrequests.py +++ b/onionr/onionrutils/basicrequests.py @@ -1,6 +1,5 @@ import requests import logger, onionrexceptions -from onionr import API_VERSION def do_post_request(utils_inst, url, data={}, port=0, proxyType='tor'): ''' Do a POST request through a local tor or i2p instance @@ -29,6 +28,7 @@ def do_get_request(utils_inst, url, port=0, proxyType='tor', ignoreAPI=False, re ''' Do a get request through a local tor or i2p instance ''' + API_VERSION = utils_inst._core.onionrInst.API_VERSION retData = False if proxyType == 'tor': if port == 0: diff --git a/onionr/onionrutils/blockmetadata.py b/onionr/onionrutils/blockmetadata.py index 0515ffc7..ace85190 100644 --- a/onionr/onionrutils/blockmetadata.py +++ b/onionr/onionrutils/blockmetadata.py @@ -2,8 +2,9 @@ import json import logger, onionrevents from onionrusers import onionrusers from etc import onionrvalues -from onionrblockapi import Block -def get_block_metadata_from_data(utils_inst, blockData): +import onionrblockapi +from . import epoch +def get_block_metadata_from_data(blockData): ''' accepts block contents as string, returns a tuple of metadata, meta (meta being internal metadata, which will be @@ -36,8 +37,8 @@ def process_block_metadata(utils_inst, blockHash): ''' Read metadata from a block and cache it to the block database ''' - curTime = utils_inst.getRoundedEpoch(roundS=60) - myBlock = Block(blockHash, utils_inst._core) + curTime = epoch.get_rounded_epoch(roundS=60) + myBlock = onionrblockapi.Block(blockHash, utils_inst._core) if myBlock.isEncrypted: myBlock.decrypt() if (myBlock.isEncrypted and myBlock.decrypted) or (not myBlock.isEncrypted): diff --git a/onionr/onionrutils/bytesconverter.py b/onionr/onionrutils/bytesconverter.py new file mode 100644 index 00000000..cf69a91d --- /dev/null +++ b/onionr/onionrutils/bytesconverter.py @@ -0,0 +1,13 @@ +def str_to_bytes(data): + try: + data = data.encode() + except AttributeError: + pass + return data + +def bytes_to_str(data): + try: + data = data.decode() + except AttributeError: + pass + return data \ No newline at end of file diff --git a/onionr/onionrutils/epoch.py b/onionr/onionrutils/epoch.py new file mode 100644 index 00000000..1ee8ae25 --- /dev/null +++ b/onionr/onionrutils/epoch.py @@ -0,0 +1,11 @@ +import math, time +def get_rounded_epoch(roundS=60): + ''' + Returns the epoch, rounded down to given seconds (Default 60) + ''' + epoch = get_epoch() + return epoch - (epoch % roundS) + +def get_epoch(self): + '''returns epoch''' + return math.floor(time.time()) \ No newline at end of file diff --git a/onionr/onionrutils/importnewblocks.py b/onionr/onionrutils/importnewblocks.py index a2c1e518..bfd985a2 100644 --- a/onionr/onionrutils/importnewblocks.py +++ b/onionr/onionrutils/importnewblocks.py @@ -1,5 +1,6 @@ import glob import logger, core +from onionrutils import blockmetadata def import_new_blocks(core_inst=None, scanDir=''): ''' This function is intended to scan for new blocks ON THE DISK and import them @@ -21,7 +22,7 @@ def import_new_blocks(core_inst=None, scanDir=''): if core_inst._crypto.sha3Hash(newBlock.read()) == block.replace('.dat', ''): core_inst.addToBlockDB(block.replace('.dat', ''), dataSaved=True) logger.info('Imported block %s.' % block) - core_inst._utils.processBlockMetadata(block) + blockmetadata.process_block_metadata(block) else: logger.warn('Failed to verify hash for %s' % block) if not exist: diff --git a/onionr/onionrutils/mnemonickeys.py b/onionr/onionrutils/mnemonickeys.py new file mode 100644 index 00000000..30c7b1c4 --- /dev/null +++ b/onionr/onionrutils/mnemonickeys.py @@ -0,0 +1,8 @@ +import base64 +from etc import pgpwords +def get_human_readable_ID(core_inst, pub=''): + '''gets a human readable ID from a public key''' + if pub == '': + pub = core_inst._crypto.pubKey + pub = base64.b16encode(base64.b32decode(pub)).decode() + return ' '.join(pgpwords.wordify(pub)) diff --git a/onionr/onionrutils/stringvalidators.py b/onionr/onionrutils/stringvalidators.py index 49f2c33a..cdfd2338 100644 --- a/onionr/onionrutils/stringvalidators.py +++ b/onionr/onionrutils/stringvalidators.py @@ -1,4 +1,4 @@ -import base64, string +import base64, string, onionrutils import unpaddedbase32, nacl.signing, nacl.encoding def validate_hash(utils_inst, data, length=64): ''' @@ -18,14 +18,14 @@ def validate_hash(utils_inst, data, length=64): return retVal -def validate_pub_key(utils_inst, key): +def validate_pub_key(key): ''' Validate if a string is a valid base32 encoded Ed25519 key ''' if type(key) is type(None): return False # Accept keys that have no = padding - key = unpaddedbase32.repad(utils_inst.strToBytes(key)) + key = unpaddedbase32.repad(onionrutils.str_to_bytes(key)) retVal = False try: diff --git a/onionr/onionrutils/validatemetadata.py b/onionr/onionrutils/validatemetadata.py index 8feb9859..aebd45e8 100644 --- a/onionr/onionrutils/validatemetadata.py +++ b/onionr/onionrutils/validatemetadata.py @@ -2,7 +2,7 @@ import json import logger, onionrexceptions from etc import onionrvalues from onionrutils import stringvalidators -def validate_metadata(utils_inst, metadata, blockData): +def validate_metadata(core_inst, metadata, blockData): '''Validate metadata meets onionr spec (does not validate proof value computation), take in either dictionary or json string''' # TODO, make this check sane sizes retData = False @@ -16,11 +16,11 @@ def validate_metadata(utils_inst, metadata, blockData): pass # Validate metadata dict for invalid keys to sizes that are too large - maxAge = utils_inst._core.config.get("general.max_block_age", onionrvalues.OnionrValues().default_expire) + maxAge = core_inst.config.get("general.max_block_age", onionrvalues.OnionrValues().default_expire) if type(metadata) is dict: for i in metadata: try: - utils_inst._core.requirements.blockMetadataLengths[i] + core_inst.requirements.blockMetadataLengths[i] except KeyError: logger.warn('Block has invalid metadata key ' + i) break @@ -30,25 +30,25 @@ def validate_metadata(utils_inst, metadata, blockData): testData = len(testData) except (TypeError, AttributeError) as e: testData = len(str(testData)) - if utils_inst._core.requirements.blockMetadataLengths[i] < testData: + if core_inst.requirements.blockMetadataLengths[i] < testData: logger.warn('Block metadata key ' + i + ' exceeded maximum size') break if i == 'time': if not stringvalidators.is_integer_string(metadata[i]): logger.warn('Block metadata time stamp is not integer string or int') break - isFuture = (metadata[i] - utils_inst.getEpoch()) + isFuture = (metadata[i] - core_inst.getEpoch()) if isFuture > maxClockDifference: logger.warn('Block timestamp is skewed to the future over the max %s: %s' (maxClockDifference, isFuture)) break - if (utils_inst.getEpoch() - metadata[i]) > maxAge: + if (core_inst.getEpoch() - metadata[i]) > maxAge: logger.warn('Block is outdated: %s' % (metadata[i],)) break elif i == 'expire': try: - assert int(metadata[i]) > utils_inst.getEpoch() + assert int(metadata[i]) > core_inst.getEpoch() except AssertionError: - logger.warn('Block is expired: %s less than %s' % (metadata[i], utils_inst.getEpoch())) + logger.warn('Block is expired: %s less than %s' % (metadata[i], core_inst.getEpoch())) break elif i == 'encryptType': try: @@ -59,9 +59,9 @@ def validate_metadata(utils_inst, metadata, blockData): else: # if metadata loop gets no errors, it does not break, therefore metadata is valid # make sure we do not have another block with the same data content (prevent data duplication and replay attacks) - nonce = utils_inst._core._utils.bytesToStr(utils_inst._core._crypto.sha3Hash(blockData)) + nonce = core_inst._utils.bytesToStr(core_inst._crypto.sha3Hash(blockData)) try: - with open(utils_inst._core.dataNonceFile, 'r') as nonceFile: + with open(core_inst.dataNonceFile, 'r') as nonceFile: if nonce in nonceFile.read(): retData = False # we've seen that nonce before, so we can't pass metadata raise onionrexceptions.DataExists diff --git a/onionr/static-data/default-plugins/encrypt/main.py b/onionr/static-data/default-plugins/encrypt/main.py index 8c874c99..5e5590ad 100755 --- a/onionr/static-data/default-plugins/encrypt/main.py +++ b/onionr/static-data/default-plugins/encrypt/main.py @@ -21,6 +21,7 @@ # Imports some useful libraries import logger, config, threading, time, datetime, sys, json from onionrblockapi import Block +from onionrutils import stringvalidators import onionrexceptions, onionrusers import locale locale.setlocale(locale.LC_ALL, '') @@ -43,7 +44,7 @@ class PlainEncryption: pass try: - if not self.api.get_core()._utils.validatePubKey(sys.argv[2]): + if not stringvalidators.validate_pub_key(sys.argv[2]): raise onionrexceptions.InvalidPubkey except (ValueError, IndexError) as e: logger.error("Peer public key not specified", terminal=True) diff --git a/onionr/static-data/default-plugins/esoteric/main.py b/onionr/static-data/default-plugins/esoteric/main.py index 1f4cdd32..0c6ae8fa 100755 --- a/onionr/static-data/default-plugins/esoteric/main.py +++ b/onionr/static-data/default-plugins/esoteric/main.py @@ -23,6 +23,7 @@ import locale, sys, os, threading, json locale.setlocale(locale.LC_ALL, '') import onionrservices, logger from onionrservices import bootstrapservice +from onionrutils import stringvalidators plugin_name = 'esoteric' PLUGIN_VERSION = '0.0.0' @@ -66,7 +67,7 @@ class Esoteric: def create(self): try: peer = sys.argv[2] - if not self.myCore._utils.validatePubKey(peer): + if not stringvalidators.validate_pub_key(peer): exit_with_error('Invalid public key specified') except IndexError: exit_with_error('You must specify a peer public key') diff --git a/onionr/static-data/default-plugins/metadataprocessor/main.py b/onionr/static-data/default-plugins/metadataprocessor/main.py index 333c68aa..4c730783 100755 --- a/onionr/static-data/default-plugins/metadataprocessor/main.py +++ b/onionr/static-data/default-plugins/metadataprocessor/main.py @@ -23,6 +23,7 @@ import logger, config import os, sys, json, time, random, shutil, base64, getpass, datetime, re from onionrblockapi import Block import onionrusers, onionrexceptions +from onionrutils import stringvalidators plugin_name = 'metadataprocessor' @@ -36,7 +37,7 @@ def _processForwardKey(api, myBlock): key = myBlock.getMetadata('newFSKey') # We don't need to validate here probably, but it helps - if api.get_utils().validatePubKey(key): + if stringvalidators.validate_pub_key(key): peer.addForwardKey(key) else: raise onionrexceptions.InvalidPubkey("%s is not a valid pubkey key" % (key,)) diff --git a/onionr/static-data/default-plugins/pluginmanager/main.py b/onionr/static-data/default-plugins/pluginmanager/main.py index 2f261bbf..bb21d35c 100755 --- a/onionr/static-data/default-plugins/pluginmanager/main.py +++ b/onionr/static-data/default-plugins/pluginmanager/main.py @@ -22,7 +22,7 @@ import logger, config import os, sys, json, time, random, shutil, base64, getpass, datetime, re from onionrblockapi import Block -from onionrutils import importnewblocks +from onionrutils import importnewblocks, stringvalidators, plugin_name = 'pluginmanager' @@ -399,13 +399,13 @@ def commandInstallPlugin(): valid_hash = pluginapi.get_utils().validateHash(pkobh) real_block = False - valid_key = pluginapi.get_utils().validatePubKey(pkobh) + valid_key = stringvalidators.validate_pub_key(pkobh) real_key = False if valid_hash: real_block = Block.exists(pkobh) elif valid_key: - real_key = pluginapi.get_utils().hasKey(pkobh) + real_key = pkobh in pluginapi.get_core().listPeers() blockhash = None @@ -493,7 +493,7 @@ def commandAddRepository(): pluginslist = dict() for pluginname, distributor in blockContent['plugins']: - if pluginapi.get_utils().validatePubKey(distributor): + if stringvalidators.validate_pub_key(distributor): pluginslist[pluginname] = distributor logger.debug('Found %s records in repository.' % len(pluginslist), terminal=True) diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py index e594c368..532320de 100755 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -23,6 +23,7 @@ import logger, config, threading, time, datetime from onionrblockapi import Block import onionrexceptions from onionrusers import onionrusers +from onionrutils import stringvalidators import locale, sys, os, json locale.setlocale(locale.LC_ALL, '') @@ -217,7 +218,7 @@ class OnionrMail: recip = logger.readline('Enter peer address, or -q to stop:').strip() if recip in ('-q', 'q'): raise EOFError - if not self.myCore._utils.validatePubKey(recip): + if not stringvalidators.validate_pub_key(recip): raise onionrexceptions.InvalidPubkey('Must be a valid ed25519 base32 encoded public key') except onionrexceptions.InvalidPubkey: logger.warn('Invalid public key', terminal=True) diff --git a/onionr/subprocesspow.py b/onionr/subprocesspow.py index 52a90c64..1ca2f9b1 100755 --- a/onionr/subprocesspow.py +++ b/onionr/subprocesspow.py @@ -23,6 +23,7 @@ import subprocess, os import multiprocessing, threading, time, json from multiprocessing import Pipe, Process import core, onionrblockapi, config, onionrutils, logger, onionrproofs +from onionrutils import bytesconverter class SubprocessPOW: def __init__(self, data, metadata, core_inst=None, subproc_count=None): @@ -51,7 +52,7 @@ class SubprocessPOW: # dump dict to measure bytes of json metadata. Cannot reuse later because the pow token must be added json_metadata = json.dumps(metadata).encode() - self.data = onionrutils.OnionrUtils.strToBytes(data) + self.data = bytesconverter.str_to_bytes(data) # Calculate difficulty. Dumb for now, may use good algorithm in the future. self.difficulty = onionrproofs.getDifficultyForNewBlock(bytes(json_metadata + b'\n' + self.data), coreInst=self.core_inst) @@ -111,7 +112,7 @@ class SubprocessPOW: payload = json.dumps(metadata).encode() + b'\n' + data # Check sha3_256 hash of block, compare to puzzle. Send payload if puzzle finished token = mcore._crypto.sha3Hash(payload) - token = onionrutils.OnionrUtils.bytesToStr(token) # ensure token is string + token = bytesconverter.bytes_to_str(token) # ensure token is string if puzzle == token[0:difficulty]: pipe.send(payload) break diff --git a/onionr/tests/test_highlevelcrypto.py b/onionr/tests/test_highlevelcrypto.py index 0a675ed7..aab2e198 100755 --- a/onionr/tests/test_highlevelcrypto.py +++ b/onionr/tests/test_highlevelcrypto.py @@ -4,6 +4,7 @@ sys.path.append(".") import unittest, uuid, hashlib, base64 import nacl.exceptions import nacl.signing, nacl.hash, nacl.encoding +from onionrutils import stringvalidators, mnemonickeys TEST_DIR = 'testdata/%s-%s' % (uuid.uuid4(), os.path.basename(__file__)) + '/' print("Test directory:", TEST_DIR) os.environ["ONIONR_HOME"] = TEST_DIR @@ -45,19 +46,12 @@ class OnionrCryptoTests(unittest.TestCase): self.assertEqual(crypto.sha3Hash(b'test'), normal) def valid_default_id(self): - self.assertTrue(c._utils.validatePubKey(crypto.pubKey)) + self.assertTrue(stringvalidators.validate_pub_key(crypto.pubKey)) def test_human_readable_length(self): - human = c._utils.getHumanReadableID() + human = mnemonickeys.get_human_readable_ID(c) self.assertTrue(len(human.split(' ')) == 32) - def test_human_readable_rebuild(self): - return # Broken right now - # Test if we can get the human readable id, and convert it back to valid base32 key - human = c._utils.getHumanReadableID() - unHuman = c._utils.convertHumanReadableID(human) - nacl.signing.VerifyKey(c._utils.convertHumanReadableID(human), encoder=nacl.encoding.Base32Encoder) - def test_safe_compare(self): self.assertTrue(crypto.safeCompare('test', 'test')) self.assertTrue(crypto.safeCompare('test', b'test')) @@ -130,7 +124,7 @@ class OnionrCryptoTests(unittest.TestCase): def test_deterministic(self): password = os.urandom(32) gen = crypto.generateDeterministic(password) - self.assertTrue(c._utils.validatePubKey(gen[0])) + self.assertTrue(stringvalidators.validate_pub_key(gen[0])) try: crypto.generateDeterministic('weakpassword') except onionrexceptions.PasswordStrengthError: @@ -151,6 +145,6 @@ class OnionrCryptoTests(unittest.TestCase): gen2 = crypto.generateDeterministic(password) self.assertFalse(gen == gen1) self.assertTrue(gen1 == gen2) - self.assertTrue(c._utils.validatePubKey(gen1[0])) + self.assertTrue(stringvalidators.validate_pub_key(gen1[0])) unittest.main() \ No newline at end of file From c7e06205b7f7ab80633484043f66f723dfc63a62 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 25 Jun 2019 18:07:35 -0500 Subject: [PATCH 120/173] OnionrUtils fully removed (but not fully bug free) flow now uses daemon thread for displaying output --- onionr/api.py | 24 ++-- onionr/communicator.py | 12 +- onionr/communicatorutils/announcenode.py | 5 +- onionr/communicatorutils/connectnewpeers.py | 4 +- onionr/communicatorutils/cooldownpeer.py | 7 +- onionr/communicatorutils/downloadblocks.py | 6 +- onionr/communicatorutils/housekeeping.py | 5 +- onionr/communicatorutils/lookupblocks.py | 8 +- onionr/communicatorutils/netcheck.py | 4 +- onionr/communicatorutils/uploadblocks.py | 11 +- onionr/core.py | 133 +++++++++--------- onionr/coredb/blockmetadb/add.py | 6 +- onionr/coredb/blockmetadb/expiredblocks.py | 3 +- onionr/coredb/daemonqueue/__init__.py | 4 +- onionr/coredb/keydb/listkeys.py | 3 +- onionr/httpapi/miscpublicapi/getblocks.py | 7 +- onionr/keymanager.py | 6 +- onionr/onionr.py | 2 +- onionr/onionrblacklist.py | 9 +- onionr/onionrblockapi.py | 4 +- onionr/onionrcommands/banblocks.py | 3 +- onionr/onionrcommands/exportblocks.py | 3 +- onionr/onionrcommands/filecommands.py | 3 +- onionr/onionrcrypto.py | 12 +- onionr/onionrpeers.py | 3 +- onionr/onionrpluginapi.py | 3 - onionr/onionrproofs.py | 5 +- onionr/onionrservices/__init__.py | 4 +- onionr/onionrservices/bootstrapservice.py | 4 +- onionr/onionrservices/connectionserver.py | 4 +- onionr/onionrstorage/__init__.py | 6 +- onionr/onionrstorage/removeblock.py | 5 +- onionr/onionrstorage/setdata.py | 2 +- onionr/onionrusers/contactmanager.py | 6 +- onionr/onionrusers/onionrusers.py | 19 ++- onionr/onionrutils/__init__.py | 120 ---------------- onionr/onionrutils/basicrequests.py | 8 +- onionr/onionrutils/blockmetadata.py | 41 ++++-- onionr/onionrutils/epoch.py | 2 +- onionr/onionrutils/escapeansi.py | 10 ++ onionr/onionrutils/stringvalidators.py | 7 +- onionr/onionrutils/validatemetadata.py | 10 +- .../default-plugins/esoteric/main.py | 8 +- .../static-data/default-plugins/flow/main.py | 7 +- .../default-plugins/pluginmanager/main.py | 8 +- .../default-plugins/pms/mailapi.py | 3 +- .../static-data/default-plugins/pms/main.py | 6 +- .../default-plugins/pms/sentboxdb.py | 3 +- onionr/utils/netutils.py | 5 +- onionr/utils/sizeutils.py | 27 ++++ 50 files changed, 280 insertions(+), 330 deletions(-) create mode 100644 onionr/onionrutils/escapeansi.py create mode 100644 onionr/utils/sizeutils.py diff --git a/onionr/api.py b/onionr/api.py index 7d8bf671..dcd912af 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -23,11 +23,12 @@ from gevent import Timeout import flask from flask import request, Response, abort, send_from_directory import core -import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config, onionrblockapi +import onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config, onionrblockapi import httpapi from httpapi import friendsapi, profilesapi, configapi, miscpublicapi from onionrservices import httpheaders import onionr +from onionrutils import bytesconverter, stringvalidators, epoch, mnemonickeys config.reload() class FDSafeHandler(WSGIHandler): @@ -98,7 +99,7 @@ class PublicAPI: resp = httpheaders.set_default_onionr_http_headers(resp) # Network API version resp.headers['X-API'] = onionr.API_VERSION - self.lastRequest = clientAPI._core._utils.getRoundedEpoch(roundS=5) + self.lastRequest = epoch.get_rounded_epoch(roundS=5) return resp @app.route('/') @@ -177,9 +178,8 @@ class API: self.debug = debug self._core = onionrInst.onionrCore - self.startTime = self._core._utils.getEpoch() + self.startTime = epoch.get_epoch() self._crypto = onionrcrypto.OnionrCrypto(self._core) - self._utils = onionrutils.OnionrUtils(self._core) app = flask.Flask(__name__) bindPort = int(config.get('client.client.port', 59496)) self.bindPort = bindPort @@ -334,7 +334,7 @@ class API: @app.route('/getblockbody/') def getBlockBodyData(name): resp = '' - if self._core._utils.validateHash(name): + if stringvalidators.validate_hash(name): try: resp = onionrblockapi.Block(name, decrypt=True).bcontent except TypeError: @@ -346,7 +346,7 @@ class API: @app.route('/getblockdata/') def getData(name): resp = "" - if self._core._utils.validateHash(name): + if stringvalidators.validate_hash(name): if name in self._core.getBlockList(): try: resp = self.getBlockData(name, decrypt=True) @@ -371,7 +371,7 @@ class API: def site(name): bHash = name resp = 'Not Found' - if self._core._utils.validateHash(bHash): + if stringvalidators.validate_hash(bHash): try: resp = onionrblockapi.Block(bHash).bcontent except onionrexceptions.NoDataAvailable: @@ -432,7 +432,7 @@ class API: @app.route('/getHumanReadable/') def getHumanReadable(name): - return Response(self._core._utils.getHumanReadableID(name)) + return Response(mnemonickeys.get_human_readable_ID(name)) @app.route('/insertblock', methods=['POST']) def insertBlock(): @@ -497,13 +497,13 @@ class API: def getUptime(self): while True: try: - return self._utils.getEpoch() - self.startTime + return epoch.get_epoch() - self.startTime except (AttributeError, NameError): # Don't error on race condition with startup pass def getBlockData(self, bHash, decrypt=False, raw=False, headerOnly=False): - assert self._core._utils.validateHash(bHash) + assert stringvalidators.validate_hash(bHash) bl = onionrblockapi.Block(bHash, core=self._core) if decrypt: bl.decrypt() @@ -520,8 +520,8 @@ class API: pass else: validSig = False - signer = onionrutils.bytes_to_str(bl.signer) - if bl.isSigned() and onionrutils.stringvalidators.validate_pub_key(signer) and bl.isSigner(signer): + signer = bytesconverter.bytes_to_str(bl.signer) + if bl.isSigned() and stringvalidators.validate_pub_key(signer) and bl.isSigner(signer): validSig = True bl.bheader['validSig'] = validSig bl.bheader['meta'] = '' diff --git a/onionr/communicator.py b/onionr/communicator.py index 57917b9a..521f93b6 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -27,7 +27,7 @@ from communicatorutils import downloadblocks, lookupblocks, lookupadders from communicatorutils import servicecreator, connectnewpeers, uploadblocks from communicatorutils import daemonqueuehandler, announcenode, deniableinserts from communicatorutils import cooldownpeer, housekeeping, netcheck -from onionrutils import localcommand +from onionrutils import localcommand, epoch, basicrequests from etc import humanreadabletime import onionrservices, onionr, onionrproofs @@ -91,7 +91,7 @@ class OnionrCommunicatorDaemon: plugins.reload() # time app started running for info/statistics purposes - self.startTime = self._core._utils.getEpoch() + self.startTime = epoch.get_epoch() if developmentMode: OnionrCommunicatorTimers(self, self.heartbeat, 30) @@ -310,9 +310,9 @@ class OnionrCommunicatorDaemon: if len(data) > 0: url += '&data=' + data - self._core.setAddressInfo(peer, 'lastConnectAttempt', self._core._utils.getEpoch()) # mark the time we're trying to request this peer + self._core.setAddressInfo(peer, 'lastConnectAttempt', epoch.get_epoch()) # mark the time we're trying to request this peer - retData = self._core._utils.doGetRequest(url, port=self.proxyPort) + retData = basicrequests.do_get_request(self._core, url, port=self.proxyPort) # if request failed, (error), mark peer offline if retData == False: try: @@ -324,7 +324,7 @@ class OnionrCommunicatorDaemon: except ValueError: pass else: - self._core.setAddressInfo(peer, 'lastConnect', self._core._utils.getEpoch()) + self._core.setAddressInfo(peer, 'lastConnect', epoch.get_epoch()) self.getPeerProfileInstance(peer).addScore(1) return retData # If returnHeaders, returns tuple of data, headers. if not, just data string @@ -341,7 +341,7 @@ class OnionrCommunicatorDaemon: return retData def getUptime(self): - return self._core._utils.getEpoch() - self.startTime + return epoch.get_epoch() - self.startTime def heartbeat(self): '''Show a heartbeat debug message''' diff --git a/onionr/communicatorutils/announcenode.py b/onionr/communicatorutils/announcenode.py index ef29c3c7..f72010f5 100755 --- a/onionr/communicatorutils/announcenode.py +++ b/onionr/communicatorutils/announcenode.py @@ -20,6 +20,7 @@ import base64 import onionrproofs, logger from etc import onionrvalues +from onionrutils import basicrequests def announce_node(daemon): '''Announce our node to our peers''' @@ -75,8 +76,8 @@ def announce_node(daemon): daemon.announceCache[peer] = data['random'] if not announceFail: logger.info('Announcing node to ' + url) - if daemon._core._utils.doPostRequest(url, data) == 'Success': - logger.info('Successfully introduced node to ' + peer) + if basicrequests.do_post_request(daemon._core, url, data) == 'Success': + logger.info('Successfully introduced node to ' + peer, terminal=True) retData = True daemon._core.setAddressInfo(peer, 'introduced', 1) daemon._core.setAddressInfo(peer, 'powValue', data['random']) diff --git a/onionr/communicatorutils/connectnewpeers.py b/onionr/communicatorutils/connectnewpeers.py index 25929c24..c3a9f77b 100755 --- a/onionr/communicatorutils/connectnewpeers.py +++ b/onionr/communicatorutils/connectnewpeers.py @@ -20,7 +20,7 @@ import time, sys import onionrexceptions, logger, onionrpeers from utils import networkmerger -from onionrutils import stringvalidators +from onionrutils import stringvalidators, epoch # secrets module was added into standard lib in 3.6+ if sys.version_info[0] == 3 and sys.version_info[1] < 6: from dependencies import secrets @@ -75,7 +75,7 @@ def connect_new_peer_to_communicator(comm_inst, peer='', useBootstrap=False): if address not in comm_inst.onlinePeers: logger.info('Connected to ' + address, terminal=True) comm_inst.onlinePeers.append(address) - comm_inst.connectTimes[address] = comm_inst._core._utils.getEpoch() + comm_inst.connectTimes[address] = epoch.get_epoch() retData = address # add peer to profile list if they're not in it diff --git a/onionr/communicatorutils/cooldownpeer.py b/onionr/communicatorutils/cooldownpeer.py index 26cf2dd9..33bcc277 100755 --- a/onionr/communicatorutils/cooldownpeer.py +++ b/onionr/communicatorutils/cooldownpeer.py @@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' +from onionrutils import epoch def cooldown_peer(comm_inst): '''Randomly add an online peer to cooldown, so we can connect a new one''' onlinePeerAmount = len(comm_inst.onlinePeers) @@ -28,7 +29,7 @@ def cooldown_peer(comm_inst): # Remove peers from cooldown that have been there long enough tempCooldown = dict(comm_inst.cooldownPeer) for peer in tempCooldown: - if (comm_inst._core._utils.getEpoch() - tempCooldown[peer]) >= cooldownTime: + if (epoch.get_epoch() - tempCooldown[peer]) >= cooldownTime: del comm_inst.cooldownPeer[peer] # Cool down a peer, if we have max connections alive for long enough @@ -38,7 +39,7 @@ def cooldown_peer(comm_inst): while finding: try: toCool = min(tempConnectTimes, key=tempConnectTimes.get) - if (comm_inst._core._utils.getEpoch() - tempConnectTimes[toCool]) < minTime: + if (epoch.get_epoch() - tempConnectTimes[toCool]) < minTime: del tempConnectTimes[toCool] else: finding = False @@ -46,6 +47,6 @@ def cooldown_peer(comm_inst): break else: comm_inst.removeOnlinePeer(toCool) - comm_inst.cooldownPeer[toCool] = comm_inst._core._utils.getEpoch() + comm_inst.cooldownPeer[toCool] = epoch.get_epoch() comm_inst.decrementThreadCount('cooldown_peer') \ No newline at end of file diff --git a/onionr/communicatorutils/downloadblocks.py b/onionr/communicatorutils/downloadblocks.py index ccf121c5..ecd7b873 100755 --- a/onionr/communicatorutils/downloadblocks.py +++ b/onionr/communicatorutils/downloadblocks.py @@ -19,7 +19,7 @@ ''' import communicator, onionrexceptions import logger, onionrpeers -from onionrutils import blockmetadata +from onionrutils import blockmetadata, stringvalidators, validatemetadata def download_blocks_from_communicator(comm_inst): assert isinstance(comm_inst, communicator.OnionrCommunicatorDaemon) @@ -48,7 +48,7 @@ def download_blocks_from_communicator(comm_inst): continue if comm_inst._core._blacklist.inBlacklist(blockHash): continue - if comm_inst._core._utils.storageCounter.isFull(): + if comm_inst._core.storage_counter.isFull(): break comm_inst.currentDownloading.append(blockHash) # So we can avoid concurrent downloading in other threads of same block if len(blockPeers) == 0: @@ -75,7 +75,7 @@ def download_blocks_from_communicator(comm_inst): content = content.decode() # decode here because sha3Hash needs bytes above metas = blockmetadata.get_block_metadata_from_data(content) # returns tuple(metadata, meta), meta is also in metadata metadata = metas[0] - if comm_inst._core._utils.validateMetadata(metadata, metas[2]): # check if metadata is valid, and verify nonce + if validatemetadata.validate_metadata(comm_inist._core, metadata, metas[2]): # check if metadata is valid, and verify nonce if comm_inst._core._crypto.verifyPow(content): # check if POW is enough/correct logger.info('Attempting to save block %s...' % blockHash[:12]) try: diff --git a/onionr/communicatorutils/housekeeping.py b/onionr/communicatorutils/housekeeping.py index 829564ab..3071a994 100755 --- a/onionr/communicatorutils/housekeeping.py +++ b/onionr/communicatorutils/housekeeping.py @@ -20,6 +20,7 @@ import sqlite3 import logger from onionrusers import onionrusers +from onionrutils import epoch def clean_old_blocks(comm_inst): '''Delete old blocks if our disk allocation is full/near full, and also expired blocks''' @@ -29,7 +30,7 @@ def clean_old_blocks(comm_inst): comm_inst._core.removeBlock(bHash) logger.info('Deleted block: %s' % (bHash,)) - while comm_inst._core._utils.storageCounter.isFull(): + while comm_inst._core.storage_counter.isFull(): oldest = comm_inst._core.getBlockList()[0] comm_inst._core._blacklist.addToDB(oldest) comm_inst._core.removeBlock(oldest) @@ -41,7 +42,7 @@ def clean_keys(comm_inst): '''Delete expired forward secrecy keys''' conn = sqlite3.connect(comm_inst._core.peerDB, timeout=10) c = conn.cursor() - time = comm_inst._core._utils.getEpoch() + time = epoch.get_epoch() deleteKeys = [] for entry in c.execute("SELECT * FROM forwardKeys WHERE expire <= ?", (time,)): diff --git a/onionr/communicatorutils/lookupblocks.py b/onionr/communicatorutils/lookupblocks.py index 490a051e..7fae6522 100755 --- a/onionr/communicatorutils/lookupblocks.py +++ b/onionr/communicatorutils/lookupblocks.py @@ -18,6 +18,8 @@ along with this program. If not, see . ''' import logger, onionrproofs +from onionrutils import stringvalidators, epoch + def lookup_blocks_from_communicator(comm_inst): logger.info('Looking up new blocks...') tryAmount = 2 @@ -34,7 +36,7 @@ def lookup_blocks_from_communicator(comm_inst): if not comm_inst.isOnline: break # check if disk allocation is used - if comm_inst._core._utils.storageCounter.isFull(): + if comm_inst._core.storage_counter.isFull(): logger.debug('Not looking up new blocks due to maximum amount of allowed disk space used') break peer = comm_inst.pickOnlinePeer() # select random online peer @@ -60,11 +62,11 @@ def lookup_blocks_from_communicator(comm_inst): logger.warn('Could not get new blocks from %s.' % peer, error = error) newBlocks = False else: - comm_inst.dbTimestamps[peer] = comm_inst._core._utils.getRoundedEpoch(roundS=60) + comm_inst.dbTimestamps[peer] = epoch.get_rounded_epoch(roundS=60) if newBlocks != False: # if request was a success for i in newBlocks.split('\n'): - if comm_inst._core._utils.validateHash(i): + if stringvalidators.validate_hash(i): # if newline seperated string is valid hash if not i in existingBlocks: # if block does not exist on disk and is not already in block queue diff --git a/onionr/communicatorutils/netcheck.py b/onionr/communicatorutils/netcheck.py index 688feeea..85d10605 100755 --- a/onionr/communicatorutils/netcheck.py +++ b/onionr/communicatorutils/netcheck.py @@ -20,14 +20,14 @@ ''' import logger from utils import netutils -from onionrutils import localcommand +from onionrutils import localcommand, epoch def net_check(comm_inst): '''Check if we are connected to the internet or not when we can't connect to any peers''' rec = False # for detecting if we have received incoming connections recently c = comm_inst._core if len(comm_inst.onlinePeers) == 0: try: - if (c._utils.getEpoch() - int(localcommand.local_command(c, '/lastconnect'))) <= 60: + if (epoch.get_epoch() - int(localcommand.local_command(c, '/lastconnect'))) <= 60: comm_inst.isOnline = True rec = True except ValueError: diff --git a/onionr/communicatorutils/uploadblocks.py b/onionr/communicatorutils/uploadblocks.py index 497e07d5..afe96525 100755 --- a/onionr/communicatorutils/uploadblocks.py +++ b/onionr/communicatorutils/uploadblocks.py @@ -20,16 +20,17 @@ import logger from communicatorutils import proxypicker import onionrblockapi as block -from onionrutils import localcommand +from onionrutils import localcommand, stringvalidators, basicrequests def upload_blocks_from_communicator(comm_inst): # when inserting a block, we try to upload it to a few peers to add some deniability triedPeers = [] finishedUploads = [] - comm_inst.blocksToUpload = comm_inst._core._crypto.randomShuffle(comm_inst.blocksToUpload) + core = comm_inst._core + comm_inst.blocksToUpload = core._crypto.randomShuffle(comm_inst.blocksToUpload) if len(comm_inst.blocksToUpload) != 0: for bl in comm_inst.blocksToUpload: - if not comm_inst._core._utils.validateHash(bl): + if not stringvalidators.validate_hash(bl): logger.warn('Requested to upload invalid block') comm_inst.decrementThreadCount('uploadBlock') return @@ -42,8 +43,8 @@ def upload_blocks_from_communicator(comm_inst): data = {'block': block.Block(bl).getRaw()} proxyType = proxypicker.pick_proxy(peer) logger.info("Uploading block to " + peer) - if not comm_inst._core._utils.doPostRequest(url, data=data, proxyType=proxyType) == False: - localcommand.local_command(comm_inst._core, 'waitforshare/' + bl, post=True) + if not basicrequests.do_post_request(core, url, data=data, proxyType=proxyType) == False: + localcommand.local_command(core, 'waitforshare/' + bl, post=True) finishedUploads.append(bl) for x in finishedUploads: try: diff --git a/onionr/core.py b/onionr/core.py index 96068a66..0d64877a 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -30,6 +30,7 @@ import dbcreator, onionrstorage, serializeddata, subprocesspow from etc import onionrvalues, powchoice from onionrutils import localcommand, stringvalidators, bytesconverter, epoch from onionrutils import blockmetadata +import storagecounter class Core: def __init__(self, torPort=0): @@ -41,76 +42,76 @@ class Core: if not self.dataDir.endswith('/'): self.dataDir += '/' - try: - self.onionrInst = None - self.queueDB = self.dataDir + 'queue.db' - self.peerDB = self.dataDir + 'peers.db' - self.blockDB = self.dataDir + 'blocks.db' - self.blockDataLocation = self.dataDir + 'blocks/' - self.blockDataDB = self.blockDataLocation + 'block-data.db' - self.publicApiHostFile = self.dataDir + 'public-host.txt' - self.privateApiHostFile = self.dataDir + 'private-host.txt' - self.addressDB = self.dataDir + 'address.db' - self.hsAddress = '' - self.i2pAddress = config.get('i2p.own_addr', None) - self.bootstrapFileLocation = 'static-data/bootstrap-nodes.txt' - self.bootstrapList = [] - self.requirements = onionrvalues.OnionrValues() - self.torPort = torPort - self.dataNonceFile = self.dataDir + 'block-nonces.dat' - self.dbCreate = dbcreator.DBCreator(self) - self.forwardKeysFile = self.dataDir + 'forward-keys.db' - self.keyStore = simplekv.DeadSimpleKV(self.dataDir + 'cachedstorage.dat', refresh_seconds=5) - - # Socket data, defined here because of multithreading constraints with gevent - self.killSockets = False - self.startSocket = {} - self.socketServerConnData = {} - self.socketReasons = {} - self.socketServerResponseData = {} + #try: + self.usageFile = self.dataDir + 'disk-usage.txt' + self.config = config + self.maxBlockSize = 10000000 # max block size in bytes - self.usageFile = self.dataDir + 'disk-usage.txt' - self.config = config + self.onionrInst = None + self.queueDB = self.dataDir + 'queue.db' + self.peerDB = self.dataDir + 'peers.db' + self.blockDB = self.dataDir + 'blocks.db' + self.blockDataLocation = self.dataDir + 'blocks/' + self.blockDataDB = self.blockDataLocation + 'block-data.db' + self.publicApiHostFile = self.dataDir + 'public-host.txt' + self.privateApiHostFile = self.dataDir + 'private-host.txt' + self.addressDB = self.dataDir + 'address.db' + self.hsAddress = '' + self.i2pAddress = config.get('i2p.own_addr', None) + self.bootstrapFileLocation = 'static-data/bootstrap-nodes.txt' + self.bootstrapList = [] + self.requirements = onionrvalues.OnionrValues() + self.torPort = torPort + self.dataNonceFile = self.dataDir + 'block-nonces.dat' + self.dbCreate = dbcreator.DBCreator(self) + self.forwardKeysFile = self.dataDir + 'forward-keys.db' + self.keyStore = simplekv.DeadSimpleKV(self.dataDir + 'cachedstorage.dat', refresh_seconds=5) + self.storage_counter = storagecounter.StorageCounter(self) + + # Socket data, defined here because of multithreading constraints with gevent + self.killSockets = False + self.startSocket = {} + self.socketServerConnData = {} + self.socketReasons = {} + self.socketServerResponseData = {} - self.maxBlockSize = 10000000 # max block size in bytes + if not os.path.exists(self.dataDir): + os.mkdir(self.dataDir) + if not os.path.exists(self.dataDir + 'blocks/'): + os.mkdir(self.dataDir + 'blocks/') + if not os.path.exists(self.blockDB): + self.createBlockDB() + if not os.path.exists(self.forwardKeysFile): + self.dbCreate.createForwardKeyDB() + if not os.path.exists(self.peerDB): + self.createPeerDB() + if not os.path.exists(self.addressDB): + self.createAddressDB() - if not os.path.exists(self.dataDir): - os.mkdir(self.dataDir) - if not os.path.exists(self.dataDir + 'blocks/'): - os.mkdir(self.dataDir + 'blocks/') - if not os.path.exists(self.blockDB): - self.createBlockDB() - if not os.path.exists(self.forwardKeysFile): - self.dbCreate.createForwardKeyDB() - if not os.path.exists(self.peerDB): - self.createPeerDB() - if not os.path.exists(self.addressDB): - self.createAddressDB() + if os.path.exists(self.dataDir + '/hs/hostname'): + with open(self.dataDir + '/hs/hostname', 'r') as hs: + self.hsAddress = hs.read().strip() - if os.path.exists(self.dataDir + '/hs/hostname'): - with open(self.dataDir + '/hs/hostname', 'r') as hs: - self.hsAddress = hs.read().strip() + # Load bootstrap address list + if os.path.exists(self.bootstrapFileLocation): + with open(self.bootstrapFileLocation, 'r') as bootstrap: + bootstrap = bootstrap.read() + for i in bootstrap.split('\n'): + self.bootstrapList.append(i) + else: + logger.warn('Warning: address bootstrap file not found ' + self.bootstrapFileLocation) - # Load bootstrap address list - if os.path.exists(self.bootstrapFileLocation): - with open(self.bootstrapFileLocation, 'r') as bootstrap: - bootstrap = bootstrap.read() - for i in bootstrap.split('\n'): - self.bootstrapList.append(i) - else: - logger.warn('Warning: address bootstrap file not found ' + self.bootstrapFileLocation) + self.use_subprocess = powchoice.use_subprocess(self) + # Initialize the crypto object + self._crypto = onionrcrypto.OnionrCrypto(self) + self._blacklist = onionrblacklist.OnionrBlackList(self) + self.serializer = serializeddata.SerializedData(self) - self.use_subprocess = powchoice.use_subprocess(self) - self._utils = onionrutils.OnionrUtils(self) - # Initialize the crypto object - self._crypto = onionrcrypto.OnionrCrypto(self) - self._blacklist = onionrblacklist.OnionrBlackList(self) - self.serializer = serializeddata.SerializedData(self) - - except Exception as error: - logger.error('Failed to initialize core Onionr library.', error=error) - logger.fatal('Cannot recover from error.') - sys.exit(1) + # except Exception as error: + # print(str(error)) + # logger.error('Failed to initialize core Onionr library.', error=error, terminal=True) + # logger.fatal('Cannot recover from error.', terminal=True) + # sys.exit(1) return def refreshFirstStartVars(self): @@ -313,7 +314,7 @@ class Core: encryptType must be specified to encrypt a block ''' allocationReachedMessage = 'Cannot insert block, disk allocation reached.' - if self._utils.storageCounter.isFull(): + if self.storage_counter.isFull(): logger.error(allocationReachedMessage) return False retData = False @@ -439,7 +440,7 @@ class Core: localcommand.local_command(self, '/waitforshare/' + retData, post=True, maxWait=5) self.daemonQueueAdd('uploadBlock', retData) self.addToBlockDB(retData, selfInsert=True, dataSaved=True) - blockmetadata.process_block_metadata(retData) + blockmetadata.process_block_metadata(self, retData) if retData != False: if plaintextPeer == onionrvalues.DENIABLE_PEER_ADDRESS: diff --git a/onionr/coredb/blockmetadb/add.py b/onionr/coredb/blockmetadb/add.py index 69ac9b27..bdf9457f 100644 --- a/onionr/coredb/blockmetadb/add.py +++ b/onionr/coredb/blockmetadb/add.py @@ -1,5 +1,5 @@ import os, sqlite3 -import onionrutils +from onionrutils import epoch, blockmetadata def add_to_block_DB(core_inst, newHash, selfInsert=False, dataSaved=False): ''' Add a hash value to the block db @@ -9,11 +9,11 @@ def add_to_block_DB(core_inst, newHash, selfInsert=False, dataSaved=False): if not os.path.exists(core_inst.blockDB): raise Exception('Block db does not exist') - if onionrutils.has_block(core_inst, newHash): + if blockmetadata.has_block(core_inst, newHash): return conn = sqlite3.connect(core_inst.blockDB, timeout=30) c = conn.cursor() - currentTime = core_inst._utils.getEpoch() + core_inst._crypto.secrets.randbelow(301) + currentTime = epoch.get_epoch() + core_inst._crypto.secrets.randbelow(301) if selfInsert or dataSaved: selfInsert = 1 else: diff --git a/onionr/coredb/blockmetadb/expiredblocks.py b/onionr/coredb/blockmetadb/expiredblocks.py index 8debbe26..0b102fe6 100644 --- a/onionr/coredb/blockmetadb/expiredblocks.py +++ b/onionr/coredb/blockmetadb/expiredblocks.py @@ -1,9 +1,10 @@ import sqlite3 +from onionrutils import epoch def get_expired_blocks(core_inst): '''Returns a list of expired blocks''' conn = sqlite3.connect(core_inst.blockDB, timeout=30) c = conn.cursor() - date = int(core_inst._utils.getEpoch()) + date = int(epoch.get_epoch()) execute = 'SELECT hash FROM hashes WHERE expire <= %s ORDER BY dateReceived;' % (date,) diff --git a/onionr/coredb/daemonqueue/__init__.py b/onionr/coredb/daemonqueue/__init__.py index a2f7a483..252cbc37 100644 --- a/onionr/coredb/daemonqueue/__init__.py +++ b/onionr/coredb/daemonqueue/__init__.py @@ -1,6 +1,6 @@ import sqlite3, os import onionrevents as events -from onionrutils import localcommand +from onionrutils import localcommand, epoch def daemon_queue(core_inst): ''' @@ -38,7 +38,7 @@ def daemon_queue_add(core_inst, command, data='', responseID=''): retData = True - date = core_inst._utils.getEpoch() + date = epoch.get_epoch() conn = sqlite3.connect(core_inst.queueDB, timeout=30) c = conn.cursor() t = (command, data, date, responseID) diff --git a/onionr/coredb/keydb/listkeys.py b/onionr/coredb/keydb/listkeys.py index 5ab44fb9..3ba23678 100644 --- a/onionr/coredb/keydb/listkeys.py +++ b/onionr/coredb/keydb/listkeys.py @@ -1,5 +1,6 @@ import sqlite3 import logger +from onionrutils import epoch def list_peers(core_inst, randomOrder=True, getPow=False, trust=0): ''' Return a list of public keys (misleading function name) @@ -56,7 +57,7 @@ def list_adders(core_inst, randomOrder=True, i2p=True, recent=0): testList = list(addressList) # create new list to iterate for address in testList: try: - if recent > 0 and (core_inst._utils.getEpoch() - core_inst.getAddressInfo(address, 'lastConnect')) > recent: + if recent > 0 and (epoch.get_epoch() - core_inst.getAddressInfo(address, 'lastConnect')) > recent: raise TypeError # If there is no last-connected date or it was too long ago, don't add peer to list if recent is not 0 except TypeError: addressList.remove(address) diff --git a/onionr/httpapi/miscpublicapi/getblocks.py b/onionr/httpapi/miscpublicapi/getblocks.py index c1e58e4f..08d9f678 100755 --- a/onionr/httpapi/miscpublicapi/getblocks.py +++ b/onionr/httpapi/miscpublicapi/getblocks.py @@ -18,7 +18,8 @@ along with this program. If not, see . ''' from flask import Response, abort -import config, onionrutils +import config +from onionrutils import bytesconverter, stringvalidators def get_public_block_list(clientAPI, publicAPI, request): # Provide a list of our blocks, with a date offset dateAdjust = request.args.get('date') @@ -33,7 +34,7 @@ def get_public_block_list(clientAPI, publicAPI, request): def get_block_data(clientAPI, publicAPI, data): '''data is the block hash in hex''' resp = '' - if clientAPI._utils.validateHash(data): + if stringvalidators.validate_hash(data): if not clientAPI._core.config.get('general.hide_created_blocks', True) or data not in publicAPI.hideBlocks: if data in clientAPI._core.getBlockList(): block = clientAPI.getBlockData(data, raw=True) @@ -41,7 +42,7 @@ def get_block_data(clientAPI, publicAPI, data): block = block.encode() # Encode in case data is binary except AttributeError: abort(404) - block = onionrutils.str_to_bytes(block) + block = bytesconverter.str_to_bytes(block) resp = block if len(resp) == 0: abort(404) diff --git a/onionr/keymanager.py b/onionr/keymanager.py index 9e2a1703..a288d87e 100755 --- a/onionr/keymanager.py +++ b/onionr/keymanager.py @@ -17,20 +17,20 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' +from onionrutils import bytesconverter import onionrcrypto class KeyManager: def __init__(self, crypto): assert isinstance(crypto, onionrcrypto.OnionrCrypto) self._core = crypto._core - self._utils = self._core._utils self.keyFile = crypto._keyFile self.crypto = crypto def addKey(self, pubKey=None, privKey=None): if type(pubKey) is type(None) and type(privKey) is type(None): pubKey, privKey = self.crypto.generatePubKey() - pubKey = self.crypto._core._utils.bytesToStr(pubKey) - privKey = self.crypto._core._utils.bytesToStr(privKey) + pubKey = bytesconverter.bytes_to_str(pubKey) + privKey = bytesconverter.bytes_to_str(privKey) try: if pubKey in self.getPubkeyList(): raise ValueError('Pubkey already in list: %s' % (pubKey,)) diff --git a/onionr/onionr.py b/onionr/onionr.py index 74c0c335..85a34020 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -32,7 +32,6 @@ if sys.version_info[0] == 2 or sys.version_info[1] < MIN_PY_VERSION: import os, base64, random, shutil, time, platform, signal from threading import Thread import api, core, config, logger, onionrplugins as plugins, onionrevents as events -import onionrutils import netcontroller from netcontroller import NetController from onionrblockapi import Block @@ -51,6 +50,7 @@ class Onionr: 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.API_VERSION = API_VERSION self.userRunDir = os.getcwd() # Directory user runs the program from self.killed = False diff --git a/onionr/onionrblacklist.py b/onionr/onionrblacklist.py index 9c4e99d7..03da61b4 100755 --- a/onionr/onionrblacklist.py +++ b/onionr/onionrblacklist.py @@ -18,6 +18,7 @@ along with this program. If not, see . ''' import sqlite3, os, logger +from onionrutils import epoch, bytesconverter class OnionrBlackList: def __init__(self, coreInst): self.blacklistDB = coreInst.dataDir + 'blacklist.db' @@ -28,7 +29,7 @@ class OnionrBlackList: return def inBlacklist(self, data): - hashed = self._core._utils.bytesToStr(self._core._crypto.sha3Hash(data)) + hashed = bytesconverter.bytes_to_str(self._core._crypto.sha3Hash(data)) retData = False if not hashed.isalnum(): @@ -56,7 +57,7 @@ class OnionrBlackList: def deleteExpired(self, dataType=0): '''Delete expired entries''' deleteList = [] - curTime = self._core._utils.getEpoch() + curTime = epoch.get_epoch() try: int(dataType) @@ -98,7 +99,7 @@ class OnionrBlackList: 2=pubkey ''' # we hash the data so we can remove data entirely from our node's disk - hashed = self._core._utils.bytesToStr(self._core._crypto.sha3Hash(data)) + hashed = bytesconverter.bytes_to_str(self._core._crypto.sha3Hash(data)) if len(hashed) > 64: raise Exception("Hashed data is too large") @@ -115,7 +116,7 @@ class OnionrBlackList: if self.inBlacklist(hashed): return insert = (hashed,) - blacklistDate = self._core._utils.getEpoch() + blacklistDate = epoch.get_epoch() try: self._dbExecute("INSERT INTO blacklist (hash, dataType, blacklistDate, expire) VALUES(?, ?, ?, ?);", (str(hashed), dataType, blacklistDate, expire)) except sqlite3.IntegrityError: diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py index cc823a1f..8464659f 100755 --- a/onionr/onionrblockapi.py +++ b/onionr/onionrblockapi.py @@ -21,7 +21,7 @@ import core as onionrcore, logger, config, onionrexceptions, nacl.exceptions import json, os, sys, datetime, base64, onionrstorage from onionrusers import onionrusers -from onionrutils import stringvalidators +from onionrutils import stringvalidators, epoch class Block: blockCacheOrder = list() # NEVER write your own code that writes to this! @@ -89,7 +89,7 @@ class Block: # Check for replay attacks try: - if self.core._utils.getEpoch() - self.core.getBlockDate(self.hash) < 60: + if epoch.get_epoch() - self.core.getBlockDate(self.hash) < 60: assert self.core._crypto.replayTimestampValidation(self.bmetadata['rply']) except (AssertionError, KeyError, TypeError) as e: if not self.bypassReplayCheck: diff --git a/onionr/onionrcommands/banblocks.py b/onionr/onionrcommands/banblocks.py index fe50d16a..426e852e 100755 --- a/onionr/onionrcommands/banblocks.py +++ b/onionr/onionrcommands/banblocks.py @@ -19,12 +19,13 @@ ''' import sys import logger +from onionrutils import stringvalidators def ban_block(o_inst): try: ban = sys.argv[2] except IndexError: ban = logger.readline('Enter a block hash:') - if o_inst.onionrUtils.validateHash(ban): + if stringvalidators.validate_hash(ban): if not o_inst.onionrCore._blacklist.inBlacklist(ban): try: o_inst.onionrCore._blacklist.addToDB(ban) diff --git a/onionr/onionrcommands/exportblocks.py b/onionr/onionrcommands/exportblocks.py index daa70f35..a941e025 100755 --- a/onionr/onionrcommands/exportblocks.py +++ b/onionr/onionrcommands/exportblocks.py @@ -19,6 +19,7 @@ ''' import sys, os import logger, onionrstorage +from onionrutils import stringvalidators def doExport(o_inst, bHash): exportDir = o_inst.dataDir + 'block-export/' if not os.path.exists(exportDir): @@ -34,7 +35,7 @@ def doExport(o_inst, bHash): def export_block(o_inst): exportDir = o_inst.dataDir + 'block-export/' try: - assert o_inst.onionrUtils.validateHash(sys.argv[2]) + assert stringvalidators.validate_hash(sys.argv[2]) except (IndexError, AssertionError): logger.error('No valid block hash specified.', terminal=True) sys.exit(1) diff --git a/onionr/onionrcommands/filecommands.py b/onionr/onionrcommands/filecommands.py index 8f97f90a..7e2cd086 100755 --- a/onionr/onionrcommands/filecommands.py +++ b/onionr/onionrcommands/filecommands.py @@ -21,6 +21,7 @@ import base64, sys, os import logger from onionrblockapi import Block +from onionrutils import stringvalidators def add_file(o_inst, singleBlock=False, blockType='bin'): ''' Adds a file to the onionr network @@ -60,7 +61,7 @@ def getFile(o_inst): if os.path.exists(fileName): logger.error("File already exists", terminal=True) return - if not o_inst.onionrUtils.validateHash(bHash): + if not stringvalidators.validate_hash(bHash): logger.error('Block hash is invalid', terminal=True) return diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py index c6dfe300..957733eb 100755 --- a/onionr/onionrcrypto.py +++ b/onionr/onionrcrypto.py @@ -21,7 +21,7 @@ import os, binascii, base64, hashlib, time, sys, hmac, secrets import nacl.signing, nacl.encoding, nacl.public, nacl.hash, nacl.pwhash, nacl.utils, nacl.secret import unpaddedbase32 import logger, onionrproofs -from onionrutils import stringvalidators +from onionrutils import stringvalidators, epoch, bytesconverter import onionrexceptions, keymanager, core, onionrutils import config config.reload() @@ -95,10 +95,10 @@ class OnionrCrypto: def pubKeyEncrypt(self, data, pubkey, encodedData=False): '''Encrypt to a public key (Curve25519, taken from base32 Ed25519 pubkey)''' - pubkey = unpaddedbase32.repad(onionrutils.str_to_bytes(pubkey)) + pubkey = unpaddedbase32.repad(bytesconverter.str_to_bytes(pubkey)) retVal = '' box = None - data = onionrutils.str_to_bytes(data) + data = bytesconverter.str_to_bytes(data) pubkey = nacl.signing.VerifyKey(pubkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_public_key() @@ -182,7 +182,7 @@ class OnionrCrypto: def generateDeterministic(self, passphrase, bypassCheck=False): '''Generate a Ed25519 public key pair from a password''' passStrength = self.deterministicRequirement - passphrase = onionrutils.str_to_bytes(passphrase) # Convert to bytes if not already + passphrase = bytesconverter.str_to_bytes(passphrase) # Convert to bytes if not already # Validate passphrase length if not bypassCheck: if len(passphrase) < passStrength: @@ -202,7 +202,7 @@ class OnionrCrypto: if pubkey == '': pubkey = self.pubKey prev = '' - pubkey = onionrutils.str_to_bytes(pubkey) + pubkey = bytesconverter.str_to_bytes(pubkey) for i in range(self.HASH_ID_ROUNDS): try: prev = prev.encode() @@ -266,7 +266,7 @@ class OnionrCrypto: @staticmethod def replayTimestampValidation(timestamp): - if core.Core()._utils.getEpoch() - int(timestamp) > 2419200: + if epoch.get_epoch() - int(timestamp) > 2419200: return False else: return True diff --git a/onionr/onionrpeers.py b/onionr/onionrpeers.py index 8bf76289..d0c0c5cb 100755 --- a/onionr/onionrpeers.py +++ b/onionr/onionrpeers.py @@ -19,6 +19,7 @@ ''' import sqlite3 import core, config, logger +from onionrutils import epoch config.reload() class PeerProfiles: ''' @@ -106,7 +107,7 @@ def peerCleanup(coreInst): if PeerProfiles(address, coreInst).score < minScore: coreInst.removeAddress(address) try: - if (int(coreInst._utils.getEpoch()) - int(coreInst.getPeerInfo(address, 'dateSeen'))) >= 600: + if (int(epoch.get_epoch()) - int(coreInst.getPeerInfo(address, 'dateSeen'))) >= 600: expireTime = 600 else: expireTime = 86400 diff --git a/onionr/onionrpluginapi.py b/onionr/onionrpluginapi.py index 24ba4f12..9b9842c3 100755 --- a/onionr/onionrpluginapi.py +++ b/onionr/onionrpluginapi.py @@ -170,9 +170,6 @@ class pluginapi: def get_core(self): return self.core - def get_utils(self): - return self.get_core()._utils - def get_crypto(self): return self.get_core()._crypto diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index 7b8aa91a..51f35325 100755 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -30,10 +30,7 @@ def getDifficultyModifier(coreOrUtilsInst=None): ''' classInst = coreOrUtilsInst retData = 0 - if isinstance(classInst, core.Core): - useFunc = classInst._utils.storageCounter.getPercent - else: - useFunc = core.Core()._utils.storageCounter.getPercent + useFunc = classInst.storage_counter.getPercent percentUse = useFunc() diff --git a/onionr/onionrservices/__init__.py b/onionr/onionrservices/__init__.py index 1c23e5fb..1f56dabe 100755 --- a/onionr/onionrservices/__init__.py +++ b/onionr/onionrservices/__init__.py @@ -21,7 +21,7 @@ import time import stem import core from . import connectionserver, bootstrapservice -from onionrutils import stringvalidators +from onionrutils import stringvalidators, basicrequests class OnionrServices: ''' @@ -47,7 +47,7 @@ class OnionrServices: base_url = 'http://%s/' % (address,) socks = self._core.config.get('tor.socksport') for x in range(BOOTSTRAP_TRIES): - if self._core._utils.doGetRequest(base_url + 'ping', port=socks, ignoreAPI=True) == 'pong!': + if basicrequests.do_get_request(self._core, base_url + 'ping', port=socks, ignoreAPI=True) == 'pong!': # if bootstrap sever is online, tell them our service address connectionserver.ConnectionServer(peer, address, core_inst=self._core) else: diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py index 777801dd..752ce90b 100755 --- a/onionr/onionrservices/bootstrapservice.py +++ b/onionr/onionrservices/bootstrapservice.py @@ -24,7 +24,7 @@ from flask import Flask, Response import core from netcontroller import getOpenPort from . import httpheaders -from onionrutils import stringvalidators +from onionrutils import stringvalidators, epoch def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): ''' @@ -77,7 +77,7 @@ def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): # Create the v3 onion service response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, key_type = 'NEW', key_content = 'ED25519-V3', await_publication = True) core_inst.insertBlock(response.service_id, header='con', sign=True, encryptType='asym', - asymPeer=peer, disableForward=True, expire=(core_inst._utils.getEpoch() + bootstrap_timeout)) + asymPeer=peer, disableForward=True, expire=(epoch.get_epoch() + bootstrap_timeout)) # Run the bootstrap server try: http_server.serve_forever() diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index 3c4238a7..b9f70e56 100755 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -24,7 +24,7 @@ import core, logger, httpapi import onionrexceptions from netcontroller import getOpenPort import api -from onionrutils import stringvalidators +from onionrutils import stringvalidators, basicrequests from . import httpheaders class ConnectionServer: @@ -72,7 +72,7 @@ class ConnectionServer: try: for x in range(3): - attempt = self.core_inst._utils.doPostRequest('http://' + address + '/bs/' + response.service_id, port=socks) + attempt = basicrequests.do_post_request(self.core_inst, 'http://' + address + '/bs/' + response.service_id, port=socks) if attempt == 'success': break else: diff --git a/onionr/onionrstorage/__init__.py b/onionr/onionrstorage/__init__.py index 2af2661b..4bfc8bf6 100755 --- a/onionr/onionrstorage/__init__.py +++ b/onionr/onionrstorage/__init__.py @@ -18,7 +18,7 @@ along with this program. If not, see . ''' import core, sys, sqlite3, os, dbcreator, onionrexceptions -from onionrutils import bytesconverter +from onionrutils import bytesconverter, stringvalidators DB_ENTRY_SIZE_LIMIT = 10000 # Will be a config option @@ -66,7 +66,7 @@ def deleteBlock(coreInst, blockHash): def store(coreInst, data, blockHash=''): assert isinstance(coreInst, core.Core) - assert coreInst._utils.validateHash(blockHash) + assert stringvalidators.validate_hash(blockHash) ourHash = coreInst._crypto.sha3Hash(data) if blockHash != '': assert ourHash == blockHash @@ -81,7 +81,7 @@ def store(coreInst, data, blockHash=''): def getData(coreInst, bHash): assert isinstance(coreInst, core.Core) - assert coreInst._utils.validateHash(bHash) + assert stringvalidators.validate_hash(bHash) bHash = bytesconverter.bytes_to_str(bHash) diff --git a/onionr/onionrstorage/removeblock.py b/onionr/onionrstorage/removeblock.py index 23e574b6..76112199 100644 --- a/onionr/onionrstorage/removeblock.py +++ b/onionr/onionrstorage/removeblock.py @@ -1,5 +1,6 @@ import sys, sqlite3 import onionrexceptions, onionrstorage +from onionrutils import stringvalidators def remove_block(core_inst, block): ''' remove a block from this node (does not automatically blacklist) @@ -7,7 +8,7 @@ def remove_block(core_inst, block): **You may want blacklist.addToDB(blockHash) ''' - if core_inst._utils.validateHash(block): + if stringvalidators.validate_hash(block): conn = sqlite3.connect(core_inst.blockDB, timeout=30) c = conn.cursor() t = (block,) @@ -15,6 +16,6 @@ def remove_block(core_inst, block): conn.commit() conn.close() dataSize = sys.getsizeof(onionrstorage.getData(core_inst, block)) - core_inst._utils.storageCounter.removeBytes(dataSize) + core_inst.storage_counter.removeBytes(dataSize) else: raise onionrexceptions.InvalidHexHash \ No newline at end of file diff --git a/onionr/onionrstorage/setdata.py b/onionr/onionrstorage/setdata.py index 60378112..50721ccf 100644 --- a/onionr/onionrstorage/setdata.py +++ b/onionr/onionrstorage/setdata.py @@ -19,7 +19,7 @@ def set_data(core_inst, data): try: onionrstorage.getData(core_inst, dataHash) except onionrexceptions.NoDataAvailable: - if core_inst._utils.storageCounter.addBytes(dataSize) != False: + if core_inst.storage_counter.addBytes(dataSize) != False: onionrstorage.store(core_inst, data, blockHash=dataHash) conn = sqlite3.connect(core_inst.blockDB, timeout=30) c = conn.cursor() diff --git a/onionr/onionrusers/contactmanager.py b/onionr/onionrusers/contactmanager.py index 1cac6953..680de9b0 100755 --- a/onionr/onionrusers/contactmanager.py +++ b/onionr/onionrusers/contactmanager.py @@ -20,7 +20,7 @@ import os, json, onionrexceptions import unpaddedbase32 from onionrusers import onionrusers -from onionrutils import bytesconverter +from onionrutils import bytesconverter, epoch class ContactManager(onionrusers.OnionrUser): def __init__(self, coreInst, publicKey, saveUser=False, recordExpireSeconds=5): @@ -42,7 +42,7 @@ class ContactManager(onionrusers.OnionrUser): dataFile.write(data) def _loadData(self): - self.lastRead = self._core._utils.getEpoch() + self.lastRead = epoch.get_epoch() retData = {} if os.path.exists(self.dataFile): with open(self.dataFile, 'r') as dataFile: @@ -62,7 +62,7 @@ class ContactManager(onionrusers.OnionrUser): if self.deleted: raise onionrexceptions.ContactDeleted - if (self._core._utils.getEpoch() - self.lastRead >= self.recordExpire) or forceReload: + if (epoch.get_epoch() - self.lastRead >= self.recordExpire) or forceReload: self.data = self._loadData() try: return self.data[key] diff --git a/onionr/onionrusers/onionrusers.py b/onionr/onionrusers/onionrusers.py index 5bb2eac8..28a5784d 100755 --- a/onionr/onionrusers/onionrusers.py +++ b/onionr/onionrusers/onionrusers.py @@ -18,8 +18,7 @@ along with this program. If not, see . ''' import logger, onionrexceptions, json, sqlite3, time -from onionrutils import stringvalidators, bytesconverter - +from onionrutils import stringvalidators, bytesconverter, epoch import unpaddedbase32 import nacl.exceptions @@ -28,7 +27,7 @@ def deleteExpiredKeys(coreInst): conn = sqlite3.connect(coreInst.forwardKeysFile, timeout=10) c = conn.cursor() - curTime = coreInst._utils.getEpoch() + curTime = epoch.get_epoch() c.execute("DELETE from myForwardKeys where expire <= ?", (curTime,)) conn.commit() conn.execute("VACUUM") @@ -40,7 +39,7 @@ def deleteTheirExpiredKeys(coreInst, pubkey): c = conn.cursor() # Prepare the insert - command = (pubkey, coreInst._utils.getEpoch()) + command = (pubkey, epoch.get_epoch()) c.execute("DELETE from forwardKeys where peerKey = ? and expire <= ?", command) @@ -160,10 +159,10 @@ class OnionrUser: conn = sqlite3.connect(self._core.forwardKeysFile, timeout=10) c = conn.cursor() # Prepare the insert - time = self._core._utils.getEpoch() + time = epoch.get_epoch() newKeys = self._core._crypto.generatePubKey() - newPub = self._core._utils.bytesToStr(newKeys[0]) - newPriv = self._core._utils.bytesToStr(newKeys[1]) + newPub = bytesconverter.bytes_to_str(newKeys[0]) + newPriv = bytesconverter.bytes_to_str(newKeys[1]) command = (self.publicKey, newPub, newPriv, time, expire + time) @@ -178,7 +177,7 @@ class OnionrUser: conn = sqlite3.connect(self._core.forwardKeysFile, timeout=10) c = conn.cursor() pubkey = self.publicKey - pubkey = self._core._utils.bytesToStr(pubkey) + pubkey = bytesconverter.bytes_to_str(pubkey) command = (pubkey,) keyList = [] # list of tuples containing pub, private for peer @@ -192,7 +191,7 @@ class OnionrUser: return list(keyList) def addForwardKey(self, newKey, expire=DEFAULT_KEY_EXPIRE): - newKey = self._core._utils.bytesToStr(unpaddedbase32.repad(bytesconverter.str_to_bytes(newKey))) + newKey = bytesconverter.bytes_to_str(unpaddedbase32.repad(bytesconverter.str_to_bytes(newKey))) if not stringvalidators.validate_pub_key(newKey): # Do not add if something went wrong with the key raise onionrexceptions.InvalidPubkey(newKey) @@ -201,7 +200,7 @@ class OnionrUser: c = conn.cursor() # Get the time we're inserting the key at - timeInsert = self._core._utils.getEpoch() + timeInsert = epoch.get_epoch() # Look at our current keys for duplicate key data or time for entry in self._getForwardKeys(): diff --git a/onionr/onionrutils/__init__.py b/onionr/onionrutils/__init__.py index e430a7b1..e69de29b 100644 --- a/onionr/onionrutils/__init__.py +++ b/onionr/onionrutils/__init__.py @@ -1,120 +0,0 @@ -''' - Onionr - Private P2P Communication - - OnionrUtils offers various useful functions to Onionr. Relatively misc. -''' -''' - 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 . -''' -# Misc functions that do not fit in the main api, but are useful -import sys, os, sqlite3, binascii, time, base64, json, glob, shutil, math, re, urllib.parse, string -import requests -import nacl.signing, nacl.encoding -import unpaddedbase32 -import onionrexceptions, config, logger -import onionrevents -import storagecounter -from etc import pgpwords, onionrvalues -from . import localcommand, blockmetadata, basicrequests, validatemetadata -from . import stringvalidators - -config.reload() -class OnionrUtils: - ''' - Various useful functions for validating things, etc functions, connectivity - ''' - def __init__(self, coreInstance): - #self.fingerprintFile = 'data/own-fingerprint.txt' #TODO Remove since probably not needed - self._core = coreInstance # onionr core instance - - self.avoidDupe = [] # list used to prevent duplicate requests per peer for certain actions - self.peerProcessing = {} # dict of current peer actions: peer, actionList - self.storageCounter = storagecounter.StorageCounter(self._core) # used to keep track of how much data onionr is using on disk - return - - def escapeAnsi(self, line): - ''' - Remove ANSI escape codes from a string with regex - - taken or adapted from: https://stackoverflow.com/a/38662876 by user https://stackoverflow.com/users/802365/%c3%89douard-lopez - cc-by-sa-3 license https://creativecommons.org/licenses/by-sa/3.0/ - ''' - ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]') - return ansi_escape.sub('', line) - - def validateHash(self, data, length=64): - ''' - Validate if a string is a valid hash hex digest (does not compare, just checks length and charset) - ''' - return stringvalidators.validate_hash(self, data, length) - - def getEpoch(self): - '''returns epoch''' - return math.floor(time.time()) - - def doPostRequest(self, url, data={}, port=0, proxyType='tor'): - ''' - Do a POST request through a local tor or i2p instance - ''' - return basicrequests.do_post_request(self, url, data, port, proxyType) - - def doGetRequest(self, url, port=0, proxyType='tor', ignoreAPI=False, returnHeaders=False): - ''' - Do a get request through a local tor or i2p instance - ''' - return basicrequests.do_get_request(self, url, port, proxyType, ignoreAPI, returnHeaders) - -def size(path='.'): - ''' - Returns the size of a folder's contents in bytes - ''' - total = 0 - if os.path.exists(path): - if os.path.isfile(path): - total = os.path.getsize(path) - else: - for entry in os.scandir(path): - if entry.is_file(): - total += entry.stat().st_size - elif entry.is_dir(): - total += size(entry.path) - return total - -def humanSize(num, suffix='B'): - ''' - Converts from bytes to a human readable format. - ''' - for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']: - if abs(num) < 1024.0: - return "%.1f %s%s" % (num, unit, suffix) - num /= 1024.0 - return "%.1f %s%s" % (num, 'Yi', suffix) - -def has_block(core_inst, hash): - ''' - Check for new block in the list - ''' - conn = sqlite3.connect(core_inst.blockDB) - c = conn.cursor() - if not stringvalidators.validate_hash(hash): - raise Exception("Invalid hash") - for result in c.execute("SELECT COUNT() FROM hashes WHERE hash = ?", (hash,)): - if result[0] >= 1: - conn.commit() - conn.close() - return True - else: - conn.commit() - conn.close() - return False \ No newline at end of file diff --git a/onionr/onionrutils/basicrequests.py b/onionr/onionrutils/basicrequests.py index e889b887..1435db28 100644 --- a/onionr/onionrutils/basicrequests.py +++ b/onionr/onionrutils/basicrequests.py @@ -1,12 +1,12 @@ import requests import logger, onionrexceptions -def do_post_request(utils_inst, url, data={}, port=0, proxyType='tor'): +def do_post_request(core_inst, url, data={}, port=0, proxyType='tor'): ''' Do a POST request through a local tor or i2p instance ''' if proxyType == 'tor': if port == 0: - port = utils_inst._core.torPort + port = core_inst.torPort proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} elif proxyType == 'i2p': proxies = {'http': 'http://127.0.0.1:4444'} @@ -24,11 +24,11 @@ def do_post_request(utils_inst, url, data={}, port=0, proxyType='tor'): retData = False return retData -def do_get_request(utils_inst, url, port=0, proxyType='tor', ignoreAPI=False, returnHeaders=False): +def do_get_request(core_inst, url, port=0, proxyType='tor', ignoreAPI=False, returnHeaders=False): ''' Do a get request through a local tor or i2p instance ''' - API_VERSION = utils_inst._core.onionrInst.API_VERSION + API_VERSION = core_inst.onionrInst.API_VERSION retData = False if proxyType == 'tor': if port == 0: diff --git a/onionr/onionrutils/blockmetadata.py b/onionr/onionrutils/blockmetadata.py index ace85190..d892257f 100644 --- a/onionr/onionrutils/blockmetadata.py +++ b/onionr/onionrutils/blockmetadata.py @@ -1,9 +1,9 @@ -import json +import json, sqlite3 import logger, onionrevents from onionrusers import onionrusers from etc import onionrvalues import onionrblockapi -from . import epoch +from . import epoch, stringvalidators, bytesconverter def get_block_metadata_from_data(blockData): ''' accepts block contents as string, returns a tuple of @@ -33,24 +33,24 @@ def get_block_metadata_from_data(blockData): meta = metadata['meta'] return (metadata, meta, data) -def process_block_metadata(utils_inst, blockHash): +def process_block_metadata(core_inst, blockHash): ''' Read metadata from a block and cache it to the block database ''' curTime = epoch.get_rounded_epoch(roundS=60) - myBlock = onionrblockapi.Block(blockHash, utils_inst._core) + myBlock = onionrblockapi.Block(blockHash, core_inst) if myBlock.isEncrypted: myBlock.decrypt() if (myBlock.isEncrypted and myBlock.decrypted) or (not myBlock.isEncrypted): blockType = myBlock.getMetadata('type') # we would use myBlock.getType() here, but it is bugged with encrypted blocks - signer = utils_inst.bytesToStr(myBlock.signer) + signer = bytesconverter.bytes_to_str(myBlock.signer) valid = myBlock.verifySig() if myBlock.getMetadata('newFSKey') is not None: - onionrusers.OnionrUser(utils_inst._core, signer).addForwardKey(myBlock.getMetadata('newFSKey')) + onionrusers.OnionrUser(core_inst, signer).addForwardKey(myBlock.getMetadata('newFSKey')) try: if len(blockType) <= 10: - utils_inst._core.updateBlockInfo(blockHash, 'dataType', blockType) + core_inst.updateBlockInfo(blockHash, 'dataType', blockType) except TypeError: logger.warn("Missing block information") pass @@ -61,9 +61,28 @@ def process_block_metadata(utils_inst, blockHash): except (AssertionError, ValueError, TypeError) as e: expireTime = onionrvalues.OnionrValues().default_expire + curTime finally: - utils_inst._core.updateBlockInfo(blockHash, 'expire', expireTime) + core_inst.updateBlockInfo(blockHash, 'expire', expireTime) if not blockType is None: - utils_inst._core.updateBlockInfo(blockHash, 'dataType', blockType) - onionrevents.event('processblocks', data = {'block': myBlock, 'type': blockType, 'signer': signer, 'validSig': valid}, onionr = utils_inst._core.onionrInst) + core_inst.updateBlockInfo(blockHash, 'dataType', blockType) + onionrevents.event('processblocks', data = {'block': myBlock, 'type': blockType, 'signer': signer, 'validSig': valid}, onionr = core_inst.onionrInst) else: - pass \ No newline at end of file + pass + +def has_block(core_inst, hash): + ''' + Check for new block in the list + ''' + conn = sqlite3.connect(core_inst.blockDB) + c = conn.cursor() + if not stringvalidators.validate_hash(hash): + raise Exception("Invalid hash") + for result in c.execute("SELECT COUNT() FROM hashes WHERE hash = ?", (hash,)): + if result[0] >= 1: + conn.commit() + conn.close() + return True + else: + conn.commit() + conn.close() + return False + return False \ No newline at end of file diff --git a/onionr/onionrutils/epoch.py b/onionr/onionrutils/epoch.py index 1ee8ae25..0cda05cb 100644 --- a/onionr/onionrutils/epoch.py +++ b/onionr/onionrutils/epoch.py @@ -6,6 +6,6 @@ def get_rounded_epoch(roundS=60): epoch = get_epoch() return epoch - (epoch % roundS) -def get_epoch(self): +def get_epoch(): '''returns epoch''' return math.floor(time.time()) \ No newline at end of file diff --git a/onionr/onionrutils/escapeansi.py b/onionr/onionrutils/escapeansi.py new file mode 100644 index 00000000..1cd5862a --- /dev/null +++ b/onionr/onionrutils/escapeansi.py @@ -0,0 +1,10 @@ +import re +def escape_ANSI(line): + ''' + Remove ANSI escape codes from a string with regex + + taken or adapted from: https://stackoverflow.com/a/38662876 by user https://stackoverflow.com/users/802365/%c3%89douard-lopez + cc-by-sa-3 license https://creativecommons.org/licenses/by-sa/3.0/ + ''' + ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]') + return ansi_escape.sub('', line) \ No newline at end of file diff --git a/onionr/onionrutils/stringvalidators.py b/onionr/onionrutils/stringvalidators.py index cdfd2338..87fa4e8f 100644 --- a/onionr/onionrutils/stringvalidators.py +++ b/onionr/onionrutils/stringvalidators.py @@ -1,6 +1,7 @@ -import base64, string, onionrutils +import base64, string import unpaddedbase32, nacl.signing, nacl.encoding -def validate_hash(utils_inst, data, length=64): +from onionrutils import bytesconverter +def validate_hash(data, length=64): ''' Validate if a string is a valid hash hex digest (does not compare, just checks length and charset) ''' @@ -25,7 +26,7 @@ def validate_pub_key(key): if type(key) is type(None): return False # Accept keys that have no = padding - key = unpaddedbase32.repad(onionrutils.str_to_bytes(key)) + key = unpaddedbase32.repad(bytesconverter.str_to_bytes(key)) retVal = False try: diff --git a/onionr/onionrutils/validatemetadata.py b/onionr/onionrutils/validatemetadata.py index aebd45e8..4e2e69f0 100644 --- a/onionr/onionrutils/validatemetadata.py +++ b/onionr/onionrutils/validatemetadata.py @@ -1,7 +1,7 @@ import json import logger, onionrexceptions from etc import onionrvalues -from onionrutils import stringvalidators +from onionrutils import stringvalidators, epoch def validate_metadata(core_inst, metadata, blockData): '''Validate metadata meets onionr spec (does not validate proof value computation), take in either dictionary or json string''' # TODO, make this check sane sizes @@ -37,18 +37,18 @@ def validate_metadata(core_inst, metadata, blockData): if not stringvalidators.is_integer_string(metadata[i]): logger.warn('Block metadata time stamp is not integer string or int') break - isFuture = (metadata[i] - core_inst.getEpoch()) + isFuture = (metadata[i] - epoch.get_epoch()) if isFuture > maxClockDifference: logger.warn('Block timestamp is skewed to the future over the max %s: %s' (maxClockDifference, isFuture)) break - if (core_inst.getEpoch() - metadata[i]) > maxAge: + if (epoch.get_epoch() - metadata[i]) > maxAge: logger.warn('Block is outdated: %s' % (metadata[i],)) break elif i == 'expire': try: - assert int(metadata[i]) > core_inst.getEpoch() + assert int(metadata[i]) > epoch.get_epoch() except AssertionError: - logger.warn('Block is expired: %s less than %s' % (metadata[i], core_inst.getEpoch())) + logger.warn('Block is expired: %s less than %s' % (metadata[i], epoch.get_epoch())) break elif i == 'encryptType': try: diff --git a/onionr/static-data/default-plugins/esoteric/main.py b/onionr/static-data/default-plugins/esoteric/main.py index 0c6ae8fa..495129c0 100755 --- a/onionr/static-data/default-plugins/esoteric/main.py +++ b/onionr/static-data/default-plugins/esoteric/main.py @@ -23,7 +23,7 @@ import locale, sys, os, threading, json locale.setlocale(locale.LC_ALL, '') import onionrservices, logger from onionrservices import bootstrapservice -from onionrutils import stringvalidators +from onionrutils import stringvalidators, epoch, basicrequests plugin_name = 'esoteric' PLUGIN_VERSION = '0.0.0' @@ -58,8 +58,8 @@ class Esoteric: else: message += '\n' except EOFError: - message = json.dumps({'m': message, 't': self.myCore._utils.getEpoch()}) - print(self.myCore._utils.doPostRequest('http://%s/esoteric/sendto' % (self.transport,), port=self.socks, data=message)) + message = json.dumps({'m': message, 't': epoch.get_epoch()}) + print(basicrequests.do_post_request(self.myCore, 'http://%s/esoteric/sendto' % (self.transport,), port=self.socks, data=message)) message = '' except KeyboardInterrupt: self.shutdown = True @@ -78,7 +78,7 @@ class Esoteric: self.socks = self.myCore.config.get('tor.socksport') print('connected with', peer, 'on', peer_transport_address) - if self.myCore._utils.doGetRequest('http://%s/ping' % (peer_transport_address,), ignoreAPI=True, port=self.socks) == 'pong!': + if basicrequests.do_get_request(self.myCore, 'http://%s/ping' % (peer_transport_address,), ignoreAPI=True, port=self.socks) == 'pong!': print('connected', peer_transport_address) threading.Thread(target=self._sender_loop).start() diff --git a/onionr/static-data/default-plugins/flow/main.py b/onionr/static-data/default-plugins/flow/main.py index 5f61aa38..de0c60e3 100755 --- a/onionr/static-data/default-plugins/flow/main.py +++ b/onionr/static-data/default-plugins/flow/main.py @@ -22,6 +22,7 @@ import threading, time, locale, sys, os from onionrblockapi import Block import logger, config +from onionrutils import escapeansi, epoch locale.setlocale(locale.LC_ALL, '') sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) @@ -43,7 +44,7 @@ class OnionrFlow: logger.warn("Please note: everything said here is public, even if a random channel name is used.", terminal=True) message = "" self.flowRunning = True - newThread = threading.Thread(target=self.showOutput) + newThread = threading.Thread(target=self.showOutput, daemon=True) newThread.start() try: self.channel = logger.readline("Enter a channel name or none for default:") @@ -59,7 +60,7 @@ class OnionrFlow: else: if message == "q": self.flowRunning = False - expireTime = self.myCore._utils.getEpoch() + 43200 + expireTime = epoch.get_epoch() + 43200 if len(message) > 0: logger.info('Inserting message as block...', terminal=True) self.myCore.insertBlock(message, header='txt', expire=expireTime, meta={'ch': self.channel}) @@ -83,7 +84,7 @@ class OnionrFlow: logger.info('\n------------------------', prompt = False, terminal=True) content = block.getContent() # Escape new lines, remove trailing whitespace, and escape ansi sequences - content = self.myCore._utils.escapeAnsi(content.replace('\n', '\\n').replace('\r', '\\r').strip()) + content = escapeansi.escape_ANSI(content.replace('\n', '\\n').replace('\r', '\\r').strip()) logger.info(block.getDate().strftime("%m/%d %H:%M") + ' - ' + logger.colors.reset + content, prompt = False, terminal=True) self.alreadyOutputed.append(block.getHash()) time.sleep(5) diff --git a/onionr/static-data/default-plugins/pluginmanager/main.py b/onionr/static-data/default-plugins/pluginmanager/main.py index bb21d35c..807feec6 100755 --- a/onionr/static-data/default-plugins/pluginmanager/main.py +++ b/onionr/static-data/default-plugins/pluginmanager/main.py @@ -22,7 +22,7 @@ import logger, config import os, sys, json, time, random, shutil, base64, getpass, datetime, re from onionrblockapi import Block -from onionrutils import importnewblocks, stringvalidators, +from onionrutils import importnewblocks, stringvalidators plugin_name = 'pluginmanager' @@ -397,7 +397,7 @@ def commandInstallPlugin(): return True - valid_hash = pluginapi.get_utils().validateHash(pkobh) + valid_hash = stringvalidators.validate_hash(pkobh) real_block = False valid_key = stringvalidators.validate_pub_key(pkobh) real_key = False @@ -485,7 +485,7 @@ def commandAddRepository(): blockhash = sys.argv[2] - if pluginapi.get_utils().validateHash(blockhash): + if stringvalidators.validate_hash(blockhash): if Block.exists(blockhash): try: blockContent = json.loads(Block(blockhash, core = pluginapi.get_core()).getContent()) @@ -521,7 +521,7 @@ def commandRemoveRepository(): blockhash = sys.argv[2] - if pluginapi.get_utils().validateHash(blockhash): + if stringvalidators.validate_hash(blockhash): if blockhash in getRepositories(): try: removeRepository(blockhash) diff --git a/onionr/static-data/default-plugins/pms/mailapi.py b/onionr/static-data/default-plugins/pms/mailapi.py index af59bd93..922432a0 100755 --- a/onionr/static-data/default-plugins/pms/mailapi.py +++ b/onionr/static-data/default-plugins/pms/mailapi.py @@ -21,6 +21,7 @@ import sys, os, json from flask import Response, request, redirect, Blueprint, abort import core from onionrusers import contactmanager +from onionrutils import stringvalidators sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) import loadinbox, sentboxdb @@ -34,7 +35,7 @@ def mail_ping(): @flask_blueprint.route('/mail/deletemsg/', methods=['POST']) def mail_delete(block): - if not c._utils.validateHash(block): + if not stringvalidators.validate_hash(block): abort(504) existing = kv.get('deleted_mail') if existing is None: diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py index 532320de..dfacc915 100755 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -23,7 +23,7 @@ import logger, config, threading, time, datetime from onionrblockapi import Block import onionrexceptions from onionrusers import onionrusers -from onionrutils import stringvalidators +from onionrutils import stringvalidators, escapeansi import locale, sys, os, json locale.setlocale(locale.LC_ALL, '') @@ -148,7 +148,7 @@ class OnionrMail: print('') if cancel != '-q': try: - print(draw_border(self.myCore._utils.escapeAnsi(readBlock.bcontent.decode().strip()))) + print(draw_border(escapeansi.escape_ANSI(readBlock.bcontent.decode().strip()))) except ValueError: logger.warn('Error presenting message. This is usually due to a malformed or blank message.', terminal=True) pass @@ -187,7 +187,7 @@ class OnionrMail: else: logger.info('Sent to: ' + self.sentMessages[self.sentboxList[int(choice)]][1], terminal=True) # Print ansi escaped sent message - logger.info(self.myCore._utils.escapeAnsi(self.sentMessages[self.sentboxList[int(choice)]][0]), terminal=True) + logger.info(escapeansi.escape_ANSI(self.sentMessages[self.sentboxList[int(choice)]][0]), terminal=True) input('Press enter to continue...') finally: if choice == '-q': diff --git a/onionr/static-data/default-plugins/pms/sentboxdb.py b/onionr/static-data/default-plugins/pms/sentboxdb.py index 7090e214..f5a56272 100755 --- a/onionr/static-data/default-plugins/pms/sentboxdb.py +++ b/onionr/static-data/default-plugins/pms/sentboxdb.py @@ -19,6 +19,7 @@ ''' import sqlite3, os import core +from onionrutils import epoch class SentBox: def __init__(self, mycore): assert isinstance(mycore, core.Core) @@ -60,7 +61,7 @@ class SentBox: def addToSent(self, blockID, peer, message, subject=''): self.connect() - args = (blockID, peer, message, subject, self.core._utils.getEpoch()) + args = (blockID, peer, message, subject, epoch.get_epoch()) self.cursor.execute('INSERT INTO sent VALUES(?, ?, ?, ?, ?)', args) self.conn.commit() self.close() diff --git a/onionr/utils/netutils.py b/onionr/utils/netutils.py index b6f16922..1c7bea1c 100755 --- a/onionr/utils/netutils.py +++ b/onionr/utils/netutils.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -def checkNetwork(utilsInst, torPort=0): +from onionrutils import basicrequests +def checkNetwork(core_inst, torPort=0): '''Check if we are connected to the internet (through Tor)''' retData = False connectURLs = [] @@ -26,7 +27,7 @@ def checkNetwork(utilsInst, torPort=0): connectURLs = connectTest.read().split(',') for url in connectURLs: - if utilsInst.doGetRequest(url, port=torPort, ignoreAPI=True) != False: + if basicrequests.do_get_request(core_inst, url, port=torPort, ignoreAPI=True) != False: retData = True break except FileNotFoundError: diff --git a/onionr/utils/sizeutils.py b/onionr/utils/sizeutils.py new file mode 100644 index 00000000..da121910 --- /dev/null +++ b/onionr/utils/sizeutils.py @@ -0,0 +1,27 @@ +import sqlite3, os +from onionrutils import stringvalidators +def human_size(num, suffix='B'): + ''' + Converts from bytes to a human readable format. + ''' + for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']: + if abs(num) < 1024.0: + return "%.1f %s%s" % (num, unit, suffix) + num /= 1024.0 + return "%.1f %s%s" % (num, 'Yi', suffix) + +def size(path='.'): + ''' + Returns the size of a folder's contents in bytes + ''' + total = 0 + if os.path.exists(path): + if os.path.isfile(path): + total = os.path.getsize(path) + else: + for entry in os.scandir(path): + if entry.is_file(): + total += entry.stat().st_size + elif entry.is_dir(): + total += size(entry.path) + return total \ No newline at end of file From 122eb4ee5fc4d9bde1d91ad1bfd708b531935b06 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 25 Jun 2019 19:15:04 -0500 Subject: [PATCH 121/173] Finished removing onionrutils for the most part, except for some possible bugs remaining --- onionr/blockimporter.py | 4 ++-- onionr/communicatorutils/announcenode.py | 4 ++-- onionr/communicatorutils/downloadblocks.py | 4 ++-- onionr/communicatorutils/uploadblocks.py | 2 +- onionr/core.py | 7 +++++-- onionr/httpapi/miscpublicapi/announce.py | 4 ++-- onionr/logger.py | 2 +- onionr/onionr.py | 1 - onionr/onionrcommands/daemonlaunch.py | 2 +- onionr/onionrutils/blockmetadata.py | 1 + onionr/onionrutils/localcommand.py | 5 +++-- onionr/onionrutils/validatemetadata.py | 4 ++-- onionr/static-data/default-plugins/pms/main.py | 8 ++++---- 13 files changed, 26 insertions(+), 22 deletions(-) diff --git a/onionr/blockimporter.py b/onionr/blockimporter.py index d5e02e7d..067ec045 100755 --- a/onionr/blockimporter.py +++ b/onionr/blockimporter.py @@ -37,7 +37,7 @@ def importBlockFromData(content, coreInst): metas = blockmetadata.get_block_metadata_from_data(content) # returns tuple(metadata, meta), meta is also in metadata metadata = metas[0] - if validatemetadata(metadata, metas[2]): # check if metadata is valid + if validatemetadata.validate_metadata(coreInst, metadata, metas[2]): # check if metadata is valid if coreInst._crypto.verifyPow(content): # check if POW is enough/correct logger.info('Block passed proof, saving.', terminal=True) try: @@ -46,6 +46,6 @@ def importBlockFromData(content, coreInst): pass else: coreInst.addToBlockDB(blockHash, dataSaved=True) - blockmetadata.process_block_metadata(blockHash) # caches block metadata values to block database + blockmetadata.process_block_metadata(coreInst, blockHash) # caches block metadata values to block database retData = True return retData \ No newline at end of file diff --git a/onionr/communicatorutils/announcenode.py b/onionr/communicatorutils/announcenode.py index f72010f5..38453647 100755 --- a/onionr/communicatorutils/announcenode.py +++ b/onionr/communicatorutils/announcenode.py @@ -20,7 +20,7 @@ import base64 import onionrproofs, logger from etc import onionrvalues -from onionrutils import basicrequests +from onionrutils import basicrequests, bytesconverter def announce_node(daemon): '''Announce our node to our peers''' @@ -53,7 +53,7 @@ def announce_node(daemon): combinedNodes = ourID + peer if ourID != 1: #TODO: Extend existingRand for i2p - existingRand = daemon._core._utils.bytesToStr(daemon._core.getAddressInfo(peer, 'powValue')) + existingRand = bytesconverter.bytes_to_str(daemon._core.getAddressInfo(peer, 'powValue')) # Reset existingRand if it no longer meets the minimum POW if type(existingRand) is type(None) or not existingRand.endswith('0' * ov.announce_pow): existingRand = '' diff --git a/onionr/communicatorutils/downloadblocks.py b/onionr/communicatorutils/downloadblocks.py index ecd7b873..1a159c93 100755 --- a/onionr/communicatorutils/downloadblocks.py +++ b/onionr/communicatorutils/downloadblocks.py @@ -75,7 +75,7 @@ def download_blocks_from_communicator(comm_inst): content = content.decode() # decode here because sha3Hash needs bytes above metas = blockmetadata.get_block_metadata_from_data(content) # returns tuple(metadata, meta), meta is also in metadata metadata = metas[0] - if validatemetadata.validate_metadata(comm_inist._core, metadata, metas[2]): # check if metadata is valid, and verify nonce + if validatemetadata.validate_metadata(comm_inst._core, metadata, metas[2]): # check if metadata is valid, and verify nonce if comm_inst._core._crypto.verifyPow(content): # check if POW is enough/correct logger.info('Attempting to save block %s...' % blockHash[:12]) try: @@ -85,7 +85,7 @@ def download_blocks_from_communicator(comm_inst): removeFromQueue = False else: comm_inst._core.addToBlockDB(blockHash, dataSaved=True) - blockmetadata.process_block_metadata(blockHash) # caches block metadata values to block database + blockmetadata.process_block_metadata(comm_inst._core, blockHash) # caches block metadata values to block database else: logger.warn('POW failed for block %s.' % blockHash) else: diff --git a/onionr/communicatorutils/uploadblocks.py b/onionr/communicatorutils/uploadblocks.py index afe96525..0a3488a8 100755 --- a/onionr/communicatorutils/uploadblocks.py +++ b/onionr/communicatorutils/uploadblocks.py @@ -42,7 +42,7 @@ def upload_blocks_from_communicator(comm_inst): url = 'http://' + peer + '/upload' data = {'block': block.Block(bl).getRaw()} proxyType = proxypicker.pick_proxy(peer) - logger.info("Uploading block to " + peer) + logger.info("Uploading block to " + peer, terminal=True) if not basicrequests.do_post_request(core, url, data=data, proxyType=proxyType) == False: localcommand.local_command(core, 'waitforshare/' + bl, post=True) finishedUploads.append(bl) diff --git a/onionr/core.py b/onionr/core.py index 0d64877a..09d2cc4d 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -306,7 +306,7 @@ class Core: dateClaimed - timestamp claimed inside the block, only as trustworthy as the block author is expire - expire date for a block ''' - return coredb.blockmetadb.updateblockinfo + return coredb.blockmetadb.updateblockinfo.update_block_info(self, hash, key, data) def insertBlock(self, data, header='txt', sign=False, encryptType='', symKey='', asymPeer='', meta = {}, expire=None, disableForward=False): ''' @@ -437,8 +437,11 @@ class Core: else: # Tell the api server through localCommand to wait for the daemon to upload this block to make statistical analysis more difficult if localcommand.local_command(self, '/ping', maxWait=10) == 'pong!': - localcommand.local_command(self, '/waitforshare/' + retData, post=True, maxWait=5) + if self.config.get('general.security_level', 1) > 0: + localcommand.local_command(self, '/waitforshare/' + retData, post=True, maxWait=5) self.daemonQueueAdd('uploadBlock', retData) + else: + print('shite', localcommand.local_command(self, '/ping', maxWait=10)) self.addToBlockDB(retData, selfInsert=True, dataSaved=True) blockmetadata.process_block_metadata(self, retData) diff --git a/onionr/httpapi/miscpublicapi/announce.py b/onionr/httpapi/miscpublicapi/announce.py index f17c8d83..060bead4 100755 --- a/onionr/httpapi/miscpublicapi/announce.py +++ b/onionr/httpapi/miscpublicapi/announce.py @@ -21,7 +21,7 @@ import base64 from flask import Response import logger from etc import onionrvalues -from onionrutils import stringvalidators +from onionrutils import stringvalidators, bytesconverter def handle_announce(clientAPI, request): ''' @@ -53,7 +53,7 @@ def handle_announce(clientAPI, request): except AttributeError: pass if powHash.startswith('0' * onionrvalues.OnionrValues().announce_pow): - newNode = clientAPI._core._utils.bytesToStr(newNode) + newNode = bytesconverter.bytes_to_str(newNode) if stringvalidators.validate_transport(newNode) and not newNode in clientAPI._core.onionrInst.communicatorInst.newPeers: clientAPI._core.onionrInst.communicatorInst.newPeers.append(newNode) resp = 'Success' diff --git a/onionr/logger.py b/onionr/logger.py index 01ada8ce..1f65d0d4 100755 --- a/onionr/logger.py +++ b/onionr/logger.py @@ -131,7 +131,7 @@ def raw(data, fd = sys.stdout, terminal = False): Outputs raw data to console without formatting ''' - if terminal and (get_settings() & OUTPUT_TO_CONSOLE): + if (get_settings() & OUTPUT_TO_CONSOLE): try: ts = fd.write('%s\n' % data) except OSError: diff --git a/onionr/onionr.py b/onionr/onionr.py index 85a34020..cd16ddfe 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -43,7 +43,6 @@ try: except ImportError: raise Exception("You need the PySocks module (for use with socks5 proxy to use Tor)") - class Onionr: def __init__(self): ''' diff --git a/onionr/onionrcommands/daemonlaunch.py b/onionr/onionrcommands/daemonlaunch.py index 28202c10..c82d13de 100755 --- a/onionr/onionrcommands/daemonlaunch.py +++ b/onionr/onionrcommands/daemonlaunch.py @@ -60,7 +60,7 @@ def daemon(o_inst): logger.debug('Python version %s' % platform.python_version()) if o_inst._developmentMode: - logger.warn('DEVELOPMENT MODE ENABLED', timestamp = False, terminal=True) + logger.warn('Development mode enabled', timestamp = False, terminal=True) net = NetController(o_inst.onionrCore.config.get('client.public.port', 59497), apiServerIP=apiHost) logger.info('Tor is starting...', terminal=True) if not net.startTor(): diff --git a/onionr/onionrutils/blockmetadata.py b/onionr/onionrutils/blockmetadata.py index d892257f..72bdb48c 100644 --- a/onionr/onionrutils/blockmetadata.py +++ b/onionr/onionrutils/blockmetadata.py @@ -43,6 +43,7 @@ def process_block_metadata(core_inst, blockHash): myBlock.decrypt() if (myBlock.isEncrypted and myBlock.decrypted) or (not myBlock.isEncrypted): blockType = myBlock.getMetadata('type') # we would use myBlock.getType() here, but it is bugged with encrypted blocks + print('blockType', blockType) signer = bytesconverter.bytes_to_str(myBlock.signer) valid = myBlock.verifySig() if myBlock.getMetadata('newFSKey') is not None: diff --git a/onionr/onionrutils/localcommand.py b/onionr/onionrutils/localcommand.py index 90b0d7ac..6b5356ee 100644 --- a/onionr/onionrutils/localcommand.py +++ b/onionr/onionrutils/localcommand.py @@ -19,11 +19,12 @@ def local_command(core_inst, command, data='', silent = True, post=False, postDa if data != '': data = '&data=' + urllib.parse.quote_plus(data) payload = 'http://%s/%s%s' % (hostname, command, data) + print(payload) try: if post: - retData = requests.post(payload, data=postData, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, maxWait)).text + retData = requests.post(payload, data=postData, headers={'token': core_inst.config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, maxWait)).text else: - retData = requests.get(payload, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, maxWait)).text + retData = requests.get(payload, headers={'token': core_inst.config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, maxWait)).text except Exception as error: if not silent: logger.error('Failed to make local request (command: %s):%s' % (command, error), terminal=True) diff --git a/onionr/onionrutils/validatemetadata.py b/onionr/onionrutils/validatemetadata.py index 4e2e69f0..ec1f51da 100644 --- a/onionr/onionrutils/validatemetadata.py +++ b/onionr/onionrutils/validatemetadata.py @@ -1,7 +1,7 @@ import json import logger, onionrexceptions from etc import onionrvalues -from onionrutils import stringvalidators, epoch +from onionrutils import stringvalidators, epoch, bytesconverter def validate_metadata(core_inst, metadata, blockData): '''Validate metadata meets onionr spec (does not validate proof value computation), take in either dictionary or json string''' # TODO, make this check sane sizes @@ -59,7 +59,7 @@ def validate_metadata(core_inst, metadata, blockData): else: # if metadata loop gets no errors, it does not break, therefore metadata is valid # make sure we do not have another block with the same data content (prevent data duplication and replay attacks) - nonce = core_inst._utils.bytesToStr(core_inst._crypto.sha3Hash(blockData)) + nonce = bytesconverter.bytes_to_str(core_inst._crypto.sha3Hash(blockData)) try: with open(core_inst.dataNonceFile, 'r') as nonceFile: if nonce in nonceFile.read(): diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py index dfacc915..5c532877 100755 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -23,7 +23,7 @@ import logger, config, threading, time, datetime from onionrblockapi import Block import onionrexceptions from onionrusers import onionrusers -from onionrutils import stringvalidators, escapeansi +from onionrutils import stringvalidators, escapeansi, bytesconverter import locale, sys, os, json locale.setlocale(locale.LC_ALL, '') @@ -136,7 +136,7 @@ class OnionrMail: else: cancel = '' readBlock.verifySig() - senderDisplay = self.myCore._utils.bytesToStr(readBlock.signer) + senderDisplay = bytesconverter.bytes_to_str(readBlock.signer) if len(senderDisplay.strip()) == 0: senderDisplay = 'Anonymous' logger.info('Message received from %s' % (senderDisplay,), terminal=True) @@ -156,7 +156,7 @@ class OnionrMail: reply = logger.readline("Press enter to continue, or enter %s to reply" % ("-r",)) print('') if reply == "-r": - self.draft_message(self.myCore._utils.bytesToStr(readBlock.signer,)) + self.draft_message(bytesconverter.bytes_to_str(readBlock.signer,)) else: logger.readline("Press enter to continue") print('') @@ -200,7 +200,7 @@ class OnionrMail: self.sentMessages = {} for i in self.sentboxTools.listSent(): self.sentboxList.append(i['hash']) - self.sentMessages[i['hash']] = (self.myCore._utils.bytesToStr(i['message']), i['peer'], i['subject']) + self.sentMessages[i['hash']] = (bytesconverter.bytes_to_str(i['message']), i['peer'], i['subject']) if display: logger.info('%s. %s - %s - (%s) - %s' % (count, i['hash'], i['peer'][:12], i['subject'], i['date']), terminal=True) count += 1 From a5a50a84d4e43f8cf2ce0761439a2ca46a3ef91f Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 25 Jun 2019 19:32:44 -0500 Subject: [PATCH 122/173] adjust test to use new stringvalidation module --- onionr/core.py | 129 ++++++++++++------------- onionr/tests/test_stringvalidations.py | 4 +- 2 files changed, 66 insertions(+), 67 deletions(-) diff --git a/onionr/core.py b/onionr/core.py index 09d2cc4d..84ef5f3a 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -42,76 +42,75 @@ class Core: if not self.dataDir.endswith('/'): self.dataDir += '/' - #try: - self.usageFile = self.dataDir + 'disk-usage.txt' - self.config = config - self.maxBlockSize = 10000000 # max block size in bytes + try: + self.usageFile = self.dataDir + 'disk-usage.txt' + self.config = config + self.maxBlockSize = 10000000 # max block size in bytes - self.onionrInst = None - self.queueDB = self.dataDir + 'queue.db' - self.peerDB = self.dataDir + 'peers.db' - self.blockDB = self.dataDir + 'blocks.db' - self.blockDataLocation = self.dataDir + 'blocks/' - self.blockDataDB = self.blockDataLocation + 'block-data.db' - self.publicApiHostFile = self.dataDir + 'public-host.txt' - self.privateApiHostFile = self.dataDir + 'private-host.txt' - self.addressDB = self.dataDir + 'address.db' - self.hsAddress = '' - self.i2pAddress = config.get('i2p.own_addr', None) - self.bootstrapFileLocation = 'static-data/bootstrap-nodes.txt' - self.bootstrapList = [] - self.requirements = onionrvalues.OnionrValues() - self.torPort = torPort - self.dataNonceFile = self.dataDir + 'block-nonces.dat' - self.dbCreate = dbcreator.DBCreator(self) - self.forwardKeysFile = self.dataDir + 'forward-keys.db' - self.keyStore = simplekv.DeadSimpleKV(self.dataDir + 'cachedstorage.dat', refresh_seconds=5) - self.storage_counter = storagecounter.StorageCounter(self) - - # Socket data, defined here because of multithreading constraints with gevent - self.killSockets = False - self.startSocket = {} - self.socketServerConnData = {} - self.socketReasons = {} - self.socketServerResponseData = {} + self.onionrInst = None + self.queueDB = self.dataDir + 'queue.db' + self.peerDB = self.dataDir + 'peers.db' + self.blockDB = self.dataDir + 'blocks.db' + self.blockDataLocation = self.dataDir + 'blocks/' + self.blockDataDB = self.blockDataLocation + 'block-data.db' + self.publicApiHostFile = self.dataDir + 'public-host.txt' + self.privateApiHostFile = self.dataDir + 'private-host.txt' + self.addressDB = self.dataDir + 'address.db' + self.hsAddress = '' + self.i2pAddress = config.get('i2p.own_addr', None) + self.bootstrapFileLocation = 'static-data/bootstrap-nodes.txt' + self.bootstrapList = [] + self.requirements = onionrvalues.OnionrValues() + self.torPort = torPort + self.dataNonceFile = self.dataDir + 'block-nonces.dat' + self.dbCreate = dbcreator.DBCreator(self) + self.forwardKeysFile = self.dataDir + 'forward-keys.db' + self.keyStore = simplekv.DeadSimpleKV(self.dataDir + 'cachedstorage.dat', refresh_seconds=5) + self.storage_counter = storagecounter.StorageCounter(self) + + # Socket data, defined here because of multithreading constraints with gevent + self.killSockets = False + self.startSocket = {} + self.socketServerConnData = {} + self.socketReasons = {} + self.socketServerResponseData = {} - if not os.path.exists(self.dataDir): - os.mkdir(self.dataDir) - if not os.path.exists(self.dataDir + 'blocks/'): - os.mkdir(self.dataDir + 'blocks/') - if not os.path.exists(self.blockDB): - self.createBlockDB() - if not os.path.exists(self.forwardKeysFile): - self.dbCreate.createForwardKeyDB() - if not os.path.exists(self.peerDB): - self.createPeerDB() - if not os.path.exists(self.addressDB): - self.createAddressDB() + if not os.path.exists(self.dataDir): + os.mkdir(self.dataDir) + if not os.path.exists(self.dataDir + 'blocks/'): + os.mkdir(self.dataDir + 'blocks/') + if not os.path.exists(self.blockDB): + self.createBlockDB() + if not os.path.exists(self.forwardKeysFile): + self.dbCreate.createForwardKeyDB() + if not os.path.exists(self.peerDB): + self.createPeerDB() + if not os.path.exists(self.addressDB): + self.createAddressDB() - if os.path.exists(self.dataDir + '/hs/hostname'): - with open(self.dataDir + '/hs/hostname', 'r') as hs: - self.hsAddress = hs.read().strip() + if os.path.exists(self.dataDir + '/hs/hostname'): + with open(self.dataDir + '/hs/hostname', 'r') as hs: + self.hsAddress = hs.read().strip() - # Load bootstrap address list - if os.path.exists(self.bootstrapFileLocation): - with open(self.bootstrapFileLocation, 'r') as bootstrap: - bootstrap = bootstrap.read() - for i in bootstrap.split('\n'): - self.bootstrapList.append(i) - else: - logger.warn('Warning: address bootstrap file not found ' + self.bootstrapFileLocation) + # Load bootstrap address list + if os.path.exists(self.bootstrapFileLocation): + with open(self.bootstrapFileLocation, 'r') as bootstrap: + bootstrap = bootstrap.read() + for i in bootstrap.split('\n'): + self.bootstrapList.append(i) + else: + logger.warn('Warning: address bootstrap file not found ' + self.bootstrapFileLocation) - self.use_subprocess = powchoice.use_subprocess(self) - # Initialize the crypto object - self._crypto = onionrcrypto.OnionrCrypto(self) - self._blacklist = onionrblacklist.OnionrBlackList(self) - self.serializer = serializeddata.SerializedData(self) + self.use_subprocess = powchoice.use_subprocess(self) + # Initialize the crypto object + self._crypto = onionrcrypto.OnionrCrypto(self) + self._blacklist = onionrblacklist.OnionrBlackList(self) + self.serializer = serializeddata.SerializedData(self) - # except Exception as error: - # print(str(error)) - # logger.error('Failed to initialize core Onionr library.', error=error, terminal=True) - # logger.fatal('Cannot recover from error.', terminal=True) - # sys.exit(1) + except Exception as error: + logger.error('Failed to initialize core Onionr library.', error=error, terminal=True) + logger.fatal('Cannot recover from error.', terminal=True) + sys.exit(1) return def refreshFirstStartVars(self): @@ -441,7 +440,7 @@ class Core: localcommand.local_command(self, '/waitforshare/' + retData, post=True, maxWait=5) self.daemonQueueAdd('uploadBlock', retData) else: - print('shite', localcommand.local_command(self, '/ping', maxWait=10)) + pass self.addToBlockDB(retData, selfInsert=True, dataSaved=True) blockmetadata.process_block_metadata(self, retData) diff --git a/onionr/tests/test_stringvalidations.py b/onionr/tests/test_stringvalidations.py index 4eb7f113..72d5fb20 100755 --- a/onionr/tests/test_stringvalidations.py +++ b/onionr/tests/test_stringvalidations.py @@ -35,11 +35,11 @@ class OnionrValidations(unittest.TestCase): for valid in valids: print('testing', valid) - self.assertTrue(c._utils.validatePubKey(valid)) + self.assertTrue(stringvalidators.validate_pub_key(valid)) for x in invalid: #print('testing', x) - self.assertFalse(c._utils.validatePubKey(x)) + self.assertFalse(stringvalidators.validate_pub_key(x)) def test_integer_string(self): valid = ["1", "100", 100, "-5", -5] From 0b34951423952ebfe7dce8e2f1d0dcd5585c7c76 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 25 Jun 2019 19:33:55 -0500 Subject: [PATCH 123/173] redo new logging --- onionr/logger.py | 2 +- onionr/onionrutils/localcommand.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/onionr/logger.py b/onionr/logger.py index 1f65d0d4..01ada8ce 100755 --- a/onionr/logger.py +++ b/onionr/logger.py @@ -131,7 +131,7 @@ def raw(data, fd = sys.stdout, terminal = False): Outputs raw data to console without formatting ''' - if (get_settings() & OUTPUT_TO_CONSOLE): + if terminal and (get_settings() & OUTPUT_TO_CONSOLE): try: ts = fd.write('%s\n' % data) except OSError: diff --git a/onionr/onionrutils/localcommand.py b/onionr/onionrutils/localcommand.py index 6b5356ee..c3bf3ceb 100644 --- a/onionr/onionrutils/localcommand.py +++ b/onionr/onionrutils/localcommand.py @@ -19,7 +19,6 @@ def local_command(core_inst, command, data='', silent = True, post=False, postDa if data != '': data = '&data=' + urllib.parse.quote_plus(data) payload = 'http://%s/%s%s' % (hostname, command, data) - print(payload) try: if post: retData = requests.post(payload, data=postData, headers={'token': core_inst.config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, maxWait)).text From c8a50c5956daae07233dcd2ca42557636e4c6dad Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 25 Jun 2019 23:48:24 -0500 Subject: [PATCH 124/173] fixed broken waitforshare --- README.md | 2 +- onionr/core.py | 6 +++--- onionr/onionrutils/blockmetadata.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4417e31f..0785e78b 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Note: probably not tax deductible Email: beardog [ at ] mailbox.org -Onionr Mail: TRH763JURNY47QPBTTQ4LLPYCYQK6Q5YA33R6GANKZK5C5DKCIGQ==== +Onionr Mail: TRH763JURNY47QPBTTQ4LLPYCYQK6Q5YA33R6GANKZK5C5DKCIGQ ## Disclaimers and legal diff --git a/onionr/core.py b/onionr/core.py index 84ef5f3a..195a8d04 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -17,12 +17,12 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import os, sys, time, json, uuid +import os, sys, json import logger, netcontroller, config from onionrblockapi import Block import coredb import deadsimplekv as simplekv -import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions +import onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions import onionrblacklist from onionrusers import onionrusers from onionrstorage import removeblock, setdata @@ -436,7 +436,7 @@ class Core: else: # Tell the api server through localCommand to wait for the daemon to upload this block to make statistical analysis more difficult if localcommand.local_command(self, '/ping', maxWait=10) == 'pong!': - if self.config.get('general.security_level', 1) > 0: + if self.config.get('general.security_level', 1) == 0: localcommand.local_command(self, '/waitforshare/' + retData, post=True, maxWait=5) self.daemonQueueAdd('uploadBlock', retData) else: diff --git a/onionr/onionrutils/blockmetadata.py b/onionr/onionrutils/blockmetadata.py index 72bdb48c..91fa1952 100644 --- a/onionr/onionrutils/blockmetadata.py +++ b/onionr/onionrutils/blockmetadata.py @@ -43,7 +43,7 @@ def process_block_metadata(core_inst, blockHash): myBlock.decrypt() if (myBlock.isEncrypted and myBlock.decrypted) or (not myBlock.isEncrypted): blockType = myBlock.getMetadata('type') # we would use myBlock.getType() here, but it is bugged with encrypted blocks - print('blockType', blockType) + signer = bytesconverter.bytes_to_str(myBlock.signer) valid = myBlock.verifySig() if myBlock.getMetadata('newFSKey') is not None: From 3bb0e48a06ed71b8baa03ed5f795f36bc31e24d4 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 26 Jun 2019 00:26:50 -0500 Subject: [PATCH 125/173] added some of the missing boilerplate --- README.md | 2 +- onionr/coredb/blockmetadb/__init__.py | 19 +++++++++++++++++++ onionr/coredb/blockmetadb/add.py | 19 +++++++++++++++++++ onionr/coredb/blockmetadb/expiredblocks.py | 19 +++++++++++++++++++ onionr/coredb/blockmetadb/updateblockinfo.py | 20 ++++++++++++++++++++ onionr/coredb/daemonqueue/__init__.py | 20 ++++++++++++++++++++ 6 files changed, 98 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0785e78b..ee96efd4 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ (***pre-alpha & experimental, not well tested or easy to use yet***) [![Open Source Love](https://badges.frapsoft.com/os/v3/open-source.png?v=103)](https://github.com/ellerbrock/open-source-badges/) - - [Onionr.net](https://onionr.net/) + - [Onionr.net](https://onionr.net/) - [.onion](http://onionr.onionkvc5ibm37bmxwr56bdxcdnb6w3wm4bdghh5qo6f6za7gn7styid.onion/)
        diff --git a/onionr/coredb/blockmetadb/__init__.py b/onionr/coredb/blockmetadb/__init__.py index 9aa991a5..409696dc 100644 --- a/onionr/coredb/blockmetadb/__init__.py +++ b/onionr/coredb/blockmetadb/__init__.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + This module works with information relating to blocks stored on the node +''' +''' + 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 sqlite3 from . import expiredblocks, updateblockinfo, add def get_block_list(core_inst, dateRec = None, unsaved = False): diff --git a/onionr/coredb/blockmetadb/add.py b/onionr/coredb/blockmetadb/add.py index bdf9457f..cfd8305c 100644 --- a/onionr/coredb/blockmetadb/add.py +++ b/onionr/coredb/blockmetadb/add.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + Add an entry to the block metadata database +''' +''' + 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, sqlite3 from onionrutils import epoch, blockmetadata def add_to_block_DB(core_inst, newHash, selfInsert=False, dataSaved=False): diff --git a/onionr/coredb/blockmetadb/expiredblocks.py b/onionr/coredb/blockmetadb/expiredblocks.py index 0b102fe6..9e01d3ac 100644 --- a/onionr/coredb/blockmetadb/expiredblocks.py +++ b/onionr/coredb/blockmetadb/expiredblocks.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + Get a list of expired blocks still stored +''' +''' + 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 sqlite3 from onionrutils import epoch def get_expired_blocks(core_inst): diff --git a/onionr/coredb/blockmetadb/updateblockinfo.py b/onionr/coredb/blockmetadb/updateblockinfo.py index 05f0fc8a..bbb5d969 100644 --- a/onionr/coredb/blockmetadb/updateblockinfo.py +++ b/onionr/coredb/blockmetadb/updateblockinfo.py @@ -1,3 +1,23 @@ +''' + Onionr - Private P2P Communication + + Update block information in the metadata database by a field name +''' +''' + 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 sqlite3 def update_block_info(core_inst, hash, key, data): if key not in ('dateReceived', 'decrypted', 'dataType', 'dataFound', 'dataSaved', 'sig', 'author', 'dateClaimed', 'expire'): diff --git a/onionr/coredb/daemonqueue/__init__.py b/onionr/coredb/daemonqueue/__init__.py index 252cbc37..b494a45a 100644 --- a/onionr/coredb/daemonqueue/__init__.py +++ b/onionr/coredb/daemonqueue/__init__.py @@ -1,3 +1,23 @@ +''' + Onionr - Private P2P Communication + + Write and read the daemon queue, which is how messages are passed into the onionr daemon in a more + direct way than the http api +''' +''' + 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 sqlite3, os import onionrevents as events from onionrutils import localcommand, epoch From 60d47e1e418350615b6d600e80d7e07c2bdc2142 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 26 Jun 2019 00:41:49 -0500 Subject: [PATCH 126/173] remove utils from netcheck --- onionr/communicatorutils/netcheck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionr/communicatorutils/netcheck.py b/onionr/communicatorutils/netcheck.py index 85d10605..81369d58 100755 --- a/onionr/communicatorutils/netcheck.py +++ b/onionr/communicatorutils/netcheck.py @@ -32,7 +32,7 @@ def net_check(comm_inst): rec = True except ValueError: pass - if not rec and not netutils.checkNetwork(c._utils, torPort=comm_inst.proxyPort): + if not rec and not netutils.checkNetwork(c, torPort=comm_inst.proxyPort): if not comm_inst.shutdown: logger.warn('Network check failed, are you connected to the Internet, and is Tor working?') comm_inst.isOnline = False From 7f4605998c4478d9d128949bb9b5670fa278b523 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 26 Jun 2019 01:34:34 -0500 Subject: [PATCH 127/173] fix some commands terminal output --- onionr/coredb/keydb/addkeys.py | 19 +++++++++++++++++++ onionr/coredb/keydb/listkeys.py | 19 +++++++++++++++++++ onionr/coredb/keydb/removekeys.py | 19 +++++++++++++++++++ onionr/coredb/keydb/transportinfo.py | 19 +++++++++++++++++++ onionr/onionr.py | 10 +++++----- 5 files changed, 81 insertions(+), 5 deletions(-) diff --git a/onionr/coredb/keydb/addkeys.py b/onionr/coredb/keydb/addkeys.py index 20d7500a..ca352d17 100644 --- a/onionr/coredb/keydb/addkeys.py +++ b/onionr/coredb/keydb/addkeys.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + add user keys or transport addresses +''' +''' + 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 sqlite3 import onionrevents as events from onionrutils import stringvalidators diff --git a/onionr/coredb/keydb/listkeys.py b/onionr/coredb/keydb/listkeys.py index 3ba23678..50aff637 100644 --- a/onionr/coredb/keydb/listkeys.py +++ b/onionr/coredb/keydb/listkeys.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + get lists for user keys or transport addresses +''' +''' + 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 sqlite3 import logger from onionrutils import epoch diff --git a/onionr/coredb/keydb/removekeys.py b/onionr/coredb/keydb/removekeys.py index ce4ba5fc..c0f1d7a6 100644 --- a/onionr/coredb/keydb/removekeys.py +++ b/onionr/coredb/keydb/removekeys.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + Remove a transport address but don't ban them +''' +''' + 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 sqlite3 import onionrevents as events from onionrutils import stringvalidators diff --git a/onionr/coredb/keydb/transportinfo.py b/onionr/coredb/keydb/transportinfo.py index 9fd642ec..2fcd873b 100644 --- a/onionr/coredb/keydb/transportinfo.py +++ b/onionr/coredb/keydb/transportinfo.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + get or set transport address meta information +''' +''' + 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 sqlite3 def get_address_info(core_inst, address, info): ''' diff --git a/onionr/onionr.py b/onionr/onionr.py index cd16ddfe..fe06457d 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -261,13 +261,13 @@ class Onionr: if len(sys.argv) >= 4: config.reload() config.set(sys.argv[2], sys.argv[3], True) - logger.debug('Configuration file updated.') + logger.info('Configuration file updated.', terminal=True) 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.'))) + logger.info(logger.colors.bold + sys.argv[2] + ': ' + logger.colors.reset + str(config.get(sys.argv[2], logger.colors.fg.red + 'Not set.')), terminal=True) 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] + ' ') + logger.info(logger.colors.bold + 'Get a value: ' + logger.colors.reset + sys.argv[0] + ' ' + sys.argv[1] + ' ', terminal=True) + logger.info(logger.colors.bold + 'Set a value: ' + logger.colors.reset + sys.argv[0] + ' ' + sys.argv[1] + ' ', terminal=True) def execute(self, argument): ''' @@ -302,7 +302,7 @@ class Onionr: ''' 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()))) + logger.info('%sPublic keys in database: \n%s%s' % (logger.colors.fg.lightgreen, logger.colors.fg.green, '\n'.join(self.onionrCore.listPeers())), terminal=True) def addPeer(self): ''' From 94ba3e29d1e119d39bdc2fd6202042905aa2cc9f Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 26 Jun 2019 14:54:13 -0500 Subject: [PATCH 128/173] do not show traceback when adding existing determiistic key --- onionr/onionrcommands/pubkeymanager.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/onionr/onionrcommands/pubkeymanager.py b/onionr/onionrcommands/pubkeymanager.py index a6b067bc..72db422a 100755 --- a/onionr/onionrcommands/pubkeymanager.py +++ b/onionr/onionrcommands/pubkeymanager.py @@ -20,7 +20,7 @@ import sys, getpass import logger, onionrexceptions -from onionrutils import stringvalidators +from onionrutils import stringvalidators, bytesconverter from onionrusers import onionrusers, contactmanager import unpaddedbase32 def add_ID(o_inst): @@ -45,9 +45,13 @@ def add_ID(o_inst): else: logger.error('Passwords do not match.', terminal=True) sys.exit(1) - o_inst.onionrCore._crypto.keyManager.addKey(pubKey=newID, - privKey=privKey) - logger.info('Added ID: %s' % (o_inst.onionrUtils.bytesToStr(newID),), terminal=True) + try: + o_inst.onionrCore._crypto.keyManager.addKey(pubKey=newID, + privKey=privKey) + except ValueError: + logger.error('That ID is already available, you can change to it with the change-id command.', terminal=True) + return + logger.info('Added ID: %s' % (bytesconverter.bytes_to_str(newID),), terminal=True) def change_ID(o_inst): try: From 683aaa39782ffe297b12e6203a3063143d4a51b3 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 26 Jun 2019 15:13:36 -0500 Subject: [PATCH 129/173] added boilerplates --- onionr/coredb/keydb/userinfo.py | 19 +++++++++++++++++++ onionr/httpapi/__init__.py | 2 +- onionr/onionrutils/basicrequests.py | 19 +++++++++++++++++++ onionr/onionrutils/blockmetadata.py | 19 +++++++++++++++++++ onionr/onionrutils/bytesconverter.py | 5 +++-- onionr/onionrutils/checkcommunicator.py | 19 +++++++++++++++++++ onionr/onionrutils/epoch.py | 19 +++++++++++++++++++ onionr/onionrutils/escapeansi.py | 2 +- onionr/onionrutils/getclientapiserver.py | 19 +++++++++++++++++++ onionr/onionrutils/importnewblocks.py | 19 +++++++++++++++++++ onionr/onionrutils/localcommand.py | 19 +++++++++++++++++++ onionr/onionrutils/mnemonickeys.py | 19 +++++++++++++++++++ onionr/onionrutils/stringvalidators.py | 19 +++++++++++++++++++ onionr/onionrutils/validatemetadata.py | 19 +++++++++++++++++++ 14 files changed, 214 insertions(+), 4 deletions(-) diff --git a/onionr/coredb/keydb/userinfo.py b/onionr/coredb/keydb/userinfo.py index 84a737d6..b7a43fd4 100644 --- a/onionr/coredb/keydb/userinfo.py +++ b/onionr/coredb/keydb/userinfo.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + get or set information about a user id +''' +''' + 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 sqlite3 def get_user_info(core_inst, peer, info): ''' diff --git a/onionr/httpapi/__init__.py b/onionr/httpapi/__init__.py index f38d80bd..b3d6c3b3 100755 --- a/onionr/httpapi/__init__.py +++ b/onionr/httpapi/__init__.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file registers plugin's flask blueprints for the client http server ''' diff --git a/onionr/onionrutils/basicrequests.py b/onionr/onionrutils/basicrequests.py index 1435db28..1595b433 100644 --- a/onionr/onionrutils/basicrequests.py +++ b/onionr/onionrutils/basicrequests.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + Do HTTP GET or POST requests through a proxy +''' +''' + 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 requests import logger, onionrexceptions def do_post_request(core_inst, url, data={}, port=0, proxyType='tor'): diff --git a/onionr/onionrutils/blockmetadata.py b/onionr/onionrutils/blockmetadata.py index 91fa1952..7b789edf 100644 --- a/onionr/onionrutils/blockmetadata.py +++ b/onionr/onionrutils/blockmetadata.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + Module to fetch block metadata from raw block data and process it +''' +''' + 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 json, sqlite3 import logger, onionrevents from onionrusers import onionrusers diff --git a/onionr/onionrutils/bytesconverter.py b/onionr/onionrutils/bytesconverter.py index cf69a91d..5df4d673 100644 --- a/onionr/onionrutils/bytesconverter.py +++ b/onionr/onionrutils/bytesconverter.py @@ -1,13 +1,14 @@ def str_to_bytes(data): + '''Converts a string to bytes with .encode()''' try: - data = data.encode() + data = data.encode('UTF-8') except AttributeError: pass return data def bytes_to_str(data): try: - data = data.decode() + data = data.decode('UTF-8') except AttributeError: pass return data \ No newline at end of file diff --git a/onionr/onionrutils/checkcommunicator.py b/onionr/onionrutils/checkcommunicator.py index 07f8dc1a..cfc2c31a 100644 --- a/onionr/onionrutils/checkcommunicator.py +++ b/onionr/onionrutils/checkcommunicator.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + Check if the communicator is running +''' +''' + 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 time, os def is_communicator_running(core_inst, timeout = 5, interval = 0.1): try: diff --git a/onionr/onionrutils/epoch.py b/onionr/onionrutils/epoch.py index 0cda05cb..01922a4c 100644 --- a/onionr/onionrutils/epoch.py +++ b/onionr/onionrutils/epoch.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + Get floored epoch, or rounded epoch +''' +''' + 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 math, time def get_rounded_epoch(roundS=60): ''' diff --git a/onionr/onionrutils/escapeansi.py b/onionr/onionrutils/escapeansi.py index 1cd5862a..1f88e149 100644 --- a/onionr/onionrutils/escapeansi.py +++ b/onionr/onionrutils/escapeansi.py @@ -3,7 +3,7 @@ def escape_ANSI(line): ''' Remove ANSI escape codes from a string with regex - taken or adapted from: https://stackoverflow.com/a/38662876 by user https://stackoverflow.com/users/802365/%c3%89douard-lopez + adapted from: https://stackoverflow.com/a/38662876 by user https://stackoverflow.com/users/802365/%c3%89douard-lopez cc-by-sa-3 license https://creativecommons.org/licenses/by-sa/3.0/ ''' ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]') diff --git a/onionr/onionrutils/getclientapiserver.py b/onionr/onionrutils/getclientapiserver.py index 23020f66..e0afe61b 100644 --- a/onionr/onionrutils/getclientapiserver.py +++ b/onionr/onionrutils/getclientapiserver.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + Return the client api server address and port, which is usually random +''' +''' + 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 . +''' def get_client_API_server(core_inst): retData = '' try: diff --git a/onionr/onionrutils/importnewblocks.py b/onionr/onionrutils/importnewblocks.py index bfd985a2..6accd947 100644 --- a/onionr/onionrutils/importnewblocks.py +++ b/onionr/onionrutils/importnewblocks.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + import new blocks from disk, providing transport agnosticism +''' +''' + 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 glob import logger, core from onionrutils import blockmetadata diff --git a/onionr/onionrutils/localcommand.py b/onionr/onionrutils/localcommand.py index c3bf3ceb..13495a3c 100644 --- a/onionr/onionrutils/localcommand.py +++ b/onionr/onionrutils/localcommand.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + send a command to the local API server +''' +''' + 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 urllib, requests, time import logger from onionrutils import getclientapiserver diff --git a/onionr/onionrutils/mnemonickeys.py b/onionr/onionrutils/mnemonickeys.py index 30c7b1c4..56085536 100644 --- a/onionr/onionrutils/mnemonickeys.py +++ b/onionr/onionrutils/mnemonickeys.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + convert a base32 string (intended for ed25519 user ids) to pgp word list +''' +''' + 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 base64 from etc import pgpwords def get_human_readable_ID(core_inst, pub=''): diff --git a/onionr/onionrutils/stringvalidators.py b/onionr/onionrutils/stringvalidators.py index 87fa4e8f..5b7fbf6f 100644 --- a/onionr/onionrutils/stringvalidators.py +++ b/onionr/onionrutils/stringvalidators.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + validate various string data types +''' +''' + 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 base64, string import unpaddedbase32, nacl.signing, nacl.encoding from onionrutils import bytesconverter diff --git a/onionr/onionrutils/validatemetadata.py b/onionr/onionrutils/validatemetadata.py index ec1f51da..2800c24a 100644 --- a/onionr/onionrutils/validatemetadata.py +++ b/onionr/onionrutils/validatemetadata.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + validate new block's metadata +''' +''' + 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 json import logger, onionrexceptions from etc import onionrvalues From 44bcbac8ef27caec9b1ad175326b11b0c21f5f41 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 27 Jun 2019 01:36:52 -0500 Subject: [PATCH 130/173] + added optimization check * fixed stats table * added main docs file (WIP) --- README.md | 2 +- docs/README.md | 3 +++ onionr/onionr.py | 8 ++++++- onionr/onionrcommands/onionrstatistics.py | 11 ++++----- onionr/utils/detectoptimization.py | 27 +++++++++++++++++++++++ 5 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 docs/README.md create mode 100755 onionr/utils/detectoptimization.py diff --git a/README.md b/README.md index ee96efd4..024b6118 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ # About -Onionr is a decentralized, peer-to-peer communication and storage network, designed to be anonymous and resistant to (meta)data analysis, spam, and corruption. +Onionr is a decentralized, peer-to-peer communication network, designed to be anonymous and resistant to (meta)data analysis, spam, and corruption. Onionr stores data in independent packages referred to as 'blocks'. The blocks are synced to all other nodes in the network. Blocks and user IDs cannot be easily proven to have been created by a particular user. Even if there is enough evidence to believe that a specific user created a block, nodes still operate behind Tor or I2P and as such cannot be trivially unmasked. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..343c6c17 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,3 @@ +# Onionr Documentation + +The Onionr [whitepaper](whitepaper.md) is the best place to start both for users and developers. diff --git a/onionr/onionr.py b/onionr/onionr.py index fe06457d..befbf439 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -27,8 +27,14 @@ 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. MIN_PY_VERSION = 6 if sys.version_info[0] == 2 or sys.version_info[1] < MIN_PY_VERSION: - sys.stderr.write('Error, Onionr requires Python 3.%s+' % (MIN_PY_VERSION,)) + sys.stderr.write('Error, Onionr requires Python 3.%s+\n' % (MIN_PY_VERSION,)) sys.exit(1) + +from utils import detectoptimization +if detectoptimization.detect_optimization(): + sys.stderr.write('Error, Onionr cannot be run in optimized mode\n') + sys.exit(1) + import os, base64, random, shutil, time, platform, signal from threading import Thread import api, core, config, logger, onionrplugins as plugins, onionrevents as events diff --git a/onionr/onionrcommands/onionrstatistics.py b/onionr/onionrcommands/onionrstatistics.py index 83d62baa..c01c5e1b 100755 --- a/onionr/onionrcommands/onionrstatistics.py +++ b/onionr/onionrcommands/onionrstatistics.py @@ -18,10 +18,11 @@ along with this program. If not, see . ''' import os, uuid, time -import logger, onionrutils +import logger from onionrblockapi import Block import onionr from onionrutils import checkcommunicator, mnemonickeys +from utils import sizeutils def show_stats(o_inst): try: @@ -34,9 +35,9 @@ def show_stats(o_inst): # file and folder size stats 'div1' : True, # this creates a solid line across the screen, a div - 'Total Block Size' : onionrutils.humanSize(onionrutils.size(o_inst.dataDir + 'blocks/')), - 'Total Plugin Size' : onionrutils.humanSize(onionrutils.size(o_inst.dataDir + 'plugins/')), - 'Log File Size' : onionrutils.humanSize(onionrutils.size(o_inst.dataDir + 'output.log')), + 'Total Block Size' : sizeutils.human_size(sizeutils.size(o_inst.dataDir + 'blocks/')), + 'Total Plugin Size' : sizeutils.human_size(sizeutils.size(o_inst.dataDir + 'plugins/')), + 'Log File Size' : sizeutils.human_size(sizeutils.size(o_inst.dataDir + 'output.log')), # count stats 'div2' : True, @@ -80,7 +81,7 @@ def show_stats(o_inst): logger.info(colors['border'] + '-' * (maxlength + 1) + '+' + colors['reset'], terminal=True) logger.info(colors['border'] + '-' * (maxlength + 1) + '+' + colors['reset'], terminal=True) except Exception as e: - logger.error('Failed to generate statistics table.', error = e, timestamp = False, terminal=True) + logger.error('Failed to generate statistics table. ' + str(e), error = e, timestamp = False, terminal=True) def show_details(o_inst): details = { diff --git a/onionr/utils/detectoptimization.py b/onionr/utils/detectoptimization.py new file mode 100755 index 00000000..20570242 --- /dev/null +++ b/onionr/utils/detectoptimization.py @@ -0,0 +1,27 @@ +''' + Onionr - Private P2P Communication + + Detect if Python is being run in optimized mode or not, which has security considerations for assert statements +''' +''' + 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 . +''' + +def detect_optimization(): + '''Returns true if Python is run in optimized mode (-o), based on optimization ignoring assert statements''' + try: + assert True is False + except AssertionError: + return False + return True \ No newline at end of file From dce453db81a200eed06f04ca60d6a5068f48520f Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 27 Jun 2019 13:45:28 -0500 Subject: [PATCH 131/173] fixed output in onionr.py, work on docs --- docs/README.md | 16 ++++++++++++++++ docs/{ => dev}/specs/block-spec.md | 0 onionr/onionr.py | 4 ++-- 3 files changed, 18 insertions(+), 2 deletions(-) rename docs/{ => dev}/specs/block-spec.md (100%) diff --git a/docs/README.md b/docs/README.md index 343c6c17..6c5f44e5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,3 +1,19 @@ # Onionr Documentation The Onionr [whitepaper](whitepaper.md) is the best place to start both for users and developers. + +## User Documentation + +* [Installation](usage/install.md) +* [First steps](usage/firststeps.md) +* [Using Onionr Mail](usage/mail.md) +* [Using Onionr web pages](usage/pages.md) +* [Staying safe/anonymous](usage/safety.md) + +## Developer Documentation + +* [Development environment setup](dev/setup.md) +* [Technical overview](dev/overview.md) +* [Project layout](dev/layout.md) +* [Plugin development guide](dev/plugins.md) +* [Testing](dev/testing.md) \ No newline at end of file diff --git a/docs/specs/block-spec.md b/docs/dev/specs/block-spec.md similarity index 100% rename from docs/specs/block-spec.md rename to docs/dev/specs/block-spec.md diff --git a/onionr/onionr.py b/onionr/onionr.py index befbf439..6ca5002e 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -236,13 +236,13 @@ class Onionr: def listPeers(self): logger.info('Peer transport address list:') for i in self.onionrCore.listAdders(): - logger.info(i) + logger.info(i, terminal=True) def getWebPassword(self): return config.get('client.webpassword') def printWebPassword(self): - logger.info(self.getWebPassword(), term_only = True) + logger.info(self.getWebPassword(), terminal=True) def getHelp(self): return self.cmdhelp From 03fbf32af91b51c5fc4b37d24a85d87f54e66f99 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 27 Jun 2019 21:43:50 -0500 Subject: [PATCH 132/173] added reconstructhash for working with block hashes without their leading zeroes --- docs/usage/install.md | 0 onionr/static-data/www/mail/mail.js | 2 +- onionr/tests/test_zfill.py | 21 ++++++++++++++ onionr/utils/reconstructhash.py | 43 +++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 docs/usage/install.md create mode 100644 onionr/tests/test_zfill.py create mode 100644 onionr/utils/reconstructhash.py diff --git a/docs/usage/install.md b/docs/usage/install.md new file mode 100644 index 00000000..e69de29b diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index 27180173..d3762135 100755 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -77,7 +77,7 @@ function openThread(bHash, sender, date, sigBool, pubkey, subjectLine){ var sigMsg = 'signature' // show add unknown contact button if peer is unknown but still has pubkey - if (sender === pubkey && sender !== myPub){ + if (sender === pubkey && sender !== myPub && sigBool){ addUnknownContact.style.display = 'inline' } diff --git a/onionr/tests/test_zfill.py b/onionr/tests/test_zfill.py new file mode 100644 index 00000000..e13ce1bb --- /dev/null +++ b/onionr/tests/test_zfill.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +import unittest, sys +sys.path.append(".") + +from utils import reconstructhash + +class ZFill_Hash(unittest.TestCase): + def test_reconstruct(self): + h = b"4d20d791cbc293999b97cc627aa011692d317dede3d0fbd390c763210b0d" + self.assertEqual(reconstructhash.reconstruct_hash(h), b"0000" + h) + h = b"4d20d791cbc293999b97cc627aa011692d317dede3d0fbd390c763210b0d" + self.assertEqual(reconstructhash.reconstruct_hash(h, 62), b"00" + h) + + def test_deconstruct(self): + h = b"0000e918d24999ad9b0ff00c1d414f36b74afc93871a0ece4bd452f82b56af87" + h_no = b"e918d24999ad9b0ff00c1d414f36b74afc93871a0ece4bd452f82b56af87" + self.assertEqual(reconstructhash.deconstruct_hash(h), h_no) + h = "0000e918d24999ad9b0ff00c1d414f36b74afc93871a0ece4bd452f82b56af87" + h_no = "e918d24999ad9b0ff00c1d414f36b74afc93871a0ece4bd452f82b56af87" + self.assertEqual(reconstructhash.deconstruct_hash(h), h_no) +unittest.main() \ No newline at end of file diff --git a/onionr/utils/reconstructhash.py b/onionr/utils/reconstructhash.py new file mode 100644 index 00000000..ae50c0b2 --- /dev/null +++ b/onionr/utils/reconstructhash.py @@ -0,0 +1,43 @@ +''' + Onionr - Private P2P Communication + + z-fill (zero fill) a string to a specific length, intended for reconstructing block hashes +''' +''' + 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 . +''' +def reconstruct_hash(hex_hash, length=64): + return hex_hash.zfill(length) + +def deconstruct_hash(hex_hash): + new_hash = '' + ret_bytes = False + try: + hex_hash = hex_hash.decode() + ret_bytes = True + except AttributeError: + pass + + c = 0 + for x in hex_hash: + if x == '0': + c += 1 + else: + break + new_hash = hex_hash[c:] + + if ret_bytes: + + new_hash = new_hash.encode() + return new_hash \ No newline at end of file From a27a5e5a4551a2165e3c145f454f57a7e6143671 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 28 Jun 2019 00:00:38 -0500 Subject: [PATCH 133/173] added support for 0-truncated hashes in stringvalidators, added test for hash validation --- onionr/onionrutils/stringvalidators.py | 4 +++- onionr/tests/test_stringvalidations.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/onionr/onionrutils/stringvalidators.py b/onionr/onionrutils/stringvalidators.py index 5b7fbf6f..951c25ed 100644 --- a/onionr/onionrutils/stringvalidators.py +++ b/onionr/onionrutils/stringvalidators.py @@ -23,12 +23,14 @@ from onionrutils import bytesconverter def validate_hash(data, length=64): ''' Validate if a string is a valid hash hex digest (does not compare, just checks length and charset) + + Length is only invalid if its *more* than the specified ''' retVal = True if data == False or data == True: return False data = data.strip() - if len(data) != length: + if len(data) > length: retVal = False else: try: diff --git a/onionr/tests/test_stringvalidations.py b/onionr/tests/test_stringvalidations.py index 72d5fb20..eef418ba 100755 --- a/onionr/tests/test_stringvalidations.py +++ b/onionr/tests/test_stringvalidations.py @@ -27,6 +27,25 @@ class OnionrValidations(unittest.TestCase): print('testing', x) self.assertFalse(stringvalidators.validate_transport(x)) + def test_hash_validator(self): + valid = ['00003b3813a166e706e490238e9515633cc3d083efe982a67753d50d87a00c96\n', '00003b3813a166e706e490238e9515633cc3d083efe982a67753d50d87a00c96', b'00003b3813a166e706e490238e9515633cc3d083efe982a67753d50d87a00c96', + '00003b3813a166e706e490238e9515633cc36', b'00003b3813a166e706e490238e9515633cc3d083'] + invalid = [None, 0, 1, True, False, '%#W483242#', '00003b3813a166e706e490238e9515633cc3d083efe982a67753d50d87a00c9666', '', b'', + b'00003b3813a166e706e490238e9515633cc3d083efe982a67753d50d87a00c9666666', b' ', '\n', '00003b3813a166e706e490238e9515633cc3d083efe982a67753d50d87a00ccccc\n'] + + for x in valid: + self.assertTrue(stringvalidators.validate_hash(x)) + for x in invalid: + try: + result = stringvalidators.validate_hash(x) + print('testing', x, result) + except AttributeError: + result = False + try: + self.assertFalse(result) + except AssertionError: + raise AssertionError("%s returned true" % (x,)) + def test_pubkey_validator(self): # Test ed25519 public key validity valids = ['JZ5VE72GUS3C7BOHDRIYZX4B5U5EJMCMLKHLYCVBQQF3UKHYIRRQ====', 'JZ5VE72GUS3C7BOHDRIYZX4B5U5EJMCMLKHLYCVBQQF3UKHYIRRQ'] From bc8080765d45128390c265f84ba3991430bd0f51 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 28 Jun 2019 15:23:03 -0500 Subject: [PATCH 134/173] work on docs, added firststeps.md --- docs/{ => dev}/http-api.md | 0 docs/usage/firststeps.md | 7 +++++++ docs/usage/install.md | 13 +++++++++++++ 3 files changed, 20 insertions(+) rename docs/{ => dev}/http-api.md (100%) create mode 100644 docs/usage/firststeps.md diff --git a/docs/http-api.md b/docs/dev/http-api.md similarity index 100% rename from docs/http-api.md rename to docs/dev/http-api.md diff --git a/docs/usage/firststeps.md b/docs/usage/firststeps.md new file mode 100644 index 00000000..8733565d --- /dev/null +++ b/docs/usage/firststeps.md @@ -0,0 +1,7 @@ +# Onionr First Steps + +After installing Onionr, there are several things to do: + +1. Setup a [deterministic address](usage/deterministic.md) (optional) +2. Add friends' ids +3. Publish your id \ No newline at end of file diff --git a/docs/usage/install.md b/docs/usage/install.md index e69de29b..f1fb6130 100644 --- a/docs/usage/install.md +++ b/docs/usage/install.md @@ -0,0 +1,13 @@ +# Onionr Installation + +The following steps work broadly speaking for Windows, Mac, and Linux. + +1. Verify python3.6+ is installed: if its not see https://www.python.org/downloads/ + +2. Verify Tor is installed (does not need to be running, binary can be put into system path or Onionr directory) + +3. [Optional but recommended]: setup virtual environment using [virtualenv](https://virtualenv.pypa.io/en/latest/), activate the virtual environment + +4. Clone Onionr: git clone https://gitlab.com/beardog/onionr + +5. Install the Python module dependencies: pip3 install --require-hashes -r requirements.txt From bb5378ec4caed494f8afd1f3a13eac4cc2212586 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 28 Jun 2019 16:10:29 -0500 Subject: [PATCH 135/173] fix importblocks metadata call not having core inst --- onionr/onionrutils/importnewblocks.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/onionr/onionrutils/importnewblocks.py b/onionr/onionrutils/importnewblocks.py index 6accd947..e1793adf 100644 --- a/onionr/onionrutils/importnewblocks.py +++ b/onionr/onionrutils/importnewblocks.py @@ -35,14 +35,14 @@ def import_new_blocks(core_inst=None, scanDir=''): for block in glob.glob(scanDir + "*.dat"): if block.replace(scanDir, '').replace('.dat', '') not in blockList: exist = True - logger.info('Found new block on dist %s' % block) + logger.info('Found new block on dist %s' % block, terminal=True) with open(block, 'rb') as newBlock: block = block.replace(scanDir, '').replace('.dat', '') if core_inst._crypto.sha3Hash(newBlock.read()) == block.replace('.dat', ''): core_inst.addToBlockDB(block.replace('.dat', ''), dataSaved=True) - logger.info('Imported block %s.' % block) - blockmetadata.process_block_metadata(block) + logger.info('Imported block %s.' % block, terminal=True) + blockmetadata.process_block_metadata(core_inst, block) else: - logger.warn('Failed to verify hash for %s' % block) + logger.warn('Failed to verify hash for %s' % block, terminal=True) if not exist: - logger.info('No blocks found to import') \ No newline at end of file + logger.info('No blocks found to import', terminal=True) \ No newline at end of file From 33340746dd5ae02985abf02e9c027a59a7b95291 Mon Sep 17 00:00:00 2001 From: Travis Date: Fri, 28 Jun 2019 16:24:34 -0500 Subject: [PATCH 136/173] Implemented Bulma for the new UI. Tried to use the template element and JavaScript for some elements. --- onionr/static-data/www/board/board.js | 32 ++- onionr/static-data/www/board/index.html | 176 ++++++++++--- onionr/static-data/www/clandestine/index.html | 99 ++++++-- onionr/static-data/www/friends/friends.js | 25 ++ onionr/static-data/www/friends/index.html | 195 +++++++++++--- onionr/static-data/www/friends/style.css | 4 +- onionr/static-data/www/mail/index.html | 222 +++++++++++----- onionr/static-data/www/private/index.html | 238 ++++++++++++++---- .../www/shared/images/onionr-text.png | Bin 0 -> 6612 bytes .../static-data/www/shared/main/bulma.min.css | 1 + .../www/shared/main/styles-new.css | 53 ++++ onionr/static-data/www/shared/misc.js | 17 +- 12 files changed, 860 insertions(+), 202 deletions(-) create mode 100644 onionr/static-data/www/shared/images/onionr-text.png create mode 100644 onionr/static-data/www/shared/main/bulma.min.css create mode 100644 onionr/static-data/www/shared/main/styles-new.css diff --git a/onionr/static-data/www/board/board.js b/onionr/static-data/www/board/board.js index 7d70f9db..ad6c4faf 100755 --- a/onionr/static-data/www/board/board.js +++ b/onionr/static-data/www/board/board.js @@ -10,7 +10,7 @@ function appendMessages(msg){ return } var msg = JSON.parse(msg) - var dateEl = document.createElement('span') + var dateEl = document.createElement('div') var el = document.createElement('div') var msgDate = msg['meta']['time'] if (msgDate === undefined){ @@ -23,9 +23,29 @@ function appendMessages(msg){ dateEl.textContent = msgDate el.className = 'entry' el.innerText = msg['content'] - document.getElementById('feed').appendChild(dateEl) - document.getElementById('feed').appendChild(el) - document.getElementById('feed').appendChild(document.createElement('br')) + + /* Template Test */ + // Test to see if the browser supports the HTML template element by checking + // for the presence of the template element's content attribute. + if ('content' in document.createElement('template')) { + + // Instantiate the table with the existing HTML tbody + // and the row with the template + var template = document.getElementById('cMsgTemplate'); + + // Clone the new row and insert it into the table + var feed = document.getElementById("feed"); + var clone = document.importNode(template.content, true); + var div = clone.querySelectorAll("div"); + div[2].textContent = msg['content']; + div[3].textContent = msgDate; + + feed.appendChild(clone); + + } else { + // Find another way to add the rows to the table because + // the HTML template element is not supported. + } } function getBlocks(){ @@ -48,6 +68,10 @@ document.getElementById('refreshFeed').onclick = function(){ getBlocks() } +window.onload = function() { + getBlocks(); +} + newPostForm.onsubmit = function(){ var message = document.getElementById('newMsgText').value var postData = {'message': message, 'type': 'txt', 'encrypt': false} diff --git a/onionr/static-data/www/board/index.html b/onionr/static-data/www/board/index.html index 37d22ea9..8101b55c 100755 --- a/onionr/static-data/www/board/index.html +++ b/onionr/static-data/www/board/index.html @@ -1,31 +1,151 @@ - - - - - Circle - - - - - - -

        Circle

        -

        - Anonymous message board -

        -
        - -

        - -
        -

        -
        - -

        - None Yet :)
        - - - + + + + + + + Circles + + + + + + + + + + +
        +
        +
        +
        +
        +

        + Circles +

        +

        + Anonymous message board +

        +
        +
        +
        + +
        +

        + Identity +

        +

        + +

        +

        + Copy +

        +
        +
        +
        +

        + Refresh Feed +

        +
        +
        +
        +
        +
        +
        + + +
        + + +
        +
        + +
        +
        +
        +

        + Post message +

        +
        +
        +
        +
        + + +
        +
        +
        + +
        +
        + +
        +
        +
        +

        + Feed +

        +
        +
        +
        +
        + None Yet :) + +
        +
        +
        +
        +
        +
        +
        + + + + + + + + + \ No newline at end of file diff --git a/onionr/static-data/www/clandestine/index.html b/onionr/static-data/www/clandestine/index.html index 91bc2e07..7752c81f 100755 --- a/onionr/static-data/www/clandestine/index.html +++ b/onionr/static-data/www/clandestine/index.html @@ -1,22 +1,87 @@ - - - - Clandestine - - - - - - -
        - - Clandestine -
          + + + + + + Clandestine + + + + + + + + + + + +
          +
          +
          +
          +
          +

          + Clandestine +

          +

          + Instant messaging +

          +
          +
          +
          + +
          +

          + Identity +

          +

          + +

          +

          + Copy +

          +
          +
          +
          +
          +
          +
          +
          + + +
          + +
          +
            +
            + + + + \ No newline at end of file diff --git a/onionr/static-data/www/friends/friends.js b/onionr/static-data/www/friends/friends.js index cd17bbc8..99973b22 100755 --- a/onionr/static-data/www/friends/friends.js +++ b/onionr/static-data/www/friends/friends.js @@ -78,6 +78,7 @@ fetch('/friends/list', { entry.appendChild(removeButton) entry.appendChild(nameText) friendListDisplay.appendChild(entry) + //I put the Template code here but it made things slightly worse entry.onclick = (function(entry, nameText, peer) {return function() { if (nameText.length == 0){ nameText = 'Anonymous' @@ -97,6 +98,30 @@ fetch('/friends/list', { removeFriend(friendKey) } } + + /* Template to add Friends to page */ + // Test to see if the browser supports the HTML template element by checking + // for the presence of the template element's content attribute. + if ('content' in document.createElement('template')) { + + // Instantiate the table with the existing HTML tbody + // and the row with the template + var template = document.getElementById('friendTemplate'); + + // Clone the new row and insert it into the table + var list = document.getElementById("friendList"); + var clone = document.importNode(template.content, true); + var div = clone.querySelectorAll("div"); + div[2].textContent = nameText.value; + div[3].textContent = peer; + div[3].setAttribute('data-pubkey', peer) + + list.appendChild(clone); + + } else { + // Find another way to add the rows to the table because + // the HTML template element is not supported. + } }) document.getElementById('defriend').onclick = function(){ diff --git a/onionr/static-data/www/friends/index.html b/onionr/static-data/www/friends/index.html index 78590d55..57418c49 100755 --- a/onionr/static-data/www/friends/index.html +++ b/onionr/static-data/www/friends/index.html @@ -1,41 +1,168 @@ - - - - Onionr - - - - - - - -
            -
            - -
            Name:
            - - + + + + + + + Friends + + + + + + + + + + + + +
            +
            +
            +
            +
            +

            + Friends +

            +

            + Manage your friend list +

            +
            +
            +
            + +
            +

            + Identity +

            +

            + +

            +

            + Copy +

            +
            +
            +
            +
            - - - +
            + + +
            + + +
            +
            + +
            +
            +
            +

            + Add Friend +

            +
            +
            +
            +
            +
            + +

            + +

            +
            +
            + +

            + +

            +
            + +
            +
            + +
            +
            + +
            +
            +
            +

            + Friend List +

            +
            +
            +
            +
            + +
            +
            +
            +
            +
            +
            +
            + +
            +
            + +
            Name:
            + + +
            +
            + + + + + + + + \ No newline at end of file diff --git a/onionr/static-data/www/friends/style.css b/onionr/static-data/www/friends/style.css index 34bf64db..b309437b 100755 --- a/onionr/static-data/www/friends/style.css +++ b/onionr/static-data/www/friends/style.css @@ -1,3 +1,4 @@ +/* h2, h3{ font-family: Arial, Helvetica, sans-serif; } @@ -38,4 +39,5 @@ form label{ #defriend{ display: block; margin-top: 1em; -} \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index 309cdc88..458cfdb4 100755 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -1,75 +1,167 @@ - - - - Onionr Mail - - - - - - - -
            + + + + + + + Onionr Mail + + + + + + + + + + + +
            +
            +
            +
            +
            +

            + Mail +

            +

            + Send email style messages to persons +

            -
            - Subject: +
            +
            + +
            +

            + Identity +

            +

            + +

            +

            + Copy +

            +
            +
            +
            +

            + Refresh Page +

            +
            -
            - -
            -
            -
            -
            -
            -
            -
            -
            - - To: -
            -
            -
            -
            -
            -
            - - - - To: - Subject: - - -
            - - - - +
            +
            + +
            + + +
            + +
            + + +
            +
            +
            +
            + API server either shutdown, has disabled mail, or has experienced a bug. +
            +
            + +
            +
            +
            Nothing here yet 😞
            +
            +
            +
            + +
            + From: Signature: +
            +
            + Subject: +
            +
            + +
            +
            +
            +
            +
            +
            +
            +
            + + To: +
            +
            +
            +
            +
            +
            + +
            + + To: + Subject: + + +
            +
            +
            +
            + + + + + + \ No newline at end of file diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index 151c3704..0beefa79 100755 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -1,58 +1,192 @@ - - - - Onionr - - - - - - -
            -
            -

            Your node will shutdown. Thank you for using Onionr.

            + + + + + + + Onionr + + + + + + + +
            +
            +

            Your node will shutdown. Thank you for using Onionr.

            +
            +
            + + + + +
            +
            +
            +
            +
            +

            + Onionr +

            +

            + Private Decentralized Communication +

            +
            +
            +
            + +
            +

            + Identity +

            +

            + +

            +

            + Copy +

            +
            +
            + +
            +
            +
            - - - - - - +
            + + +
            + + +
            +
            +
            + +
            +
            +

            + Onionr +

            +
            +
            +
            +
            + +
            +

            + +

            +

            + Open Onionr Site +

            +
            +
            +
            +
            +
            +
            +
            + +
            +
            +

            + Statistics +

            +
            +
            +
            +
            +
            + Security level here +
            +
            + 🕰️ Uptime: +
            +
            +
            Connections
            +
            +
            + ️ Last Received: None since start +
            +
            + ⬇️ Total Requests: None since start +
            +
            + 🔗 Outgoing Connections: +
            +
            Unable to get nodes
            +
            +
            +
            Blocks
            +
            +
            + 💾 Stored Blocks: +
            +
            + 📨 Blocks in queue: +
            +
            +
            +
            +
            +
            +
            + + +
            +
            +

            + Edit Configuration +

            +
            +
            +
            +

            Warning: Some values can be dangerous to change. Use caution.

            + + Save Config +
            +
            +
            +
            +
            + + + + + + + \ No newline at end of file diff --git a/onionr/static-data/www/shared/images/onionr-text.png b/onionr/static-data/www/shared/images/onionr-text.png new file mode 100644 index 0000000000000000000000000000000000000000..dd85097c46b39ca8c32fe261564d2e4d67b989e3 GIT binary patch literal 6612 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.delete{position:absolute;right:.5rem;top:.5rem}.notification .content,.notification .subtitle,.notification .title{color:currentColor}.notification.is-white{background-color:#fff;color:#0a0a0a}.notification.is-black{background-color:#0a0a0a;color:#fff}.notification.is-light{background-color:#f5f5f5;color:#363636}.notification.is-dark{background-color:#363636;color:#f5f5f5}.notification.is-primary{background-color:#00d1b2;color:#fff}.notification.is-link{background-color:#3273dc;color:#fff}.notification.is-info{background-color:#209cee;color:#fff}.notification.is-success{background-color:#23d160;color:#fff}.notification.is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.notification.is-danger{background-color:#ff3860;color:#fff}.progress{-moz-appearance:none;-webkit-appearance:none;border:none;border-radius:290486px;display:block;height:1rem;overflow:hidden;padding:0;width:100%}.progress::-webkit-progress-bar{background-color:#dbdbdb}.progress::-webkit-progress-value{background-color:#4a4a4a}.progress::-moz-progress-bar{background-color:#4a4a4a}.progress::-ms-fill{background-color:#4a4a4a;border:none}.progress.is-white::-webkit-progress-value{background-color:#fff}.progress.is-white::-moz-progress-bar{background-color:#fff}.progress.is-white::-ms-fill{background-color:#fff}.progress.is-white:indeterminate{background-image:linear-gradient(to right,#fff 30%,#dbdbdb 30%)}.progress.is-black::-webkit-progress-value{background-color:#0a0a0a}.progress.is-black::-moz-progress-bar{background-color:#0a0a0a}.progress.is-black::-ms-fill{background-color:#0a0a0a}.progress.is-black:indeterminate{background-image:linear-gradient(to right,#0a0a0a 30%,#dbdbdb 30%)}.progress.is-light::-webkit-progress-value{background-color:#f5f5f5}.progress.is-light::-moz-progress-bar{background-color:#f5f5f5}.progress.is-light::-ms-fill{background-color:#f5f5f5}.progress.is-light:indeterminate{background-image:linear-gradient(to right,#f5f5f5 30%,#dbdbdb 30%)}.progress.is-dark::-webkit-progress-value{background-color:#363636}.progress.is-dark::-moz-progress-bar{background-color:#363636}.progress.is-dark::-ms-fill{background-color:#363636}.progress.is-dark:indeterminate{background-image:linear-gradient(to right,#363636 30%,#dbdbdb 30%)}.progress.is-primary::-webkit-progress-value{background-color:#00d1b2}.progress.is-primary::-moz-progress-bar{background-color:#00d1b2}.progress.is-primary::-ms-fill{background-color:#00d1b2}.progress.is-primary:indeterminate{background-image:linear-gradient(to right,#00d1b2 30%,#dbdbdb 30%)}.progress.is-link::-webkit-progress-value{background-color:#3273dc}.progress.is-link::-moz-progress-bar{background-color:#3273dc}.progress.is-link::-ms-fill{background-color:#3273dc}.progress.is-link:indeterminate{background-image:linear-gradient(to right,#3273dc 30%,#dbdbdb 30%)}.progress.is-info::-webkit-progress-value{background-color:#209cee}.progress.is-info::-moz-progress-bar{background-color:#209cee}.progress.is-info::-ms-fill{background-color:#209cee}.progress.is-info:indeterminate{background-image:linear-gradient(to right,#209cee 30%,#dbdbdb 30%)}.progress.is-success::-webkit-progress-value{background-color:#23d160}.progress.is-success::-moz-progress-bar{background-color:#23d160}.progress.is-success::-ms-fill{background-color:#23d160}.progress.is-success:indeterminate{background-image:linear-gradient(to right,#23d160 30%,#dbdbdb 30%)}.progress.is-warning::-webkit-progress-value{background-color:#ffdd57}.progress.is-warning::-moz-progress-bar{background-color:#ffdd57}.progress.is-warning::-ms-fill{background-color:#ffdd57}.progress.is-warning:indeterminate{background-image:linear-gradient(to right,#ffdd57 30%,#dbdbdb 30%)}.progress.is-danger::-webkit-progress-value{background-color:#ff3860}.progress.is-danger::-moz-progress-bar{background-color:#ff3860}.progress.is-danger::-ms-fill{background-color:#ff3860}.progress.is-danger:indeterminate{background-image:linear-gradient(to right,#ff3860 30%,#dbdbdb 30%)}.progress:indeterminate{-webkit-animation-duration:1.5s;animation-duration:1.5s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-name:moveIndeterminate;animation-name:moveIndeterminate;-webkit-animation-timing-function:linear;animation-timing-function:linear;background-color:#dbdbdb;background-image:linear-gradient(to right,#4a4a4a 30%,#dbdbdb 30%);background-position:top left;background-repeat:no-repeat;background-size:150% 150%}.progress:indeterminate::-webkit-progress-bar{background-color:transparent}.progress:indeterminate::-moz-progress-bar{background-color:transparent}.progress.is-small{height:.75rem}.progress.is-medium{height:1.25rem}.progress.is-large{height:1.5rem}@-webkit-keyframes moveIndeterminate{from{background-position:200% 0}to{background-position:-200% 0}}@keyframes moveIndeterminate{from{background-position:200% 0}to{background-position:-200% 0}}.table{background-color:#fff;color:#363636}.table td,.table th{border:1px solid #dbdbdb;border-width:0 0 1px;padding:.5em .75em;vertical-align:top}.table td.is-white,.table th.is-white{background-color:#fff;border-color:#fff;color:#0a0a0a}.table td.is-black,.table th.is-black{background-color:#0a0a0a;border-color:#0a0a0a;color:#fff}.table td.is-light,.table th.is-light{background-color:#f5f5f5;border-color:#f5f5f5;color:#363636}.table td.is-dark,.table th.is-dark{background-color:#363636;border-color:#363636;color:#f5f5f5}.table td.is-primary,.table th.is-primary{background-color:#00d1b2;border-color:#00d1b2;color:#fff}.table td.is-link,.table th.is-link{background-color:#3273dc;border-color:#3273dc;color:#fff}.table td.is-info,.table th.is-info{background-color:#209cee;border-color:#209cee;color:#fff}.table td.is-success,.table th.is-success{background-color:#23d160;border-color:#23d160;color:#fff}.table td.is-warning,.table th.is-warning{background-color:#ffdd57;border-color:#ffdd57;color:rgba(0,0,0,.7)}.table td.is-danger,.table th.is-danger{background-color:#ff3860;border-color:#ff3860;color:#fff}.table td.is-narrow,.table th.is-narrow{white-space:nowrap;width:1%}.table td.is-selected,.table th.is-selected{background-color:#00d1b2;color:#fff}.table td.is-selected a,.table td.is-selected strong,.table th.is-selected a,.table th.is-selected strong{color:currentColor}.table th{color:#363636}.table th:not([align]){text-align:left}.table tr.is-selected{background-color:#00d1b2;color:#fff}.table tr.is-selected a,.table tr.is-selected strong{color:currentColor}.table tr.is-selected td,.table tr.is-selected th{border-color:#fff;color:currentColor}.table thead{background-color:transparent}.table thead td,.table thead th{border-width:0 0 2px;color:#363636}.table tfoot{background-color:transparent}.table tfoot td,.table tfoot th{border-width:2px 0 0;color:#363636}.table tbody{background-color:transparent}.table tbody tr:last-child td,.table tbody tr:last-child th{border-bottom-width:0}.table.is-bordered td,.table.is-bordered th{border-width:1px}.table.is-bordered tr:last-child td,.table.is-bordered tr:last-child th{border-bottom-width:1px}.table.is-fullwidth{width:100%}.table.is-hoverable tbody tr:not(.is-selected):hover{background-color:#fafafa}.table.is-hoverable.is-striped tbody tr:not(.is-selected):hover{background-color:#fafafa}.table.is-hoverable.is-striped tbody tr:not(.is-selected):hover:nth-child(even){background-color:#f5f5f5}.table.is-narrow td,.table.is-narrow th{padding:.25em .5em}.table.is-striped tbody tr:not(.is-selected):nth-child(even){background-color:#fafafa}.table-container{-webkit-overflow-scrolling:touch;overflow:auto;overflow-y:hidden;max-width:100%}.tags{align-items:center;display:flex;flex-wrap:wrap;justify-content:flex-start}.tags .tag{margin-bottom:.5rem}.tags .tag:not(:last-child){margin-right:.5rem}.tags:last-child{margin-bottom:-.5rem}.tags:not(:last-child){margin-bottom:1rem}.tags.are-medium .tag:not(.is-normal):not(.is-large){font-size:1rem}.tags.are-large .tag:not(.is-normal):not(.is-medium){font-size:1.25rem}.tags.is-centered{justify-content:center}.tags.is-centered .tag{margin-right:.25rem;margin-left:.25rem}.tags.is-right{justify-content:flex-end}.tags.is-right .tag:not(:first-child){margin-left:.5rem}.tags.is-right .tag:not(:last-child){margin-right:0}.tags.has-addons .tag{margin-right:0}.tags.has-addons .tag:not(:first-child){margin-left:0;border-bottom-left-radius:0;border-top-left-radius:0}.tags.has-addons .tag:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.tag:not(body){align-items:center;background-color:#f5f5f5;border-radius:4px;color:#4a4a4a;display:inline-flex;font-size:.75rem;height:2em;justify-content:center;line-height:1.5;padding-left:.75em;padding-right:.75em;white-space:nowrap}.tag:not(body) .delete{margin-left:.25rem;margin-right:-.375rem}.tag:not(body).is-white{background-color:#fff;color:#0a0a0a}.tag:not(body).is-black{background-color:#0a0a0a;color:#fff}.tag:not(body).is-light{background-color:#f5f5f5;color:#363636}.tag:not(body).is-dark{background-color:#363636;color:#f5f5f5}.tag:not(body).is-primary{background-color:#00d1b2;color:#fff}.tag:not(body).is-link{background-color:#3273dc;color:#fff}.tag:not(body).is-info{background-color:#209cee;color:#fff}.tag:not(body).is-success{background-color:#23d160;color:#fff}.tag:not(body).is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.tag:not(body).is-danger{background-color:#ff3860;color:#fff}.tag:not(body).is-normal{font-size:.75rem}.tag:not(body).is-medium{font-size:1rem}.tag:not(body).is-large{font-size:1.25rem}.tag:not(body) .icon:first-child:not(:last-child){margin-left:-.375em;margin-right:.1875em}.tag:not(body) .icon:last-child:not(:first-child){margin-left:.1875em;margin-right:-.375em}.tag:not(body) .icon:first-child:last-child{margin-left:-.375em;margin-right:-.375em}.tag:not(body).is-delete{margin-left:1px;padding:0;position:relative;width:2em}.tag:not(body).is-delete::after,.tag:not(body).is-delete::before{background-color:currentColor;content:"";display:block;left:50%;position:absolute;top:50%;-webkit-transform:translateX(-50%) translateY(-50%) rotate(45deg);transform:translateX(-50%) translateY(-50%) rotate(45deg);-webkit-transform-origin:center center;transform-origin:center center}.tag:not(body).is-delete::before{height:1px;width:50%}.tag:not(body).is-delete::after{height:50%;width:1px}.tag:not(body).is-delete:focus,.tag:not(body).is-delete:hover{background-color:#e8e8e8}.tag:not(body).is-delete:active{background-color:#dbdbdb}.tag:not(body).is-rounded{border-radius:290486px}a.tag:hover{text-decoration:underline}.subtitle,.title{word-break:break-word}.subtitle em,.subtitle span,.title em,.title span{font-weight:inherit}.subtitle sub,.title sub{font-size:.75em}.subtitle sup,.title sup{font-size:.75em}.subtitle .tag,.title .tag{vertical-align:middle}.title{color:#363636;font-size:2rem;font-weight:600;line-height:1.125}.title strong{color:inherit;font-weight:inherit}.title+.highlight{margin-top:-.75rem}.title:not(.is-spaced)+.subtitle{margin-top:-1.25rem}.title.is-1{font-size:3rem}.title.is-2{font-size:2.5rem}.title.is-3{font-size:2rem}.title.is-4{font-size:1.5rem}.title.is-5{font-size:1.25rem}.title.is-6{font-size:1rem}.title.is-7{font-size:.75rem}.subtitle{color:#4a4a4a;font-size:1.25rem;font-weight:400;line-height:1.25}.subtitle strong{color:#363636;font-weight:600}.subtitle:not(.is-spaced)+.title{margin-top:-1.25rem}.subtitle.is-1{font-size:3rem}.subtitle.is-2{font-size:2.5rem}.subtitle.is-3{font-size:2rem}.subtitle.is-4{font-size:1.5rem}.subtitle.is-5{font-size:1.25rem}.subtitle.is-6{font-size:1rem}.subtitle.is-7{font-size:.75rem}.heading{display:block;font-size:11px;letter-spacing:1px;margin-bottom:5px;text-transform:uppercase}.highlight{font-weight:400;max-width:100%;overflow:hidden;padding:0}.highlight pre{overflow:auto;max-width:100%}.number{align-items:center;background-color:#f5f5f5;border-radius:290486px;display:inline-flex;font-size:1.25rem;height:2em;justify-content:center;margin-right:1.5rem;min-width:2.5em;padding:.25rem .5rem;text-align:center;vertical-align:top}.input,.select select,.textarea{background-color:#fff;border-color:#dbdbdb;border-radius:4px;color:#363636}.input::-moz-placeholder,.select select::-moz-placeholder,.textarea::-moz-placeholder{color:rgba(54,54,54,.3)}.input::-webkit-input-placeholder,.select select::-webkit-input-placeholder,.textarea::-webkit-input-placeholder{color:rgba(54,54,54,.3)}.input:-moz-placeholder,.select select:-moz-placeholder,.textarea:-moz-placeholder{color:rgba(54,54,54,.3)}.input:-ms-input-placeholder,.select select:-ms-input-placeholder,.textarea:-ms-input-placeholder{color:rgba(54,54,54,.3)}.input:hover,.is-hovered.input,.is-hovered.textarea,.select select.is-hovered,.select select:hover,.textarea:hover{border-color:#b5b5b5}.input:active,.input:focus,.is-active.input,.is-active.textarea,.is-focused.input,.is-focused.textarea,.select select.is-active,.select select.is-focused,.select select:active,.select select:focus,.textarea:active,.textarea:focus{border-color:#3273dc;box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.input[disabled],.select fieldset[disabled] select,.select select[disabled],.textarea[disabled],fieldset[disabled] .input,fieldset[disabled] .select select,fieldset[disabled] .textarea{background-color:#f5f5f5;border-color:#f5f5f5;box-shadow:none;color:#7a7a7a}.input[disabled]::-moz-placeholder,.select fieldset[disabled] select::-moz-placeholder,.select select[disabled]::-moz-placeholder,.textarea[disabled]::-moz-placeholder,fieldset[disabled] .input::-moz-placeholder,fieldset[disabled] .select select::-moz-placeholder,fieldset[disabled] .textarea::-moz-placeholder{color:rgba(122,122,122,.3)}.input[disabled]::-webkit-input-placeholder,.select fieldset[disabled] select::-webkit-input-placeholder,.select select[disabled]::-webkit-input-placeholder,.textarea[disabled]::-webkit-input-placeholder,fieldset[disabled] .input::-webkit-input-placeholder,fieldset[disabled] .select select::-webkit-input-placeholder,fieldset[disabled] .textarea::-webkit-input-placeholder{color:rgba(122,122,122,.3)}.input[disabled]:-moz-placeholder,.select fieldset[disabled] select:-moz-placeholder,.select select[disabled]:-moz-placeholder,.textarea[disabled]:-moz-placeholder,fieldset[disabled] .input:-moz-placeholder,fieldset[disabled] .select select:-moz-placeholder,fieldset[disabled] .textarea:-moz-placeholder{color:rgba(122,122,122,.3)}.input[disabled]:-ms-input-placeholder,.select fieldset[disabled] select:-ms-input-placeholder,.select select[disabled]:-ms-input-placeholder,.textarea[disabled]:-ms-input-placeholder,fieldset[disabled] .input:-ms-input-placeholder,fieldset[disabled] .select select:-ms-input-placeholder,fieldset[disabled] .textarea:-ms-input-placeholder{color:rgba(122,122,122,.3)}.input,.textarea{box-shadow:inset 0 1px 2px rgba(10,10,10,.1);max-width:100%;width:100%}.input[readonly],.textarea[readonly]{box-shadow:none}.is-white.input,.is-white.textarea{border-color:#fff}.is-white.input:active,.is-white.input:focus,.is-white.is-active.input,.is-white.is-active.textarea,.is-white.is-focused.input,.is-white.is-focused.textarea,.is-white.textarea:active,.is-white.textarea:focus{box-shadow:0 0 0 .125em rgba(255,255,255,.25)}.is-black.input,.is-black.textarea{border-color:#0a0a0a}.is-black.input:active,.is-black.input:focus,.is-black.is-active.input,.is-black.is-active.textarea,.is-black.is-focused.input,.is-black.is-focused.textarea,.is-black.textarea:active,.is-black.textarea:focus{box-shadow:0 0 0 .125em rgba(10,10,10,.25)}.is-light.input,.is-light.textarea{border-color:#f5f5f5}.is-light.input:active,.is-light.input:focus,.is-light.is-active.input,.is-light.is-active.textarea,.is-light.is-focused.input,.is-light.is-focused.textarea,.is-light.textarea:active,.is-light.textarea:focus{box-shadow:0 0 0 .125em rgba(245,245,245,.25)}.is-dark.input,.is-dark.textarea{border-color:#363636}.is-dark.input:active,.is-dark.input:focus,.is-dark.is-active.input,.is-dark.is-active.textarea,.is-dark.is-focused.input,.is-dark.is-focused.textarea,.is-dark.textarea:active,.is-dark.textarea:focus{box-shadow:0 0 0 .125em rgba(54,54,54,.25)}.is-primary.input,.is-primary.textarea{border-color:#00d1b2}.is-primary.input:active,.is-primary.input:focus,.is-primary.is-active.input,.is-primary.is-active.textarea,.is-primary.is-focused.input,.is-primary.is-focused.textarea,.is-primary.textarea:active,.is-primary.textarea:focus{box-shadow:0 0 0 .125em rgba(0,209,178,.25)}.is-link.input,.is-link.textarea{border-color:#3273dc}.is-link.input:active,.is-link.input:focus,.is-link.is-active.input,.is-link.is-active.textarea,.is-link.is-focused.input,.is-link.is-focused.textarea,.is-link.textarea:active,.is-link.textarea:focus{box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.is-info.input,.is-info.textarea{border-color:#209cee}.is-info.input:active,.is-info.input:focus,.is-info.is-active.input,.is-info.is-active.textarea,.is-info.is-focused.input,.is-info.is-focused.textarea,.is-info.textarea:active,.is-info.textarea:focus{box-shadow:0 0 0 .125em rgba(32,156,238,.25)}.is-success.input,.is-success.textarea{border-color:#23d160}.is-success.input:active,.is-success.input:focus,.is-success.is-active.input,.is-success.is-active.textarea,.is-success.is-focused.input,.is-success.is-focused.textarea,.is-success.textarea:active,.is-success.textarea:focus{box-shadow:0 0 0 .125em rgba(35,209,96,.25)}.is-warning.input,.is-warning.textarea{border-color:#ffdd57}.is-warning.input:active,.is-warning.input:focus,.is-warning.is-active.input,.is-warning.is-active.textarea,.is-warning.is-focused.input,.is-warning.is-focused.textarea,.is-warning.textarea:active,.is-warning.textarea:focus{box-shadow:0 0 0 .125em rgba(255,221,87,.25)}.is-danger.input,.is-danger.textarea{border-color:#ff3860}.is-danger.input:active,.is-danger.input:focus,.is-danger.is-active.input,.is-danger.is-active.textarea,.is-danger.is-focused.input,.is-danger.is-focused.textarea,.is-danger.textarea:active,.is-danger.textarea:focus{box-shadow:0 0 0 .125em rgba(255,56,96,.25)}.is-small.input,.is-small.textarea{border-radius:2px;font-size:.75rem}.is-medium.input,.is-medium.textarea{font-size:1.25rem}.is-large.input,.is-large.textarea{font-size:1.5rem}.is-fullwidth.input,.is-fullwidth.textarea{display:block;width:100%}.is-inline.input,.is-inline.textarea{display:inline;width:auto}.input.is-rounded{border-radius:290486px;padding-left:1em;padding-right:1em}.input.is-static{background-color:transparent;border-color:transparent;box-shadow:none;padding-left:0;padding-right:0}.textarea{display:block;max-width:100%;min-width:100%;padding:.625em;resize:vertical}.textarea:not([rows]){max-height:600px;min-height:120px}.textarea[rows]{height:initial}.textarea.has-fixed-size{resize:none}.checkbox,.radio{cursor:pointer;display:inline-block;line-height:1.25;position:relative}.checkbox input,.radio input{cursor:pointer}.checkbox:hover,.radio:hover{color:#363636}.checkbox[disabled],.radio[disabled],fieldset[disabled] .checkbox,fieldset[disabled] .radio{color:#7a7a7a;cursor:not-allowed}.radio+.radio{margin-left:.5em}.select{display:inline-block;max-width:100%;position:relative;vertical-align:top}.select:not(.is-multiple){height:2.25em}.select:not(.is-multiple):not(.is-loading)::after{border-color:#3273dc;right:1.125em;z-index:4}.select.is-rounded select{border-radius:290486px;padding-left:1em}.select select{cursor:pointer;display:block;font-size:1em;max-width:100%;outline:0}.select select::-ms-expand{display:none}.select select[disabled]:hover,fieldset[disabled] .select select:hover{border-color:#f5f5f5}.select select:not([multiple]){padding-right:2.5em}.select select[multiple]{height:auto;padding:0}.select select[multiple] option{padding:.5em 1em}.select:not(.is-multiple):not(.is-loading):hover::after{border-color:#363636}.select.is-white:not(:hover)::after{border-color:#fff}.select.is-white select{border-color:#fff}.select.is-white select.is-hovered,.select.is-white select:hover{border-color:#f2f2f2}.select.is-white select.is-active,.select.is-white select.is-focused,.select.is-white select:active,.select.is-white select:focus{box-shadow:0 0 0 .125em rgba(255,255,255,.25)}.select.is-black:not(:hover)::after{border-color:#0a0a0a}.select.is-black select{border-color:#0a0a0a}.select.is-black select.is-hovered,.select.is-black select:hover{border-color:#000}.select.is-black select.is-active,.select.is-black select.is-focused,.select.is-black select:active,.select.is-black select:focus{box-shadow:0 0 0 .125em rgba(10,10,10,.25)}.select.is-light:not(:hover)::after{border-color:#f5f5f5}.select.is-light select{border-color:#f5f5f5}.select.is-light select.is-hovered,.select.is-light select:hover{border-color:#e8e8e8}.select.is-light select.is-active,.select.is-light select.is-focused,.select.is-light select:active,.select.is-light select:focus{box-shadow:0 0 0 .125em rgba(245,245,245,.25)}.select.is-dark:not(:hover)::after{border-color:#363636}.select.is-dark select{border-color:#363636}.select.is-dark select.is-hovered,.select.is-dark select:hover{border-color:#292929}.select.is-dark select.is-active,.select.is-dark select.is-focused,.select.is-dark select:active,.select.is-dark select:focus{box-shadow:0 0 0 .125em rgba(54,54,54,.25)}.select.is-primary:not(:hover)::after{border-color:#00d1b2}.select.is-primary select{border-color:#00d1b2}.select.is-primary select.is-hovered,.select.is-primary select:hover{border-color:#00b89c}.select.is-primary select.is-active,.select.is-primary select.is-focused,.select.is-primary select:active,.select.is-primary select:focus{box-shadow:0 0 0 .125em rgba(0,209,178,.25)}.select.is-link:not(:hover)::after{border-color:#3273dc}.select.is-link select{border-color:#3273dc}.select.is-link select.is-hovered,.select.is-link select:hover{border-color:#2366d1}.select.is-link select.is-active,.select.is-link select.is-focused,.select.is-link select:active,.select.is-link select:focus{box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.select.is-info:not(:hover)::after{border-color:#209cee}.select.is-info select{border-color:#209cee}.select.is-info select.is-hovered,.select.is-info select:hover{border-color:#118fe4}.select.is-info select.is-active,.select.is-info select.is-focused,.select.is-info select:active,.select.is-info select:focus{box-shadow:0 0 0 .125em rgba(32,156,238,.25)}.select.is-success:not(:hover)::after{border-color:#23d160}.select.is-success select{border-color:#23d160}.select.is-success select.is-hovered,.select.is-success select:hover{border-color:#20bc56}.select.is-success select.is-active,.select.is-success select.is-focused,.select.is-success select:active,.select.is-success select:focus{box-shadow:0 0 0 .125em rgba(35,209,96,.25)}.select.is-warning:not(:hover)::after{border-color:#ffdd57}.select.is-warning select{border-color:#ffdd57}.select.is-warning select.is-hovered,.select.is-warning select:hover{border-color:#ffd83d}.select.is-warning select.is-active,.select.is-warning select.is-focused,.select.is-warning select:active,.select.is-warning select:focus{box-shadow:0 0 0 .125em rgba(255,221,87,.25)}.select.is-danger:not(:hover)::after{border-color:#ff3860}.select.is-danger select{border-color:#ff3860}.select.is-danger select.is-hovered,.select.is-danger select:hover{border-color:#ff1f4b}.select.is-danger select.is-active,.select.is-danger select.is-focused,.select.is-danger select:active,.select.is-danger select:focus{box-shadow:0 0 0 .125em rgba(255,56,96,.25)}.select.is-small{border-radius:2px;font-size:.75rem}.select.is-medium{font-size:1.25rem}.select.is-large{font-size:1.5rem}.select.is-disabled::after{border-color:#7a7a7a}.select.is-fullwidth{width:100%}.select.is-fullwidth select{width:100%}.select.is-loading::after{margin-top:0;position:absolute;right:.625em;top:.625em;-webkit-transform:none;transform:none}.select.is-loading.is-small:after{font-size:.75rem}.select.is-loading.is-medium:after{font-size:1.25rem}.select.is-loading.is-large:after{font-size:1.5rem}.file{align-items:stretch;display:flex;justify-content:flex-start;position:relative}.file.is-white .file-cta{background-color:#fff;border-color:transparent;color:#0a0a0a}.file.is-white.is-hovered .file-cta,.file.is-white:hover .file-cta{background-color:#f9f9f9;border-color:transparent;color:#0a0a0a}.file.is-white.is-focused .file-cta,.file.is-white:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(255,255,255,.25);color:#0a0a0a}.file.is-white.is-active .file-cta,.file.is-white:active .file-cta{background-color:#f2f2f2;border-color:transparent;color:#0a0a0a}.file.is-black .file-cta{background-color:#0a0a0a;border-color:transparent;color:#fff}.file.is-black.is-hovered .file-cta,.file.is-black:hover .file-cta{background-color:#040404;border-color:transparent;color:#fff}.file.is-black.is-focused .file-cta,.file.is-black:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(10,10,10,.25);color:#fff}.file.is-black.is-active .file-cta,.file.is-black:active .file-cta{background-color:#000;border-color:transparent;color:#fff}.file.is-light .file-cta{background-color:#f5f5f5;border-color:transparent;color:#363636}.file.is-light.is-hovered .file-cta,.file.is-light:hover .file-cta{background-color:#eee;border-color:transparent;color:#363636}.file.is-light.is-focused .file-cta,.file.is-light:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(245,245,245,.25);color:#363636}.file.is-light.is-active .file-cta,.file.is-light:active .file-cta{background-color:#e8e8e8;border-color:transparent;color:#363636}.file.is-dark .file-cta{background-color:#363636;border-color:transparent;color:#f5f5f5}.file.is-dark.is-hovered .file-cta,.file.is-dark:hover .file-cta{background-color:#2f2f2f;border-color:transparent;color:#f5f5f5}.file.is-dark.is-focused .file-cta,.file.is-dark:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(54,54,54,.25);color:#f5f5f5}.file.is-dark.is-active .file-cta,.file.is-dark:active .file-cta{background-color:#292929;border-color:transparent;color:#f5f5f5}.file.is-primary .file-cta{background-color:#00d1b2;border-color:transparent;color:#fff}.file.is-primary.is-hovered .file-cta,.file.is-primary:hover .file-cta{background-color:#00c4a7;border-color:transparent;color:#fff}.file.is-primary.is-focused .file-cta,.file.is-primary:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(0,209,178,.25);color:#fff}.file.is-primary.is-active .file-cta,.file.is-primary:active .file-cta{background-color:#00b89c;border-color:transparent;color:#fff}.file.is-link .file-cta{background-color:#3273dc;border-color:transparent;color:#fff}.file.is-link.is-hovered .file-cta,.file.is-link:hover .file-cta{background-color:#276cda;border-color:transparent;color:#fff}.file.is-link.is-focused .file-cta,.file.is-link:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(50,115,220,.25);color:#fff}.file.is-link.is-active .file-cta,.file.is-link:active .file-cta{background-color:#2366d1;border-color:transparent;color:#fff}.file.is-info .file-cta{background-color:#209cee;border-color:transparent;color:#fff}.file.is-info.is-hovered .file-cta,.file.is-info:hover .file-cta{background-color:#1496ed;border-color:transparent;color:#fff}.file.is-info.is-focused .file-cta,.file.is-info:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(32,156,238,.25);color:#fff}.file.is-info.is-active .file-cta,.file.is-info:active .file-cta{background-color:#118fe4;border-color:transparent;color:#fff}.file.is-success .file-cta{background-color:#23d160;border-color:transparent;color:#fff}.file.is-success.is-hovered .file-cta,.file.is-success:hover .file-cta{background-color:#22c65b;border-color:transparent;color:#fff}.file.is-success.is-focused .file-cta,.file.is-success:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(35,209,96,.25);color:#fff}.file.is-success.is-active .file-cta,.file.is-success:active .file-cta{background-color:#20bc56;border-color:transparent;color:#fff}.file.is-warning .file-cta{background-color:#ffdd57;border-color:transparent;color:rgba(0,0,0,.7)}.file.is-warning.is-hovered .file-cta,.file.is-warning:hover .file-cta{background-color:#ffdb4a;border-color:transparent;color:rgba(0,0,0,.7)}.file.is-warning.is-focused .file-cta,.file.is-warning:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(255,221,87,.25);color:rgba(0,0,0,.7)}.file.is-warning.is-active .file-cta,.file.is-warning:active .file-cta{background-color:#ffd83d;border-color:transparent;color:rgba(0,0,0,.7)}.file.is-danger .file-cta{background-color:#ff3860;border-color:transparent;color:#fff}.file.is-danger.is-hovered .file-cta,.file.is-danger:hover .file-cta{background-color:#ff2b56;border-color:transparent;color:#fff}.file.is-danger.is-focused .file-cta,.file.is-danger:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(255,56,96,.25);color:#fff}.file.is-danger.is-active .file-cta,.file.is-danger:active .file-cta{background-color:#ff1f4b;border-color:transparent;color:#fff}.file.is-small{font-size:.75rem}.file.is-medium{font-size:1.25rem}.file.is-medium .file-icon .fa{font-size:21px}.file.is-large{font-size:1.5rem}.file.is-large .file-icon .fa{font-size:28px}.file.has-name .file-cta{border-bottom-right-radius:0;border-top-right-radius:0}.file.has-name .file-name{border-bottom-left-radius:0;border-top-left-radius:0}.file.has-name.is-empty .file-cta{border-radius:4px}.file.has-name.is-empty .file-name{display:none}.file.is-boxed .file-label{flex-direction:column}.file.is-boxed .file-cta{flex-direction:column;height:auto;padding:1em 3em}.file.is-boxed .file-name{border-width:0 1px 1px}.file.is-boxed .file-icon{height:1.5em;width:1.5em}.file.is-boxed .file-icon .fa{font-size:21px}.file.is-boxed.is-small .file-icon .fa{font-size:14px}.file.is-boxed.is-medium .file-icon .fa{font-size:28px}.file.is-boxed.is-large .file-icon .fa{font-size:35px}.file.is-boxed.has-name .file-cta{border-radius:4px 4px 0 0}.file.is-boxed.has-name .file-name{border-radius:0 0 4px 4px;border-width:0 1px 1px}.file.is-centered{justify-content:center}.file.is-fullwidth .file-label{width:100%}.file.is-fullwidth .file-name{flex-grow:1;max-width:none}.file.is-right{justify-content:flex-end}.file.is-right .file-cta{border-radius:0 4px 4px 0}.file.is-right .file-name{border-radius:4px 0 0 4px;border-width:1px 0 1px 1px;order:-1}.file-label{align-items:stretch;display:flex;cursor:pointer;justify-content:flex-start;overflow:hidden;position:relative}.file-label:hover .file-cta{background-color:#eee;color:#363636}.file-label:hover .file-name{border-color:#d5d5d5}.file-label:active .file-cta{background-color:#e8e8e8;color:#363636}.file-label:active .file-name{border-color:#cfcfcf}.file-input{height:100%;left:0;opacity:0;outline:0;position:absolute;top:0;width:100%}.file-cta,.file-name{border-color:#dbdbdb;border-radius:4px;font-size:1em;padding-left:1em;padding-right:1em;white-space:nowrap}.file-cta{background-color:#f5f5f5;color:#4a4a4a}.file-name{border-color:#dbdbdb;border-style:solid;border-width:1px 1px 1px 0;display:block;max-width:16em;overflow:hidden;text-align:left;text-overflow:ellipsis}.file-icon{align-items:center;display:flex;height:1em;justify-content:center;margin-right:.5em;width:1em}.file-icon .fa{font-size:14px}.label{color:#363636;display:block;font-size:1rem;font-weight:700}.label:not(:last-child){margin-bottom:.5em}.label.is-small{font-size:.75rem}.label.is-medium{font-size:1.25rem}.label.is-large{font-size:1.5rem}.help{display:block;font-size:.75rem;margin-top:.25rem}.help.is-white{color:#fff}.help.is-black{color:#0a0a0a}.help.is-light{color:#f5f5f5}.help.is-dark{color:#363636}.help.is-primary{color:#00d1b2}.help.is-link{color:#3273dc}.help.is-info{color:#209cee}.help.is-success{color:#23d160}.help.is-warning{color:#ffdd57}.help.is-danger{color:#ff3860}.field:not(:last-child){margin-bottom:.75rem}.field.has-addons{display:flex;justify-content:flex-start}.field.has-addons .control:not(:last-child){margin-right:-1px}.field.has-addons .control:not(:first-child):not(:last-child) .button,.field.has-addons .control:not(:first-child):not(:last-child) .input,.field.has-addons .control:not(:first-child):not(:last-child) .select select{border-radius:0}.field.has-addons .control:first-child:not(:only-child) .button,.field.has-addons .control:first-child:not(:only-child) .input,.field.has-addons .control:first-child:not(:only-child) .select select{border-bottom-right-radius:0;border-top-right-radius:0}.field.has-addons .control:last-child:not(:only-child) .button,.field.has-addons .control:last-child:not(:only-child) .input,.field.has-addons .control:last-child:not(:only-child) .select select{border-bottom-left-radius:0;border-top-left-radius:0}.field.has-addons .control .button:not([disabled]).is-hovered,.field.has-addons .control .button:not([disabled]):hover,.field.has-addons .control .input:not([disabled]).is-hovered,.field.has-addons .control .input:not([disabled]):hover,.field.has-addons .control .select select:not([disabled]).is-hovered,.field.has-addons .control .select select:not([disabled]):hover{z-index:2}.field.has-addons .control .button:not([disabled]).is-active,.field.has-addons .control .button:not([disabled]).is-focused,.field.has-addons .control .button:not([disabled]):active,.field.has-addons .control .button:not([disabled]):focus,.field.has-addons .control .input:not([disabled]).is-active,.field.has-addons .control .input:not([disabled]).is-focused,.field.has-addons .control .input:not([disabled]):active,.field.has-addons .control .input:not([disabled]):focus,.field.has-addons .control .select select:not([disabled]).is-active,.field.has-addons .control .select select:not([disabled]).is-focused,.field.has-addons .control .select select:not([disabled]):active,.field.has-addons .control .select select:not([disabled]):focus{z-index:3}.field.has-addons .control .button:not([disabled]).is-active:hover,.field.has-addons .control .button:not([disabled]).is-focused:hover,.field.has-addons .control .button:not([disabled]):active:hover,.field.has-addons .control .button:not([disabled]):focus:hover,.field.has-addons .control .input:not([disabled]).is-active:hover,.field.has-addons .control .input:not([disabled]).is-focused:hover,.field.has-addons .control .input:not([disabled]):active:hover,.field.has-addons .control .input:not([disabled]):focus:hover,.field.has-addons .control .select select:not([disabled]).is-active:hover,.field.has-addons .control .select select:not([disabled]).is-focused:hover,.field.has-addons .control .select select:not([disabled]):active:hover,.field.has-addons .control .select select:not([disabled]):focus:hover{z-index:4}.field.has-addons .control.is-expanded{flex-grow:1;flex-shrink:1}.field.has-addons.has-addons-centered{justify-content:center}.field.has-addons.has-addons-right{justify-content:flex-end}.field.has-addons.has-addons-fullwidth .control{flex-grow:1;flex-shrink:0}.field.is-grouped{display:flex;justify-content:flex-start}.field.is-grouped>.control{flex-shrink:0}.field.is-grouped>.control:not(:last-child){margin-bottom:0;margin-right:.75rem}.field.is-grouped>.control.is-expanded{flex-grow:1;flex-shrink:1}.field.is-grouped.is-grouped-centered{justify-content:center}.field.is-grouped.is-grouped-right{justify-content:flex-end}.field.is-grouped.is-grouped-multiline{flex-wrap:wrap}.field.is-grouped.is-grouped-multiline>.control:last-child,.field.is-grouped.is-grouped-multiline>.control:not(:last-child){margin-bottom:.75rem}.field.is-grouped.is-grouped-multiline:last-child{margin-bottom:-.75rem}.field.is-grouped.is-grouped-multiline:not(:last-child){margin-bottom:0}@media screen and (min-width:769px),print{.field.is-horizontal{display:flex}}.field-label .label{font-size:inherit}@media screen and (max-width:768px){.field-label{margin-bottom:.5rem}}@media screen and (min-width:769px),print{.field-label{flex-basis:0;flex-grow:1;flex-shrink:0;margin-right:1.5rem;text-align:right}.field-label.is-small{font-size:.75rem;padding-top:.375em}.field-label.is-normal{padding-top:.375em}.field-label.is-medium{font-size:1.25rem;padding-top:.375em}.field-label.is-large{font-size:1.5rem;padding-top:.375em}}.field-body .field .field{margin-bottom:0}@media screen and (min-width:769px),print{.field-body{display:flex;flex-basis:0;flex-grow:5;flex-shrink:1}.field-body .field{margin-bottom:0}.field-body>.field{flex-shrink:1}.field-body>.field:not(.is-narrow){flex-grow:1}.field-body>.field:not(:last-child){margin-right:.75rem}}.control{box-sizing:border-box;clear:both;font-size:1rem;position:relative;text-align:left}.control.has-icons-left .input:focus~.icon,.control.has-icons-left .select:focus~.icon,.control.has-icons-right .input:focus~.icon,.control.has-icons-right .select:focus~.icon{color:#7a7a7a}.control.has-icons-left .input.is-small~.icon,.control.has-icons-left .select.is-small~.icon,.control.has-icons-right .input.is-small~.icon,.control.has-icons-right .select.is-small~.icon{font-size:.75rem}.control.has-icons-left .input.is-medium~.icon,.control.has-icons-left .select.is-medium~.icon,.control.has-icons-right .input.is-medium~.icon,.control.has-icons-right .select.is-medium~.icon{font-size:1.25rem}.control.has-icons-left .input.is-large~.icon,.control.has-icons-left .select.is-large~.icon,.control.has-icons-right .input.is-large~.icon,.control.has-icons-right .select.is-large~.icon{font-size:1.5rem}.control.has-icons-left .icon,.control.has-icons-right .icon{color:#dbdbdb;height:2.25em;pointer-events:none;position:absolute;top:0;width:2.25em;z-index:4}.control.has-icons-left .input,.control.has-icons-left .select select{padding-left:2.25em}.control.has-icons-left .icon.is-left{left:0}.control.has-icons-right .input,.control.has-icons-right .select select{padding-right:2.25em}.control.has-icons-right .icon.is-right{right:0}.control.is-loading::after{position:absolute!important;right:.625em;top:.625em;z-index:4}.control.is-loading.is-small:after{font-size:.75rem}.control.is-loading.is-medium:after{font-size:1.25rem}.control.is-loading.is-large:after{font-size:1.5rem}.breadcrumb{font-size:1rem;white-space:nowrap}.breadcrumb a{align-items:center;color:#3273dc;display:flex;justify-content:center;padding:0 .75em}.breadcrumb a:hover{color:#363636}.breadcrumb li{align-items:center;display:flex}.breadcrumb li:first-child a{padding-left:0}.breadcrumb li.is-active a{color:#363636;cursor:default;pointer-events:none}.breadcrumb li+li::before{color:#b5b5b5;content:"\0002f"}.breadcrumb ol,.breadcrumb ul{align-items:flex-start;display:flex;flex-wrap:wrap;justify-content:flex-start}.breadcrumb .icon:first-child{margin-right:.5em}.breadcrumb .icon:last-child{margin-left:.5em}.breadcrumb.is-centered ol,.breadcrumb.is-centered ul{justify-content:center}.breadcrumb.is-right ol,.breadcrumb.is-right ul{justify-content:flex-end}.breadcrumb.is-small{font-size:.75rem}.breadcrumb.is-medium{font-size:1.25rem}.breadcrumb.is-large{font-size:1.5rem}.breadcrumb.has-arrow-separator li+li::before{content:"\02192"}.breadcrumb.has-bullet-separator li+li::before{content:"\02022"}.breadcrumb.has-dot-separator li+li::before{content:"\000b7"}.breadcrumb.has-succeeds-separator li+li::before{content:"\0227B"}.card{background-color:#fff;box-shadow:0 2px 3px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1);color:#4a4a4a;max-width:100%;position:relative}.card-header{background-color:transparent;align-items:stretch;box-shadow:0 1px 2px rgba(10,10,10,.1);display:flex}.card-header-title{align-items:center;color:#363636;display:flex;flex-grow:1;font-weight:700;padding:.75rem}.card-header-title.is-centered{justify-content:center}.card-header-icon{align-items:center;cursor:pointer;display:flex;justify-content:center;padding:.75rem}.card-image{display:block;position:relative}.card-content{background-color:transparent;padding:1.5rem}.card-footer{background-color:transparent;border-top:1px solid #dbdbdb;align-items:stretch;display:flex}.card-footer-item{align-items:center;display:flex;flex-basis:0;flex-grow:1;flex-shrink:0;justify-content:center;padding:.75rem}.card-footer-item:not(:last-child){border-right:1px solid #dbdbdb}.card .media:not(:last-child){margin-bottom:1.5rem}.dropdown{display:inline-flex;position:relative;vertical-align:top}.dropdown.is-active .dropdown-menu,.dropdown.is-hoverable:hover .dropdown-menu{display:block}.dropdown.is-right .dropdown-menu{left:auto;right:0}.dropdown.is-up .dropdown-menu{bottom:100%;padding-bottom:4px;padding-top:initial;top:auto}.dropdown-menu{display:none;left:0;min-width:12rem;padding-top:4px;position:absolute;top:100%;z-index:20}.dropdown-content{background-color:#fff;border-radius:4px;box-shadow:0 2px 3px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1);padding-bottom:.5rem;padding-top:.5rem}.dropdown-item{color:#4a4a4a;display:block;font-size:.875rem;line-height:1.5;padding:.375rem 1rem;position:relative}a.dropdown-item,button.dropdown-item{padding-right:3rem;text-align:left;white-space:nowrap;width:100%}a.dropdown-item:hover,button.dropdown-item:hover{background-color:#f5f5f5;color:#0a0a0a}a.dropdown-item.is-active,button.dropdown-item.is-active{background-color:#3273dc;color:#fff}.dropdown-divider{background-color:#dbdbdb;border:none;display:block;height:1px;margin:.5rem 0}.level{align-items:center;justify-content:space-between}.level code{border-radius:4px}.level img{display:inline-block;vertical-align:top}.level.is-mobile{display:flex}.level.is-mobile .level-left,.level.is-mobile .level-right{display:flex}.level.is-mobile .level-left+.level-right{margin-top:0}.level.is-mobile .level-item:not(:last-child){margin-bottom:0;margin-right:.75rem}.level.is-mobile .level-item:not(.is-narrow){flex-grow:1}@media screen and (min-width:769px),print{.level{display:flex}.level>.level-item:not(.is-narrow){flex-grow:1}}.level-item{align-items:center;display:flex;flex-basis:auto;flex-grow:0;flex-shrink:0;justify-content:center}.level-item .subtitle,.level-item .title{margin-bottom:0}@media screen and (max-width:768px){.level-item:not(:last-child){margin-bottom:.75rem}}.level-left,.level-right{flex-basis:auto;flex-grow:0;flex-shrink:0}.level-left .level-item.is-flexible,.level-right .level-item.is-flexible{flex-grow:1}@media screen and (min-width:769px),print{.level-left .level-item:not(:last-child),.level-right .level-item:not(:last-child){margin-right:.75rem}}.level-left{align-items:center;justify-content:flex-start}@media screen and (max-width:768px){.level-left+.level-right{margin-top:1.5rem}}@media screen and (min-width:769px),print{.level-left{display:flex}}.level-right{align-items:center;justify-content:flex-end}@media screen and (min-width:769px),print{.level-right{display:flex}}.list{background-color:#fff;border-radius:4px;box-shadow:0 2px 3px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1)}.list-item{display:block;padding:.5em 1em}.list-item:not(a){color:#4a4a4a}.list-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-item:last-child{border-bottom-left-radius:4px;border-bottom-right-radius:4px}.list-item:not(:last-child){border-bottom:1px solid #dbdbdb}.list-item.is-active{background-color:#3273dc;color:#fff}a.list-item{background-color:#f5f5f5;cursor:pointer}.media{align-items:flex-start;display:flex;text-align:left}.media .content:not(:last-child){margin-bottom:.75rem}.media .media{border-top:1px solid rgba(219,219,219,.5);display:flex;padding-top:.75rem}.media .media .content:not(:last-child),.media .media .control:not(:last-child){margin-bottom:.5rem}.media .media .media{padding-top:.5rem}.media .media .media+.media{margin-top:.5rem}.media+.media{border-top:1px solid rgba(219,219,219,.5);margin-top:1rem;padding-top:1rem}.media.is-large+.media{margin-top:1.5rem;padding-top:1.5rem}.media-left,.media-right{flex-basis:auto;flex-grow:0;flex-shrink:0}.media-left{margin-right:1rem}.media-right{margin-left:1rem}.media-content{flex-basis:auto;flex-grow:1;flex-shrink:1;text-align:left}@media screen and (max-width:768px){.media-content{overflow-x:auto}}.menu{font-size:1rem}.menu.is-small{font-size:.75rem}.menu.is-medium{font-size:1.25rem}.menu.is-large{font-size:1.5rem}.menu-list{line-height:1.25}.menu-list a{border-radius:2px;color:#4a4a4a;display:block;padding:.5em .75em}.menu-list a:hover{background-color:#f5f5f5;color:#363636}.menu-list a.is-active{background-color:#3273dc;color:#fff}.menu-list li ul{border-left:1px solid #dbdbdb;margin:.75em;padding-left:.75em}.menu-label{color:#7a7a7a;font-size:.75em;letter-spacing:.1em;text-transform:uppercase}.menu-label:not(:first-child){margin-top:1em}.menu-label:not(:last-child){margin-bottom:1em}.message{background-color:#f5f5f5;border-radius:4px;font-size:1rem}.message strong{color:currentColor}.message a:not(.button):not(.tag):not(.dropdown-item){color:currentColor;text-decoration:underline}.message.is-small{font-size:.75rem}.message.is-medium{font-size:1.25rem}.message.is-large{font-size:1.5rem}.message.is-white{background-color:#fff}.message.is-white .message-header{background-color:#fff;color:#0a0a0a}.message.is-white .message-body{border-color:#fff;color:#4d4d4d}.message.is-black{background-color:#fafafa}.message.is-black .message-header{background-color:#0a0a0a;color:#fff}.message.is-black .message-body{border-color:#0a0a0a;color:#090909}.message.is-light{background-color:#fafafa}.message.is-light .message-header{background-color:#f5f5f5;color:#363636}.message.is-light .message-body{border-color:#f5f5f5;color:#505050}.message.is-dark{background-color:#fafafa}.message.is-dark .message-header{background-color:#363636;color:#f5f5f5}.message.is-dark .message-body{border-color:#363636;color:#2a2a2a}.message.is-primary{background-color:#f5fffd}.message.is-primary .message-header{background-color:#00d1b2;color:#fff}.message.is-primary .message-body{border-color:#00d1b2;color:#021310}.message.is-link{background-color:#f6f9fe}.message.is-link .message-header{background-color:#3273dc;color:#fff}.message.is-link .message-body{border-color:#3273dc;color:#22509a}.message.is-info{background-color:#f6fbfe}.message.is-info .message-header{background-color:#209cee;color:#fff}.message.is-info .message-body{border-color:#209cee;color:#12537e}.message.is-success{background-color:#f6fef9}.message.is-success .message-header{background-color:#23d160;color:#fff}.message.is-success .message-body{border-color:#23d160;color:#0e301a}.message.is-warning{background-color:#fffdf5}.message.is-warning .message-header{background-color:#ffdd57;color:rgba(0,0,0,.7)}.message.is-warning .message-body{border-color:#ffdd57;color:#3b3108}.message.is-danger{background-color:#fff5f7}.message.is-danger .message-header{background-color:#ff3860;color:#fff}.message.is-danger .message-body{border-color:#ff3860;color:#cd0930}.message-header{align-items:center;background-color:#4a4a4a;border-radius:4px 4px 0 0;color:#fff;display:flex;font-weight:700;justify-content:space-between;line-height:1.25;padding:.75em 1em;position:relative}.message-header .delete{flex-grow:0;flex-shrink:0;margin-left:.75em}.message-header+.message-body{border-width:0;border-top-left-radius:0;border-top-right-radius:0}.message-body{border-color:#dbdbdb;border-radius:4px;border-style:solid;border-width:0 0 0 4px;color:#4a4a4a;padding:1.25em 1.5em}.message-body code,.message-body pre{background-color:#fff}.message-body pre code{background-color:transparent}.modal{align-items:center;display:none;flex-direction:column;justify-content:center;overflow:hidden;position:fixed;z-index:40}.modal.is-active{display:flex}.modal-background{background-color:rgba(10,10,10,.86)}.modal-card,.modal-content{margin:0 20px;max-height:calc(100vh - 160px);overflow:auto;position:relative;width:100%}@media screen and (min-width:769px),print{.modal-card,.modal-content{margin:0 auto;max-height:calc(100vh - 40px);width:640px}}.modal-close{background:0 0;height:40px;position:fixed;right:20px;top:20px;width:40px}.modal-card{display:flex;flex-direction:column;max-height:calc(100vh - 40px);overflow:hidden;-ms-overflow-y:visible}.modal-card-foot,.modal-card-head{align-items:center;background-color:#f5f5f5;display:flex;flex-shrink:0;justify-content:flex-start;padding:20px;position:relative}.modal-card-head{border-bottom:1px solid #dbdbdb;border-top-left-radius:6px;border-top-right-radius:6px}.modal-card-title{color:#363636;flex-grow:1;flex-shrink:0;font-size:1.5rem;line-height:1}.modal-card-foot{border-bottom-left-radius:6px;border-bottom-right-radius:6px;border-top:1px solid #dbdbdb}.modal-card-foot .button:not(:last-child){margin-right:.5em}.modal-card-body{-webkit-overflow-scrolling:touch;background-color:#fff;flex-grow:1;flex-shrink:1;overflow:auto;padding:20px}.navbar{background-color:#fff;min-height:3.25rem;position:relative;z-index:30}.navbar.is-white{background-color:#fff;color:#0a0a0a}.navbar.is-white .navbar-brand .navbar-link,.navbar.is-white .navbar-brand>.navbar-item{color:#0a0a0a}.navbar.is-white .navbar-brand .navbar-link.is-active,.navbar.is-white .navbar-brand .navbar-link:focus,.navbar.is-white .navbar-brand .navbar-link:hover,.navbar.is-white .navbar-brand>a.navbar-item.is-active,.navbar.is-white .navbar-brand>a.navbar-item:focus,.navbar.is-white .navbar-brand>a.navbar-item:hover{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-brand .navbar-link::after{border-color:#0a0a0a}.navbar.is-white .navbar-burger{color:#0a0a0a}@media screen and (min-width:1024px){.navbar.is-white .navbar-end .navbar-link,.navbar.is-white .navbar-end>.navbar-item,.navbar.is-white .navbar-start .navbar-link,.navbar.is-white .navbar-start>.navbar-item{color:#0a0a0a}.navbar.is-white .navbar-end .navbar-link.is-active,.navbar.is-white .navbar-end .navbar-link:focus,.navbar.is-white .navbar-end .navbar-link:hover,.navbar.is-white .navbar-end>a.navbar-item.is-active,.navbar.is-white .navbar-end>a.navbar-item:focus,.navbar.is-white .navbar-end>a.navbar-item:hover,.navbar.is-white .navbar-start .navbar-link.is-active,.navbar.is-white .navbar-start .navbar-link:focus,.navbar.is-white .navbar-start .navbar-link:hover,.navbar.is-white .navbar-start>a.navbar-item.is-active,.navbar.is-white .navbar-start>a.navbar-item:focus,.navbar.is-white .navbar-start>a.navbar-item:hover{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-end .navbar-link::after,.navbar.is-white .navbar-start .navbar-link::after{border-color:#0a0a0a}.navbar.is-white .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-white .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-white .navbar-item.has-dropdown:hover .navbar-link{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-dropdown a.navbar-item.is-active{background-color:#fff;color:#0a0a0a}}.navbar.is-black{background-color:#0a0a0a;color:#fff}.navbar.is-black .navbar-brand .navbar-link,.navbar.is-black .navbar-brand>.navbar-item{color:#fff}.navbar.is-black .navbar-brand .navbar-link.is-active,.navbar.is-black .navbar-brand .navbar-link:focus,.navbar.is-black .navbar-brand .navbar-link:hover,.navbar.is-black .navbar-brand>a.navbar-item.is-active,.navbar.is-black .navbar-brand>a.navbar-item:focus,.navbar.is-black .navbar-brand>a.navbar-item:hover{background-color:#000;color:#fff}.navbar.is-black .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-black .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-black .navbar-end .navbar-link,.navbar.is-black .navbar-end>.navbar-item,.navbar.is-black .navbar-start .navbar-link,.navbar.is-black .navbar-start>.navbar-item{color:#fff}.navbar.is-black .navbar-end .navbar-link.is-active,.navbar.is-black .navbar-end .navbar-link:focus,.navbar.is-black .navbar-end .navbar-link:hover,.navbar.is-black .navbar-end>a.navbar-item.is-active,.navbar.is-black .navbar-end>a.navbar-item:focus,.navbar.is-black .navbar-end>a.navbar-item:hover,.navbar.is-black .navbar-start .navbar-link.is-active,.navbar.is-black .navbar-start .navbar-link:focus,.navbar.is-black .navbar-start .navbar-link:hover,.navbar.is-black .navbar-start>a.navbar-item.is-active,.navbar.is-black .navbar-start>a.navbar-item:focus,.navbar.is-black .navbar-start>a.navbar-item:hover{background-color:#000;color:#fff}.navbar.is-black .navbar-end .navbar-link::after,.navbar.is-black .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-black .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-black .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-black .navbar-item.has-dropdown:hover .navbar-link{background-color:#000;color:#fff}.navbar.is-black .navbar-dropdown a.navbar-item.is-active{background-color:#0a0a0a;color:#fff}}.navbar.is-light{background-color:#f5f5f5;color:#363636}.navbar.is-light .navbar-brand .navbar-link,.navbar.is-light .navbar-brand>.navbar-item{color:#363636}.navbar.is-light .navbar-brand .navbar-link.is-active,.navbar.is-light .navbar-brand .navbar-link:focus,.navbar.is-light .navbar-brand .navbar-link:hover,.navbar.is-light .navbar-brand>a.navbar-item.is-active,.navbar.is-light .navbar-brand>a.navbar-item:focus,.navbar.is-light .navbar-brand>a.navbar-item:hover{background-color:#e8e8e8;color:#363636}.navbar.is-light .navbar-brand .navbar-link::after{border-color:#363636}.navbar.is-light .navbar-burger{color:#363636}@media screen and (min-width:1024px){.navbar.is-light .navbar-end .navbar-link,.navbar.is-light .navbar-end>.navbar-item,.navbar.is-light .navbar-start .navbar-link,.navbar.is-light .navbar-start>.navbar-item{color:#363636}.navbar.is-light .navbar-end .navbar-link.is-active,.navbar.is-light .navbar-end .navbar-link:focus,.navbar.is-light .navbar-end .navbar-link:hover,.navbar.is-light .navbar-end>a.navbar-item.is-active,.navbar.is-light .navbar-end>a.navbar-item:focus,.navbar.is-light .navbar-end>a.navbar-item:hover,.navbar.is-light .navbar-start .navbar-link.is-active,.navbar.is-light .navbar-start .navbar-link:focus,.navbar.is-light .navbar-start .navbar-link:hover,.navbar.is-light .navbar-start>a.navbar-item.is-active,.navbar.is-light .navbar-start>a.navbar-item:focus,.navbar.is-light .navbar-start>a.navbar-item:hover{background-color:#e8e8e8;color:#363636}.navbar.is-light .navbar-end .navbar-link::after,.navbar.is-light .navbar-start .navbar-link::after{border-color:#363636}.navbar.is-light .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-light .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-light .navbar-item.has-dropdown:hover .navbar-link{background-color:#e8e8e8;color:#363636}.navbar.is-light .navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:#363636}}.navbar.is-dark{background-color:#363636;color:#f5f5f5}.navbar.is-dark .navbar-brand .navbar-link,.navbar.is-dark .navbar-brand>.navbar-item{color:#f5f5f5}.navbar.is-dark .navbar-brand .navbar-link.is-active,.navbar.is-dark .navbar-brand .navbar-link:focus,.navbar.is-dark .navbar-brand .navbar-link:hover,.navbar.is-dark .navbar-brand>a.navbar-item.is-active,.navbar.is-dark .navbar-brand>a.navbar-item:focus,.navbar.is-dark .navbar-brand>a.navbar-item:hover{background-color:#292929;color:#f5f5f5}.navbar.is-dark .navbar-brand .navbar-link::after{border-color:#f5f5f5}.navbar.is-dark .navbar-burger{color:#f5f5f5}@media screen and (min-width:1024px){.navbar.is-dark .navbar-end .navbar-link,.navbar.is-dark .navbar-end>.navbar-item,.navbar.is-dark .navbar-start .navbar-link,.navbar.is-dark .navbar-start>.navbar-item{color:#f5f5f5}.navbar.is-dark .navbar-end .navbar-link.is-active,.navbar.is-dark .navbar-end .navbar-link:focus,.navbar.is-dark .navbar-end .navbar-link:hover,.navbar.is-dark .navbar-end>a.navbar-item.is-active,.navbar.is-dark .navbar-end>a.navbar-item:focus,.navbar.is-dark .navbar-end>a.navbar-item:hover,.navbar.is-dark .navbar-start .navbar-link.is-active,.navbar.is-dark .navbar-start .navbar-link:focus,.navbar.is-dark .navbar-start .navbar-link:hover,.navbar.is-dark .navbar-start>a.navbar-item.is-active,.navbar.is-dark .navbar-start>a.navbar-item:focus,.navbar.is-dark .navbar-start>a.navbar-item:hover{background-color:#292929;color:#f5f5f5}.navbar.is-dark .navbar-end .navbar-link::after,.navbar.is-dark .navbar-start .navbar-link::after{border-color:#f5f5f5}.navbar.is-dark .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-dark .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-dark .navbar-item.has-dropdown:hover .navbar-link{background-color:#292929;color:#f5f5f5}.navbar.is-dark .navbar-dropdown a.navbar-item.is-active{background-color:#363636;color:#f5f5f5}}.navbar.is-primary{background-color:#00d1b2;color:#fff}.navbar.is-primary .navbar-brand .navbar-link,.navbar.is-primary .navbar-brand>.navbar-item{color:#fff}.navbar.is-primary .navbar-brand .navbar-link.is-active,.navbar.is-primary .navbar-brand .navbar-link:focus,.navbar.is-primary .navbar-brand .navbar-link:hover,.navbar.is-primary .navbar-brand>a.navbar-item.is-active,.navbar.is-primary .navbar-brand>a.navbar-item:focus,.navbar.is-primary .navbar-brand>a.navbar-item:hover{background-color:#00b89c;color:#fff}.navbar.is-primary .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-primary .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-primary .navbar-end .navbar-link,.navbar.is-primary .navbar-end>.navbar-item,.navbar.is-primary .navbar-start .navbar-link,.navbar.is-primary .navbar-start>.navbar-item{color:#fff}.navbar.is-primary .navbar-end .navbar-link.is-active,.navbar.is-primary .navbar-end .navbar-link:focus,.navbar.is-primary .navbar-end .navbar-link:hover,.navbar.is-primary .navbar-end>a.navbar-item.is-active,.navbar.is-primary .navbar-end>a.navbar-item:focus,.navbar.is-primary .navbar-end>a.navbar-item:hover,.navbar.is-primary .navbar-start .navbar-link.is-active,.navbar.is-primary .navbar-start .navbar-link:focus,.navbar.is-primary .navbar-start .navbar-link:hover,.navbar.is-primary .navbar-start>a.navbar-item.is-active,.navbar.is-primary .navbar-start>a.navbar-item:focus,.navbar.is-primary .navbar-start>a.navbar-item:hover{background-color:#00b89c;color:#fff}.navbar.is-primary .navbar-end .navbar-link::after,.navbar.is-primary .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-primary .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-primary .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-primary .navbar-item.has-dropdown:hover .navbar-link{background-color:#00b89c;color:#fff}.navbar.is-primary .navbar-dropdown a.navbar-item.is-active{background-color:#00d1b2;color:#fff}}.navbar.is-link{background-color:#3273dc;color:#fff}.navbar.is-link .navbar-brand .navbar-link,.navbar.is-link .navbar-brand>.navbar-item{color:#fff}.navbar.is-link .navbar-brand .navbar-link.is-active,.navbar.is-link .navbar-brand .navbar-link:focus,.navbar.is-link .navbar-brand .navbar-link:hover,.navbar.is-link .navbar-brand>a.navbar-item.is-active,.navbar.is-link .navbar-brand>a.navbar-item:focus,.navbar.is-link .navbar-brand>a.navbar-item:hover{background-color:#2366d1;color:#fff}.navbar.is-link .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-link .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-link .navbar-end .navbar-link,.navbar.is-link .navbar-end>.navbar-item,.navbar.is-link .navbar-start .navbar-link,.navbar.is-link .navbar-start>.navbar-item{color:#fff}.navbar.is-link .navbar-end .navbar-link.is-active,.navbar.is-link .navbar-end .navbar-link:focus,.navbar.is-link .navbar-end .navbar-link:hover,.navbar.is-link .navbar-end>a.navbar-item.is-active,.navbar.is-link .navbar-end>a.navbar-item:focus,.navbar.is-link .navbar-end>a.navbar-item:hover,.navbar.is-link .navbar-start .navbar-link.is-active,.navbar.is-link .navbar-start .navbar-link:focus,.navbar.is-link .navbar-start .navbar-link:hover,.navbar.is-link .navbar-start>a.navbar-item.is-active,.navbar.is-link .navbar-start>a.navbar-item:focus,.navbar.is-link .navbar-start>a.navbar-item:hover{background-color:#2366d1;color:#fff}.navbar.is-link .navbar-end .navbar-link::after,.navbar.is-link .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-link .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-link .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-link .navbar-item.has-dropdown:hover .navbar-link{background-color:#2366d1;color:#fff}.navbar.is-link .navbar-dropdown a.navbar-item.is-active{background-color:#3273dc;color:#fff}}.navbar.is-info{background-color:#209cee;color:#fff}.navbar.is-info .navbar-brand .navbar-link,.navbar.is-info .navbar-brand>.navbar-item{color:#fff}.navbar.is-info .navbar-brand .navbar-link.is-active,.navbar.is-info .navbar-brand .navbar-link:focus,.navbar.is-info .navbar-brand .navbar-link:hover,.navbar.is-info .navbar-brand>a.navbar-item.is-active,.navbar.is-info .navbar-brand>a.navbar-item:focus,.navbar.is-info .navbar-brand>a.navbar-item:hover{background-color:#118fe4;color:#fff}.navbar.is-info .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-info .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-info .navbar-end .navbar-link,.navbar.is-info .navbar-end>.navbar-item,.navbar.is-info .navbar-start .navbar-link,.navbar.is-info .navbar-start>.navbar-item{color:#fff}.navbar.is-info .navbar-end .navbar-link.is-active,.navbar.is-info .navbar-end .navbar-link:focus,.navbar.is-info .navbar-end .navbar-link:hover,.navbar.is-info .navbar-end>a.navbar-item.is-active,.navbar.is-info .navbar-end>a.navbar-item:focus,.navbar.is-info .navbar-end>a.navbar-item:hover,.navbar.is-info .navbar-start .navbar-link.is-active,.navbar.is-info .navbar-start .navbar-link:focus,.navbar.is-info .navbar-start .navbar-link:hover,.navbar.is-info .navbar-start>a.navbar-item.is-active,.navbar.is-info .navbar-start>a.navbar-item:focus,.navbar.is-info .navbar-start>a.navbar-item:hover{background-color:#118fe4;color:#fff}.navbar.is-info .navbar-end .navbar-link::after,.navbar.is-info .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-info .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-info .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-info .navbar-item.has-dropdown:hover .navbar-link{background-color:#118fe4;color:#fff}.navbar.is-info .navbar-dropdown a.navbar-item.is-active{background-color:#209cee;color:#fff}}.navbar.is-success{background-color:#23d160;color:#fff}.navbar.is-success .navbar-brand .navbar-link,.navbar.is-success .navbar-brand>.navbar-item{color:#fff}.navbar.is-success .navbar-brand .navbar-link.is-active,.navbar.is-success .navbar-brand .navbar-link:focus,.navbar.is-success .navbar-brand .navbar-link:hover,.navbar.is-success .navbar-brand>a.navbar-item.is-active,.navbar.is-success .navbar-brand>a.navbar-item:focus,.navbar.is-success .navbar-brand>a.navbar-item:hover{background-color:#20bc56;color:#fff}.navbar.is-success .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-success .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-success .navbar-end .navbar-link,.navbar.is-success .navbar-end>.navbar-item,.navbar.is-success .navbar-start .navbar-link,.navbar.is-success .navbar-start>.navbar-item{color:#fff}.navbar.is-success .navbar-end .navbar-link.is-active,.navbar.is-success .navbar-end .navbar-link:focus,.navbar.is-success .navbar-end .navbar-link:hover,.navbar.is-success .navbar-end>a.navbar-item.is-active,.navbar.is-success .navbar-end>a.navbar-item:focus,.navbar.is-success .navbar-end>a.navbar-item:hover,.navbar.is-success .navbar-start .navbar-link.is-active,.navbar.is-success .navbar-start .navbar-link:focus,.navbar.is-success .navbar-start .navbar-link:hover,.navbar.is-success .navbar-start>a.navbar-item.is-active,.navbar.is-success .navbar-start>a.navbar-item:focus,.navbar.is-success .navbar-start>a.navbar-item:hover{background-color:#20bc56;color:#fff}.navbar.is-success .navbar-end .navbar-link::after,.navbar.is-success .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-success .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-success .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-success .navbar-item.has-dropdown:hover .navbar-link{background-color:#20bc56;color:#fff}.navbar.is-success .navbar-dropdown a.navbar-item.is-active{background-color:#23d160;color:#fff}}.navbar.is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-brand .navbar-link,.navbar.is-warning .navbar-brand>.navbar-item{color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-brand .navbar-link.is-active,.navbar.is-warning .navbar-brand .navbar-link:focus,.navbar.is-warning .navbar-brand .navbar-link:hover,.navbar.is-warning .navbar-brand>a.navbar-item.is-active,.navbar.is-warning .navbar-brand>a.navbar-item:focus,.navbar.is-warning .navbar-brand>a.navbar-item:hover{background-color:#ffd83d;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-brand .navbar-link::after{border-color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-burger{color:rgba(0,0,0,.7)}@media screen and (min-width:1024px){.navbar.is-warning .navbar-end .navbar-link,.navbar.is-warning .navbar-end>.navbar-item,.navbar.is-warning .navbar-start .navbar-link,.navbar.is-warning .navbar-start>.navbar-item{color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-end .navbar-link.is-active,.navbar.is-warning .navbar-end .navbar-link:focus,.navbar.is-warning .navbar-end .navbar-link:hover,.navbar.is-warning .navbar-end>a.navbar-item.is-active,.navbar.is-warning .navbar-end>a.navbar-item:focus,.navbar.is-warning .navbar-end>a.navbar-item:hover,.navbar.is-warning .navbar-start .navbar-link.is-active,.navbar.is-warning .navbar-start .navbar-link:focus,.navbar.is-warning .navbar-start .navbar-link:hover,.navbar.is-warning .navbar-start>a.navbar-item.is-active,.navbar.is-warning .navbar-start>a.navbar-item:focus,.navbar.is-warning .navbar-start>a.navbar-item:hover{background-color:#ffd83d;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-end .navbar-link::after,.navbar.is-warning .navbar-start .navbar-link::after{border-color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-warning .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-warning .navbar-item.has-dropdown:hover .navbar-link{background-color:#ffd83d;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-dropdown a.navbar-item.is-active{background-color:#ffdd57;color:rgba(0,0,0,.7)}}.navbar.is-danger{background-color:#ff3860;color:#fff}.navbar.is-danger .navbar-brand .navbar-link,.navbar.is-danger .navbar-brand>.navbar-item{color:#fff}.navbar.is-danger .navbar-brand .navbar-link.is-active,.navbar.is-danger .navbar-brand .navbar-link:focus,.navbar.is-danger .navbar-brand .navbar-link:hover,.navbar.is-danger .navbar-brand>a.navbar-item.is-active,.navbar.is-danger .navbar-brand>a.navbar-item:focus,.navbar.is-danger .navbar-brand>a.navbar-item:hover{background-color:#ff1f4b;color:#fff}.navbar.is-danger .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-danger .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-danger .navbar-end .navbar-link,.navbar.is-danger .navbar-end>.navbar-item,.navbar.is-danger .navbar-start .navbar-link,.navbar.is-danger .navbar-start>.navbar-item{color:#fff}.navbar.is-danger .navbar-end .navbar-link.is-active,.navbar.is-danger .navbar-end .navbar-link:focus,.navbar.is-danger .navbar-end .navbar-link:hover,.navbar.is-danger .navbar-end>a.navbar-item.is-active,.navbar.is-danger .navbar-end>a.navbar-item:focus,.navbar.is-danger .navbar-end>a.navbar-item:hover,.navbar.is-danger .navbar-start .navbar-link.is-active,.navbar.is-danger .navbar-start .navbar-link:focus,.navbar.is-danger .navbar-start .navbar-link:hover,.navbar.is-danger .navbar-start>a.navbar-item.is-active,.navbar.is-danger .navbar-start>a.navbar-item:focus,.navbar.is-danger .navbar-start>a.navbar-item:hover{background-color:#ff1f4b;color:#fff}.navbar.is-danger .navbar-end .navbar-link::after,.navbar.is-danger .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-danger .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-danger .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-danger .navbar-item.has-dropdown:hover .navbar-link{background-color:#ff1f4b;color:#fff}.navbar.is-danger .navbar-dropdown a.navbar-item.is-active{background-color:#ff3860;color:#fff}}.navbar>.container{align-items:stretch;display:flex;min-height:3.25rem;width:100%}.navbar.has-shadow{box-shadow:0 2px 0 0 #f5f5f5}.navbar.is-fixed-bottom,.navbar.is-fixed-top{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom{bottom:0}.navbar.is-fixed-bottom.has-shadow{box-shadow:0 -2px 0 0 #f5f5f5}.navbar.is-fixed-top{top:0}body.has-navbar-fixed-top,html.has-navbar-fixed-top{padding-top:3.25rem}body.has-navbar-fixed-bottom,html.has-navbar-fixed-bottom{padding-bottom:3.25rem}.navbar-brand,.navbar-tabs{align-items:stretch;display:flex;flex-shrink:0;min-height:3.25rem}.navbar-brand a.navbar-item:focus,.navbar-brand a.navbar-item:hover{background-color:transparent}.navbar-tabs{-webkit-overflow-scrolling:touch;max-width:100vw;overflow-x:auto;overflow-y:hidden}.navbar-burger{color:#4a4a4a;cursor:pointer;display:block;height:3.25rem;position:relative;width:3.25rem;margin-left:auto}.navbar-burger span{background-color:currentColor;display:block;height:1px;left:calc(50% - 8px);position:absolute;-webkit-transform-origin:center;transform-origin:center;transition-duration:86ms;transition-property:background-color,opacity,-webkit-transform;transition-property:background-color,opacity,transform;transition-property:background-color,opacity,transform,-webkit-transform;transition-timing-function:ease-out;width:16px}.navbar-burger span:nth-child(1){top:calc(50% - 6px)}.navbar-burger span:nth-child(2){top:calc(50% - 1px)}.navbar-burger span:nth-child(3){top:calc(50% + 4px)}.navbar-burger:hover{background-color:rgba(0,0,0,.05)}.navbar-burger.is-active span:nth-child(1){-webkit-transform:translateY(5px) rotate(45deg);transform:translateY(5px) rotate(45deg)}.navbar-burger.is-active span:nth-child(2){opacity:0}.navbar-burger.is-active span:nth-child(3){-webkit-transform:translateY(-5px) rotate(-45deg);transform:translateY(-5px) rotate(-45deg)}.navbar-menu{display:none}.navbar-item,.navbar-link{color:#4a4a4a;display:block;line-height:1.5;padding:.5rem .75rem;position:relative}.navbar-item .icon:only-child,.navbar-link .icon:only-child{margin-left:-.25rem;margin-right:-.25rem}.navbar-link,a.navbar-item{cursor:pointer}.navbar-link.is-active,.navbar-link:focus,.navbar-link:focus-within,.navbar-link:hover,a.navbar-item.is-active,a.navbar-item:focus,a.navbar-item:focus-within,a.navbar-item:hover{background-color:#fafafa;color:#3273dc}.navbar-item{display:block;flex-grow:0;flex-shrink:0}.navbar-item img{max-height:1.75rem}.navbar-item.has-dropdown{padding:0}.navbar-item.is-expanded{flex-grow:1;flex-shrink:1}.navbar-item.is-tab{border-bottom:1px solid transparent;min-height:3.25rem;padding-bottom:calc(.5rem - 1px)}.navbar-item.is-tab:focus,.navbar-item.is-tab:hover{background-color:transparent;border-bottom-color:#3273dc}.navbar-item.is-tab.is-active{background-color:transparent;border-bottom-color:#3273dc;border-bottom-style:solid;border-bottom-width:3px;color:#3273dc;padding-bottom:calc(.5rem - 3px)}.navbar-content{flex-grow:1;flex-shrink:1}.navbar-link:not(.is-arrowless){padding-right:2.5em}.navbar-link:not(.is-arrowless)::after{border-color:#3273dc;margin-top:-.375em;right:1.125em}.navbar-dropdown{font-size:.875rem;padding-bottom:.5rem;padding-top:.5rem}.navbar-dropdown .navbar-item{padding-left:1.5rem;padding-right:1.5rem}.navbar-divider{background-color:#f5f5f5;border:none;display:none;height:2px;margin:.5rem 0}@media screen and (max-width:1023px){.navbar>.container{display:block}.navbar-brand .navbar-item,.navbar-tabs .navbar-item{align-items:center;display:flex}.navbar-link::after{display:none}.navbar-menu{background-color:#fff;box-shadow:0 8px 16px rgba(10,10,10,.1);padding:.5rem 0}.navbar-menu.is-active{display:block}.navbar.is-fixed-bottom-touch,.navbar.is-fixed-top-touch{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom-touch{bottom:0}.navbar.is-fixed-bottom-touch.has-shadow{box-shadow:0 -2px 3px rgba(10,10,10,.1)}.navbar.is-fixed-top-touch{top:0}.navbar.is-fixed-top .navbar-menu,.navbar.is-fixed-top-touch .navbar-menu{-webkit-overflow-scrolling:touch;max-height:calc(100vh - 3.25rem);overflow:auto}body.has-navbar-fixed-top-touch,html.has-navbar-fixed-top-touch{padding-top:3.25rem}body.has-navbar-fixed-bottom-touch,html.has-navbar-fixed-bottom-touch{padding-bottom:3.25rem}}@media screen and (min-width:1024px){.navbar,.navbar-end,.navbar-menu,.navbar-start{align-items:stretch;display:flex}.navbar{min-height:3.25rem}.navbar.is-spaced{padding:1rem 2rem}.navbar.is-spaced .navbar-end,.navbar.is-spaced .navbar-start{align-items:center}.navbar.is-spaced .navbar-link,.navbar.is-spaced a.navbar-item{border-radius:4px}.navbar.is-transparent .navbar-link.is-active,.navbar.is-transparent .navbar-link:focus,.navbar.is-transparent .navbar-link:hover,.navbar.is-transparent a.navbar-item.is-active,.navbar.is-transparent a.navbar-item:focus,.navbar.is-transparent a.navbar-item:hover{background-color:transparent!important}.navbar.is-transparent .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-transparent .navbar-item.has-dropdown.is-hoverable:focus .navbar-link,.navbar.is-transparent .navbar-item.has-dropdown.is-hoverable:focus-within .navbar-link,.navbar.is-transparent .navbar-item.has-dropdown.is-hoverable:hover .navbar-link{background-color:transparent!important}.navbar.is-transparent .navbar-dropdown a.navbar-item:focus,.navbar.is-transparent .navbar-dropdown a.navbar-item:hover{background-color:#f5f5f5;color:#0a0a0a}.navbar.is-transparent .navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:#3273dc}.navbar-burger{display:none}.navbar-item,.navbar-link{align-items:center;display:flex}.navbar-item{display:flex}.navbar-item.has-dropdown{align-items:stretch}.navbar-item.has-dropdown-up .navbar-link::after{-webkit-transform:rotate(135deg) translate(.25em,-.25em);transform:rotate(135deg) translate(.25em,-.25em)}.navbar-item.has-dropdown-up .navbar-dropdown{border-bottom:2px solid #dbdbdb;border-radius:6px 6px 0 0;border-top:none;bottom:100%;box-shadow:0 -8px 8px rgba(10,10,10,.1);top:auto}.navbar-item.is-active .navbar-dropdown,.navbar-item.is-hoverable:focus .navbar-dropdown,.navbar-item.is-hoverable:focus-within .navbar-dropdown,.navbar-item.is-hoverable:hover .navbar-dropdown{display:block}.navbar-item.is-active .navbar-dropdown.is-boxed,.navbar-item.is-hoverable:focus .navbar-dropdown.is-boxed,.navbar-item.is-hoverable:focus-within .navbar-dropdown.is-boxed,.navbar-item.is-hoverable:hover .navbar-dropdown.is-boxed,.navbar.is-spaced .navbar-item.is-active .navbar-dropdown,.navbar.is-spaced .navbar-item.is-hoverable:focus .navbar-dropdown,.navbar.is-spaced .navbar-item.is-hoverable:focus-within .navbar-dropdown,.navbar.is-spaced .navbar-item.is-hoverable:hover .navbar-dropdown{opacity:1;pointer-events:auto;-webkit-transform:translateY(0);transform:translateY(0)}.navbar-menu{flex-grow:1;flex-shrink:0}.navbar-start{justify-content:flex-start;margin-right:auto}.navbar-end{justify-content:flex-end;margin-left:auto}.navbar-dropdown{background-color:#fff;border-bottom-left-radius:6px;border-bottom-right-radius:6px;border-top:2px solid #dbdbdb;box-shadow:0 8px 8px rgba(10,10,10,.1);display:none;font-size:.875rem;left:0;min-width:100%;position:absolute;top:100%;z-index:20}.navbar-dropdown .navbar-item{padding:.375rem 1rem;white-space:nowrap}.navbar-dropdown a.navbar-item{padding-right:3rem}.navbar-dropdown a.navbar-item:focus,.navbar-dropdown a.navbar-item:hover{background-color:#f5f5f5;color:#0a0a0a}.navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:#3273dc}.navbar-dropdown.is-boxed,.navbar.is-spaced .navbar-dropdown{border-radius:6px;border-top:none;box-shadow:0 8px 8px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1);display:block;opacity:0;pointer-events:none;top:calc(100% + (-4px));-webkit-transform:translateY(-5px);transform:translateY(-5px);transition-duration:86ms;transition-property:opacity,-webkit-transform;transition-property:opacity,transform;transition-property:opacity,transform,-webkit-transform}.navbar-dropdown.is-right{left:auto;right:0}.navbar-divider{display:block}.container>.navbar .navbar-brand,.navbar>.container .navbar-brand{margin-left:-.75rem}.container>.navbar .navbar-menu,.navbar>.container .navbar-menu{margin-right:-.75rem}.navbar.is-fixed-bottom-desktop,.navbar.is-fixed-top-desktop{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom-desktop{bottom:0}.navbar.is-fixed-bottom-desktop.has-shadow{box-shadow:0 -2px 3px rgba(10,10,10,.1)}.navbar.is-fixed-top-desktop{top:0}body.has-navbar-fixed-top-desktop,html.has-navbar-fixed-top-desktop{padding-top:3.25rem}body.has-navbar-fixed-bottom-desktop,html.has-navbar-fixed-bottom-desktop{padding-bottom:3.25rem}body.has-spaced-navbar-fixed-top,html.has-spaced-navbar-fixed-top{padding-top:5.25rem}body.has-spaced-navbar-fixed-bottom,html.has-spaced-navbar-fixed-bottom{padding-bottom:5.25rem}.navbar-link.is-active,a.navbar-item.is-active{color:#0a0a0a}.navbar-link.is-active:not(:focus):not(:hover),a.navbar-item.is-active:not(:focus):not(:hover){background-color:transparent}.navbar-item.has-dropdown.is-active .navbar-link,.navbar-item.has-dropdown:focus .navbar-link,.navbar-item.has-dropdown:hover .navbar-link{background-color:#fafafa}}.hero.is-fullheight-with-navbar{min-height:calc(100vh - 3.25rem)}.pagination{font-size:1rem;margin:-.25rem}.pagination.is-small{font-size:.75rem}.pagination.is-medium{font-size:1.25rem}.pagination.is-large{font-size:1.5rem}.pagination.is-rounded .pagination-next,.pagination.is-rounded .pagination-previous{padding-left:1em;padding-right:1em;border-radius:290486px}.pagination.is-rounded .pagination-link{border-radius:290486px}.pagination,.pagination-list{align-items:center;display:flex;justify-content:center;text-align:center}.pagination-ellipsis,.pagination-link,.pagination-next,.pagination-previous{font-size:1em;justify-content:center;margin:.25rem;padding-left:.5em;padding-right:.5em;text-align:center}.pagination-link,.pagination-next,.pagination-previous{border-color:#dbdbdb;color:#363636;min-width:2.25em}.pagination-link:hover,.pagination-next:hover,.pagination-previous:hover{border-color:#b5b5b5;color:#363636}.pagination-link:focus,.pagination-next:focus,.pagination-previous:focus{border-color:#3273dc}.pagination-link:active,.pagination-next:active,.pagination-previous:active{box-shadow:inset 0 1px 2px rgba(10,10,10,.2)}.pagination-link[disabled],.pagination-next[disabled],.pagination-previous[disabled]{background-color:#dbdbdb;border-color:#dbdbdb;box-shadow:none;color:#7a7a7a;opacity:.5}.pagination-next,.pagination-previous{padding-left:.75em;padding-right:.75em;white-space:nowrap}.pagination-link.is-current{background-color:#3273dc;border-color:#3273dc;color:#fff}.pagination-ellipsis{color:#b5b5b5;pointer-events:none}.pagination-list{flex-wrap:wrap}@media screen and (max-width:768px){.pagination{flex-wrap:wrap}.pagination-next,.pagination-previous{flex-grow:1;flex-shrink:1}.pagination-list li{flex-grow:1;flex-shrink:1}}@media screen and (min-width:769px),print{.pagination-list{flex-grow:1;flex-shrink:1;justify-content:flex-start;order:1}.pagination-previous{order:2}.pagination-next{order:3}.pagination{justify-content:space-between}.pagination.is-centered .pagination-previous{order:1}.pagination.is-centered .pagination-list{justify-content:center;order:2}.pagination.is-centered .pagination-next{order:3}.pagination.is-right .pagination-previous{order:1}.pagination.is-right .pagination-next{order:2}.pagination.is-right .pagination-list{justify-content:flex-end;order:3}}.panel{font-size:1rem}.panel:not(:last-child){margin-bottom:1.5rem}.panel-block,.panel-heading,.panel-tabs{border-bottom:1px solid #dbdbdb;border-left:1px solid #dbdbdb;border-right:1px solid #dbdbdb}.panel-block:first-child,.panel-heading:first-child,.panel-tabs:first-child{border-top:1px solid #dbdbdb}.panel-heading{background-color:#f5f5f5;border-radius:4px 4px 0 0;color:#363636;font-size:1.25em;font-weight:300;line-height:1.25;padding:.5em .75em}.panel-tabs{align-items:flex-end;display:flex;font-size:.875em;justify-content:center}.panel-tabs a{border-bottom:1px solid #dbdbdb;margin-bottom:-1px;padding:.5em}.panel-tabs a.is-active{border-bottom-color:#4a4a4a;color:#363636}.panel-list a{color:#4a4a4a}.panel-list a:hover{color:#3273dc}.panel-block{align-items:center;color:#363636;display:flex;justify-content:flex-start;padding:.5em .75em}.panel-block input[type=checkbox]{margin-right:.75em}.panel-block>.control{flex-grow:1;flex-shrink:1;width:100%}.panel-block.is-wrapped{flex-wrap:wrap}.panel-block.is-active{border-left-color:#3273dc;color:#363636}.panel-block.is-active .panel-icon{color:#3273dc}a.panel-block,label.panel-block{cursor:pointer}a.panel-block:hover,label.panel-block:hover{background-color:#f5f5f5}.panel-icon{display:inline-block;font-size:14px;height:1em;line-height:1em;text-align:center;vertical-align:top;width:1em;color:#7a7a7a;margin-right:.75em}.panel-icon .fa{font-size:inherit;line-height:inherit}.tabs{-webkit-overflow-scrolling:touch;align-items:stretch;display:flex;font-size:1rem;justify-content:space-between;overflow:hidden;overflow-x:auto;white-space:nowrap}.tabs a{align-items:center;border-bottom-color:#dbdbdb;border-bottom-style:solid;border-bottom-width:1px;color:#4a4a4a;display:flex;justify-content:center;margin-bottom:-1px;padding:.5em 1em;vertical-align:top}.tabs a:hover{border-bottom-color:#363636;color:#363636}.tabs li{display:block}.tabs li.is-active a{border-bottom-color:#3273dc;color:#3273dc}.tabs ul{align-items:center;border-bottom-color:#dbdbdb;border-bottom-style:solid;border-bottom-width:1px;display:flex;flex-grow:1;flex-shrink:0;justify-content:flex-start}.tabs ul.is-left{padding-right:.75em}.tabs ul.is-center{flex:none;justify-content:center;padding-left:.75em;padding-right:.75em}.tabs ul.is-right{justify-content:flex-end;padding-left:.75em}.tabs .icon:first-child{margin-right:.5em}.tabs .icon:last-child{margin-left:.5em}.tabs.is-centered ul{justify-content:center}.tabs.is-right ul{justify-content:flex-end}.tabs.is-boxed a{border:1px solid transparent;border-radius:4px 4px 0 0}.tabs.is-boxed a:hover{background-color:#f5f5f5;border-bottom-color:#dbdbdb}.tabs.is-boxed li.is-active a{background-color:#fff;border-color:#dbdbdb;border-bottom-color:transparent!important}.tabs.is-fullwidth li{flex-grow:1;flex-shrink:0}.tabs.is-toggle a{border-color:#dbdbdb;border-style:solid;border-width:1px;margin-bottom:0;position:relative}.tabs.is-toggle a:hover{background-color:#f5f5f5;border-color:#b5b5b5;z-index:2}.tabs.is-toggle li+li{margin-left:-1px}.tabs.is-toggle li:first-child a{border-radius:4px 0 0 4px}.tabs.is-toggle li:last-child a{border-radius:0 4px 4px 0}.tabs.is-toggle li.is-active a{background-color:#3273dc;border-color:#3273dc;color:#fff;z-index:1}.tabs.is-toggle ul{border-bottom:none}.tabs.is-toggle.is-toggle-rounded li:first-child a{border-bottom-left-radius:290486px;border-top-left-radius:290486px;padding-left:1.25em}.tabs.is-toggle.is-toggle-rounded li:last-child a{border-bottom-right-radius:290486px;border-top-right-radius:290486px;padding-right:1.25em}.tabs.is-small{font-size:.75rem}.tabs.is-medium{font-size:1.25rem}.tabs.is-large{font-size:1.5rem}.column{display:block;flex-basis:0;flex-grow:1;flex-shrink:1;padding:.75rem}.columns.is-mobile>.column.is-narrow{flex:none}.columns.is-mobile>.column.is-full{flex:none;width:100%}.columns.is-mobile>.column.is-three-quarters{flex:none;width:75%}.columns.is-mobile>.column.is-two-thirds{flex:none;width:66.6666%}.columns.is-mobile>.column.is-half{flex:none;width:50%}.columns.is-mobile>.column.is-one-third{flex:none;width:33.3333%}.columns.is-mobile>.column.is-one-quarter{flex:none;width:25%}.columns.is-mobile>.column.is-one-fifth{flex:none;width:20%}.columns.is-mobile>.column.is-two-fifths{flex:none;width:40%}.columns.is-mobile>.column.is-three-fifths{flex:none;width:60%}.columns.is-mobile>.column.is-four-fifths{flex:none;width:80%}.columns.is-mobile>.column.is-offset-three-quarters{margin-left:75%}.columns.is-mobile>.column.is-offset-two-thirds{margin-left:66.6666%}.columns.is-mobile>.column.is-offset-half{margin-left:50%}.columns.is-mobile>.column.is-offset-one-third{margin-left:33.3333%}.columns.is-mobile>.column.is-offset-one-quarter{margin-left:25%}.columns.is-mobile>.column.is-offset-one-fifth{margin-left:20%}.columns.is-mobile>.column.is-offset-two-fifths{margin-left:40%}.columns.is-mobile>.column.is-offset-three-fifths{margin-left:60%}.columns.is-mobile>.column.is-offset-four-fifths{margin-left:80%}.columns.is-mobile>.column.is-0{flex:none;width:0%}.columns.is-mobile>.column.is-offset-0{margin-left:0}.columns.is-mobile>.column.is-1{flex:none;width:8.33333%}.columns.is-mobile>.column.is-offset-1{margin-left:8.33333%}.columns.is-mobile>.column.is-2{flex:none;width:16.66667%}.columns.is-mobile>.column.is-offset-2{margin-left:16.66667%}.columns.is-mobile>.column.is-3{flex:none;width:25%}.columns.is-mobile>.column.is-offset-3{margin-left:25%}.columns.is-mobile>.column.is-4{flex:none;width:33.33333%}.columns.is-mobile>.column.is-offset-4{margin-left:33.33333%}.columns.is-mobile>.column.is-5{flex:none;width:41.66667%}.columns.is-mobile>.column.is-offset-5{margin-left:41.66667%}.columns.is-mobile>.column.is-6{flex:none;width:50%}.columns.is-mobile>.column.is-offset-6{margin-left:50%}.columns.is-mobile>.column.is-7{flex:none;width:58.33333%}.columns.is-mobile>.column.is-offset-7{margin-left:58.33333%}.columns.is-mobile>.column.is-8{flex:none;width:66.66667%}.columns.is-mobile>.column.is-offset-8{margin-left:66.66667%}.columns.is-mobile>.column.is-9{flex:none;width:75%}.columns.is-mobile>.column.is-offset-9{margin-left:75%}.columns.is-mobile>.column.is-10{flex:none;width:83.33333%}.columns.is-mobile>.column.is-offset-10{margin-left:83.33333%}.columns.is-mobile>.column.is-11{flex:none;width:91.66667%}.columns.is-mobile>.column.is-offset-11{margin-left:91.66667%}.columns.is-mobile>.column.is-12{flex:none;width:100%}.columns.is-mobile>.column.is-offset-12{margin-left:100%}@media screen and (max-width:768px){.column.is-narrow-mobile{flex:none}.column.is-full-mobile{flex:none;width:100%}.column.is-three-quarters-mobile{flex:none;width:75%}.column.is-two-thirds-mobile{flex:none;width:66.6666%}.column.is-half-mobile{flex:none;width:50%}.column.is-one-third-mobile{flex:none;width:33.3333%}.column.is-one-quarter-mobile{flex:none;width:25%}.column.is-one-fifth-mobile{flex:none;width:20%}.column.is-two-fifths-mobile{flex:none;width:40%}.column.is-three-fifths-mobile{flex:none;width:60%}.column.is-four-fifths-mobile{flex:none;width:80%}.column.is-offset-three-quarters-mobile{margin-left:75%}.column.is-offset-two-thirds-mobile{margin-left:66.6666%}.column.is-offset-half-mobile{margin-left:50%}.column.is-offset-one-third-mobile{margin-left:33.3333%}.column.is-offset-one-quarter-mobile{margin-left:25%}.column.is-offset-one-fifth-mobile{margin-left:20%}.column.is-offset-two-fifths-mobile{margin-left:40%}.column.is-offset-three-fifths-mobile{margin-left:60%}.column.is-offset-four-fifths-mobile{margin-left:80%}.column.is-0-mobile{flex:none;width:0%}.column.is-offset-0-mobile{margin-left:0}.column.is-1-mobile{flex:none;width:8.33333%}.column.is-offset-1-mobile{margin-left:8.33333%}.column.is-2-mobile{flex:none;width:16.66667%}.column.is-offset-2-mobile{margin-left:16.66667%}.column.is-3-mobile{flex:none;width:25%}.column.is-offset-3-mobile{margin-left:25%}.column.is-4-mobile{flex:none;width:33.33333%}.column.is-offset-4-mobile{margin-left:33.33333%}.column.is-5-mobile{flex:none;width:41.66667%}.column.is-offset-5-mobile{margin-left:41.66667%}.column.is-6-mobile{flex:none;width:50%}.column.is-offset-6-mobile{margin-left:50%}.column.is-7-mobile{flex:none;width:58.33333%}.column.is-offset-7-mobile{margin-left:58.33333%}.column.is-8-mobile{flex:none;width:66.66667%}.column.is-offset-8-mobile{margin-left:66.66667%}.column.is-9-mobile{flex:none;width:75%}.column.is-offset-9-mobile{margin-left:75%}.column.is-10-mobile{flex:none;width:83.33333%}.column.is-offset-10-mobile{margin-left:83.33333%}.column.is-11-mobile{flex:none;width:91.66667%}.column.is-offset-11-mobile{margin-left:91.66667%}.column.is-12-mobile{flex:none;width:100%}.column.is-offset-12-mobile{margin-left:100%}}@media screen and (min-width:769px),print{.column.is-narrow,.column.is-narrow-tablet{flex:none}.column.is-full,.column.is-full-tablet{flex:none;width:100%}.column.is-three-quarters,.column.is-three-quarters-tablet{flex:none;width:75%}.column.is-two-thirds,.column.is-two-thirds-tablet{flex:none;width:66.6666%}.column.is-half,.column.is-half-tablet{flex:none;width:50%}.column.is-one-third,.column.is-one-third-tablet{flex:none;width:33.3333%}.column.is-one-quarter,.column.is-one-quarter-tablet{flex:none;width:25%}.column.is-one-fifth,.column.is-one-fifth-tablet{flex:none;width:20%}.column.is-two-fifths,.column.is-two-fifths-tablet{flex:none;width:40%}.column.is-three-fifths,.column.is-three-fifths-tablet{flex:none;width:60%}.column.is-four-fifths,.column.is-four-fifths-tablet{flex:none;width:80%}.column.is-offset-three-quarters,.column.is-offset-three-quarters-tablet{margin-left:75%}.column.is-offset-two-thirds,.column.is-offset-two-thirds-tablet{margin-left:66.6666%}.column.is-offset-half,.column.is-offset-half-tablet{margin-left:50%}.column.is-offset-one-third,.column.is-offset-one-third-tablet{margin-left:33.3333%}.column.is-offset-one-quarter,.column.is-offset-one-quarter-tablet{margin-left:25%}.column.is-offset-one-fifth,.column.is-offset-one-fifth-tablet{margin-left:20%}.column.is-offset-two-fifths,.column.is-offset-two-fifths-tablet{margin-left:40%}.column.is-offset-three-fifths,.column.is-offset-three-fifths-tablet{margin-left:60%}.column.is-offset-four-fifths,.column.is-offset-four-fifths-tablet{margin-left:80%}.column.is-0,.column.is-0-tablet{flex:none;width:0%}.column.is-offset-0,.column.is-offset-0-tablet{margin-left:0}.column.is-1,.column.is-1-tablet{flex:none;width:8.33333%}.column.is-offset-1,.column.is-offset-1-tablet{margin-left:8.33333%}.column.is-2,.column.is-2-tablet{flex:none;width:16.66667%}.column.is-offset-2,.column.is-offset-2-tablet{margin-left:16.66667%}.column.is-3,.column.is-3-tablet{flex:none;width:25%}.column.is-offset-3,.column.is-offset-3-tablet{margin-left:25%}.column.is-4,.column.is-4-tablet{flex:none;width:33.33333%}.column.is-offset-4,.column.is-offset-4-tablet{margin-left:33.33333%}.column.is-5,.column.is-5-tablet{flex:none;width:41.66667%}.column.is-offset-5,.column.is-offset-5-tablet{margin-left:41.66667%}.column.is-6,.column.is-6-tablet{flex:none;width:50%}.column.is-offset-6,.column.is-offset-6-tablet{margin-left:50%}.column.is-7,.column.is-7-tablet{flex:none;width:58.33333%}.column.is-offset-7,.column.is-offset-7-tablet{margin-left:58.33333%}.column.is-8,.column.is-8-tablet{flex:none;width:66.66667%}.column.is-offset-8,.column.is-offset-8-tablet{margin-left:66.66667%}.column.is-9,.column.is-9-tablet{flex:none;width:75%}.column.is-offset-9,.column.is-offset-9-tablet{margin-left:75%}.column.is-10,.column.is-10-tablet{flex:none;width:83.33333%}.column.is-offset-10,.column.is-offset-10-tablet{margin-left:83.33333%}.column.is-11,.column.is-11-tablet{flex:none;width:91.66667%}.column.is-offset-11,.column.is-offset-11-tablet{margin-left:91.66667%}.column.is-12,.column.is-12-tablet{flex:none;width:100%}.column.is-offset-12,.column.is-offset-12-tablet{margin-left:100%}}@media screen and (max-width:1023px){.column.is-narrow-touch{flex:none}.column.is-full-touch{flex:none;width:100%}.column.is-three-quarters-touch{flex:none;width:75%}.column.is-two-thirds-touch{flex:none;width:66.6666%}.column.is-half-touch{flex:none;width:50%}.column.is-one-third-touch{flex:none;width:33.3333%}.column.is-one-quarter-touch{flex:none;width:25%}.column.is-one-fifth-touch{flex:none;width:20%}.column.is-two-fifths-touch{flex:none;width:40%}.column.is-three-fifths-touch{flex:none;width:60%}.column.is-four-fifths-touch{flex:none;width:80%}.column.is-offset-three-quarters-touch{margin-left:75%}.column.is-offset-two-thirds-touch{margin-left:66.6666%}.column.is-offset-half-touch{margin-left:50%}.column.is-offset-one-third-touch{margin-left:33.3333%}.column.is-offset-one-quarter-touch{margin-left:25%}.column.is-offset-one-fifth-touch{margin-left:20%}.column.is-offset-two-fifths-touch{margin-left:40%}.column.is-offset-three-fifths-touch{margin-left:60%}.column.is-offset-four-fifths-touch{margin-left:80%}.column.is-0-touch{flex:none;width:0%}.column.is-offset-0-touch{margin-left:0}.column.is-1-touch{flex:none;width:8.33333%}.column.is-offset-1-touch{margin-left:8.33333%}.column.is-2-touch{flex:none;width:16.66667%}.column.is-offset-2-touch{margin-left:16.66667%}.column.is-3-touch{flex:none;width:25%}.column.is-offset-3-touch{margin-left:25%}.column.is-4-touch{flex:none;width:33.33333%}.column.is-offset-4-touch{margin-left:33.33333%}.column.is-5-touch{flex:none;width:41.66667%}.column.is-offset-5-touch{margin-left:41.66667%}.column.is-6-touch{flex:none;width:50%}.column.is-offset-6-touch{margin-left:50%}.column.is-7-touch{flex:none;width:58.33333%}.column.is-offset-7-touch{margin-left:58.33333%}.column.is-8-touch{flex:none;width:66.66667%}.column.is-offset-8-touch{margin-left:66.66667%}.column.is-9-touch{flex:none;width:75%}.column.is-offset-9-touch{margin-left:75%}.column.is-10-touch{flex:none;width:83.33333%}.column.is-offset-10-touch{margin-left:83.33333%}.column.is-11-touch{flex:none;width:91.66667%}.column.is-offset-11-touch{margin-left:91.66667%}.column.is-12-touch{flex:none;width:100%}.column.is-offset-12-touch{margin-left:100%}}@media screen and (min-width:1024px){.column.is-narrow-desktop{flex:none}.column.is-full-desktop{flex:none;width:100%}.column.is-three-quarters-desktop{flex:none;width:75%}.column.is-two-thirds-desktop{flex:none;width:66.6666%}.column.is-half-desktop{flex:none;width:50%}.column.is-one-third-desktop{flex:none;width:33.3333%}.column.is-one-quarter-desktop{flex:none;width:25%}.column.is-one-fifth-desktop{flex:none;width:20%}.column.is-two-fifths-desktop{flex:none;width:40%}.column.is-three-fifths-desktop{flex:none;width:60%}.column.is-four-fifths-desktop{flex:none;width:80%}.column.is-offset-three-quarters-desktop{margin-left:75%}.column.is-offset-two-thirds-desktop{margin-left:66.6666%}.column.is-offset-half-desktop{margin-left:50%}.column.is-offset-one-third-desktop{margin-left:33.3333%}.column.is-offset-one-quarter-desktop{margin-left:25%}.column.is-offset-one-fifth-desktop{margin-left:20%}.column.is-offset-two-fifths-desktop{margin-left:40%}.column.is-offset-three-fifths-desktop{margin-left:60%}.column.is-offset-four-fifths-desktop{margin-left:80%}.column.is-0-desktop{flex:none;width:0%}.column.is-offset-0-desktop{margin-left:0}.column.is-1-desktop{flex:none;width:8.33333%}.column.is-offset-1-desktop{margin-left:8.33333%}.column.is-2-desktop{flex:none;width:16.66667%}.column.is-offset-2-desktop{margin-left:16.66667%}.column.is-3-desktop{flex:none;width:25%}.column.is-offset-3-desktop{margin-left:25%}.column.is-4-desktop{flex:none;width:33.33333%}.column.is-offset-4-desktop{margin-left:33.33333%}.column.is-5-desktop{flex:none;width:41.66667%}.column.is-offset-5-desktop{margin-left:41.66667%}.column.is-6-desktop{flex:none;width:50%}.column.is-offset-6-desktop{margin-left:50%}.column.is-7-desktop{flex:none;width:58.33333%}.column.is-offset-7-desktop{margin-left:58.33333%}.column.is-8-desktop{flex:none;width:66.66667%}.column.is-offset-8-desktop{margin-left:66.66667%}.column.is-9-desktop{flex:none;width:75%}.column.is-offset-9-desktop{margin-left:75%}.column.is-10-desktop{flex:none;width:83.33333%}.column.is-offset-10-desktop{margin-left:83.33333%}.column.is-11-desktop{flex:none;width:91.66667%}.column.is-offset-11-desktop{margin-left:91.66667%}.column.is-12-desktop{flex:none;width:100%}.column.is-offset-12-desktop{margin-left:100%}}@media screen and (min-width:1216px){.column.is-narrow-widescreen{flex:none}.column.is-full-widescreen{flex:none;width:100%}.column.is-three-quarters-widescreen{flex:none;width:75%}.column.is-two-thirds-widescreen{flex:none;width:66.6666%}.column.is-half-widescreen{flex:none;width:50%}.column.is-one-third-widescreen{flex:none;width:33.3333%}.column.is-one-quarter-widescreen{flex:none;width:25%}.column.is-one-fifth-widescreen{flex:none;width:20%}.column.is-two-fifths-widescreen{flex:none;width:40%}.column.is-three-fifths-widescreen{flex:none;width:60%}.column.is-four-fifths-widescreen{flex:none;width:80%}.column.is-offset-three-quarters-widescreen{margin-left:75%}.column.is-offset-two-thirds-widescreen{margin-left:66.6666%}.column.is-offset-half-widescreen{margin-left:50%}.column.is-offset-one-third-widescreen{margin-left:33.3333%}.column.is-offset-one-quarter-widescreen{margin-left:25%}.column.is-offset-one-fifth-widescreen{margin-left:20%}.column.is-offset-two-fifths-widescreen{margin-left:40%}.column.is-offset-three-fifths-widescreen{margin-left:60%}.column.is-offset-four-fifths-widescreen{margin-left:80%}.column.is-0-widescreen{flex:none;width:0%}.column.is-offset-0-widescreen{margin-left:0}.column.is-1-widescreen{flex:none;width:8.33333%}.column.is-offset-1-widescreen{margin-left:8.33333%}.column.is-2-widescreen{flex:none;width:16.66667%}.column.is-offset-2-widescreen{margin-left:16.66667%}.column.is-3-widescreen{flex:none;width:25%}.column.is-offset-3-widescreen{margin-left:25%}.column.is-4-widescreen{flex:none;width:33.33333%}.column.is-offset-4-widescreen{margin-left:33.33333%}.column.is-5-widescreen{flex:none;width:41.66667%}.column.is-offset-5-widescreen{margin-left:41.66667%}.column.is-6-widescreen{flex:none;width:50%}.column.is-offset-6-widescreen{margin-left:50%}.column.is-7-widescreen{flex:none;width:58.33333%}.column.is-offset-7-widescreen{margin-left:58.33333%}.column.is-8-widescreen{flex:none;width:66.66667%}.column.is-offset-8-widescreen{margin-left:66.66667%}.column.is-9-widescreen{flex:none;width:75%}.column.is-offset-9-widescreen{margin-left:75%}.column.is-10-widescreen{flex:none;width:83.33333%}.column.is-offset-10-widescreen{margin-left:83.33333%}.column.is-11-widescreen{flex:none;width:91.66667%}.column.is-offset-11-widescreen{margin-left:91.66667%}.column.is-12-widescreen{flex:none;width:100%}.column.is-offset-12-widescreen{margin-left:100%}}@media screen and (min-width:1408px){.column.is-narrow-fullhd{flex:none}.column.is-full-fullhd{flex:none;width:100%}.column.is-three-quarters-fullhd{flex:none;width:75%}.column.is-two-thirds-fullhd{flex:none;width:66.6666%}.column.is-half-fullhd{flex:none;width:50%}.column.is-one-third-fullhd{flex:none;width:33.3333%}.column.is-one-quarter-fullhd{flex:none;width:25%}.column.is-one-fifth-fullhd{flex:none;width:20%}.column.is-two-fifths-fullhd{flex:none;width:40%}.column.is-three-fifths-fullhd{flex:none;width:60%}.column.is-four-fifths-fullhd{flex:none;width:80%}.column.is-offset-three-quarters-fullhd{margin-left:75%}.column.is-offset-two-thirds-fullhd{margin-left:66.6666%}.column.is-offset-half-fullhd{margin-left:50%}.column.is-offset-one-third-fullhd{margin-left:33.3333%}.column.is-offset-one-quarter-fullhd{margin-left:25%}.column.is-offset-one-fifth-fullhd{margin-left:20%}.column.is-offset-two-fifths-fullhd{margin-left:40%}.column.is-offset-three-fifths-fullhd{margin-left:60%}.column.is-offset-four-fifths-fullhd{margin-left:80%}.column.is-0-fullhd{flex:none;width:0%}.column.is-offset-0-fullhd{margin-left:0}.column.is-1-fullhd{flex:none;width:8.33333%}.column.is-offset-1-fullhd{margin-left:8.33333%}.column.is-2-fullhd{flex:none;width:16.66667%}.column.is-offset-2-fullhd{margin-left:16.66667%}.column.is-3-fullhd{flex:none;width:25%}.column.is-offset-3-fullhd{margin-left:25%}.column.is-4-fullhd{flex:none;width:33.33333%}.column.is-offset-4-fullhd{margin-left:33.33333%}.column.is-5-fullhd{flex:none;width:41.66667%}.column.is-offset-5-fullhd{margin-left:41.66667%}.column.is-6-fullhd{flex:none;width:50%}.column.is-offset-6-fullhd{margin-left:50%}.column.is-7-fullhd{flex:none;width:58.33333%}.column.is-offset-7-fullhd{margin-left:58.33333%}.column.is-8-fullhd{flex:none;width:66.66667%}.column.is-offset-8-fullhd{margin-left:66.66667%}.column.is-9-fullhd{flex:none;width:75%}.column.is-offset-9-fullhd{margin-left:75%}.column.is-10-fullhd{flex:none;width:83.33333%}.column.is-offset-10-fullhd{margin-left:83.33333%}.column.is-11-fullhd{flex:none;width:91.66667%}.column.is-offset-11-fullhd{margin-left:91.66667%}.column.is-12-fullhd{flex:none;width:100%}.column.is-offset-12-fullhd{margin-left:100%}}.columns{margin-left:-.75rem;margin-right:-.75rem;margin-top:-.75rem}.columns:last-child{margin-bottom:-.75rem}.columns:not(:last-child){margin-bottom:calc(1.5rem - .75rem)}.columns.is-centered{justify-content:center}.columns.is-gapless{margin-left:0;margin-right:0;margin-top:0}.columns.is-gapless>.column{margin:0;padding:0!important}.columns.is-gapless:not(:last-child){margin-bottom:1.5rem}.columns.is-gapless:last-child{margin-bottom:0}.columns.is-mobile{display:flex}.columns.is-multiline{flex-wrap:wrap}.columns.is-vcentered{align-items:center}@media screen and (min-width:769px),print{.columns:not(.is-desktop){display:flex}}@media screen and (min-width:1024px){.columns.is-desktop{display:flex}}.columns.is-variable{--columnGap:0.75rem;margin-left:calc(-1 * var(--columnGap));margin-right:calc(-1 * var(--columnGap))}.columns.is-variable .column{padding-left:var(--columnGap);padding-right:var(--columnGap)}.columns.is-variable.is-0{--columnGap:0rem}@media screen and (max-width:768px){.columns.is-variable.is-0-mobile{--columnGap:0rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-0-tablet{--columnGap:0rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-0-tablet-only{--columnGap:0rem}}@media screen and (max-width:1023px){.columns.is-variable.is-0-touch{--columnGap:0rem}}@media screen and (min-width:1024px){.columns.is-variable.is-0-desktop{--columnGap:0rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-0-desktop-only{--columnGap:0rem}}@media screen and (min-width:1216px){.columns.is-variable.is-0-widescreen{--columnGap:0rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-0-widescreen-only{--columnGap:0rem}}@media screen and (min-width:1408px){.columns.is-variable.is-0-fullhd{--columnGap:0rem}}.columns.is-variable.is-1{--columnGap:0.25rem}@media screen and (max-width:768px){.columns.is-variable.is-1-mobile{--columnGap:0.25rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-1-tablet{--columnGap:0.25rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-1-tablet-only{--columnGap:0.25rem}}@media screen and (max-width:1023px){.columns.is-variable.is-1-touch{--columnGap:0.25rem}}@media screen and (min-width:1024px){.columns.is-variable.is-1-desktop{--columnGap:0.25rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-1-desktop-only{--columnGap:0.25rem}}@media screen and (min-width:1216px){.columns.is-variable.is-1-widescreen{--columnGap:0.25rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-1-widescreen-only{--columnGap:0.25rem}}@media screen and (min-width:1408px){.columns.is-variable.is-1-fullhd{--columnGap:0.25rem}}.columns.is-variable.is-2{--columnGap:0.5rem}@media screen and (max-width:768px){.columns.is-variable.is-2-mobile{--columnGap:0.5rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-2-tablet{--columnGap:0.5rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-2-tablet-only{--columnGap:0.5rem}}@media screen and (max-width:1023px){.columns.is-variable.is-2-touch{--columnGap:0.5rem}}@media screen and (min-width:1024px){.columns.is-variable.is-2-desktop{--columnGap:0.5rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-2-desktop-only{--columnGap:0.5rem}}@media screen and (min-width:1216px){.columns.is-variable.is-2-widescreen{--columnGap:0.5rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-2-widescreen-only{--columnGap:0.5rem}}@media screen and (min-width:1408px){.columns.is-variable.is-2-fullhd{--columnGap:0.5rem}}.columns.is-variable.is-3{--columnGap:0.75rem}@media screen and (max-width:768px){.columns.is-variable.is-3-mobile{--columnGap:0.75rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-3-tablet{--columnGap:0.75rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-3-tablet-only{--columnGap:0.75rem}}@media screen and (max-width:1023px){.columns.is-variable.is-3-touch{--columnGap:0.75rem}}@media screen and (min-width:1024px){.columns.is-variable.is-3-desktop{--columnGap:0.75rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-3-desktop-only{--columnGap:0.75rem}}@media screen and (min-width:1216px){.columns.is-variable.is-3-widescreen{--columnGap:0.75rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-3-widescreen-only{--columnGap:0.75rem}}@media screen and (min-width:1408px){.columns.is-variable.is-3-fullhd{--columnGap:0.75rem}}.columns.is-variable.is-4{--columnGap:1rem}@media screen and (max-width:768px){.columns.is-variable.is-4-mobile{--columnGap:1rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-4-tablet{--columnGap:1rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-4-tablet-only{--columnGap:1rem}}@media screen and (max-width:1023px){.columns.is-variable.is-4-touch{--columnGap:1rem}}@media screen and (min-width:1024px){.columns.is-variable.is-4-desktop{--columnGap:1rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-4-desktop-only{--columnGap:1rem}}@media screen and (min-width:1216px){.columns.is-variable.is-4-widescreen{--columnGap:1rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-4-widescreen-only{--columnGap:1rem}}@media screen and (min-width:1408px){.columns.is-variable.is-4-fullhd{--columnGap:1rem}}.columns.is-variable.is-5{--columnGap:1.25rem}@media screen and (max-width:768px){.columns.is-variable.is-5-mobile{--columnGap:1.25rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-5-tablet{--columnGap:1.25rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-5-tablet-only{--columnGap:1.25rem}}@media screen and (max-width:1023px){.columns.is-variable.is-5-touch{--columnGap:1.25rem}}@media screen and (min-width:1024px){.columns.is-variable.is-5-desktop{--columnGap:1.25rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-5-desktop-only{--columnGap:1.25rem}}@media screen and (min-width:1216px){.columns.is-variable.is-5-widescreen{--columnGap:1.25rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-5-widescreen-only{--columnGap:1.25rem}}@media screen and (min-width:1408px){.columns.is-variable.is-5-fullhd{--columnGap:1.25rem}}.columns.is-variable.is-6{--columnGap:1.5rem}@media screen and (max-width:768px){.columns.is-variable.is-6-mobile{--columnGap:1.5rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-6-tablet{--columnGap:1.5rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-6-tablet-only{--columnGap:1.5rem}}@media screen and (max-width:1023px){.columns.is-variable.is-6-touch{--columnGap:1.5rem}}@media screen and (min-width:1024px){.columns.is-variable.is-6-desktop{--columnGap:1.5rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-6-desktop-only{--columnGap:1.5rem}}@media screen and (min-width:1216px){.columns.is-variable.is-6-widescreen{--columnGap:1.5rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-6-widescreen-only{--columnGap:1.5rem}}@media screen and (min-width:1408px){.columns.is-variable.is-6-fullhd{--columnGap:1.5rem}}.columns.is-variable.is-7{--columnGap:1.75rem}@media screen and (max-width:768px){.columns.is-variable.is-7-mobile{--columnGap:1.75rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-7-tablet{--columnGap:1.75rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-7-tablet-only{--columnGap:1.75rem}}@media screen and (max-width:1023px){.columns.is-variable.is-7-touch{--columnGap:1.75rem}}@media screen and (min-width:1024px){.columns.is-variable.is-7-desktop{--columnGap:1.75rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-7-desktop-only{--columnGap:1.75rem}}@media screen and (min-width:1216px){.columns.is-variable.is-7-widescreen{--columnGap:1.75rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-7-widescreen-only{--columnGap:1.75rem}}@media screen and (min-width:1408px){.columns.is-variable.is-7-fullhd{--columnGap:1.75rem}}.columns.is-variable.is-8{--columnGap:2rem}@media screen and (max-width:768px){.columns.is-variable.is-8-mobile{--columnGap:2rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-8-tablet{--columnGap:2rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-8-tablet-only{--columnGap:2rem}}@media screen and (max-width:1023px){.columns.is-variable.is-8-touch{--columnGap:2rem}}@media screen and (min-width:1024px){.columns.is-variable.is-8-desktop{--columnGap:2rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-8-desktop-only{--columnGap:2rem}}@media screen and (min-width:1216px){.columns.is-variable.is-8-widescreen{--columnGap:2rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-8-widescreen-only{--columnGap:2rem}}@media screen and (min-width:1408px){.columns.is-variable.is-8-fullhd{--columnGap:2rem}}.tile{align-items:stretch;display:block;flex-basis:0;flex-grow:1;flex-shrink:1;min-height:-webkit-min-content;min-height:-moz-min-content;min-height:min-content}.tile.is-ancestor{margin-left:-.75rem;margin-right:-.75rem;margin-top:-.75rem}.tile.is-ancestor:last-child{margin-bottom:-.75rem}.tile.is-ancestor:not(:last-child){margin-bottom:.75rem}.tile.is-child{margin:0!important}.tile.is-parent{padding:.75rem}.tile.is-vertical{flex-direction:column}.tile.is-vertical>.tile.is-child:not(:last-child){margin-bottom:1.5rem!important}@media screen and (min-width:769px),print{.tile:not(.is-child){display:flex}.tile.is-1{flex:none;width:8.33333%}.tile.is-2{flex:none;width:16.66667%}.tile.is-3{flex:none;width:25%}.tile.is-4{flex:none;width:33.33333%}.tile.is-5{flex:none;width:41.66667%}.tile.is-6{flex:none;width:50%}.tile.is-7{flex:none;width:58.33333%}.tile.is-8{flex:none;width:66.66667%}.tile.is-9{flex:none;width:75%}.tile.is-10{flex:none;width:83.33333%}.tile.is-11{flex:none;width:91.66667%}.tile.is-12{flex:none;width:100%}}.hero{align-items:stretch;display:flex;flex-direction:column;justify-content:space-between}.hero .navbar{background:0 0}.hero .tabs ul{border-bottom:none}.hero.is-white{background-color:#fff;color:#0a0a0a}.hero.is-white a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-white strong{color:inherit}.hero.is-white .title{color:#0a0a0a}.hero.is-white .subtitle{color:rgba(10,10,10,.9)}.hero.is-white .subtitle a:not(.button),.hero.is-white .subtitle strong{color:#0a0a0a}@media screen and (max-width:1023px){.hero.is-white .navbar-menu{background-color:#fff}}.hero.is-white .navbar-item,.hero.is-white .navbar-link{color:rgba(10,10,10,.7)}.hero.is-white .navbar-link.is-active,.hero.is-white .navbar-link:hover,.hero.is-white a.navbar-item.is-active,.hero.is-white a.navbar-item:hover{background-color:#f2f2f2;color:#0a0a0a}.hero.is-white .tabs a{color:#0a0a0a;opacity:.9}.hero.is-white .tabs a:hover{opacity:1}.hero.is-white .tabs li.is-active a{opacity:1}.hero.is-white .tabs.is-boxed a,.hero.is-white .tabs.is-toggle a{color:#0a0a0a}.hero.is-white .tabs.is-boxed a:hover,.hero.is-white .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-white .tabs.is-boxed li.is-active a,.hero.is-white .tabs.is-boxed li.is-active a:hover,.hero.is-white .tabs.is-toggle li.is-active a,.hero.is-white .tabs.is-toggle li.is-active a:hover{background-color:#0a0a0a;border-color:#0a0a0a;color:#fff}.hero.is-white.is-bold{background-image:linear-gradient(141deg,#e6e6e6 0,#fff 71%,#fff 100%)}@media screen and (max-width:768px){.hero.is-white.is-bold .navbar-menu{background-image:linear-gradient(141deg,#e6e6e6 0,#fff 71%,#fff 100%)}}.hero.is-black{background-color:#0a0a0a;color:#fff}.hero.is-black a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-black strong{color:inherit}.hero.is-black .title{color:#fff}.hero.is-black .subtitle{color:rgba(255,255,255,.9)}.hero.is-black .subtitle a:not(.button),.hero.is-black .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-black .navbar-menu{background-color:#0a0a0a}}.hero.is-black .navbar-item,.hero.is-black .navbar-link{color:rgba(255,255,255,.7)}.hero.is-black .navbar-link.is-active,.hero.is-black .navbar-link:hover,.hero.is-black a.navbar-item.is-active,.hero.is-black a.navbar-item:hover{background-color:#000;color:#fff}.hero.is-black .tabs a{color:#fff;opacity:.9}.hero.is-black .tabs a:hover{opacity:1}.hero.is-black .tabs li.is-active a{opacity:1}.hero.is-black .tabs.is-boxed a,.hero.is-black .tabs.is-toggle a{color:#fff}.hero.is-black .tabs.is-boxed a:hover,.hero.is-black .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-black .tabs.is-boxed li.is-active a,.hero.is-black .tabs.is-boxed li.is-active a:hover,.hero.is-black .tabs.is-toggle li.is-active a,.hero.is-black .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#0a0a0a}.hero.is-black.is-bold{background-image:linear-gradient(141deg,#000 0,#0a0a0a 71%,#181616 100%)}@media screen and (max-width:768px){.hero.is-black.is-bold .navbar-menu{background-image:linear-gradient(141deg,#000 0,#0a0a0a 71%,#181616 100%)}}.hero.is-light{background-color:#f5f5f5;color:#363636}.hero.is-light a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-light strong{color:inherit}.hero.is-light .title{color:#363636}.hero.is-light .subtitle{color:rgba(54,54,54,.9)}.hero.is-light .subtitle a:not(.button),.hero.is-light .subtitle strong{color:#363636}@media screen and (max-width:1023px){.hero.is-light .navbar-menu{background-color:#f5f5f5}}.hero.is-light .navbar-item,.hero.is-light .navbar-link{color:rgba(54,54,54,.7)}.hero.is-light .navbar-link.is-active,.hero.is-light .navbar-link:hover,.hero.is-light a.navbar-item.is-active,.hero.is-light a.navbar-item:hover{background-color:#e8e8e8;color:#363636}.hero.is-light .tabs a{color:#363636;opacity:.9}.hero.is-light .tabs a:hover{opacity:1}.hero.is-light .tabs li.is-active a{opacity:1}.hero.is-light .tabs.is-boxed a,.hero.is-light .tabs.is-toggle a{color:#363636}.hero.is-light .tabs.is-boxed a:hover,.hero.is-light .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-light .tabs.is-boxed li.is-active a,.hero.is-light .tabs.is-boxed li.is-active a:hover,.hero.is-light .tabs.is-toggle li.is-active a,.hero.is-light .tabs.is-toggle li.is-active a:hover{background-color:#363636;border-color:#363636;color:#f5f5f5}.hero.is-light.is-bold{background-image:linear-gradient(141deg,#dfd8d9 0,#f5f5f5 71%,#fff 100%)}@media screen and (max-width:768px){.hero.is-light.is-bold .navbar-menu{background-image:linear-gradient(141deg,#dfd8d9 0,#f5f5f5 71%,#fff 100%)}}.hero.is-dark{background-color:#363636;color:#f5f5f5}.hero.is-dark a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-dark strong{color:inherit}.hero.is-dark .title{color:#f5f5f5}.hero.is-dark .subtitle{color:rgba(245,245,245,.9)}.hero.is-dark .subtitle a:not(.button),.hero.is-dark .subtitle strong{color:#f5f5f5}@media screen and (max-width:1023px){.hero.is-dark .navbar-menu{background-color:#363636}}.hero.is-dark .navbar-item,.hero.is-dark .navbar-link{color:rgba(245,245,245,.7)}.hero.is-dark .navbar-link.is-active,.hero.is-dark .navbar-link:hover,.hero.is-dark a.navbar-item.is-active,.hero.is-dark a.navbar-item:hover{background-color:#292929;color:#f5f5f5}.hero.is-dark .tabs a{color:#f5f5f5;opacity:.9}.hero.is-dark .tabs a:hover{opacity:1}.hero.is-dark .tabs li.is-active a{opacity:1}.hero.is-dark .tabs.is-boxed a,.hero.is-dark .tabs.is-toggle a{color:#f5f5f5}.hero.is-dark .tabs.is-boxed a:hover,.hero.is-dark .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-dark .tabs.is-boxed li.is-active a,.hero.is-dark .tabs.is-boxed li.is-active a:hover,.hero.is-dark .tabs.is-toggle li.is-active a,.hero.is-dark .tabs.is-toggle li.is-active a:hover{background-color:#f5f5f5;border-color:#f5f5f5;color:#363636}.hero.is-dark.is-bold{background-image:linear-gradient(141deg,#1f191a 0,#363636 71%,#46403f 100%)}@media screen and (max-width:768px){.hero.is-dark.is-bold .navbar-menu{background-image:linear-gradient(141deg,#1f191a 0,#363636 71%,#46403f 100%)}}.hero.is-primary{background-color:#00d1b2;color:#fff}.hero.is-primary a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-primary strong{color:inherit}.hero.is-primary .title{color:#fff}.hero.is-primary .subtitle{color:rgba(255,255,255,.9)}.hero.is-primary .subtitle a:not(.button),.hero.is-primary .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-primary .navbar-menu{background-color:#00d1b2}}.hero.is-primary .navbar-item,.hero.is-primary .navbar-link{color:rgba(255,255,255,.7)}.hero.is-primary .navbar-link.is-active,.hero.is-primary .navbar-link:hover,.hero.is-primary a.navbar-item.is-active,.hero.is-primary a.navbar-item:hover{background-color:#00b89c;color:#fff}.hero.is-primary .tabs a{color:#fff;opacity:.9}.hero.is-primary .tabs a:hover{opacity:1}.hero.is-primary .tabs li.is-active a{opacity:1}.hero.is-primary .tabs.is-boxed a,.hero.is-primary .tabs.is-toggle a{color:#fff}.hero.is-primary .tabs.is-boxed a:hover,.hero.is-primary .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-primary .tabs.is-boxed li.is-active a,.hero.is-primary .tabs.is-boxed li.is-active a:hover,.hero.is-primary .tabs.is-toggle li.is-active a,.hero.is-primary .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#00d1b2}.hero.is-primary.is-bold{background-image:linear-gradient(141deg,#009e6c 0,#00d1b2 71%,#00e7eb 100%)}@media screen and (max-width:768px){.hero.is-primary.is-bold .navbar-menu{background-image:linear-gradient(141deg,#009e6c 0,#00d1b2 71%,#00e7eb 100%)}}.hero.is-link{background-color:#3273dc;color:#fff}.hero.is-link a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-link strong{color:inherit}.hero.is-link .title{color:#fff}.hero.is-link .subtitle{color:rgba(255,255,255,.9)}.hero.is-link .subtitle a:not(.button),.hero.is-link .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-link .navbar-menu{background-color:#3273dc}}.hero.is-link .navbar-item,.hero.is-link .navbar-link{color:rgba(255,255,255,.7)}.hero.is-link .navbar-link.is-active,.hero.is-link .navbar-link:hover,.hero.is-link a.navbar-item.is-active,.hero.is-link a.navbar-item:hover{background-color:#2366d1;color:#fff}.hero.is-link .tabs a{color:#fff;opacity:.9}.hero.is-link .tabs a:hover{opacity:1}.hero.is-link .tabs li.is-active a{opacity:1}.hero.is-link .tabs.is-boxed a,.hero.is-link .tabs.is-toggle a{color:#fff}.hero.is-link .tabs.is-boxed a:hover,.hero.is-link .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-link .tabs.is-boxed li.is-active a,.hero.is-link .tabs.is-boxed li.is-active a:hover,.hero.is-link .tabs.is-toggle li.is-active a,.hero.is-link .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#3273dc}.hero.is-link.is-bold{background-image:linear-gradient(141deg,#1577c6 0,#3273dc 71%,#4366e5 100%)}@media screen and (max-width:768px){.hero.is-link.is-bold .navbar-menu{background-image:linear-gradient(141deg,#1577c6 0,#3273dc 71%,#4366e5 100%)}}.hero.is-info{background-color:#209cee;color:#fff}.hero.is-info a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-info strong{color:inherit}.hero.is-info .title{color:#fff}.hero.is-info .subtitle{color:rgba(255,255,255,.9)}.hero.is-info .subtitle a:not(.button),.hero.is-info .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-info .navbar-menu{background-color:#209cee}}.hero.is-info .navbar-item,.hero.is-info .navbar-link{color:rgba(255,255,255,.7)}.hero.is-info .navbar-link.is-active,.hero.is-info .navbar-link:hover,.hero.is-info a.navbar-item.is-active,.hero.is-info a.navbar-item:hover{background-color:#118fe4;color:#fff}.hero.is-info .tabs a{color:#fff;opacity:.9}.hero.is-info .tabs a:hover{opacity:1}.hero.is-info .tabs li.is-active a{opacity:1}.hero.is-info .tabs.is-boxed a,.hero.is-info .tabs.is-toggle a{color:#fff}.hero.is-info .tabs.is-boxed a:hover,.hero.is-info .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-info .tabs.is-boxed li.is-active a,.hero.is-info .tabs.is-boxed li.is-active a:hover,.hero.is-info .tabs.is-toggle li.is-active a,.hero.is-info .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#209cee}.hero.is-info.is-bold{background-image:linear-gradient(141deg,#04a6d7 0,#209cee 71%,#3287f5 100%)}@media screen and (max-width:768px){.hero.is-info.is-bold .navbar-menu{background-image:linear-gradient(141deg,#04a6d7 0,#209cee 71%,#3287f5 100%)}}.hero.is-success{background-color:#23d160;color:#fff}.hero.is-success a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-success strong{color:inherit}.hero.is-success .title{color:#fff}.hero.is-success .subtitle{color:rgba(255,255,255,.9)}.hero.is-success .subtitle a:not(.button),.hero.is-success .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-success .navbar-menu{background-color:#23d160}}.hero.is-success .navbar-item,.hero.is-success .navbar-link{color:rgba(255,255,255,.7)}.hero.is-success .navbar-link.is-active,.hero.is-success .navbar-link:hover,.hero.is-success a.navbar-item.is-active,.hero.is-success a.navbar-item:hover{background-color:#20bc56;color:#fff}.hero.is-success .tabs a{color:#fff;opacity:.9}.hero.is-success .tabs a:hover{opacity:1}.hero.is-success .tabs li.is-active a{opacity:1}.hero.is-success .tabs.is-boxed a,.hero.is-success .tabs.is-toggle a{color:#fff}.hero.is-success .tabs.is-boxed a:hover,.hero.is-success .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-success .tabs.is-boxed li.is-active a,.hero.is-success .tabs.is-boxed li.is-active a:hover,.hero.is-success .tabs.is-toggle li.is-active a,.hero.is-success .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#23d160}.hero.is-success.is-bold{background-image:linear-gradient(141deg,#12af2f 0,#23d160 71%,#2ce28a 100%)}@media screen and (max-width:768px){.hero.is-success.is-bold .navbar-menu{background-image:linear-gradient(141deg,#12af2f 0,#23d160 71%,#2ce28a 100%)}}.hero.is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.hero.is-warning a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-warning strong{color:inherit}.hero.is-warning .title{color:rgba(0,0,0,.7)}.hero.is-warning .subtitle{color:rgba(0,0,0,.9)}.hero.is-warning .subtitle a:not(.button),.hero.is-warning .subtitle strong{color:rgba(0,0,0,.7)}@media screen and (max-width:1023px){.hero.is-warning .navbar-menu{background-color:#ffdd57}}.hero.is-warning .navbar-item,.hero.is-warning .navbar-link{color:rgba(0,0,0,.7)}.hero.is-warning .navbar-link.is-active,.hero.is-warning .navbar-link:hover,.hero.is-warning a.navbar-item.is-active,.hero.is-warning a.navbar-item:hover{background-color:#ffd83d;color:rgba(0,0,0,.7)}.hero.is-warning .tabs a{color:rgba(0,0,0,.7);opacity:.9}.hero.is-warning .tabs a:hover{opacity:1}.hero.is-warning .tabs li.is-active a{opacity:1}.hero.is-warning .tabs.is-boxed a,.hero.is-warning .tabs.is-toggle a{color:rgba(0,0,0,.7)}.hero.is-warning .tabs.is-boxed a:hover,.hero.is-warning .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-warning .tabs.is-boxed li.is-active a,.hero.is-warning .tabs.is-boxed li.is-active a:hover,.hero.is-warning .tabs.is-toggle li.is-active a,.hero.is-warning .tabs.is-toggle li.is-active a:hover{background-color:rgba(0,0,0,.7);border-color:rgba(0,0,0,.7);color:#ffdd57}.hero.is-warning.is-bold{background-image:linear-gradient(141deg,#ffaf24 0,#ffdd57 71%,#fffa70 100%)}@media screen and (max-width:768px){.hero.is-warning.is-bold .navbar-menu{background-image:linear-gradient(141deg,#ffaf24 0,#ffdd57 71%,#fffa70 100%)}}.hero.is-danger{background-color:#ff3860;color:#fff}.hero.is-danger a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-danger strong{color:inherit}.hero.is-danger .title{color:#fff}.hero.is-danger .subtitle{color:rgba(255,255,255,.9)}.hero.is-danger .subtitle a:not(.button),.hero.is-danger .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-danger .navbar-menu{background-color:#ff3860}}.hero.is-danger .navbar-item,.hero.is-danger .navbar-link{color:rgba(255,255,255,.7)}.hero.is-danger .navbar-link.is-active,.hero.is-danger .navbar-link:hover,.hero.is-danger a.navbar-item.is-active,.hero.is-danger a.navbar-item:hover{background-color:#ff1f4b;color:#fff}.hero.is-danger .tabs a{color:#fff;opacity:.9}.hero.is-danger .tabs a:hover{opacity:1}.hero.is-danger .tabs li.is-active a{opacity:1}.hero.is-danger .tabs.is-boxed a,.hero.is-danger .tabs.is-toggle a{color:#fff}.hero.is-danger .tabs.is-boxed a:hover,.hero.is-danger .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-danger .tabs.is-boxed li.is-active a,.hero.is-danger .tabs.is-boxed li.is-active a:hover,.hero.is-danger .tabs.is-toggle li.is-active a,.hero.is-danger .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#ff3860}.hero.is-danger.is-bold{background-image:linear-gradient(141deg,#ff0561 0,#ff3860 71%,#ff5257 100%)}@media screen and (max-width:768px){.hero.is-danger.is-bold .navbar-menu{background-image:linear-gradient(141deg,#ff0561 0,#ff3860 71%,#ff5257 100%)}}.hero.is-small .hero-body{padding-bottom:1.5rem;padding-top:1.5rem}@media screen and (min-width:769px),print{.hero.is-medium .hero-body{padding-bottom:9rem;padding-top:9rem}}@media screen and (min-width:769px),print{.hero.is-large .hero-body{padding-bottom:18rem;padding-top:18rem}}.hero.is-fullheight .hero-body,.hero.is-fullheight-with-navbar .hero-body,.hero.is-halfheight .hero-body{align-items:center;display:flex}.hero.is-fullheight .hero-body>.container,.hero.is-fullheight-with-navbar .hero-body>.container,.hero.is-halfheight .hero-body>.container{flex-grow:1;flex-shrink:1}.hero.is-halfheight{min-height:50vh}.hero.is-fullheight{min-height:100vh}.hero-video{overflow:hidden}.hero-video video{left:50%;min-height:100%;min-width:100%;position:absolute;top:50%;-webkit-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0)}.hero-video.is-transparent{opacity:.3}@media screen and (max-width:768px){.hero-video{display:none}}.hero-buttons{margin-top:1.5rem}@media screen and (max-width:768px){.hero-buttons .button{display:flex}.hero-buttons .button:not(:last-child){margin-bottom:.75rem}}@media screen and (min-width:769px),print{.hero-buttons{display:flex;justify-content:center}.hero-buttons .button:not(:last-child){margin-right:1.5rem}}.hero-foot,.hero-head{flex-grow:0;flex-shrink:0}.hero-body{flex-grow:1;flex-shrink:0;padding:3rem 1.5rem}.section{padding:3rem 1.5rem}@media screen and (min-width:1024px){.section.is-medium{padding:9rem 1.5rem}.section.is-large{padding:18rem 1.5rem}}.footer{background-color:#fafafa;padding:3rem 1.5rem 6rem} \ No newline at end of file diff --git a/onionr/static-data/www/shared/main/styles-new.css b/onionr/static-data/www/shared/main/styles-new.css new file mode 100644 index 00000000..98f2f7d9 --- /dev/null +++ b/onionr/static-data/www/shared/main/styles-new.css @@ -0,0 +1,53 @@ +html { + background-color: #f5f5f5; +} + + +/* Config on homepage */ +#configContent{ + display:none; + } +#configContent.show{ + display:block; /* P.S: Use `!important` if missing `#content` (selector specificity). */ + } + + + +.hiddenOverlay { + visibility: hidden; + position: absolute; + left: 0px; + top: 0px; + width:100%; + height:100%; +} + + +/* https://stackoverflow.com/a/16778646/ +* Kept due to shutdown message on homepage +*/ +.overlay { + visibility: hidden; + position: absolute; + left: 0px; + top: 0px; + width:100%; + height:100%; + text-align:left; + z-index: 1000; + background-color: #2c2b3f; + color: white; + } + + .closeOverlay{ + background-color: white; + color: black; + border: 1px solid red; + border-radius: 5px; + float: right; + font-family: sans-serif; + } + .closeOverlay:after{ + content: '❌'; + padding: 5px; + } \ No newline at end of file diff --git a/onionr/static-data/www/shared/misc.js b/onionr/static-data/www/shared/misc.js index ca0cb58b..18ae2319 100755 --- a/onionr/static-data/www/shared/misc.js +++ b/onionr/static-data/www/shared/misc.js @@ -103,4 +103,19 @@ for (var i = 0; i < idStrings.length; i++){ else{ idStrings[i].innerText = myPub } -} \ No newline at end of file +} + +/* Copy public ID on homepage */ +myPubCopy.onclick = function() { + var copyText = document.getElementById("myPub"); + copyText.select(); + document.execCommand("copy"); +} + +/* For Config toggle on homepage */ +var toggle = document.getElementById("configToggle"); +var content = document.getElementById("configContent"); + +toggle.addEventListener("click", function() { + content.classList.toggle("show"); +}); \ No newline at end of file From cff38cb7c23b0bca82454025b8dbfeadd642507d Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 29 Jun 2019 13:18:31 -0500 Subject: [PATCH 137/173] sync and onionrservices fixes --- onionr/api.py | 18 ++++++++++++------ onionr/communicatorutils/downloadblocks.py | 8 +++++--- onionr/communicatorutils/lookupadders.py | 2 +- onionr/communicatorutils/servicecreator.py | 7 +++---- onionr/onionrservices/connectionserver.py | 4 ++-- onionr/onionrstorage/setdata.py | 2 +- 6 files changed, 24 insertions(+), 17 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index dcd912af..38350e4c 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -33,13 +33,19 @@ from onionrutils import bytesconverter, stringvalidators, epoch, mnemonickeys config.reload() class FDSafeHandler(WSGIHandler): '''Our WSGI handler. Doesn't do much non-default except timeouts''' + def __init__(self, sock, address, server, rfile=None): + self.socket = sock + self.address = address + self.server = server + self.rfile = rfile def handle(self): - timeout = Timeout(60, exception=Exception) - timeout.start() - try: - WSGIHandler.handle(self) - except Timeout as ex: - raise + while True: + timeout = Timeout(120, exception=Exception) + try: + FDSafeHandler.handle(self) + timeout.start() + except Timeout as ex: + raise def setBindIP(filePath=''): '''Set a random localhost IP to a specified file (intended for private or public API localhost IPs)''' diff --git a/onionr/communicatorutils/downloadblocks.py b/onionr/communicatorutils/downloadblocks.py index 1a159c93..76ad178f 100755 --- a/onionr/communicatorutils/downloadblocks.py +++ b/onionr/communicatorutils/downloadblocks.py @@ -80,19 +80,21 @@ def download_blocks_from_communicator(comm_inst): logger.info('Attempting to save block %s...' % blockHash[:12]) try: comm_inst._core.setData(content) + except onionrexceptions.DataExists: + logger.warn('Data is already set for %s ' % (blockHash,)) except onionrexceptions.DiskAllocationReached: - logger.error('Reached disk allocation allowance, cannot save block %s.' % blockHash) + logger.error('Reached disk allocation allowance, cannot save block %s.' % (blockHash,)) removeFromQueue = False else: comm_inst._core.addToBlockDB(blockHash, dataSaved=True) blockmetadata.process_block_metadata(comm_inst._core, blockHash) # caches block metadata values to block database else: - logger.warn('POW failed for block %s.' % blockHash) + logger.warn('POW failed for block %s.' % (blockHash,)) else: if comm_inst._core._blacklist.inBlacklist(realHash): logger.warn('Block %s is blacklisted.' % (realHash,)) else: - logger.warn('Metadata for block %s is invalid.' % blockHash) + logger.warn('Metadata for block %s is invalid.' % (blockHash,)) comm_inst._core._blacklist.addToDB(blockHash) else: # if block didn't meet expected hash diff --git a/onionr/communicatorutils/lookupadders.py b/onionr/communicatorutils/lookupadders.py index fc4527dc..c0394e3d 100755 --- a/onionr/communicatorutils/lookupadders.py +++ b/onionr/communicatorutils/lookupadders.py @@ -21,7 +21,7 @@ import logger from onionrutils import stringvalidators def lookup_new_peer_transports_with_communicator(comm_inst): - logger.info('Looking up new addresses...') + logger.info('Looking up new addresses...', terminal=True) tryAmount = 1 newPeers = [] for i in range(tryAmount): diff --git a/onionr/communicatorutils/servicecreator.py b/onionr/communicatorutils/servicecreator.py index 07d3adfb..769f663b 100755 --- a/onionr/communicatorutils/servicecreator.py +++ b/onionr/communicatorutils/servicecreator.py @@ -18,12 +18,11 @@ along with this program. If not, see . ''' import communicator, onionrblockapi -from onionrutils import stringvalidators +from onionrutils import stringvalidators, bytesconverter def service_creator(daemon): assert isinstance(daemon, communicator.OnionrCommunicatorDaemon) core = daemon._core - utils = core._utils # Find socket connection blocks # TODO cache blocks and only look at recently received ones @@ -31,9 +30,9 @@ def service_creator(daemon): for b in con_blocks: if not b in daemon.active_services: bl = onionrblockapi.Block(b, core=core, decrypt=True) - bs = utils.bytesToStr(bl.bcontent) + '.onion' + bs = bytesconverter.bytes_to_str(bl.bcontent) + '.onion' if stringvalidators.validate_pub_key(bl.signer) and stringvalidators.validate_transport(bs): - signer = utils.bytesToStr(bl.signer) + signer = bytesconverter.bytes_to_str(bl.signer) daemon.active_services.append(b) daemon.active_services.append(signer) daemon.services.create_server(signer, bs) diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py index b9f70e56..413aa6ca 100755 --- a/onionr/onionrservices/connectionserver.py +++ b/onionr/onionrservices/connectionserver.py @@ -24,7 +24,7 @@ import core, logger, httpapi import onionrexceptions from netcontroller import getOpenPort import api -from onionrutils import stringvalidators, basicrequests +from onionrutils import stringvalidators, basicrequests, bytesconverter from . import httpheaders class ConnectionServer: @@ -82,7 +82,7 @@ class ConnectionServer: raise ConnectionError('Could not reach %s bootstrap address %s' % (peer, address)) else: # If no connection error, create the service and save it to local global key store - self.core_inst.keyStore.put('dc-' + response.service_id, self.core_inst._utils.bytesToStr(peer)) + self.core_inst.keyStore.put('dc-' + response.service_id, bytesconverter.bytes_to_str(peer)) logger.info('hosting on %s with %s' % (response.service_id, peer)) http_server.serve_forever() http_server.stop() diff --git a/onionr/onionrstorage/setdata.py b/onionr/onionrstorage/setdata.py index 50721ccf..05cd24d1 100644 --- a/onionr/onionrstorage/setdata.py +++ b/onionr/onionrstorage/setdata.py @@ -31,6 +31,6 @@ def set_data(core_inst, data): else: raise onionrexceptions.DiskAllocationReached else: - raise Exception("Data is already set for " + dataHash) + raise onionrexceptions.DataExists("Data is already set for " + dataHash) return dataHash \ No newline at end of file From 02c71eab2f63ad7128ca9b544dbed4e37cb93156 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 29 Jun 2019 15:17:48 -0500 Subject: [PATCH 138/173] * fixed fd handler probably * fixed security level in ui redesign * moved client api insertblock to its own blueprint --- onionr/api.py | 69 ++++++----------------- onionr/httpapi/insertblock.py | 63 +++++++++++++++++++++ onionr/static-data/www/private/index.html | 2 +- 3 files changed, 82 insertions(+), 52 deletions(-) create mode 100644 onionr/httpapi/insertblock.py diff --git a/onionr/api.py b/onionr/api.py index 38350e4c..2be268c8 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -25,7 +25,7 @@ from flask import request, Response, abort, send_from_directory import core import onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config, onionrblockapi import httpapi -from httpapi import friendsapi, profilesapi, configapi, miscpublicapi +from httpapi import friendsapi, profilesapi, configapi, miscpublicapi, insertblock from onionrservices import httpheaders import onionr from onionrutils import bytesconverter, stringvalidators, epoch, mnemonickeys @@ -33,19 +33,23 @@ from onionrutils import bytesconverter, stringvalidators, epoch, mnemonickeys config.reload() class FDSafeHandler(WSGIHandler): '''Our WSGI handler. Doesn't do much non-default except timeouts''' - def __init__(self, sock, address, server, rfile=None): - self.socket = sock - self.address = address - self.server = server - self.rfile = rfile def handle(self): - while True: - timeout = Timeout(120, exception=Exception) - try: - FDSafeHandler.handle(self) - timeout.start() - except Timeout as ex: - raise + self.timeout = Timeout(120, Exception) + self.timeout.start() + try: + WSGIHandler.handle(self) + except Exception: + self.handle_error() + finally: + self.timeout.close() + + def handle_error(self): + if v is self.timeout: + self.result = [b"Timeout"] + self.start_response("200 OK", []) + self.process_result() + else: + WSGIHandler.handle_error(self) def setBindIP(filePath=''): '''Set a random localhost IP to a specified file (intended for private or public API localhost IPs)''' @@ -209,6 +213,7 @@ class API: app.register_blueprint(friendsapi.friends) app.register_blueprint(profilesapi.profile_BP) app.register_blueprint(configapi.config_BP) + app.register_blueprint(insertblock.ib) httpapi.load_plugin_blueprints(app) @app.before_request @@ -440,44 +445,6 @@ class API: def getHumanReadable(name): return Response(mnemonickeys.get_human_readable_ID(name)) - @app.route('/insertblock', methods=['POST']) - def insertBlock(): - encrypt = False - bData = request.get_json(force=True) - message = bData['message'] - - # Detect if message (block body) is not specified - if type(message) is None: - return 'failure', 406 - - subject = 'temp' - encryptType = '' - sign = True - meta = {} - to = '' - try: - if bData['encrypt']: - to = bData['to'] - encrypt = True - encryptType = 'asym' - except KeyError: - pass - try: - if not bData['sign']: - sign = False - except KeyError: - pass - try: - bType = bData['type'] - except KeyError: - bType = 'bin' - try: - meta = json.loads(bData['meta']) - except KeyError: - pass - threading.Thread(target=self._core.insertBlock, args=(message,), kwargs={'header': bType, 'encryptType': encryptType, 'sign':sign, 'asymPeer': to, 'meta': meta}).start() - return Response('success') - self.httpServer = WSGIServer((self.host, bindPort), app, log=None, handler_class=FDSafeHandler) self.httpServer.serve_forever() diff --git a/onionr/httpapi/insertblock.py b/onionr/httpapi/insertblock.py new file mode 100644 index 00000000..d208bf70 --- /dev/null +++ b/onionr/httpapi/insertblock.py @@ -0,0 +1,63 @@ +''' + Onionr - Private P2P Communication + + Create blocks with the client api server +''' +''' + 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 json, threading +from flask import Blueprint, Response, request +import core + +ib = Blueprint('insertblock', __name__) + +@ib.route('/insertblock', methods=['POST']) +def client_api_insert_block(): + c = core.Core() + encrypt = False + bData = request.get_json(force=True) + message = bData['message'] + + # Detect if message (block body) is not specified + if type(message) is None: + return 'failure', 406 + + subject = 'temp' + encryptType = '' + sign = True + meta = {} + to = '' + try: + if bData['encrypt']: + to = bData['to'] + encrypt = True + encryptType = 'asym' + except KeyError: + pass + try: + if not bData['sign']: + sign = False + except KeyError: + pass + try: + bType = bData['type'] + except KeyError: + bType = 'bin' + try: + meta = json.loads(bData['meta']) + except KeyError: + pass + threading.Thread(target=c.insertBlock, args=(message,), kwargs={'header': bType, 'encryptType': encryptType, 'sign':sign, 'asymPeer': to, 'meta': meta}).start() + return Response('success') \ No newline at end of file diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index 0beefa79..ac46b195 100755 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -130,7 +130,7 @@
            - Security level here + 🔒 Security level:
            🕰️ Uptime: From 54dabefe45164fdd28954abaf9ca80c52491faeb Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 1 Jul 2019 15:38:55 -0500 Subject: [PATCH 139/173] incomplete work on refactoring client api endpoints --- onionr/api.py | 187 +----------------- onionr/httpapi/apiutils/__init__.py | 44 +++++ onionr/httpapi/apiutils/shutdown.py | 38 ++++ onionr/httpapi/miscclientapi/__init__.py | 1 + onionr/httpapi/miscclientapi/getblocks.py | 66 +++++++ onionr/httpapi/miscclientapi/staticfiles.py | 75 +++++++ onionr/httpapi/miscpublicapi/getblocks.py | 1 + onionr/httpapi/onionrsitesapi/__init__.py | 44 +++++ onionr/httpapi/security/__init__.py | 1 + onionr/httpapi/security/client.py | 36 ++++ onionr/httpapi/security/public.py | 0 onionr/static-data/www/board/index.html | 2 +- onionr/static-data/www/clandestine/index.html | 2 +- onionr/static-data/www/friends/index.html | 2 +- onionr/static-data/www/mail/index.html | 2 +- onionr/static-data/www/private/index.html | 2 +- onionr/static-data/www/private/main.css | 2 +- .../www/shared/main/styles-new.css | 9 +- 18 files changed, 328 insertions(+), 186 deletions(-) create mode 100644 onionr/httpapi/apiutils/__init__.py create mode 100644 onionr/httpapi/apiutils/shutdown.py create mode 100644 onionr/httpapi/miscclientapi/__init__.py create mode 100644 onionr/httpapi/miscclientapi/getblocks.py create mode 100644 onionr/httpapi/miscclientapi/staticfiles.py create mode 100644 onionr/httpapi/onionrsitesapi/__init__.py create mode 100644 onionr/httpapi/security/__init__.py create mode 100644 onionr/httpapi/security/client.py create mode 100644 onionr/httpapi/security/public.py diff --git a/onionr/api.py b/onionr/api.py index 2be268c8..adaa3cd7 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -25,10 +25,11 @@ from flask import request, Response, abort, send_from_directory import core import onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config, onionrblockapi import httpapi -from httpapi import friendsapi, profilesapi, configapi, miscpublicapi, insertblock +from httpapi import friendsapi, profilesapi, configapi, miscpublicapi, miscclientapi, insertblock, onionrsitesapi from onionrservices import httpheaders import onionr from onionrutils import bytesconverter, stringvalidators, epoch, mnemonickeys +from httpapi import apiutils config.reload() class FDSafeHandler(WSGIHandler): @@ -194,11 +195,6 @@ class API: bindPort = int(config.get('client.client.port', 59496)) self.bindPort = bindPort - # Be extremely mindful of this. These are endpoints available without a password - self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'homedata', 'board', 'profiles', 'profilesindex', - 'boardContent', 'sharedContent', 'mail', 'mailindex', 'friends', 'friendsindex', - 'clandestine', 'clandestineIndex') - self.clientToken = config.get('client.webpassword') self.timeBypassToken = base64.b16encode(os.urandom(32)).decode() @@ -214,67 +210,12 @@ class API: app.register_blueprint(profilesapi.profile_BP) app.register_blueprint(configapi.config_BP) app.register_blueprint(insertblock.ib) + app.register_blueprint(miscclientapi.getblocks.client_get_blocks) + app.register_blueprint(miscclientapi.staticfiles.static_files_bp) + app.register_blueprint(onionrsitesapi.site_api) + app.register_blueprint(apiutils.shutdown.shutdown_bp) httpapi.load_plugin_blueprints(app) - - @app.before_request - def validateRequest(): - '''Validate request has set password and is the correct hostname''' - # For the purpose of preventing DNS rebinding attacks - if request.host != '%s:%s' % (self.host, self.bindPort): - abort(403) - if request.endpoint in self.whitelistEndpoints: - return - try: - if not hmac.compare_digest(request.headers['token'], self.clientToken): - if not hmac.compare_digest(request.form['token'], self.clientToken): - abort(403) - except KeyError: - if not hmac.compare_digest(request.form['token'], self.clientToken): - abort(403) - - @app.after_request - def afterReq(resp): - # Security headers - resp = httpheaders.set_default_onionr_http_headers(resp) - if request.endpoint == 'site': - resp.headers['Content-Security-Policy'] = "default-src 'none'; style-src data: 'unsafe-inline'; img-src data:" - else: - resp.headers['Content-Security-Policy'] = "default-src 'none'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'self'" - return resp - - @app.route('/board/', endpoint='board') - def loadBoard(): - return send_from_directory('static-data/www/board/', "index.html") - - @app.route('/mail/', endpoint='mail') - def loadMail(path): - return send_from_directory('static-data/www/mail/', path) - @app.route('/mail/', endpoint='mailindex') - def loadMailIndex(): - return send_from_directory('static-data/www/mail/', 'index.html') - - @app.route('/clandestine/', endpoint='clandestine') - def loadClandestine(path): - return send_from_directory('static-data/www/clandestine/', path) - @app.route('/clandestine/', endpoint='clandestineIndex') - def loadClandestineIndex(): - return send_from_directory('static-data/www/clandestine/', 'index.html') - - @app.route('/friends/', endpoint='friends') - def loadContacts(path): - return send_from_directory('static-data/www/friends/', path) - - @app.route('/friends/', endpoint='friendsindex') - def loadContacts(): - return send_from_directory('static-data/www/friends/', 'index.html') - - @app.route('/profiles/', endpoint='profiles') - def loadContacts(path): - return send_from_directory('static-data/www/profiles/', path) - - @app.route('/profiles/', endpoint='profilesindex') - def loadContacts(): - return send_from_directory('static-data/www/profiles/', 'index.html') + self.get_block_data = apiutils.GetBlockData(self) @app.route('/serviceactive/') def serviceActive(pubkey): @@ -285,22 +226,6 @@ class API: pass return Response('false') - @app.route('/board/', endpoint='boardContent') - def boardContent(path): - return send_from_directory('static-data/www/board/', path) - @app.route('/shared/', endpoint='sharedContent') - def sharedContent(path): - return send_from_directory('static-data/www/shared/', path) - - @app.route('/', endpoint='onionrhome') - def hello(): - # ui home - return send_from_directory('static-data/www/private/', 'index.html') - - @app.route('/private/', endpoint='homedata') - def homedata(path): - return send_from_directory('static-data/www/private/', path) - @app.route('/www/', endpoint='www') def wwwPublic(path): if not config.get("www.private.run", True): @@ -336,67 +261,11 @@ class API: def ping(): # Used to check if client api is working return Response("pong!") - - @app.route('/getblocksbytype/') - def getBlocksByType(name): - blocks = self._core.getBlocksByType(name) - return Response(','.join(blocks)) - - @app.route('/getblockbody/') - def getBlockBodyData(name): - resp = '' - if stringvalidators.validate_hash(name): - try: - resp = onionrblockapi.Block(name, decrypt=True).bcontent - except TypeError: - pass - else: - abort(404) - return Response(resp) - - @app.route('/getblockdata/') - def getData(name): - resp = "" - if stringvalidators.validate_hash(name): - if name in self._core.getBlockList(): - try: - resp = self.getBlockData(name, decrypt=True) - except ValueError: - pass - else: - abort(404) - else: - abort(404) - return Response(resp) - - @app.route('/getblockheader/') - def getBlockHeader(name): - resp = self.getBlockData(name, decrypt=True, headerOnly=True) - return Response(resp) @app.route('/lastconnect') def lastConnect(): return Response(str(self.publicAPI.lastRequest)) - @app.route('/site/', endpoint='site') - def site(name): - bHash = name - resp = 'Not Found' - if stringvalidators.validate_hash(bHash): - try: - resp = onionrblockapi.Block(bHash).bcontent - except onionrexceptions.NoDataAvailable: - abort(404) - except TypeError: - pass - try: - resp = base64.b64decode(resp) - except: - pass - if resp == 'Not Found' or not resp: - abort(404) - return Response(resp) - @app.route('/waitforshare/', methods=['post']) def waitforshare(name): '''Used to prevent the **public** api from sharing blocks we just created''' @@ -410,18 +279,7 @@ class API: @app.route('/shutdown') def shutdown(): - try: - self.publicAPI.httpServer.stop() - self.httpServer.stop() - except AttributeError: - pass - return Response("bye") - - @app.route('/shutdownclean') - def shutdownClean(): - # good for calling from other clients - self._core.daemonQueueAdd('shutdown') - return Response("bye") + return apiutils.shutdown.shutdown(self) @app.route('/getstats') def getStats(): @@ -474,31 +332,6 @@ class API: except (AttributeError, NameError): # Don't error on race condition with startup pass - - def getBlockData(self, bHash, decrypt=False, raw=False, headerOnly=False): - assert stringvalidators.validate_hash(bHash) - bl = onionrblockapi.Block(bHash, core=self._core) - if decrypt: - bl.decrypt() - if bl.isEncrypted and not bl.decrypted: - raise ValueError - if not raw: - if not headerOnly: - retData = {'meta':bl.bheader, 'metadata': bl.bmetadata, 'content': bl.bcontent} - for x in list(retData.keys()): - try: - retData[x] = retData[x].decode() - except AttributeError: - pass - else: - validSig = False - signer = bytesconverter.bytes_to_str(bl.signer) - if bl.isSigned() and stringvalidators.validate_pub_key(signer) and bl.isSigner(signer): - validSig = True - bl.bheader['validSig'] = validSig - bl.bheader['meta'] = '' - retData = {'meta': bl.bheader, 'metadata': bl.bmetadata} - return json.dumps(retData) - else: - return bl.raw + def getBlockData(self, bHash, decrypt=False, raw=False, headerOnly=False): + return self.get_block_data.get_block_data(self, bHash, decrypt, raw, headerOnly) diff --git a/onionr/httpapi/apiutils/__init__.py b/onionr/httpapi/apiutils/__init__.py new file mode 100644 index 00000000..29b0c778 --- /dev/null +++ b/onionr/httpapi/apiutils/__init__.py @@ -0,0 +1,44 @@ +import json +import core, onionrblockapi +from onionrutils import bytesconverter, stringvalidators +from . import shutdown + +class GetBlockData: + def __init__(self, client_api_inst=None): + if client_api_inst is None: + self.client_api_inst = None + self.c = core.Core() + elif isinstance(client_api_inst, core.Core): + self.client_api_inst = None + self.c = client_api_inst + else: + self.client_api_Inst = client_api_inst + self.c = core.Core() + + def get_block_data(self, bHash, decrypt=False, raw=False, headerOnly=False): + assert stringvalidators.validate_hash(bHash) + bl = onionrblockapi.Block(bHash, core=self.c) + if decrypt: + bl.decrypt() + if bl.isEncrypted and not bl.decrypted: + raise ValueError + + if not raw: + if not headerOnly: + retData = {'meta':bl.bheader, 'metadata': bl.bmetadata, 'content': bl.bcontent} + for x in list(retData.keys()): + try: + retData[x] = retData[x].decode() + except AttributeError: + pass + else: + validSig = False + signer = bytesconverter.bytes_to_str(bl.signer) + if bl.isSigned() and stringvalidators.validate_pub_key(signer) and bl.isSigner(signer): + validSig = True + bl.bheader['validSig'] = validSig + bl.bheader['meta'] = '' + retData = {'meta': bl.bheader, 'metadata': bl.bmetadata} + return json.dumps(retData) + else: + return bl.raw \ No newline at end of file diff --git a/onionr/httpapi/apiutils/shutdown.py b/onionr/httpapi/apiutils/shutdown.py new file mode 100644 index 00000000..90cd33cb --- /dev/null +++ b/onionr/httpapi/apiutils/shutdown.py @@ -0,0 +1,38 @@ +''' + Onionr - Private P2P Communication + + Shutdown the node either hard or cleanly +''' +''' + 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 . +''' +from flask import Blueprint, Response +import core, onionrblockapi, onionrexceptions +from onionrutils import stringvalidators + +shutdown_bp = Blueprint('shutdown', __name__) + +def shutdown(client_api_inst): + try: + client_api_inst.publicAPI.httpServer.stop() + client_api_inst.httpServer.stop() + except AttributeError: + pass + return Response("bye") + +@shutdown_bp.route('/shutdownclean') +def shutdown_clean(): + # good for calling from other clients + core.Core().daemonQueueAdd('shutdown') + return Response("bye") \ No newline at end of file diff --git a/onionr/httpapi/miscclientapi/__init__.py b/onionr/httpapi/miscclientapi/__init__.py new file mode 100644 index 00000000..161fe6d5 --- /dev/null +++ b/onionr/httpapi/miscclientapi/__init__.py @@ -0,0 +1 @@ +from . import getblocks, staticfiles \ No newline at end of file diff --git a/onionr/httpapi/miscclientapi/getblocks.py b/onionr/httpapi/miscclientapi/getblocks.py new file mode 100644 index 00000000..f9a6aa90 --- /dev/null +++ b/onionr/httpapi/miscclientapi/getblocks.py @@ -0,0 +1,66 @@ +''' + Onionr - Private P2P Communication + + Create blocks with the client api server +''' +''' + 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 . +''' +from flask import Blueprint, Response, abort +import core, onionrblockapi +from httpapi import apiutils +from onionrutils import stringvalidators + +c = core.Core() + +client_get_block = apiutils.GetBlockData(c) + +client_get_blocks = Blueprint('miscclient', __name__) + +@client_get_blocks.route('/getblocksbytype/') +def getBlocksByType(name): + blocks = c.getBlocksByType(name) + return Response(','.join(blocks)) + +@client_get_blocks.route('/getblockbody/') +def getBlockBodyData(name): + resp = '' + if stringvalidators.validate_hash(name): + try: + resp = onionrblockapi.Block(name, decrypt=True, core=c).bcontent + except TypeError: + pass + else: + abort(404) + return Response(resp) + +@client_get_blocks.route('/getblockdata/') +def getData(name): + resp = "" + if stringvalidators.validate_hash(name): + if name in c.getBlockList(): + try: + resp = client_get_block.get_block_data(name, decrypt=True) + except ValueError: + pass + else: + abort(404) + else: + abort(404) + return Response(resp) + +@client_get_blocks.route('/getblockheader/') +def getBlockHeader(name): + resp = client_get_block.get_block_data(name, decrypt=True, headerOnly=True) + return Response(resp) \ No newline at end of file diff --git a/onionr/httpapi/miscclientapi/staticfiles.py b/onionr/httpapi/miscclientapi/staticfiles.py new file mode 100644 index 00000000..f943d6f1 --- /dev/null +++ b/onionr/httpapi/miscclientapi/staticfiles.py @@ -0,0 +1,75 @@ +''' + Onionr - Private P2P Communication + + Register static file routes +''' +''' + 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 . +''' +from flask import Blueprint, send_from_directory + +static_files_bp = Blueprint('staticfiles', __name__) + +@static_files_bp.route('/board/', endpoint='board') +def loadBoard(): + return send_from_directory('static-data/www/board/', "index.html") + +@static_files_bp.route('/mail/', endpoint='mail') +def loadMail(path): + return send_from_directory('static-data/www/mail/', path) + +@static_files_bp.route('/mail/', endpoint='mailindex') +def loadMailIndex(): + return send_from_directory('static-data/www/mail/', 'index.html') + +@static_files_bp.route('/clandestine/', endpoint='clandestine') +def loadClandestine(path): + return send_from_directory('static-data/www/clandestine/', path) + +@static_files_bp.route('/clandestine/', endpoint='clandestineIndex') +def loadClandestineIndex(): + return send_from_directory('static-data/www/clandestine/', 'index.html') + +@static_files_bp.route('/friends/', endpoint='friends') +def loadContacts(path): + return send_from_directory('static-data/www/friends/', path) + +@static_files_bp.route('/friends/', endpoint='friendsindex') +def loadContacts(): + return send_from_directory('static-data/www/friends/', 'index.html') + +@static_files_bp.route('/profiles/', endpoint='profiles') +def loadContacts(path): + return send_from_directory('static-data/www/profiles/', path) + +@static_files_bp.route('/profiles/', endpoint='profilesindex') +def loadContacts(): + return send_from_directory('static-data/www/profiles/', 'index.html') + +@static_files_bp.route('/board/', endpoint='boardContent') +def boardContent(path): + return send_from_directory('static-data/www/board/', path) + +@static_files_bp.route('/shared/', endpoint='sharedContent') +def sharedContent(path): + return send_from_directory('static-data/www/shared/', path) + +@static_files_bp.route('/', endpoint='onionrhome') +def hello(): + # ui home + return send_from_directory('static-data/www/private/', 'index.html') + +@static_files_bp.route('/private/', endpoint='homedata') +def homedata(path): + return send_from_directory('static-data/www/private/', path) \ No newline at end of file diff --git a/onionr/httpapi/miscpublicapi/getblocks.py b/onionr/httpapi/miscpublicapi/getblocks.py index 08d9f678..8fe5cbf4 100755 --- a/onionr/httpapi/miscpublicapi/getblocks.py +++ b/onionr/httpapi/miscpublicapi/getblocks.py @@ -20,6 +20,7 @@ from flask import Response, abort import config from onionrutils import bytesconverter, stringvalidators + def get_public_block_list(clientAPI, publicAPI, request): # Provide a list of our blocks, with a date offset dateAdjust = request.args.get('date') diff --git a/onionr/httpapi/onionrsitesapi/__init__.py b/onionr/httpapi/onionrsitesapi/__init__.py new file mode 100644 index 00000000..82805192 --- /dev/null +++ b/onionr/httpapi/onionrsitesapi/__init__.py @@ -0,0 +1,44 @@ +''' + Onionr - Private P2P Communication + + view and interact with onionr sites +''' +''' + 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 base64 +from flask import Blueprint, Response, request, abort +import core, onionrblockapi, onionrexceptions +from onionrutils import stringvalidators + +site_api = Blueprint('siteapi', __name__) + +@site_api.route('/site/', endpoint='site') +def site(name): + bHash = name + resp = 'Not Found' + if stringvalidators.validate_hash(bHash): + try: + resp = onionrblockapi.Block(bHash).bcontent + except onionrexceptions.NoDataAvailable: + abort(404) + except TypeError: + pass + try: + resp = base64.b64decode(resp) + except: + pass + if resp == 'Not Found' or not resp: + abort(404) + return Response(resp) \ No newline at end of file diff --git a/onionr/httpapi/security/__init__.py b/onionr/httpapi/security/__init__.py new file mode 100644 index 00000000..653f6af4 --- /dev/null +++ b/onionr/httpapi/security/__init__.py @@ -0,0 +1 @@ +from . import client, public \ No newline at end of file diff --git a/onionr/httpapi/security/client.py b/onionr/httpapi/security/client.py new file mode 100644 index 00000000..65798ad7 --- /dev/null +++ b/onionr/httpapi/security/client.py @@ -0,0 +1,36 @@ +import hmac +from flask import Blueprint, request, abort + +# Be extremely mindful of this. These are endpoints available without a password +whitelist_endpoints = ('siteapi.site', 'www', 'staticfiles.onionrhome', 'staticfiles.homedata', +'staticfiles.board', 'staticfiles.profiles', +'staticfiles.profilesindex', +'staticfiles.boardContent', 'staticfiles.sharedContent', +'staticfiles.mail', 'staticfiles.mailindex', 'staticfiles.friends', 'staticfiles.friendsindex', +'staticfiles.clandestine', 'staticfiles.clandestineIndex') + +@app.before_request +def validateRequest(): + '''Validate request has set password and is the correct hostname''' + # For the purpose of preventing DNS rebinding attacks + if request.host != '%s:%s' % (self.host, self.bindPort): + abort(403) + if request.endpoint in whitelist_endpoints: + return + try: + if not hmac.compare_digest(request.headers['token'], self.clientToken): + if not hmac.compare_digest(request.form['token'], self.clientToken): + abort(403) + except KeyError: + if not hmac.compare_digest(request.form['token'], self.clientToken): + abort(403) + +@app.after_request +def afterReq(resp): + # Security headers + resp = httpheaders.set_default_onionr_http_headers(resp) + if request.endpoint == 'site': + resp.headers['Content-Security-Policy'] = "default-src 'none'; style-src data: 'unsafe-inline'; img-src data:" + else: + resp.headers['Content-Security-Policy'] = "default-src 'none'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'self'" + return resp \ No newline at end of file diff --git a/onionr/httpapi/security/public.py b/onionr/httpapi/security/public.py new file mode 100644 index 00000000..e69de29b diff --git a/onionr/static-data/www/board/index.html b/onionr/static-data/www/board/index.html index 8101b55c..e4fc7db2 100755 --- a/onionr/static-data/www/board/index.html +++ b/onionr/static-data/www/board/index.html @@ -17,7 +17,7 @@ @@ -135,14 +134,6 @@
            -
            -
            - -
            Name:
            - - -
            -
            - + diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index 198f6f03..b02b27d1 100755 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -77,7 +77,6 @@
            -

            @@ -153,6 +152,7 @@
            + diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index add1c223..6b09f976 100755 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -181,6 +181,7 @@

            + diff --git a/onionr/static-data/www/profiles/index.html b/onionr/static-data/www/profiles/index.html index 7134b943..c03c3eb4 100755 --- a/onionr/static-data/www/profiles/index.html +++ b/onionr/static-data/www/profiles/index.html @@ -13,8 +13,8 @@

            + - \ No newline at end of file diff --git a/onionr/static-data/www/shared/navbar.js b/onionr/static-data/www/shared/navbar.js new file mode 100644 index 00000000..0b281b38 --- /dev/null +++ b/onionr/static-data/www/shared/navbar.js @@ -0,0 +1,49 @@ +document.addEventListener('DOMContentLoaded', () => { + // This function taken from the official bulma docs + /* + The MIT License (MIT) + + Copyright (c) 2019 Jeremy Thomas + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + + // Get all "navbar-burger" elements + const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0); + + // Check if there are any navbar burgers + if ($navbarBurgers.length > 0) { + + // Add a click event on each of them + $navbarBurgers.forEach( el => { + el.addEventListener('click', () => { + + // Get the target from the "data-target" attribute + const target = el.dataset.target; + const $target = document.getElementById(target); + + // Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu" + el.classList.toggle('is-active'); + $target.classList.toggle('is-active'); + + }); + }); + } + + }); \ No newline at end of file diff --git a/onionr/static-data/www/shared/onionrblocks.js b/onionr/static-data/www/shared/onionrblocks.js deleted file mode 100755 index 6be0210d..00000000 --- a/onionr/static-data/www/shared/onionrblocks.js +++ /dev/null @@ -1,7 +0,0 @@ -class Block { - constructor(hash, raw) { - this.hash = hash; - this.raw = raw; - } -} - \ No newline at end of file