From 302f6964757ea03fc54776b3298b4c64d2a60304 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 14 Dec 2018 11:23:56 -0600 Subject: [PATCH 01/94] removed randomness beacon stuff --- .gitmodules | 0 onionr/communicator2.py | 1 - onionr/core.py | 1 - onionr/onionrutils.py | 23 ----------------------- 4 files changed, 25 deletions(-) create mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..e69de29b diff --git a/onionr/communicator2.py b/onionr/communicator2.py index 0bce1511..bddb21da 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -82,7 +82,6 @@ class OnionrCommunicatorDaemon: # daemon tools are misc daemon functions, e.g. announce to online peers # intended only for use by OnionrCommunicatorDaemon - #self.daemonTools = onionrdaemontools.DaemonTools(self) self.daemonTools = onionrdaemontools.DaemonTools(self) self._chat = onionrchat.OnionrChat(self) diff --git a/onionr/core.py b/onionr/core.py index 0219a875..017b4313 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -678,7 +678,6 @@ class Core: ''' retData = False - # check nonce dataNonce = self._utils.bytesToStr(self._crypto.sha3Hash(data)) try: diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index dbcf01e6..e06c1e27 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -23,7 +23,6 @@ import nacl.signing, nacl.encoding from onionrblockapi import Block import onionrexceptions from onionr import API_VERSION -from defusedxml import minidom import onionrevents import pgpwords, onionrusers, storagecounter if sys.version_info < (3, 6): @@ -653,28 +652,6 @@ class OnionrUtils: retData = False return retData - def getNistBeaconSalt(self, torPort=0, rounding=3600): - ''' - Get the token for the current hour from the NIST randomness beacon - ''' - if torPort == 0: - try: - sys.argv[2] - except IndexError: - raise onionrexceptions.MissingPort('Missing Tor socks port') - retData = '' - curTime = self.getRoundedEpoch(rounding) - self.nistSaltTimestamp = curTime - data = self.doGetRequest('https://beacon.nist.gov/rest/record/' + str(curTime), port = torPort) - dataXML = minidom.parseString(data, forbid_dtd = True, forbid_entities = True, forbid_external = True) - try: - retData = dataXML.getElementsByTagName('outputValue')[0].childNodes[0].data - except ValueError: - logger.warn('Failed to get the NIST beacon value.') - else: - self.powSalt = retData - return retData - def strToBytes(self, data): try: data = data.encode() From 8e35b8b7ad452ce3afb7ec2729d9108e09ee65f6 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 14 Dec 2018 21:23:02 -0600 Subject: [PATCH 02/94] do not save blocks that are set for the future --- onionr/onionrutils.py | 3 +++ onionr/static-data/default_config.json | 1 + 2 files changed, 4 insertions(+) diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index e06c1e27..a6f57013 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -391,6 +391,9 @@ class OnionrUtils: if not self.isIntegerString(metadata[i]): logger.warn('Block metadata time stamp is not integer string') break + if metadata[i] > self.getEpoch(): + logger.warn('Block metadata time stamp is set for the future, which is not allowed.') + break elif i == 'expire': try: assert int(metadata[i]) > self.getEpoch() diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index 5003d73b..5532797d 100644 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -6,6 +6,7 @@ "minimum_send_pow": 5, "socket_servers": false, "security_level": 0, + "max_block_age": 2678400, "public_key": "" }, From a4370c26b0420fe59b1183e6f3fd1c747d11c6d2 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 14 Dec 2018 21:27:05 -0600 Subject: [PATCH 03/94] do not save blocks that are too old --- onionr/onionrutils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index a6f57013..f94116f0 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -371,6 +371,7 @@ class OnionrUtils: pass # Validate metadata dict for invalid keys to sizes that are too large + maxAge = config.get("general.max_block_age", 2678400) if type(metadata) is dict: for i in metadata: try: @@ -394,6 +395,8 @@ class OnionrUtils: if metadata[i] > self.getEpoch(): logger.warn('Block metadata time stamp is set for the future, which is not allowed.') break + if (self.getEpoch() - metadata[i]) > maxAge: + logger.warn('Block is older than allowed: %s' % (maxAge,)) elif i == 'expire': try: assert int(metadata[i]) > self.getEpoch() From 98bc3b3271ccb527f02d42be58ffa7a120731b67 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 15 Dec 2018 23:35:06 -0600 Subject: [PATCH 04/94] actually handle future-set blocks properly --- onionr/onionrcrypto.py | 9 +++------ onionr/onionrutils.py | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py index 35a8050d..04c821cd 100644 --- a/onionr/onionrcrypto.py +++ b/onionr/onionrcrypto.py @@ -33,9 +33,7 @@ class OnionrCrypto: self._keyFile = self._core.dataDir + 'keys.txt' self.pubKey = None self.privKey = None - self.secrets = secrets - self.deterministicRequirement = 25 # Min deterministic password/phrase length self.HASH_ID_ROUNDS = 2000 self.keyManager = keymanager.KeyManager(self) @@ -99,7 +97,6 @@ class OnionrCrypto: def pubKeyEncrypt(self, data, pubkey, anonymous=True, encodedData=False): '''Encrypt to a public key (Curve25519, taken from base32 Ed25519 pubkey)''' retVal = '' - try: pubkey = pubkey.encode() except AttributeError: @@ -198,7 +195,7 @@ class OnionrCrypto: private_key = nacl.signing.SigningKey.generate() public_key = private_key.verify_key.encode(encoder=nacl.encoding.Base32Encoder()) return (public_key.decode(), private_key.encode(encoder=nacl.encoding.Base32Encoder()).decode()) - + def generateDeterministic(self, passphrase, bypassCheck=False): '''Generate a Ed25519 public key pair from a password''' passStrength = self.deterministicRequirement @@ -212,7 +209,7 @@ class OnionrCrypto: salt = b"U81Q7llrQcdTP0Ux" # Does not need to be unique or secret, but must be 16 bytes ops = nacl.pwhash.argon2id.OPSLIMIT_SENSITIVE mem = nacl.pwhash.argon2id.MEMLIMIT_SENSITIVE - + key = kdf(nacl.secret.SecretBox.KEY_SIZE, passphrase, salt, opslimit=ops, memlimit=mem) key = nacl.public.PrivateKey(key, nacl.encoding.RawEncoder()) publicKey = key.public_key @@ -285,6 +282,6 @@ class OnionrCrypto: logger.debug("Invalid token, bad proof") return retData - + def safeCompare(self, one, two): return hmac.compare_digest(one, two) diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index f94116f0..948563e3 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -392,7 +392,7 @@ class OnionrUtils: if not self.isIntegerString(metadata[i]): logger.warn('Block metadata time stamp is not integer string') break - if metadata[i] > self.getEpoch(): + if (metadata[i] - self.getEpoch()) > 30: logger.warn('Block metadata time stamp is set for the future, which is not allowed.') break if (self.getEpoch() - metadata[i]) > maxAge: From dc51ab8980ffc91503472f25a997a91e3a79c404 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 16 Dec 2018 12:21:44 -0600 Subject: [PATCH 05/94] started work on db block data, improved block time stamping messages a bit --- onionr/core.py | 1 + onionr/dbcreator.py | 15 ++++++++++++++- onionr/onionrutils.py | 12 +++++++----- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/onionr/core.py b/onionr/core.py index 017b4313..84863b40 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -48,6 +48,7 @@ class Core: self.queueDB = self.dataDir + 'queue.db' self.peerDB = self.dataDir + 'peers.db' self.blockDB = self.dataDir + 'blocks.db' + self.blockDataDB = self.dataDir + 'block-data.db' self.blockDataLocation = self.dataDir + 'blocks/' self.addressDB = self.dataDir + 'address.db' self.hsAddress = '' diff --git a/onionr/dbcreator.py b/onionr/dbcreator.py index f2f12f07..cde4fd83 100644 --- a/onionr/dbcreator.py +++ b/onionr/dbcreator.py @@ -96,7 +96,7 @@ class DBCreator: conn = sqlite3.connect(self.core.blockDB) c = conn.cursor() c.execute('''CREATE TABLE hashes( - hash text not null, + hash text distinct not null, dateReceived int, decrypted int, dataType text, @@ -111,6 +111,19 @@ class DBCreator: conn.commit() conn.close() return + + def createBlockDataDB(self): + if os.path.exists(self.core.blockDataDB): + raise Exception("Block data database already exists") + conn = sqlite3.connect(self.core.blockDataDB) + c = conn.cursor() + c.execute('''CREATE TABLE blockData( + hash text distinct not null, + data blob not null + ); + ''') + conn.commit() + conn.close() def createForwardKeyDB(self): ''' diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 948563e3..7f5606ba 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -362,6 +362,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 # convert to dict if it is json string if type(metadata) is str: @@ -390,13 +391,14 @@ class OnionrUtils: break if i == 'time': if not self.isIntegerString(metadata[i]): - logger.warn('Block metadata time stamp is not integer string') + logger.warn('Block metadata time stamp is not integer string or int') break - if (metadata[i] - self.getEpoch()) > 30: - logger.warn('Block metadata time stamp is set for the future, which is not allowed.') + 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 older than allowed: %s' % (maxAge,)) + logger.warn('Block is outdated: %s' % (metadata[i],)) elif i == 'expire': try: assert int(metadata[i]) > self.getEpoch() @@ -440,7 +442,7 @@ class OnionrUtils: return retVal def isIntegerString(self, data): - '''Check if a string is a valid base10 integer''' + '''Check if a string is a valid base10 integer (also returns true if already an int)''' try: int(data) except ValueError: From f8cebd5bd5c512138ae0a585b01b6f2e8db876a0 Mon Sep 17 00:00:00 2001 From: Kevin Date: Thu, 13 Dec 2018 04:35:01 +0000 Subject: [PATCH 06/94] Cleanup and fixes --- onionr/core.py | 13 ++++---- onionr/onionr.py | 14 ++++----- onionr/onionrdaemontools.py | 10 +++++-- .../static-data/default-plugins/cliui/main.py | 30 ++++--------------- 4 files changed, 26 insertions(+), 41 deletions(-) diff --git a/onionr/core.py b/onionr/core.py index 84863b40..49cc3456 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -514,17 +514,18 @@ class Core: speed int, 3 success int, 4 DBHash text, 5 - failure int 6 - lastConnect 7 - trust 8 - introduced 9 + powValue 6 + failure int 7 + lastConnect 8 + trust 9 + introduced 10 ''' conn = sqlite3.connect(self.addressDB, timeout=10) c = conn.cursor() command = (address,) - infoNumbers = {'address': 0, 'type': 1, 'knownPeer': 2, 'speed': 3, 'success': 4, 'DBHash': 5, 'failure': 6, 'lastConnect': 7, 'trust': 8, 'introduced': 9} + infoNumbers = {'address': 0, 'type': 1, 'knownPeer': 2, 'speed': 3, 'success': 4, 'DBHash': 5, 'powValue': 6, 'failure': 7, 'lastConnect': 8, 'trust': 9, 'introduced': 10} info = infoNumbers[info] iterCount = 0 retVal = '' @@ -550,7 +551,7 @@ class Core: command = (data, address) - if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'DBHash', 'failure', 'lastConnect', 'lastConnectAttempt', 'trust', 'introduced'): + if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'DBHash', '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) diff --git a/onionr/onionr.py b/onionr/onionr.py index e2f88225..cf515819 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -182,7 +182,6 @@ class Onionr: 'introduce': self.onionrCore.introduceNode, 'connect': self.addAddress, - 'kex': self.doKEX, 'pex': self.doPEX, 'ui' : self.openUI, @@ -227,7 +226,6 @@ class Onionr: 'get-file': 'Get a file from Onionr blocks', 'import-blocks': 'import blocks from the disk (Onionr is transport-agnostic!)', 'listconn': 'list connected peers', - 'kex': 'exchange keys with peers (done automatically)', 'pex': 'exchange addresses with peers (done automatically)', 'blacklist-block': 'deletes a block by hash and permanently removes it from your node', 'introduce': 'Introduce your node to the public Onionr network', @@ -495,11 +493,6 @@ class Onionr: return - def doKEX(self): - '''make communicator do kex''' - logger.info('Sending kex to command queue...') - self.onionrCore.daemonQueueAdd('kex') - def doPEX(self): '''make communicator do pex''' logger.info('Sending pex to command queue...') @@ -694,7 +687,10 @@ class Onionr: self.daemon() self.running = False if not self.debug and not self._developmentMode: - os.remove('.onionr-lock') + try: + os.remove('.onionr-lock') + except FileNotFoundError: + pass def daemon(self): ''' @@ -728,7 +724,7 @@ class Onionr: Onionr.setupConfig('data/', self = self) if self._developmentMode: - logger.warn('DEVELOPMENT MODE ENABLED (THIS IS LESS SECURE!)', timestamp = False) + logger.warn('DEVELOPMENT MODE ENABLED (LESS SECURE)', timestamp = False) net = NetController(config.get('client.port', 59496), apiServerIP=apiHost) logger.debug('Tor is starting...') if not net.startTor(): diff --git a/onionr/onionrdaemontools.py b/onionr/onionrdaemontools.py index 890603dd..ebe181c6 100644 --- a/onionr/onionrdaemontools.py +++ b/onionr/onionrdaemontools.py @@ -49,9 +49,14 @@ class DaemonTools: data = {'node': ourID} combinedNodes = ourID + peer + existingRand = self.daemon._core.getAddressInfo(peer, 'powValue') + if type(existingRand) is type(None): + existingRand = '' if peer in self.announceCache: data['random'] = self.announceCache[peer] + elif len(existingRand) > 0: + data['random'] = existingRand else: proof = onionrproofs.DataPOW(combinedNodes, forceDifficulty=4) try: @@ -68,6 +73,7 @@ class DaemonTools: 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 @@ -152,8 +158,8 @@ class DaemonTools: self.daemon.decrementThreadCount('cooldownPeer') def runCheck(self): - if os.path.isfile('data/.runcheck'): - os.remove('data/.runcheck') + if os.path.isfile(self.daemon._core.dataDir + '.runcheck'): + os.remove(self.daemon._core.dataDir + '.runcheck') return True return False diff --git a/onionr/static-data/default-plugins/cliui/main.py b/onionr/static-data/default-plugins/cliui/main.py index e40eb4b4..323b9419 100644 --- a/onionr/static-data/default-plugins/cliui/main.py +++ b/onionr/static-data/default-plugins/cliui/main.py @@ -34,7 +34,8 @@ class OnionrCLIUI: def subCommand(self, command): try: #subprocess.run(["./onionr.py", command]) - subprocess.Popen(['./onionr.py', command], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + #subprocess.Popen(['./onionr.py', command], stdin=subprocess.STD, stdout=subprocess.STDOUT, stderr=subprocess.STDOUT) + subprocess.call(['./onionr.py', command]) except KeyboardInterrupt: pass @@ -52,24 +53,17 @@ class OnionrCLIUI: firstRun = False while showMenu: - if firstRun: - logger.info('Please wait while Onionr starts...') - daemon = subprocess.Popen(["./onionr.py", "start"], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL) - time.sleep(30) - firstRun = False - if self.myCore._utils.localCommand('ping') == 'pong': isOnline = "Yes" else: isOnline = "No" - logger.info('''Daemon Running: ''' + isOnline + ''' + print('''Daemon Running: ''' + isOnline + ''' 1. Flow (Anonymous public chat, use at your own risk) 2. Mail (Secure email-like service) 3. File Sharing 4. User Settings -5. Start/Stop Daemon -6. Quit (Does not shutdown daemon) +5. Quit (Does not shutdown daemon) ''') try: choice = input(">").strip().lower() @@ -81,25 +75,13 @@ class OnionrCLIUI: elif choice in ("2", "mail"): self.subCommand("mail") elif choice in ("3", "file sharing", "file"): - logger.warn("Not supported yet") + print("Not supported yet") elif choice in ("4", "user settings", "settings"): try: self.setName() except (KeyboardInterrupt, EOFError) as e: pass - elif choice in ("5", "daemon"): - if isOnline == "Yes": - logger.info("Onionr daemon will shutdown...") - self.myCore.daemonQueueAdd('shutdown') - - try: - daemon.kill() - except UnboundLocalError: - pass - else: - logger.info("Starting Daemon...") - daemon = subprocess.Popen(["./onionr.py", "start"], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) - elif choice in ("6", "quit"): + elif choice in ("5", "quit"): showMenu = False elif choice == "": pass From 074a0e796f64edfeec4738d124e1971aa128181c Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 16 Dec 2018 12:27:51 -0600 Subject: [PATCH 07/94] merging master --- onionr/onionrutils.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index c353e237..7f5606ba 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -393,20 +393,12 @@ class OnionrUtils: if not self.isIntegerString(metadata[i]): logger.warn('Block metadata time stamp is not integer string or int') break -<<<<<<< HEAD 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],)) -======= - if (metadata[i] - self.getEpoch()) > 30: - logger.warn('Block metadata time stamp is set for the future, which is not allowed.') - break - if (self.getEpoch() - metadata[i]) > maxAge: - logger.warn('Block is older than allowed: %s' % (maxAge,)) ->>>>>>> 66da0b87cf12cf75cdecd90881385b17ad2b07ef elif i == 'expire': try: assert int(metadata[i]) > self.getEpoch() From 643ddec43078e3278cfe3231fcffedc17ccc936e Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 16 Dec 2018 16:12:47 -0600 Subject: [PATCH 08/94] added api rework files --- onionr/apimanager.py | 75 ++++++++++++++++++++++++++++++++++++++++++++ onionr/apiprivate.py | 32 +++++++++++++++++++ onionr/apipublic.py | 32 +++++++++++++++++++ 3 files changed, 139 insertions(+) create mode 100644 onionr/apimanager.py create mode 100644 onionr/apiprivate.py create mode 100644 onionr/apipublic.py diff --git a/onionr/apimanager.py b/onionr/apimanager.py new file mode 100644 index 00000000..05a1ba91 --- /dev/null +++ b/onionr/apimanager.py @@ -0,0 +1,75 @@ +''' + Onionr - P2P Anonymous Storage Network + + Handles api data exchange, interfaced by both public and client 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 config, apipublic, apiprivate, core, socket, random, threading, time +config.reload() + +PRIVATE_API_VERSION = 0 +PUBLIC_API_VERSION = 1 + +DEV_MODE = config.get('general.dev_mode') + +def getOpenPort(): + '''Get a random open port''' + p = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + p.bind(("127.0.0.1",0)) + p.listen(1) + port = s.getsockname()[1] + p.close() + return port + +def getRandomLocalIP(): + '''Get a random local ip address''' + hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))] + host = '.'.join(hostOctets) + return host + +class APIManager: + def __init__(self, coreInst): + assert isinstance(coreInst, core.Core) + self.core = core + self.utils = core._utils + self.crypto = core._crypto + + # if this gets set to true, both the public and private apis will shutdown + self.shutdown = False + + publicIP = '127.0.0.1' + privateIP = '127.0.0.1' + if DEV_MODE: + # set private and local api servers bind IPs to random localhost (127.x.x.x), make sure not the same + privateIP = getRandomLocalIP() + while True: + publicIP = getRandomLocalIP() + if publicIP != privateIP: + break + + # Make official the IPs and Ports + self.publicIP = publicIP + self.privateIP = privateIP + self.publicPort = getOpenPort() + self.privatePort = getOpenPort() + + # Run the API servers in new threads + self.publicAPI = apipublic.APIPublic() + self.privateAPI = apiprivate.privateAPI() + threading.Thread(target=self.publicAPI.run).start() + threading.Thread(target=self.privateAPI.run).start() + while not self.shutdown: + time.sleep(1) \ No newline at end of file diff --git a/onionr/apiprivate.py b/onionr/apiprivate.py new file mode 100644 index 00000000..7f62b9d5 --- /dev/null +++ b/onionr/apiprivate.py @@ -0,0 +1,32 @@ +''' + Onionr - P2P Anonymous Storage Network + + Handle incoming commands from the client. Intended for localhost use +''' +''' + 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 flask, apimanager +from flask import request, Response, abort, send_from_directory +from gevent.pywsgi import WSGIServer + +class APIPrivate: + def __init__(self, managerInst): + assert isinstance(managerInst, apimanager.APIManager) + self.app = flask.Flask(__name__) # The flask application, which recieves data from the greenlet wsgiserver + self.httpServer = WSGIServer((managerInst.privateIP, managerInst.privatePort), self.app, log=None) + + def run(self): + self.httpServer.serve_forever() + return \ No newline at end of file diff --git a/onionr/apipublic.py b/onionr/apipublic.py new file mode 100644 index 00000000..110a6934 --- /dev/null +++ b/onionr/apipublic.py @@ -0,0 +1,32 @@ +''' + Onionr - P2P Anonymous Storage Network + + Handle incoming commands from other Onionr nodes, over HTTP +''' +''' + 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 flask, apimanager +from flask import request, Response, abort, send_from_directory +from gevent.pywsgi import WSGIServer + +class APIPublic: + def __init__(self, managerInst): + assert isinstance(managerInst, apimanager.APIManager) + self.app = flask.Flask(__name__) # The flask application, which recieves data from the greenlet wsgiserver + self.httpServer = WSGIServer((managerInst.publicIP, managerInst.publicPort), self.app, log=None) + + def run(self): + self.httpServer.serve_forever() + return \ No newline at end of file From a20769fb68fe48939f95bcb7eb0c2e78d2b46ae8 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 16 Dec 2018 16:19:21 -0600 Subject: [PATCH 09/94] config option for new api server --- onionr/onionr.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/onionr/onionr.py b/onionr/onionr.py index cf515819..ad6499e4 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -28,7 +28,7 @@ if sys.version_info[0] == 2 or sys.version_info[1] < 5: import os, base64, random, getpass, shutil, subprocess, requests, time, platform, datetime, re, json, getpass, sqlite3 import webbrowser from threading import Thread -import api, core, config, logger, onionrplugins as plugins, onionrevents as events +import api, apimanager, core, config, logger, onionrplugins as plugins, onionrevents as events import onionrutils from netcontroller import NetController from onionrblockapi import Block @@ -704,7 +704,12 @@ class Onionr: logger.debug('Runcheck file found on daemon start, deleting in advance.') os.remove('data/.runcheck') - apiThread = Thread(target = api.API, args = (self.debug, API_VERSION)) + apiTarget = api.API + if config.get('general.use_new_api_server', False): + apiTarget = apimanager.APIManager + apiThread = Thread(target = apiTarget, args = (self.onionrCore)) + else: + apiThread = Thread(target = apiTarget, args = (self.debug, API_VERSION)) apiThread.start() try: From a8f8aea35f505d408e6f5b803ff3c0415cca9006 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 18 Dec 2018 17:48:17 -0600 Subject: [PATCH 10/94] work on revising api --- onionr/api.py | 632 ++++--------------------- onionr/apimanager.py | 16 +- onionr/apipublic.py | 15 +- onionr/communicator2.py | 2 + onionr/core.py | 2 + onionr/netcontroller.py | 15 +- onionr/onionr.py | 27 +- onionr/onionrutils.py | 10 +- onionr/static-data/default_config.json | 3 +- 9 files changed, 147 insertions(+), 575 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 94cf2cd9..6499b4d3 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -19,54 +19,71 @@ ''' import flask from flask import request, Response, abort, send_from_directory -from multiprocessing import Process from gevent.pywsgi import WSGIServer import sys, random, threading, hmac, hashlib, base64, time, math, os, json import core from onionrblockapi import Block import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config, onionr +def guessMime(path): + ''' + Guesses the mime type from the input filename + ''' + mimetypes = { + 'html' : 'text/html', + 'js' : 'application/javascript', + 'css' : 'text/css', + 'png' : 'image/png', + 'jpg' : 'image/jpeg' + } + + for mimetype in mimetypes: + if path.endswith('.%s' % mimetype): + return mimetypes[mimetype] + + return 'text/plain' + +def setBindIP(filePath): + '''Set a random localhost IP to a specified file (intended for private or public API localhost IPs)''' + hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))] + data = '.'.join(hostOctets) + + with open(filePath, 'w') as bindFile: + bindFile.write(data) + return data + +class PublicAPI: + ''' + The new client api server, isolated from the public api + ''' + def __init__(self, clientAPI): + assert isinstance(clientAPI, API) + app = flask.Flask('PublicAPI') + self.i2pEnabled = config.get('i2p.host', False) + self.hideBlocks = [] # Blocks to be denied sharing + self.host = setBindIP(clientAPI._core.publicApiHostFile) + bindPort = config.get('client.public.port') + + @app.route('/') + def banner(): + #validateHost('public') + try: + with open('static-data/index.html', 'r') as html: + resp = Response(html.read(), mimetype='text/html') + except FileNotFoundError: + resp = Response("") + return resp + clientAPI.setPublicAPIInstance(self) + self.httpServer = WSGIServer((self.host, bindPort), app, log=None) + self.httpServer.serve_forever() + class API: ''' - Main HTTP API (Flask) + Client HTTP api ''' callbacks = {'public' : {}, 'private' : {}} - def validateToken(self, token): - ''' - Validate that the client token matches the given token - ''' - if len(self.clientToken) == 0: - logger.error("client password needs to be set") - return False - try: - if not hmac.compare_digest(self.clientToken, token): - return False - else: - return True - except TypeError: - return False - - def guessMime(path): - ''' - Guesses the mime type from the input filename - ''' - - mimetypes = { - 'html' : 'text/html', - 'js' : 'application/javascript', - 'css' : 'text/css', - 'png' : 'image/png', - 'jpg' : 'image/jpeg' - } - - for mimetype in mimetypes: - if path.endswith('.%s' % mimetype): - return mimetypes[mimetype] - - return 'text/plain' - def __init__(self, debug, API_VERSION): ''' Initialize the api server, preping variables for later use @@ -84,520 +101,49 @@ class API: self._crypto = onionrcrypto.OnionrCrypto(self._core) self._utils = onionrutils.OnionrUtils(self._core) app = flask.Flask(__name__) - bindPort = int(config.get('client.port', 59496)) + bindPort = int(config.get('client.client.port', 59496)) self.bindPort = bindPort + self.clientToken = config.get('client.webpassword') self.timeBypassToken = base64.b16encode(os.urandom(32)).decode() - self.i2pEnabled = config.get('i2p.host', False) - - self.hideBlocks = [] # Blocks to be denied sharing - - with open(self._core.dataDir + 'time-bypass.txt', 'w') as bypass: - bypass.write(self.timeBypassToken) - - if not debug and not self._developmentMode: - hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))] - self.host = '.'.join(hostOctets) - else: - self.host = '127.0.0.1' - - with open(self._core.dataDir + 'host.txt', 'w') as file: - file.write(self.host) - - @app.before_request - def beforeReq(): - ''' - Simply define the request as not having yet failed, before every request. - ''' - self.requestFailed = False - return - - @app.after_request - def afterReq(resp): - if not self.requestFailed: - resp.headers['Access-Control-Allow-Origin'] = '*' - #else: - # resp.headers['server'] = 'Onionr' - 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'" - resp.headers['X-Frame-Options'] = 'deny' - resp.headers['X-Content-Type-Options'] = "nosniff" - resp.headers['X-API'] = API_VERSION - resp.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch. - return resp - - @app.route('/site/') - def site(block): - self.validateHost('private') - bHash = block - resp = 'Not Found' - if self._core._utils.validateHash(bHash): - resp = Block(bHash).bcontent - try: - resp = base64.b64decode(resp) - except: - pass - return Response(resp) - - @app.route('/www/private/') - def www_private(path): - startTime = math.floor(time.time()) - - if request.args.get('timingToken') is None: - timingToken = '' - else: - timingToken = request.args.get('timingToken') - - if not config.get("www.private.run", True): - abort(403) - - self.validateHost('private') - - endTime = math.floor(time.time()) - elapsed = endTime - startTime - - if not hmac.compare_digest(timingToken, self.timeBypassToken): - if (elapsed < self._privateDelayTime) and config.get('www.private.timing_protection', True): - time.sleep(self._privateDelayTime - elapsed) - - return send_from_directory(config.get('www.private.path', 'static-data/www/private/'), path) - - @app.route('/www/public/') - def www_public(path): - if not config.get("www.public.run", True): - abort(403) - - self.validateHost('public') - - return send_from_directory(config.get('www.public.path', 'static-data/www/public/'), path) - - @app.route('/ui/') - def ui_private(path): - startTime = math.floor(time.time()) - - ''' - if request.args.get('timingToken') is None: - timingToken = '' - else: - timingToken = request.args.get('timingToken') - ''' - - if not config.get("www.ui.run", True): - abort(403) - - if config.get("www.ui.private", True): - self.validateHost('private') - else: - self.validateHost('public') - - ''' - endTime = math.floor(time.time()) - elapsed = endTime - startTime - - if not hmac.compare_digest(timingToken, self.timeBypassToken): - if elapsed < self._privateDelayTime: - time.sleep(self._privateDelayTime - elapsed) - ''' - - mime = API.guessMime(path) - - logger.debug('Serving %s (mime: %s)' % (path, mime)) - - return send_from_directory('static-data/www/ui/dist/', path) - - @app.route('/client/') - def private_handler(): - if request.args.get('timingToken') is None: - timingToken = '' - else: - timingToken = request.args.get('timingToken') - data = request.args.get('data') - try: - data = data - except: - data = '' - startTime = math.floor(time.time()) - - action = request.args.get('action') - #if not self.debug: - token = request.args.get('token') - - if not self.validateToken(token): - abort(403) - - events.event('webapi_private', onionr = None, data = {'action' : action, 'data' : data, 'timingToken' : timingToken, 'token' : token}) - - self.validateHost('private') - if action == 'hello': - resp = Response('Hello, World! ' + request.host) - elif action == 'getIP': - resp = Response(self.host) - elif action == 'waitForShare': - if self._core._utils.validateHash(data): - if data not in self.hideBlocks: - self.hideBlocks.append(data) - else: - self.hideBlocks.remove(data) - resp = "success" - else: - resp = "failed to validate hash" - elif action == 'shutdown': - # request.environ.get('werkzeug.server.shutdown')() - self.http_server.stop() - resp = Response('Goodbye') - elif action == 'ping': - resp = Response('pong') - elif action == 'info': - resp = Response(json.dumps({'pubkey' : self._core._crypto.pubKey, 'host' : self._core.hsAddress}), mimetype='text/plain') - elif action == "insertBlock": - response = {'success' : False, 'reason' : 'An unknown error occurred'} - - if not ((data is None) or (len(str(data).strip()) == 0)): - try: - decoded = json.loads(data) - - block = Block() - - sign = False - - for key in decoded: - val = decoded[key] - - key = key.lower() - - if key == 'type': - block.setType(val) - elif key in ['body', 'content']: - block.setContent(val) - elif key == 'parent': - block.setParent(val) - elif key == 'sign': - sign = (str(val).lower() == 'true') - - hash = block.save(sign = sign) - - if not hash is False: - response['success'] = True - response['hash'] = hash - response['reason'] = 'Successfully wrote block to file' - else: - response['reason'] = 'Failed to save the block' - except Exception as e: - logger.warn('insertBlock api request failed', error = e) - logger.debug('Here\'s the request: %s' % data) - else: - response = {'success' : False, 'reason' : 'Missing `data` parameter.', 'blocks' : {}} - - resp = Response(json.dumps(response)) - elif action == 'searchBlocks': - response = {'success' : False, 'reason' : 'An unknown error occurred', 'blocks' : {}} - - if not ((data is None) or (len(str(data).strip()) == 0)): - try: - decoded = json.loads(data) - - type = None - signer = None - signed = None - parent = None - reverse = False - limit = None - - for key in decoded: - val = decoded[key] - - key = key.lower() - - if key == 'type': - type = str(val) - elif key == 'signer': - if isinstance(val, list): - signer = val - else: - signer = str(val) - elif key == 'signed': - signed = (str(val).lower() == 'true') - elif key == 'parent': - parent = str(val) - elif key == 'reverse': - reverse = (str(val).lower() == 'true') - elif key == 'limit': - limit = 10000 - - if val is None: - val = limit - - limit = min(limit, int(val)) - - blockObjects = Block.getBlocks(type = type, signer = signer, signed = signed, parent = parent, reverse = reverse, limit = limit) - - logger.debug('%s results for query %s' % (len(blockObjects), decoded)) - - blocks = list() - - for block in blockObjects: - blocks.append({ - 'hash' : block.getHash(), - 'type' : block.getType(), - 'content' : block.getContent(), - 'signature' : block.getSignature(), - 'signedData' : block.getSignedData(), - 'signed' : block.isSigned(), - 'valid' : block.isValid(), - 'date' : (int(block.getDate().strftime("%s")) if not block.getDate() is None else None), - 'parent' : (block.getParent().getHash() if not block.getParent() is None else None), - 'metadata' : block.getMetadata(), - 'header' : block.getHeader() - }) - - response['success'] = True - response['blocks'] = blocks - response['reason'] = 'Success' - except Exception as e: - logger.warn('searchBlock api request failed', error = e) - logger.debug('Here\'s the request: %s' % data) - else: - response = {'success' : False, 'reason' : 'Missing `data` parameter.', 'blocks' : {}} - - resp = Response(json.dumps(response), mimetype='text/plain') - - elif action in API.callbacks['private']: - resp = Response(str(getCallback(action, scope = 'private')(request)), mimetype='text/plain') - else: - resp = Response('invalid command') - endTime = math.floor(time.time()) - elapsed = endTime - startTime - - # if bypass token not used, delay response to prevent timing attacks - if not hmac.compare_digest(timingToken, self.timeBypassToken): - if elapsed < self._privateDelayTime: - time.sleep(self._privateDelayTime - elapsed) - - return resp + self.publicAPI = None # gets set when the thread calls our setter... bad hack but kinda necessary with flask + threading.Thread(target=PublicAPI, args=(self,)).start() + self.host = setBindIP(self._core.privateApiHostFile) + logger.info('Running api on %s:%s' % (self.host, self.bindPort)) + self.httpServer = '' @app.route('/') - def banner(): - self.validateHost('public') + def hello(): + return Response("hello client") + + @app.route('/shutdown') + def shutdown(): try: - with open('static-data/index.html', 'r') as html: - resp = Response(html.read(), mimetype='text/html') - except FileNotFoundError: - resp = Response("") - return resp - - @app.route('/public/upload/', methods=['POST']) - def blockUpload(): - self.validateHost('public') - resp = 'failure' - try: - data = request.form['block'] - except KeyError: - logger.warn('No block specified for upload') + self.publicAPI.httpServer.stop() + self.httpServer.stop() + except AttributeError: pass - else: - if sys.getsizeof(data) < 100000000: - try: - if blockimporter.importBlockFromData(data, self._core): - resp = 'success' - else: - logger.warn('Error encountered importing uploaded block') - except onionrexceptions.BlacklistedBlock: - logger.debug('uploaded block is blacklisted') - pass - - resp = Response(resp) - return resp - - @app.route('/public/announce/', methods=['POST']) - def acceptAnnounce(): - self.validateHost('public') - resp = 'failure' - powHash = '' - randomData = '' - newNode = '' - ourAdder = self._core.hsAddress.encode() - try: - newNode = request.form['node'].encode() - except KeyError: - logger.warn('No block 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 + self._core.hsAddress.encode() - nodes = self._core._crypto.blake2bHash(nodes) - powHash = self._core._crypto.blake2bHash(randomData + nodes) - try: - powHash = powHash.decode() - except AttributeError: - pass - if powHash.startswith('0000'): - try: - newNode = newNode.decode() - except AttributeError: - pass - if self._core.addAddress(newNode): - resp = 'Success' - else: - logger.warn(newNode.decode() + ' failed to meet POW: ' + powHash) - resp = Response(resp) - return resp - - @app.route('/public/') - def public_handler(): - # Public means it is publicly network accessible - self.validateHost('public') - if config.get('general.security_level') != 0: - abort(403) - action = request.args.get('action') - requestingPeer = request.args.get('myID') - data = request.args.get('data') - try: - data = data - except: - data = '' - - events.event('webapi_public', onionr = None, data = {'action' : action, 'data' : data, 'requestingPeer' : requestingPeer, 'request' : request}) - - if action == 'firstConnect': - pass - elif action == 'ping': - resp = Response("pong!") - elif action == 'getSymmetric': - resp = Response(self._crypto.generateSymmetric()) - elif action == 'getDBHash': - resp = Response(self._utils.getBlockDBHash()) - elif action == 'getBlockHashes': - bList = self._core.getBlockList() - for b in self.hideBlocks: - if b in bList: - bList.remove(b) - resp = Response('\n'.join(bList)) - # setData should be something the communicator initiates, not this api - elif action == 'getData': - resp = '' - if self._utils.validateHash(data): - if data not in self.hideBlocks: - if os.path.exists(self._core.dataDir + 'blocks/' + data + '.dat'): - block = Block(hash=data.encode(), core=self._core) - resp = base64.b64encode(block.getRaw().encode()).decode() - if len(resp) == 0: - abort(404) - resp = "" - resp = Response(resp) - elif action == 'pex': - response = ','.join(self._core.listAdders()) - if len(response) == 0: - response = 'none' - resp = Response(response) - elif action == 'kex': - peers = self._core.listPeers(getPow=True) - response = ','.join(peers) - resp = Response(response) - elif action in API.callbacks['public']: - resp = Response(str(getCallback(action, scope = 'public')(request))) - else: - resp = Response("") - - return resp - - @app.errorhandler(404) - def notfound(err): - self.requestFailed = True - resp = Response("") - - return resp - - @app.errorhandler(403) - def authFail(err): - self.requestFailed = True - resp = Response("403") - return resp - - @app.errorhandler(401) - def clientError(err): - self.requestFailed = True - resp = Response("Invalid request") - - return resp - - logger.info('Starting client on ' + self.host + ':' + str(bindPort), timestamp=False) + return Response("bye") + + self.httpServer = WSGIServer((self.host, bindPort), app, log=None) + self.httpServer.serve_forever() + + def setPublicAPIInstance(self, inst): + assert isinstance(inst, PublicAPI) + self.publicAPI = inst + def validateToken(self, token): + ''' + Validate that the client token matches the given token + ''' + if len(self.clientToken) == 0: + logger.error("client password needs to be set") + return False try: - while len(self._core.hsAddress) == 0: - self._core.refreshFirstStartVars() - time.sleep(0.5) - self.http_server = WSGIServer((self.host, bindPort), app, log=None) - self.http_server.serve_forever() - except KeyboardInterrupt: - pass - except Exception as e: - logger.error(str(e)) - logger.fatal('Failed to start client on ' + self.host + ':' + str(bindPort) + ', exiting...') - - def validateHost(self, hostType): - ''' - Validate various features of the request including: - - If private (/client/), is the host header local? - If public (/public/), is the host header onion or i2p? - - Was X-Request-With used? - ''' - if self.debug: - return - # Validate host header, to protect against DNS rebinding attacks - host = self.host - if hostType == 'private': - if not request.host.startswith('127') and not self._utils.checkIsIP(request.host): - abort(403) - elif hostType == 'public': - if not request.host.endswith('onion') and not request.host.endswith('i2p'): - abort(403) - # Validate x-requested-with, to protect against CSRF/metadata leaks - - if not self.i2pEnabled and request.host.endswith('i2p'): - abort(403) - - ''' - if not self._developmentMode: - try: - request.headers['X-Requested-With'] - except: - pass - # we exit rather than abort to avoid fingerprinting - logger.debug('Avoiding fingerprinting, exiting...') - #sys.exit(1) - ''' - - def setCallback(action, callback, scope = 'public'): - if not scope in API.callbacks: - return False - - API.callbacks[scope][action] = callback - - return True - - def removeCallback(action, scope = 'public'): - if (not scope in API.callbacks) or (not action in API.callbacks[scope]): - return False - - del API.callbacks[scope][action] - - return True - - def getCallback(action, scope = 'public'): - if (not scope in API.callbacks) or (not action in API.callbacks[scope]): - return None - - return API.callbacks[scope][action] - - def getCallbacks(scope = None): - if (not scope is None) and (scope in API.callbacks): - return API.callbacks[scope] - - return API.callbacks + if not hmac.compare_digest(self.clientToken, token): + return False + else: + return True + except TypeError: + return False \ No newline at end of file diff --git a/onionr/apimanager.py b/onionr/apimanager.py index 05a1ba91..44737097 100644 --- a/onionr/apimanager.py +++ b/onionr/apimanager.py @@ -30,7 +30,7 @@ def getOpenPort(): p = socket.socket(socket.AF_INET, socket.SOCK_STREAM) p.bind(("127.0.0.1",0)) p.listen(1) - port = s.getsockname()[1] + port = p.getsockname()[1] p.close() return port @@ -43,9 +43,9 @@ def getRandomLocalIP(): class APIManager: def __init__(self, coreInst): assert isinstance(coreInst, core.Core) - self.core = core - self.utils = core._utils - self.crypto = core._crypto + self.core = coreInst + self.utils = coreInst._utils + self.crypto = coreInst._crypto # if this gets set to true, both the public and private apis will shutdown self.shutdown = False @@ -63,12 +63,12 @@ class APIManager: # Make official the IPs and Ports self.publicIP = publicIP self.privateIP = privateIP - self.publicPort = getOpenPort() - self.privatePort = getOpenPort() + self.publicPort = config.get('client.port', 59496) + self.privatePort = config.get('client.port', 59496) # Run the API servers in new threads - self.publicAPI = apipublic.APIPublic() - self.privateAPI = apiprivate.privateAPI() + self.publicAPI = apipublic.APIPublic(self) + self.privateAPI = apiprivate.APIPrivate(self) threading.Thread(target=self.publicAPI.run).start() threading.Thread(target=self.privateAPI.run).start() while not self.shutdown: diff --git a/onionr/apipublic.py b/onionr/apipublic.py index 110a6934..9308f50a 100644 --- a/onionr/apipublic.py +++ b/onionr/apipublic.py @@ -21,12 +21,21 @@ import flask, apimanager from flask import request, Response, abort, send_from_directory from gevent.pywsgi import WSGIServer + class APIPublic: def __init__(self, managerInst): assert isinstance(managerInst, apimanager.APIManager) - self.app = flask.Flask(__name__) # The flask application, which recieves data from the greenlet wsgiserver - self.httpServer = WSGIServer((managerInst.publicIP, managerInst.publicPort), self.app, log=None) + app = flask.Flask(__name__) + @app.route('/') + def banner(): + try: + with open('static-data/index.html', 'r') as html: + resp = Response(html.read(), mimetype='text/html') + except FileNotFoundError: + resp = Response("") + return resp + self.httpServer = WSGIServer((managerInst.publicIP, managerInst.publicPort), app) def run(self): self.httpServer.serve_forever() - return \ No newline at end of file + return diff --git a/onionr/communicator2.py b/onionr/communicator2.py index bddb21da..5b6f8ba9 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -19,6 +19,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' +import gevent.monkey +gevent.monkey.patch_all() import sys, os, core, config, json, requests, time, logger, threading, base64, onionr, uuid import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block import onionrdaemontools, onionrsockets, onionrchat, onionr, onionrproofs diff --git a/onionr/core.py b/onionr/core.py index 59e802a5..f463dd35 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -49,6 +49,8 @@ class Core: self.peerDB = self.dataDir + 'peers.db' self.blockDB = self.dataDir + 'blocks.db' self.blockDataLocation = self.dataDir + 'blocks/' + self.publicApiHostFile = self.dataDir + 'public-host.txt' + self.privateApiHostFile = self.dataDir + 'private-host.txt' self.addressDB = self.dataDir + 'address.db' self.hsAddress = '' self.bootstrapFileLocation = 'static-data/bootstrap-nodes.txt' diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py index 76d8ece6..7566623e 100644 --- a/onionr/netcontroller.py +++ b/onionr/netcontroller.py @@ -18,10 +18,19 @@ along with this program. If not, see . ''' -import subprocess, os, random, sys, logger, time, signal, config, base64 +import subprocess, os, random, sys, logger, time, signal, config, base64, socket from stem.control import Controller from onionrblockapi import Block from dependencies import secrets + +def getOpenPort(): + # taken from (but modified) https://stackoverflow.com/a/2838309 + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(("127.0.0.1",0)) + s.listen(1) + port = s.getsockname()[1] + s.close() + return port class NetController: ''' This class handles hidden service setup on Tor and I2P @@ -37,7 +46,7 @@ class NetController: self.torConfigLocation = self.dataDir + 'torrc' self.readyState = False - self.socksPort = random.randint(1024, 65535) + self.socksPort = getOpenPort() self.hsPort = hsPort self._torInstnace = '' self.myID = '' @@ -78,7 +87,7 @@ class NetController: config.set('tor.controlpassword', plaintext, savefile=True) config.set('tor.socksport', self.socksPort, savefile=True) - controlPort = random.randint(1025, 65535) + controlPort = getOpenPort() config.set('tor.controlPort', controlPort, savefile=True) diff --git a/onionr/onionr.py b/onionr/onionr.py index ad6499e4..1d47972a 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -20,7 +20,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' - +import gevent.monkey +gevent.monkey.patch_all() import sys if sys.version_info[0] == 2 or sys.version_info[1] < 5: print('Error, Onionr requires Python 3.5+') @@ -28,8 +29,9 @@ if sys.version_info[0] == 2 or sys.version_info[1] < 5: import os, base64, random, getpass, shutil, subprocess, requests, time, platform, datetime, re, json, getpass, sqlite3 import webbrowser from threading import Thread -import api, apimanager, core, config, logger, onionrplugins as plugins, onionrevents as events +import api, core, config, logger, onionrplugins as plugins, onionrevents as events import onionrutils +import netcontroller from netcontroller import NetController from onionrblockapi import Block import onionrproofs, onionrexceptions, onionrusers @@ -105,11 +107,12 @@ class Onionr: # Get configuration if type(config.get('client.webpassword')) is type(None): config.set('client.webpassword', base64.b16encode(os.urandom(32)).decode('utf-8'), savefile=True) - if type(config.get('client.port')) is type(None): - randomPort = 0 - while randomPort < 1024: - randomPort = self.onionrCore._crypto.secrets.randbelow(65535) - config.set('client.port', randomPort, savefile=True) + if type(config.get('client.client.port')) is type(None): + randomPort = netcontroller.getOpenPort() + config.set('client.client.port', randomPort, savefile=True) + if type(config.get('client.public.port')) is type(None): + randomPort = netcontroller.getOpenPort() + 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): @@ -705,11 +708,7 @@ class Onionr: os.remove('data/.runcheck') apiTarget = api.API - if config.get('general.use_new_api_server', False): - apiTarget = apimanager.APIManager - apiThread = Thread(target = apiTarget, args = (self.onionrCore)) - else: - apiThread = Thread(target = apiTarget, args = (self.debug, API_VERSION)) + apiThread = Thread(target = apiTarget, args = (self.debug, API_VERSION)) apiThread.start() try: @@ -722,7 +721,7 @@ class Onionr: apiHost = '127.0.0.1' if apiThread.isAlive(): try: - with open(self.onionrCore.dataDir + 'host.txt', 'r') as hostFile: + with open(self.onionrCore.publicApiHostFile, 'r') as hostFile: apiHost = hostFile.read() except FileNotFoundError: pass @@ -730,7 +729,7 @@ class Onionr: if self._developmentMode: logger.warn('DEVELOPMENT MODE ENABLED (LESS SECURE)', timestamp = False) - net = NetController(config.get('client.port', 59496), apiServerIP=apiHost) + net = NetController(config.get('client.public.port', 59497), apiServerIP=apiHost) logger.debug('Tor is starting...') if not net.startTor(): self.onionrUtils.localCommand('shutdown') diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 948563e3..6c8ba3e4 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -160,13 +160,17 @@ class OnionrUtils: self.getTimeBypassToken() # TODO: URL encode parameters, just as an extra measure. May not be needed, but should be added regardless. try: - with open(self._core.dataDir + 'host.txt', 'r') as host: + with open(self._core.privateApiHostFile, 'r') as host: hostname = host.read() except FileNotFoundError: return False - payload = 'http://%s:%s/client/?action=%s&token=%s&timingToken=%s' % (hostname, config.get('client.port'), command, config.get('client.webpassword'), self.timingToken) if data != '': - payload += '&data=' + urllib.parse.quote_plus(data) + data = '&data=' + urllib.parse.quote_plus(data) + payload = 'http://%s:%s/%s%s' % (hostname, config.get('client.client.port'), command, data) + logger.info(payload) + #payload = 'http://%s:%s/client/?action=%s&token=%s&timingToken=%s' % (hostname, config.get('client.client.port'), command, config.get('client.webpassword'), self.timingToken) + #if data != '': + # payload += '&data=' + urllib.parse.quote_plus(data) try: retData = requests.get(payload).text except Exception as error: diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index 5532797d..13436852 100644 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -7,7 +7,8 @@ "socket_servers": false, "security_level": 0, "max_block_age": 2678400, - "public_key": "" + "public_key": "", + "use_new_api_server": false }, "www" : { From a148826b39b4fee043d0f0c48cea54fe1027b764 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 19 Dec 2018 00:06:25 -0600 Subject: [PATCH 11/94] work on revising api --- onionr/api.py | 39 ++++++++++++++++++++++++++++++++++++++- onionr/onionrutils.py | 3 +-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 6499b4d3..afe63d82 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -25,9 +25,11 @@ import core from onionrblockapi import Block import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config, onionr +API_VERSION = 0 + def guessMime(path): ''' - Guesses the mime type from the input filename + Guesses the mime type of a file from the input filename ''' mimetypes = { 'html' : 'text/html', @@ -113,10 +115,45 @@ class API: logger.info('Running api on %s:%s' % (self.host, self.bindPort)) self.httpServer = '' + @app.before_request + def validateRequest(): + '''Validate request has set password and is the correct hostname''' + if request.host != '%s:%s' % (self.host, self.bindPort): + abort(403) + try: + if not hmac.compare_digest(request.headers['token'], self.clientToken): + abort(403) + except KeyError: + abort(403) + + @app.after_request + def afterReq(resp): + 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'" + resp.headers['X-Frame-Options'] = 'deny' + resp.headers['X-Content-Type-Options'] = "nosniff" + resp.headers['X-API'] = API_VERSION + resp.headers['Server'] = 'nginx' + resp.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch. + return resp + + @app.route('/ping') + def ping(): + return Respose("pong!") + @app.route('/') def hello(): return Response("hello client") + @app.route('/waitforshare/', methods='post') + def waitforshare(): + assert name.isalnum() + if name in self.publicAPI.hideBlocks: + self.publicAPI.hideBlocks.remove(name) + return Response("removed") + else: + self.publicAPI.hideBlocks.append(name) + return Response("added") + @app.route('/shutdown') def shutdown(): try: diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 6c8ba3e4..75fcb7c2 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -167,12 +167,11 @@ class OnionrUtils: if data != '': data = '&data=' + urllib.parse.quote_plus(data) payload = 'http://%s:%s/%s%s' % (hostname, config.get('client.client.port'), command, data) - logger.info(payload) #payload = 'http://%s:%s/client/?action=%s&token=%s&timingToken=%s' % (hostname, config.get('client.client.port'), command, config.get('client.webpassword'), self.timingToken) #if data != '': # payload += '&data=' + urllib.parse.quote_plus(data) try: - retData = requests.get(payload).text + retData = requests.get(payload, headers={'token': config.get('client.webpassword')}).text except Exception as error: if not silent: logger.error('Failed to make local request (command: %s):%s' % (command, error)) From 0b38f78a64f86c7cc803fd41de8b02ad496950fb Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 20 Dec 2018 00:01:53 -0600 Subject: [PATCH 12/94] more endpoints reimplemented in new api --- onionr/api.py | 104 ++++++++++++++++++++++++++++++++++++++-- onionr/communicator2.py | 12 ++--- onionr/core.py | 3 +- 3 files changed, 107 insertions(+), 12 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index afe63d82..e2a6604c 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -64,19 +64,99 @@ class PublicAPI: self.i2pEnabled = config.get('i2p.host', False) self.hideBlocks = [] # Blocks to be denied sharing self.host = setBindIP(clientAPI._core.publicApiHostFile) - bindPort = config.get('client.public.port') + self.torAdder = clientAPI._core.hsAddress + self.i2pAdder = clientAPI._core.i2pAddress + self.bindPort = config.get('client.public.port') + + @app.before_request + def validateRequest(): + '''Validate request has the correct hostname''' + if type(self.torAdder) is None and type(self.i2pAdder) is None: + # abort if our hs addresses are not known + abort(403) + if request.host not in (self.i2pAdder, self.torAdder): + abort(403) + + @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. + 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'" + resp.headers['X-Frame-Options'] = 'deny' + resp.headers['X-Content-Type-Options'] = "nosniff" + resp.headers['X-API'] = API_VERSION + return resp @app.route('/') def banner(): - #validateHost('public') try: with open('static-data/index.html', 'r') as html: resp = Response(html.read(), mimetype='text/html') except FileNotFoundError: resp = Response("") return resp + + @app.route('/getblocklist') + def getBlockList(): + bList = clientAPI._core.getBlockList() + for b in self.hideBlocks: + if b in bList: + bList.remove(b) + return Response('\n'.join(bList)) + + @app.route('/getdata/') + def getBlockData(): + resp = '' + if clientAPI._utils.validateHash(data): + if data not in self.hideBlocks: + if os.path.exists(clientAPI._core.dataDir + 'blocks/' + data + '.dat'): + block = Block(hash=data.encode(), core=clientAPI._core) + resp = base64.b64encode(block.getRaw().encode()).decode() + if len(resp) == 0: + abort(404) + resp = "" + return Response(resp) + + @app.route('/ping') + def ping(): + return Response("pong!") + + @app.route('/getdbhash') + def getDBHash(): + return Response(clientAPI._utils.getBlockDBHash()) + + @app.route('/pex') + def peerExchange(): + response = ','.join(self._core.listAdders()) + if len(response) == 0: + response = 'none' + resp = Response(response) + + @app.route('/upload', methods=['post']) + def upload(): + 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 + clientAPI.setPublicAPIInstance(self) - self.httpServer = WSGIServer((self.host, bindPort), app, log=None) + self.httpServer = WSGIServer((self.host, self.bindPort), app, log=None) self.httpServer.serve_forever() class API: @@ -143,8 +223,22 @@ class API: @app.route('/') def hello(): return Response("hello client") - - @app.route('/waitforshare/', methods='post') + + @app.route('/site/') + def site(): + bHash = block + resp = 'Not Found' + if self._core._utils.validateHash(bHash): + resp = Block(bHash).bcontent + try: + resp = base64.b64decode(resp) + except: + pass + if resp == 'Not Found': + abourt(404) + return Response(resp) + + @app.route('/waitforshare/', methods=['post']) def waitforshare(): assert name.isalnum() if name in self.publicAPI.hideBlocks: diff --git a/onionr/communicator2.py b/onionr/communicator2.py index 5b6f8ba9..fa14864e 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -180,14 +180,14 @@ class OnionrCommunicatorDaemon: break else: continue - newDBHash = self.peerAction(peer, 'getDBHash') # get their db hash + newDBHash = self.peerAction(peer, 'getdbhash') # get their db hash if newDBHash == False or not self._core._utils.validateHash(newDBHash): continue # if request failed, restart loop (peer is added to offline peers automatically) triedPeers.append(peer) if newDBHash != self._core.getAddressInfo(peer, 'DBHash'): self._core.setAddressInfo(peer, 'DBHash', newDBHash) try: - newBlocks = self.peerAction(peer, 'getBlockHashes') # get list of new block hashes + newBlocks = self.peerAction(peer, 'getblocklist') # get list of new block hashes except Exception as error: logger.warn('Could not get new blocks from %s.' % peer, error = error) newBlocks = False @@ -226,7 +226,7 @@ class OnionrCommunicatorDaemon: self.currentDownloading.append(blockHash) # So we can avoid concurrent downloading in other threads of same block logger.info("Attempting to download %s..." % blockHash) peerUsed = self.pickOnlinePeer() - content = self.peerAction(peerUsed, 'getData', data=blockHash) # block content from random peer (includes metadata) + content = self.peerAction(peerUsed, 'getdata/' + blockHash) # block content from random peer (includes metadata) if content != False and len(content) > 0: try: content = content.encode() @@ -423,7 +423,7 @@ class OnionrCommunicatorDaemon: if len(peer) == 0: return False #logger.debug('Performing ' + action + ' with ' + peer + ' on port ' + str(self.proxyPort)) - url = 'http://' + peer + '/public/?action=' + action + url = 'http://' + peer + '/' + action if len(data) > 0: url += '&data=' + data @@ -520,7 +520,7 @@ class OnionrCommunicatorDaemon: if peer in triedPeers: continue triedPeers.append(peer) - url = 'http://' + peer + '/public/upload/' + url = 'http://' + peer + '/upload' data = {'block': block.Block(bl).getRaw()} proxyType = '' if peer.endswith('.onion'): @@ -529,7 +529,7 @@ class OnionrCommunicatorDaemon: proxyType = 'i2p' logger.info("Uploading block to " + peer) if not self._core._utils.doPostRequest(url, data=data, proxyType=proxyType) == False: - self._core._utils.localCommand('waitForShare', data=bl) + self._core._utils.localCommand('waitforshare/' + bl) finishedUploads.append(bl) break for x in finishedUploads: diff --git a/onionr/core.py b/onionr/core.py index f463dd35..d2d55811 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -53,6 +53,7 @@ class Core: self.privateApiHostFile = self.dataDir + 'private-host.txt' self.addressDB = self.dataDir + 'address.db' self.hsAddress = '' + self.i2pAddress = config.get('i2p.ownAddr', None) self.bootstrapFileLocation = 'static-data/bootstrap-nodes.txt' self.bootstrapList = [] self.requirements = onionrvalues.OnionrValues() @@ -775,7 +776,7 @@ class Core: if payload != False: retData = self.setData(payload) # 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', data=retData) + self._utils.localCommand('waitforshare/' + retData) self.addToBlockDB(retData, selfInsert=True, dataSaved=True) #self.setBlockType(retData, meta['type']) self._utils.processBlockMetadata(retData) From 53f98c34498028bbdc07b444b4f64c28b2bca02e Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 20 Dec 2018 14:24:46 -0600 Subject: [PATCH 13/94] more endpoints reimplemented in new api --- onionr/communicator2.py | 4 ++-- onionr/onionr.py | 5 +++-- onionr/onionrproofs.py | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/onionr/communicator2.py b/onionr/communicator2.py index fa14864e..15b0d047 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -19,8 +19,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import gevent.monkey -gevent.monkey.patch_all() +#import gevent.monkey +#gevent.monkey.patch_all() import sys, os, core, config, json, requests, time, logger, threading, base64, onionr, uuid import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block import onionrdaemontools, onionrsockets, onionrchat, onionr, onionrproofs diff --git a/onionr/onionr.py b/onionr/onionr.py index 1d47972a..0493f1e4 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -20,8 +20,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import gevent.monkey -gevent.monkey.patch_all() import sys if sys.version_info[0] == 2 or sys.version_info[1] < 5: print('Error, Onionr requires Python 3.5+') @@ -71,6 +69,9 @@ class Onionr: self.onionrCore = core.Core() self.onionrUtils = onionrutils.OnionrUtils(self.onionrCore) + self.clientAPIInst = '' # Client http api instance + self.publicAPIInst = '' # Public http api instance + # Handle commands self.debug = False # Whole application debugging diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index 9665a4cb..0bbfee6b 100644 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -238,6 +238,7 @@ class POW: break else: time.sleep(2) + print('boi') except KeyboardInterrupt: self.shutdown() logger.warn('Got keyboard interrupt while waiting for POW result, stopping') From 1dd471b91e714750dcfea61be6a9ab85d03da032 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 22 Dec 2018 13:02:09 -0600 Subject: [PATCH 14/94] + Reformatted API, more efficient, standard, and secure now * Various bug fixes --- onionr/api.py | 83 +++++++++++++++++++------ onionr/communicator2.py | 15 +++-- onionr/core.py | 2 +- onionr/netcontroller.py | 2 +- onionr/onionr.py | 112 ++++++++++++++++++++-------------- onionr/onionrdaemontools.py | 2 +- onionr/onionrpeers.py | 38 ++++++------ onionr/onionrproofs.py | 1 - onionr/onionrutils.py | 16 ++--- onionr/static-data/index.html | 4 +- requirements.txt | 1 - 11 files changed, 174 insertions(+), 102 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index e2a6604c..f983677d 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -25,8 +25,6 @@ import core from onionrblockapi import Block import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config, onionr -API_VERSION = 0 - def guessMime(path): ''' Guesses the mime type of a file from the input filename @@ -67,6 +65,7 @@ class PublicAPI: self.torAdder = clientAPI._core.hsAddress self.i2pAdder = clientAPI._core.i2pAddress self.bindPort = config.get('client.public.port') + logger.info('Running public api on %s:%s' % (self.host, self.bindPort)) @app.before_request def validateRequest(): @@ -84,7 +83,7 @@ class PublicAPI: 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'" resp.headers['X-Frame-Options'] = 'deny' resp.headers['X-Content-Type-Options'] = "nosniff" - resp.headers['X-API'] = API_VERSION + resp.headers['X-API'] = onionr.API_VERSION return resp @app.route('/') @@ -105,8 +104,9 @@ class PublicAPI: return Response('\n'.join(bList)) @app.route('/getdata/') - def getBlockData(): + def getBlockData(name): resp = '' + data = name if clientAPI._utils.validateHash(data): if data not in self.hideBlocks: if os.path.exists(clientAPI._core.dataDir + 'blocks/' + data + '.dat'): @@ -117,20 +117,64 @@ class PublicAPI: resp = "" return Response(resp) + @app.route('/www/') + def wwwPublic(path): + if not config.get("www.public.run", True): + abort(403) + return send_from_directory(config.get('www.public.path', 'static-data/www/public/'), path) + @app.route('/ping') def ping(): return Response("pong!") - + @app.route('/getdbhash') def getDBHash(): return Response(clientAPI._utils.getBlockDBHash()) - + @app.route('/pex') def peerExchange(): - response = ','.join(self._core.listAdders()) + response = ','.join(clientAPI._core.listAdders()) if len(response) == 0: response = 'none' - resp = Response(response) + return Response(response) + + @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 block 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('0000'): + try: + newNode = newNode.decode() + except AttributeError: + pass + if clientAPI._core.addAddress(newNode): + resp = 'Success' + else: + logger.warn(newNode.decode() + ' failed to meet POW: ' + powHash) + resp = Response(resp) + return resp @app.route('/upload', methods=['post']) def upload(): @@ -156,6 +200,10 @@ class PublicAPI: return resp clientAPI.setPublicAPIInstance(self) + while self.torAdder == '': + clientAPI._core.refreshFirstStartVars() + self.torAdder = clientAPI._core.hsAddress + time.sleep(1) self.httpServer = WSGIServer((self.host, self.bindPort), app, log=None) self.httpServer.serve_forever() @@ -166,7 +214,7 @@ class API: callbacks = {'public' : {}, 'private' : {}} - def __init__(self, debug, API_VERSION): + def __init__(self, onionrInst, debug, API_VERSION): ''' Initialize the api server, preping variables for later use @@ -190,10 +238,11 @@ class API: self.timeBypassToken = base64.b16encode(os.urandom(32)).decode() self.publicAPI = None # gets set when the thread calls our setter... bad hack but kinda necessary with flask - threading.Thread(target=PublicAPI, args=(self,)).start() + #threading.Thread(target=PublicAPI, args=(self,)).start() self.host = setBindIP(self._core.privateApiHostFile) logger.info('Running api on %s:%s' % (self.host, self.bindPort)) self.httpServer = '' + onionrInst.setClientAPIInst(self) @app.before_request def validateRequest(): @@ -205,20 +254,20 @@ class API: abort(403) except KeyError: abort(403) - + @app.after_request def afterReq(resp): 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'" resp.headers['X-Frame-Options'] = 'deny' resp.headers['X-Content-Type-Options'] = "nosniff" - resp.headers['X-API'] = API_VERSION - resp.headers['Server'] = 'nginx' + resp.headers['X-API'] = onionr.API_VERSION + 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. return resp @app.route('/ping') def ping(): - return Respose("pong!") + return Response("pong!") @app.route('/') def hello(): @@ -256,10 +305,10 @@ class API: except AttributeError: pass return Response("bye") - + self.httpServer = WSGIServer((self.host, bindPort), app, log=None) self.httpServer.serve_forever() - + def setPublicAPIInstance(self, inst): assert isinstance(inst, PublicAPI) self.publicAPI = inst @@ -277,4 +326,4 @@ class API: else: return True except TypeError: - return False \ No newline at end of file + return False diff --git a/onionr/communicator2.py b/onionr/communicator2.py index 15b0d047..5d045a69 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -19,11 +19,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -#import gevent.monkey -#gevent.monkey.patch_all() import sys, os, core, config, json, requests, time, logger, threading, base64, onionr, uuid import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block import onionrdaemontools, onionrsockets, onionrchat, onionr, onionrproofs +import binascii from dependencies import secrets from defusedxml import minidom @@ -103,6 +102,7 @@ class OnionrCommunicatorDaemon: OnionrCommunicatorTimers(self, self.daemonTools.cooldownPeer, 30, requiresPeer=True) OnionrCommunicatorTimers(self, self.uploadBlock, 10, requiresPeer=True, maxThreads=1) OnionrCommunicatorTimers(self, self.daemonCommands, 6, maxThreads=1) + OnionrCommunicatorTimers(self, self.detectAPICrash, 5, maxThreads=1) deniableBlockTimer = OnionrCommunicatorTimers(self, self.daemonTools.insertDeniableBlock, 180, requiresPeer=True, maxThreads=1) netCheckTimer = OnionrCommunicatorTimers(self, self.daemonTools.netCheck, 600) @@ -232,7 +232,10 @@ class OnionrCommunicatorDaemon: content = content.encode() except AttributeError: pass - content = base64.b64decode(content) # content is base64 encoded in transport + try: + content = base64.b64decode(content) # content is base64 encoded in transport + except binascii.Error: + pass realHash = self._core._crypto.sha3Hash(content) try: realHash = realHash.decode() # bytes on some versions for some reason @@ -423,7 +426,7 @@ class OnionrCommunicatorDaemon: if len(peer) == 0: return False #logger.debug('Performing ' + action + ' with ' + peer + ' on port ' + str(self.proxyPort)) - url = 'http://' + peer + '/' + action + url = 'http://%s/%s' % (peer, action) if len(data) > 0: url += '&data=' + data @@ -546,9 +549,9 @@ class OnionrCommunicatorDaemon: def detectAPICrash(self): '''exit if the api server crashes/stops''' - if self._core._utils.localCommand('ping', silent=False) != 'pong': + if self._core._utils.localCommand('ping', silent=False) not in ('pong', 'pong!'): for i in range(5): - if self._core._utils.localCommand('ping') == 'pong': + if self._core._utils.localCommand('ping') in ('pong', 'pong!'): break # break for loop time.sleep(1) else: diff --git a/onionr/core.py b/onionr/core.py index d2d55811..a1d78e0a 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -154,7 +154,7 @@ class Core: if address == config.get('i2p.ownAddr', None) or address == self.hsAddress: return False - if type(address) is type(None) or len(address) == 0: + if type(address) is None or len(address) == 0: return False if self._utils.validateID(address): conn = sqlite3.connect(self.addressDB, timeout=10) diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py index 7566623e..c87b5e46 100644 --- a/onionr/netcontroller.py +++ b/onionr/netcontroller.py @@ -77,7 +77,7 @@ class NetController: hsVer = '# v2 onions' if config.get('tor.v3onions'): hsVer = 'HiddenServiceVersion 3' - logger.debug('Using v3 onions :)') + logger.debug('Using v3 onions') if os.path.exists(self.torConfigLocation): os.remove(self.torConfigLocation) diff --git a/onionr/onionr.py b/onionr/onionr.py index 0493f1e4..b9dd6260 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -67,6 +67,7 @@ class Onionr: data_exists = Onionr.setupConfig(self.dataDir, self = self) self.onionrCore = core.Core() + #self.deleteRunFiles() self.onionrUtils = onionrutils.OnionrUtils(self.onionrCore) self.clientAPIInst = '' # Client http api instance @@ -399,6 +400,16 @@ class Onionr: logger.info('Syntax: friend add/remove/list [address]') + def deleteRunFiles(self): + try: + os.remove(self.onionrCore.publicApiHostFile) + except FileNotFoundError: + pass + try: + os.remove(self.onionrCore.privateApiHostFile) + except FileNotFoundError: + pass + def banBlock(self): try: ban = sys.argv[2] @@ -695,6 +706,13 @@ class Onionr: os.remove('.onionr-lock') except FileNotFoundError: pass + def setClientAPIInst(self, inst): + self.clientAPIInst = inst + + def getClientApi(self): + while self.clientAPIInst == '': + time.sleep(0.5) + return self.clientAPIInst def daemon(self): ''' @@ -708,64 +726,66 @@ class Onionr: logger.debug('Runcheck file found on daemon start, deleting in advance.') os.remove('data/.runcheck') - apiTarget = api.API - apiThread = Thread(target = apiTarget, args = (self.debug, API_VERSION)) - apiThread.start() - + Thread(target=api.API, args=(self, self.debug, API_VERSION)).start() + Thread(target=api.PublicAPI, args=[self.getClientApi()]).start() try: - time.sleep(3) + time.sleep(0) except KeyboardInterrupt: logger.debug('Got keyboard interrupt, shutting down...') time.sleep(1) self.onionrUtils.localCommand('shutdown') + + apiHost = '' + while apiHost == '': + try: + with open(self.onionrCore.publicApiHostFile, 'r') as hostFile: + apiHost = hostFile.read() + except FileNotFoundError: + pass + time.sleep(0.5) + Onionr.setupConfig('data/', self = self) + + if self._developmentMode: + logger.warn('DEVELOPMENT MODE ENABLED (LESS SECURE)', timestamp = False) + net = NetController(config.get('client.public.port', 59497), apiServerIP=apiHost) + logger.debug('Tor is starting...') + if not net.startTor(): + self.onionrUtils.localCommand('shutdown') + sys.exit(1) + if len(net.myID) > 0 and config.get('general.security_level') == 0: + logger.debug('Started .onion service: %s' % (logger.colors.underline + net.myID)) else: - apiHost = '127.0.0.1' - if apiThread.isAlive(): - try: - with open(self.onionrCore.publicApiHostFile, 'r') as hostFile: - apiHost = hostFile.read() - except FileNotFoundError: - pass - Onionr.setupConfig('data/', self = self) + logger.debug('.onion service disabled') + logger.debug('Using public key: %s' % (logger.colors.underline + self.onionrCore._crypto.pubKey)) + time.sleep(1) - if self._developmentMode: - logger.warn('DEVELOPMENT MODE ENABLED (LESS SECURE)', timestamp = False) - net = NetController(config.get('client.public.port', 59497), apiServerIP=apiHost) - logger.debug('Tor is starting...') - if not net.startTor(): - self.onionrUtils.localCommand('shutdown') - sys.exit(1) - if len(net.myID) > 0 and config.get('general.security_level') == 0: - 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 + self.onionrCore._crypto.pubKey)) - time.sleep(1) + # TODO: make runable on windows + communicatorProc = subprocess.Popen([communicatorDaemon, 'run', str(net.socksPort)]) - # TODO: make runable on windows - communicatorProc = subprocess.Popen([communicatorDaemon, 'run', str(net.socksPort)]) + # print nice header thing :) + if config.get('general.display_header', True): + self.header() - # print nice header thing :) - if config.get('general.display_header', True): - self.header() + # print out debug info + self.version(verbosity = 5, function = logger.debug) + logger.debug('Python version %s' % platform.python_version()) - # print out debug info - self.version(verbosity = 5, function = logger.debug) - logger.debug('Python version %s' % platform.python_version()) + logger.debug('Started communicator.') - logger.debug('Started communicator.') + events.event('daemon_start', onionr = self) + try: + while True: + time.sleep(5) - events.event('daemon_start', onionr = self) - try: - while True: - time.sleep(5) - - # Break if communicator process ends, so we don't have left over processes - if communicatorProc.poll() is not None: - break - except KeyboardInterrupt: - self.onionrCore.daemonQueueAdd('shutdown') - self.onionrUtils.localCommand('shutdown') + # Break if communicator process ends, so we don't have left over processes + if communicatorProc.poll() is not None: + break + except KeyboardInterrupt: + self.onionrCore.daemonQueueAdd('shutdown') + self.onionrUtils.localCommand('shutdown') + time.sleep(3) + self.deleteRunFiles() + net.killTor() return def killDaemon(self): diff --git a/onionr/onionrdaemontools.py b/onionr/onionrdaemontools.py index ebe181c6..9f1ab2a2 100644 --- a/onionr/onionrdaemontools.py +++ b/onionr/onionrdaemontools.py @@ -45,7 +45,7 @@ class DaemonTools: ourID = self.daemon._core.hsAddress.strip() - url = 'http://' + peer + '/public/announce/' + url = 'http://' + peer + '/announce' data = {'node': ourID} combinedNodes = ourID + peer diff --git a/onionr/onionrpeers.py b/onionr/onionrpeers.py index 62716eee..e4793dfb 100644 --- a/onionr/onionrpeers.py +++ b/onionr/onionrpeers.py @@ -79,27 +79,29 @@ def peerCleanup(coreInst): logger.info('Cleaning peers...') config.reload() - minScore = int(config.get('peers.minimum_score', -100)) - maxPeers = int(config.get('peers.max_stored', 5000)) - adders = getScoreSortedPeerList(coreInst) adders.reverse() + + if len(adders) > 1: - for address in adders: - # Remove peers that go below the negative score - if PeerProfiles(address, coreInst).score < minScore: - coreInst.removeAddress(address) - try: - if (int(coreInst._utils.getEpoch()) - int(coreInst.getPeerInfo(address, 'dateSeen'))) >= 600: - expireTime = 600 - else: - expireTime = 86400 - coreInst._blacklist.addToDB(address, dataType=1, expire=expireTime) - except sqlite3.IntegrityError: #TODO just make sure its not a unique constraint issue - pass - except ValueError: - pass - logger.warn('Removed address ' + address + '.') + minScore = int(config.get('peers.minimum_score', -100)) + maxPeers = int(config.get('peers.max_stored', 5000)) + + for address in adders: + # Remove peers that go below the negative score + if PeerProfiles(address, coreInst).score < minScore: + coreInst.removeAddress(address) + try: + if (int(coreInst._utils.getEpoch()) - int(coreInst.getPeerInfo(address, 'dateSeen'))) >= 600: + expireTime = 600 + else: + expireTime = 86400 + coreInst._blacklist.addToDB(address, dataType=1, expire=expireTime) + except sqlite3.IntegrityError: #TODO just make sure its not a unique constraint issue + pass + except ValueError: + pass + logger.warn('Removed address ' + address + '.') # Unban probably not malicious peers TODO improve coreInst._blacklist.deleteExpired(dataType=1) diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index 0bbfee6b..9665a4cb 100644 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -238,7 +238,6 @@ class POW: break else: time.sleep(2) - print('boi') except KeyboardInterrupt: self.shutdown() logger.warn('Got keyboard interrupt while waiting for POW result, stopping') diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 75fcb7c2..c6cfdfb9 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -159,17 +159,17 @@ class OnionrUtils: config.reload() self.getTimeBypassToken() # TODO: URL encode parameters, just as an extra measure. May not be needed, but should be added regardless. - try: - with open(self._core.privateApiHostFile, 'r') as host: - hostname = host.read() - except FileNotFoundError: - return False + hostname = '' + while hostname == '': + try: + with open(self._core.privateApiHostFile, 'r') as host: + hostname = host.read() + except FileNotFoundError: + print('wat') + time.sleep(1) if data != '': data = '&data=' + urllib.parse.quote_plus(data) payload = 'http://%s:%s/%s%s' % (hostname, config.get('client.client.port'), command, data) - #payload = 'http://%s:%s/client/?action=%s&token=%s&timingToken=%s' % (hostname, config.get('client.client.port'), command, config.get('client.webpassword'), self.timingToken) - #if data != '': - # payload += '&data=' + urllib.parse.quote_plus(data) try: retData = requests.get(payload, headers={'token': config.get('client.webpassword')}).text except Exception as error: diff --git a/onionr/static-data/index.html b/onionr/static-data/index.html index 157f1586..916bfe9c 100644 --- a/onionr/static-data/index.html +++ b/onionr/static-data/index.html @@ -1,7 +1,7 @@

This is an Onionr Node

-

The content on this server is not necessarily created by the server owner, and was not necessarily stored specifically with the owner's knowledge of its contents.

+

The content on this server was not necessarily intentionally stored or created by the owner(s) of this server.

-

Onionr is a decentralized data storage system that anyone can insert data into.

+

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

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

diff --git a/requirements.txt b/requirements.txt index 2375324d..6bceb49a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ urllib3==1.23 requests==2.20.0 PyNaCl==1.2.1 gevent==1.3.6 -sha3==0.2.1 defusedxml==0.5.0 Flask==1.0.2 PySocks==1.6.8 From 8c79cd9583ebdaa5fdf293b63ec9bea60dce0847 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 22 Dec 2018 15:48:05 -0600 Subject: [PATCH 15/94] * removed randomized block insert times for now * listconn has a new alias --- onionr/core.py | 4 ++-- onionr/onionr.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/onionr/core.py b/onionr/core.py index a1d78e0a..1e698a4c 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -260,7 +260,7 @@ class Core: return conn = sqlite3.connect(self.blockDB, timeout=10) c = conn.cursor() - currentTime = self._utils.getEpoch() + currentTime = self._utils.getEpoch() + self._crypto.secrets.randbelow(301) if selfInsert or dataSaved: selfInsert = 1 else: @@ -763,7 +763,7 @@ class Core: metadata['meta'] = jsonMeta metadata['sig'] = signature metadata['signer'] = signer - metadata['time'] = self._utils.getRoundedEpoch() + self._crypto.secrets.randbelow(301) + metadata['time'] = self._utils.getRoundedEpoch() # ensure expire is integer and of sane length if type(expire) is not type(None): diff --git a/onionr/onionr.py b/onionr/onionr.py index b9dd6260..aadceb56 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -181,6 +181,7 @@ class Onionr: 'getfile': self.getFile, 'listconn': self.listConn, + 'list-conn': self.listConn, 'import-blocks': self.onionrUtils.importNewBlocks, 'importblocks': self.onionrUtils.importNewBlocks, From b45bb94375c047195c85537b02a87772123cc466 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 24 Dec 2018 00:12:46 -0600 Subject: [PATCH 16/94] added dynamic proof of work --- onionr/communicator2.py | 2 +- onionr/core.py | 24 +++++--- onionr/onionr.py | 1 - onionr/onionrblockapi.py | 4 +- onionr/onionrcrypto.py | 3 +- onionr/onionrproofs.py | 76 +++++++++++++++++++++++--- onionr/onionrutils.py | 7 ++- onionr/static-data/default_config.json | 11 ++-- onionr/storagecounter.py | 5 ++ 9 files changed, 104 insertions(+), 29 deletions(-) diff --git a/onionr/communicator2.py b/onionr/communicator2.py index 5d045a69..8e10e8b1 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -102,7 +102,7 @@ class OnionrCommunicatorDaemon: OnionrCommunicatorTimers(self, self.daemonTools.cooldownPeer, 30, requiresPeer=True) OnionrCommunicatorTimers(self, self.uploadBlock, 10, requiresPeer=True, maxThreads=1) OnionrCommunicatorTimers(self, self.daemonCommands, 6, maxThreads=1) - OnionrCommunicatorTimers(self, self.detectAPICrash, 5, maxThreads=1) + OnionrCommunicatorTimers(self, self.detectAPICrash, 30, maxThreads=1) deniableBlockTimer = OnionrCommunicatorTimers(self, self.daemonTools.insertDeniableBlock, 180, requiresPeer=True, maxThreads=1) netCheckTimer = OnionrCommunicatorTimers(self, self.daemonTools.netCheck, 600) diff --git a/onionr/core.py b/onionr/core.py index 1e698a4c..2a349f68 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -680,7 +680,10 @@ class Core: Inserts a block into the network encryptType must be specified to encrypt a block ''' - + allocationReachedMessage = 'Cannot insert block, disk allocation reached.' + if self._utils.storageCounter.isFull(): + logger.error(allocationReachedMessage) + return False retData = False # check nonce dataNonce = self._utils.bytesToStr(self._crypto.sha3Hash(data)) @@ -774,13 +777,18 @@ class Core: proof = onionrproofs.POW(metadata, data) payload = proof.waitForResult() if payload != False: - retData = self.setData(payload) - # 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) - self.addToBlockDB(retData, selfInsert=True, dataSaved=True) - #self.setBlockType(retData, meta['type']) - self._utils.processBlockMetadata(retData) - self.daemonQueueAdd('uploadBlock', retData) + try: + retData = self.setData(payload) + except onionrexceptions.DiskAllocationReached: + 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 + self._utils.localCommand('waitforshare/' + retData) + self.addToBlockDB(retData, selfInsert=True, dataSaved=True) + #self.setBlockType(retData, meta['type']) + self._utils.processBlockMetadata(retData) + self.daemonQueueAdd('uploadBlock', retData) if retData != False: events.event('insertBlock', onionr = None, threaded = False) diff --git a/onionr/onionr.py b/onionr/onionr.py index aadceb56..e108a3c7 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -719,7 +719,6 @@ class Onionr: ''' Starts the Onionr communication daemon ''' - communicatorDaemon = './communicator2.py' # remove runcheck if it exists diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py index e6506faf..93bc3201 100644 --- a/onionr/onionrblockapi.py +++ b/onionr/onionrblockapi.py @@ -245,8 +245,8 @@ class Block: blockFile.write(self.getRaw().encode()) else: self.hash = self.getCore().insertBlock(self.getContent(), header = self.getType(), sign = sign, meta = self.getMetadata(), expire = self.getExpire()) - - self.update() + if self.hash != False: + self.update() return self.getHash() else: diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py index 04c821cd..0daf6e3e 100644 --- a/onionr/onionrcrypto.py +++ b/onionr/onionrcrypto.py @@ -269,7 +269,8 @@ class OnionrCrypto: except AttributeError: pass - difficulty = math.floor(dataLen / 1000000) + difficulty = onionrproofs.getDifficultyForNewBlock(blockContent, ourBlock=False) + if difficulty < int(config.get('general.minimum_block_pow')): difficulty = int(config.get('general.minimum_block_pow')) mainHash = '0000000000000000000000000000000000000000000000000000000000000000'#nacl.hash.blake2b(nacl.utils.random()).decode() diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index 9665a4cb..8e3da0a4 100644 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -19,7 +19,55 @@ ''' import nacl.encoding, nacl.hash, nacl.utils, time, math, threading, binascii, logger, sys, base64, json -import core, config +import core, onionrutils, config +import onionrblockapi + +def getDifficultyModifier(coreOrUtilsInst=None): + '''Accepts a core or utils instance returns + the difficulty modifier for block storage based + on a variety of factors, currently only disk use. + ''' + classInst = coreOrUtilsInst + 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 + + percentUse = useFunc() + + if percentUse >= 0.50: + retData += 1 + elif percentUse >= 0.75: + retData += 2 + elif percentUse >= 0.95: + retData += 3 + + return retData + +def getDifficultyForNewBlock(data, ourBlock=True): + ''' + Get difficulty for block. Accepts size in integer, Block instance, or str/bytes full block contents + ''' + retData = 0 + 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, int): + dataSize = data + else: + raise ValueError('not Block, str, or int') + if ourBlock: + minDifficulty = config.get('general.minimum_send_pow') + else: + minDifficulty = config.get('general.minimum_block_pow') + + retData = max(minDifficulty, math.floor(dataSize / 1000000)) + getDifficultyModifier() + return retData def getHashDifficulty(h): ''' @@ -55,6 +103,7 @@ class DataPOW: self.difficulty = 0 self.data = data self.threadCount = threadCount + self.rounds = 0 config.reload() if forceDifficulty == 0: @@ -96,6 +145,7 @@ class DataPOW: while self.hashing: rand = nacl.utils.random() token = nacl.hash.blake2b(rand + self.data).decode() + self.rounds += 1 #print(token) if self.puzzle == token[0:self.difficulty]: self.hashing = False @@ -106,6 +156,7 @@ class DataPOW: endTime = math.floor(time.time()) if self.reporting: logger.debug('Found token after %s seconds: %s' % (endTime - startTime, token), timestamp=True) + logger.debug('Round count: %s' % (self.rounds,)) self.result = (token, rand) def shutdown(self): @@ -146,18 +197,28 @@ class DataPOW: return result class POW: - def __init__(self, metadata, data, threadCount = 5): + def __init__(self, metadata, data, threadCount = 5, forceDifficulty=0, coreInst=None): self.foundHash = False self.difficulty = 0 self.data = data self.metadata = metadata self.threadCount = threadCount - dataLen = len(data) + len(json.dumps(metadata)) - self.difficulty = math.floor(dataLen / 1000000) - if self.difficulty <= 2: - self.difficulty = int(config.get('general.minimum_block_pow')) + try: + assert isinstance(coreInst, core.Core) + except AssertionError: + myCore = core.Core() + else: + myCore = coreInst + dataLen = len(data) + len(json.dumps(metadata)) + + 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: @@ -167,8 +228,7 @@ class POW: self.mainHash = '0' * 64 self.puzzle = self.mainHash[0:min(self.difficulty, len(self.mainHash))] - - myCore = core.Core() + for i in range(max(1, threadCount)): t = threading.Thread(name = 'thread%s' % i, target = self.pow, args = (True,myCore)) t.start() diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index c6cfdfb9..afd61866 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -155,18 +155,21 @@ 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 = '' + maxWait = 5 + waited = 0 while hostname == '': try: with open(self._core.privateApiHostFile, 'r') as host: hostname = host.read() except FileNotFoundError: - print('wat') time.sleep(1) + waited += 1 + if waited == maxWait: + return False if data != '': data = '&data=' + urllib.parse.quote_plus(data) payload = 'http://%s:%s/%s%s' % (hostname, config.get('client.client.port'), command, data) diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index 13436852..524ee185 100644 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -1,14 +1,13 @@ { "general" : { "dev_mode" : true, - "display_header" : true, - "minimum_block_pow": 5, - "minimum_send_pow": 5, + "display_header" : false, + "minimum_block_pow": 4, + "minimum_send_pow": 4, "socket_servers": false, "security_level": 0, "max_block_age": 2678400, - "public_key": "", - "use_new_api_server": false + "public_key": "" }, "www" : { @@ -70,7 +69,7 @@ }, "allocations" : { - "disk" : 10000000000, + "disk" : 2000, "net_total" : 1000000000, "blockCache" : 5000000, "blockCacheTotal" : 50000000 diff --git a/onionr/storagecounter.py b/onionr/storagecounter.py index 4468dacc..863145f9 100644 --- a/onionr/storagecounter.py +++ b/onionr/storagecounter.py @@ -43,6 +43,11 @@ class StorageCounter: except FileNotFoundError: pass return retData + + def getPercent(self): + '''Return percent (decimal/float) of disk space we're using''' + amount = self.getAmount() + return round(amount / self._core.config.get('allocations.disk'), 2) def addBytes(self, amount): '''Record that we are now using more disk space, unless doing so would exceed configured max''' From 2289171b0f9be8fb83370a2477b04e243c94ab02 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 26 Dec 2018 00:14:05 -0600 Subject: [PATCH 17/94] started a simple board plugin --- .gitlab-ci.yml | 6 --- .travis.yml | 8 ---- onionr-daemon-linux | 2 + onionr/api.py | 58 +++++++++++++++++++++---- onionr/onionrcrypto.py | 2 +- onionr/onionrproofs.py | 2 + onionr/static-data/default_config.json | 6 +-- onionr/static-data/www/board/board.js | 32 ++++++++++++++ onionr/static-data/www/board/index.html | 16 +++++++ onionr/static-data/www/board/theme.css | 0 10 files changed, 105 insertions(+), 27 deletions(-) delete mode 100644 .gitlab-ci.yml delete mode 100644 .travis.yml create mode 100644 onionr-daemon-linux create mode 100644 onionr/static-data/www/board/board.js create mode 100644 onionr/static-data/www/board/index.html create mode 100644 onionr/static-data/www/board/theme.css diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 292dfb14..00000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,6 +0,0 @@ -test: - script: - - apt-get update -qy - - apt-get install -y python3-dev python3-pip tor - - pip3 install -r requirements.txt - - make test \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 603021b5..00000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: python -python: - - "3.6.4" -# install dependencies -install: - - sudo apt install tor - - pip install -r requirements.txt -script: make test diff --git a/onionr-daemon-linux b/onionr-daemon-linux new file mode 100644 index 00000000..d72ac015 --- /dev/null +++ b/onionr-daemon-linux @@ -0,0 +1,2 @@ +#!/usr/bin/sh +nohup ./run-linux start & disown diff --git a/onionr/api.py b/onionr/api.py index f983677d..753afcce 100755 --- a/onionr/api.py +++ b/onionr/api.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 flask +import flask, cgi from flask import request, Response, abort, send_from_directory from gevent.pywsgi import WSGIServer import sys, random, threading, hmac, hashlib, base64, time, math, os, json @@ -221,7 +221,8 @@ class API: This initilization 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 ''' - + # assert isinstance(onionrInst, onionr.Onionr) + print(type(onionrInst)) # configure logger and stuff onionr.Onionr.setupConfig('data/', self = self) @@ -234,6 +235,8 @@ class API: bindPort = int(config.get('client.client.port', 59496)) self.bindPort = bindPort + self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'board', 'boardContent') + self.clientToken = config.get('client.webpassword') self.timeBypassToken = base64.b16encode(os.urandom(32)).decode() @@ -249,6 +252,8 @@ class API: '''Validate request has set password and is the correct hostname''' 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): abort(403) @@ -257,7 +262,8 @@ class API: @app.after_request def afterReq(resp): - 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'" + #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'" + 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['X-API'] = onionr.API_VERSION @@ -265,20 +271,54 @@ class API: resp.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch. return resp + @app.route('/board/', endpoint='board') + def loadBoard(): + return send_from_directory('static-data/www/board/', "index.html") + + @app.route('/board/', endpoint='boardContent') + def boardContent(path): + return send_from_directory('static-data/www/board/', path) + + @app.route('/www/', endpoint='www') + def wwwPublic(path): + if not config.get("www.private.run", True): + abort(403) + return send_from_directory(config.get('www.private.path', 'static-data/www/private/'), path) + @app.route('/ping') def ping(): return Response("pong!") - @app.route('/') + @app.route('/', endpoint='onionrhome') def hello(): - return Response("hello client") + return Response("Welcome to Onionr") + + @app.route('/getblocksbytype/') + def getBlocksByType(name): + blocks = self._core.getBlocksByType(name) + return Response(','.join(blocks)) + + @app.route('/gethtmlsafeblockdata/') + def getData(name): + resp = '' + if self._core._utils.validateHash(name): + try: + resp = cgi.escape(Block(name).bcontent, quote=True) + except TypeError: + pass + else: + abort(404) + return Response(resp) - @app.route('/site/') - def site(): - bHash = block + @app.route('/site/', endpoint='site') + def site(name): + bHash = name resp = 'Not Found' if self._core._utils.validateHash(bHash): - resp = Block(bHash).bcontent + try: + resp = Block(bHash).bcontent + except TypeError: + pass try: resp = base64.b64decode(resp) except: diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py index 0daf6e3e..03da9213 100644 --- a/onionr/onionrcrypto.py +++ b/onionr/onionrcrypto.py @@ -268,7 +268,7 @@ class OnionrCrypto: blockHash = blockHash.decode() # bytes on some versions for some reason except AttributeError: pass - + difficulty = onionrproofs.getDifficultyForNewBlock(blockContent, ourBlock=False) if difficulty < int(config.get('general.minimum_block_pow')): diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index 8e3da0a4..f1645a49 100644 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -57,6 +57,8 @@ def getDifficultyForNewBlock(data, ourBlock=True): 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: diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index 524ee185..ed9270db 100644 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -2,8 +2,8 @@ "general" : { "dev_mode" : true, "display_header" : false, - "minimum_block_pow": 4, - "minimum_send_pow": 4, + "minimum_block_pow": 3, + "minimum_send_pow": 3, "socket_servers": false, "security_level": 0, "max_block_age": 2678400, @@ -69,7 +69,7 @@ }, "allocations" : { - "disk" : 2000, + "disk" : 100000000, "net_total" : 1000000000, "blockCache" : 5000000, "blockCacheTotal" : 50000000 diff --git a/onionr/static-data/www/board/board.js b/onionr/static-data/www/board/board.js new file mode 100644 index 00000000..fbdddd51 --- /dev/null +++ b/onionr/static-data/www/board/board.js @@ -0,0 +1,32 @@ +webpassword = '' +requested = {} +document.getElementById('feed').innerText = 'none :)' + +function httpGet(theUrl) { + var xmlHttp = new XMLHttpRequest() + xmlHttp.open( "GET", theUrl, false ) // false for synchronous request + xmlHttp.setRequestHeader('token', webpassword) + xmlHttp.send( null ) + return xmlHttp.responseText +} +function appendMessages(msg){ + document.getElementById('feed').append(msg) + document.getElementById('feed').appendChild(document.createElement('br')) +} + +function getBlocks(){ + var feedText = httpGet('/getblocksbytype/txt') + var blockList = feedText.split(',') + for (i = 0; i < blockList.length; i++){ + bl = httpGet('/gethtmlsafeblockdata/' + blockList[i]) + appendMessages(bl) + } +} + +document.getElementById('webpassword').oninput = function(){ + webpassword = document.getElementById('webpassword').value +} + +document.getElementById('refreshFeed').onclick = function(){ + getBlocks() +} \ No newline at end of file diff --git a/onionr/static-data/www/board/index.html b/onionr/static-data/www/board/index.html new file mode 100644 index 00000000..3b1c3426 --- /dev/null +++ b/onionr/static-data/www/board/index.html @@ -0,0 +1,16 @@ + + + + + + OnionrBoard + + + +

Onionr Board

+ + +
+ + + \ No newline at end of file diff --git a/onionr/static-data/www/board/theme.css b/onionr/static-data/www/board/theme.css new file mode 100644 index 00000000..e69de29b From c0fe0896ee51b72fc9c52e1ce4c9990be5ae9cc9 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 26 Dec 2018 23:27:46 -0600 Subject: [PATCH 18/94] work on board plugin and api --- onionr/api.py | 6 ++-- onionr/static-data/www/board/board.js | 42 ++++++++++++++++++++----- onionr/static-data/www/board/index.html | 13 +++++--- onionr/static-data/www/board/theme.css | 31 ++++++++++++++++++ 4 files changed, 78 insertions(+), 14 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 753afcce..75f02a13 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -222,7 +222,6 @@ class API: This also saves the used host (random localhost IP address) to the data folder in host.txt ''' # assert isinstance(onionrInst, onionr.Onionr) - print(type(onionrInst)) # configure logger and stuff onionr.Onionr.setupConfig('data/', self = self) @@ -235,7 +234,7 @@ class API: bindPort = int(config.get('client.client.port', 59496)) self.bindPort = bindPort - self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'board', 'boardContent') + self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'board', 'boardContent', 'sharedContent') self.clientToken = config.get('client.webpassword') self.timeBypassToken = base64.b16encode(os.urandom(32)).decode() @@ -278,6 +277,9 @@ class API: @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('/www/', endpoint='www') def wwwPublic(path): diff --git a/onionr/static-data/www/board/board.js b/onionr/static-data/www/board/board.js index fbdddd51..7f513357 100644 --- a/onionr/static-data/www/board/board.js +++ b/onionr/static-data/www/board/board.js @@ -1,30 +1,56 @@ webpassword = '' -requested = {} -document.getElementById('feed').innerText = 'none :)' +requested = [] + +document.getElementById('webpassWindow').style.display = 'block'; + +var windowHeight = window.innerHeight; +document.getElementById('webpassWindow').style.height = windowHeight + "px"; function httpGet(theUrl) { var xmlHttp = new XMLHttpRequest() xmlHttp.open( "GET", theUrl, false ) // false for synchronous request xmlHttp.setRequestHeader('token', webpassword) xmlHttp.send( null ) - return xmlHttp.responseText + if (xmlHttp.status == 200){ + return xmlHttp.responseText + } + else{ + return ""; + } } function appendMessages(msg){ - document.getElementById('feed').append(msg) + el = document.createElement('div') + el.className = 'entry' + el.innerText = msg + document.getElementById('feed').appendChild(el) document.getElementById('feed').appendChild(document.createElement('br')) } function getBlocks(){ + if (document.getElementById('none') !== null){ + document.getElementById('none').remove(); + + } var feedText = httpGet('/getblocksbytype/txt') var blockList = feedText.split(',') for (i = 0; i < blockList.length; i++){ - bl = httpGet('/gethtmlsafeblockdata/' + blockList[i]) - appendMessages(bl) - } + if (! requested.includes(blockList[i])){ + bl = httpGet('/gethtmlsafeblockdata/' + blockList[i]) + appendMessages(bl) + requested.push(blockList[i]) + } + } } -document.getElementById('webpassword').oninput = function(){ +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(){ diff --git a/onionr/static-data/www/board/index.html b/onionr/static-data/www/board/index.html index 3b1c3426..df48e912 100644 --- a/onionr/static-data/www/board/index.html +++ b/onionr/static-data/www/board/index.html @@ -5,12 +5,17 @@ OnionrBoard + - -

Onionr Board

- + + -
+
None Yet :)
\ No newline at end of file diff --git a/onionr/static-data/www/board/theme.css b/onionr/static-data/www/board/theme.css index e69de29b..766e4407 100644 --- a/onionr/static-data/www/board/theme.css +++ b/onionr/static-data/www/board/theme.css @@ -0,0 +1,31 @@ +h1, h2, h3{ + font-family: sans-serif; +} +.hidden{ + display: none; +} +p{ + font-family: sans-serif; +} +#webpassWindow{ + background-color: black; + border: 1px solid black; + border-radius: 5px; + width: 100%; + z-index: 2; + color: white; + text-align: center; +} + +.entry{ + color: red; +} + +#feed{ + margin-left: 2%; + margin-right: 25%; + margin-top: 1em; + border: 2px solid black; + padding: 5px; + min-height: 50px; +} \ No newline at end of file From 04421c6b6c81e72ac59d1ee7aa0948b9f2c4f192 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 28 Dec 2018 18:52:46 -0600 Subject: [PATCH 19/94] fixed friend command somewhat --- onionr/onionr.py | 42 ++---------------------------------------- 1 file changed, 2 insertions(+), 40 deletions(-) diff --git a/onionr/onionr.py b/onionr/onionr.py index e108a3c7..04bee60a 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -351,46 +351,9 @@ class Onionr: except IndexError: logger.error('Friend ID is required.') except onionrexceptions.KeyNotKnown: - logger.error('That peer is not in our database') - else: - if action == 'add': - friend.setTrust(1) - logger.info('Added %s as friend.' % (friend.publicKey,)) - else: - friend.setTrust(0) - logger.info('Removed %s as friend.' % (friend.publicKey,)) - else: - logger.info('Syntax: friend add/remove/list [address]') - - - def friendCmd(self): - '''List, add, or remove friend(s) - Changes their peer DB entry. - ''' - friend = '' - try: - # Get the friend command - action = sys.argv[2] - except IndexError: - logger.info('Syntax: friend add/remove/list [address]') - else: - action = action.lower() - if action == 'list': - # List out peers marked as our friend - for friend in self.onionrCore.listPeers(randomOrder=False, trust=1): - if friend == self.onionrCore._crypto.pubKey: # do not list our key - continue - friendProfile = onionrusers.OnionrUser(self.onionrCore, friend) - logger.info(friend + ' - ' + friendProfile.getName()) - elif action in ('add', 'remove'): - try: - friend = sys.argv[3] - if not self.onionrUtils.validatePubKey(friend): - raise onionrexceptions.InvalidPubkey('Public key is invalid') + self.onionrCore.addPeer(friend) friend = onionrusers.OnionrUser(self.onionrCore, friend) - except IndexError: - logger.error('Friend ID is required.') - else: + finally: if action == 'add': friend.setTrust(1) logger.info('Added %s as friend.' % (friend.publicKey,)) @@ -400,7 +363,6 @@ class Onionr: else: logger.info('Syntax: friend add/remove/list [address]') - def deleteRunFiles(self): try: os.remove(self.onionrCore.publicApiHostFile) From f53d3a11a641409088dd0d3e25873437e62e7ee1 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 30 Dec 2018 22:49:27 -0600 Subject: [PATCH 20/94] add dbstorage class --- onionr/onionrstorage.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 onionr/onionrstorage.py diff --git a/onionr/onionrstorage.py b/onionr/onionrstorage.py new file mode 100644 index 00000000..fc460917 --- /dev/null +++ b/onionr/onionrstorage.py @@ -0,0 +1,31 @@ +''' + Onionr - P2P Anonymous Storage Network + + This file handles block storage, providing an abstraction for storing blocks between file system and 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 core +class OnionrStorage: + def __init__(self, coreInst): + assert isinstance(coreInst, core.Core) + self._core = coreInst + return + + def store(self, hash, data): + return + + def getData(self, hash): + return \ No newline at end of file From 7eddb0a879ee349e77f989eb7182e249ead42476 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 5 Jan 2019 00:15:31 -0600 Subject: [PATCH 21/94] work on new storage system --- onionr/apimanager.py | 75 ----------------------- onionr/apiprivate.py | 32 ---------- onionr/apipublic.py | 41 ------------- onionr/core.py | 2 +- onionr/onionrstorage.py | 58 ++++++++++++++---- onionr/static-data/www/ui/dist/js/main.js | 2 +- 6 files changed, 49 insertions(+), 161 deletions(-) delete mode 100644 onionr/apimanager.py delete mode 100644 onionr/apiprivate.py delete mode 100644 onionr/apipublic.py diff --git a/onionr/apimanager.py b/onionr/apimanager.py deleted file mode 100644 index 44737097..00000000 --- a/onionr/apimanager.py +++ /dev/null @@ -1,75 +0,0 @@ -''' - Onionr - P2P Anonymous Storage Network - - Handles api data exchange, interfaced by both public and client 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 config, apipublic, apiprivate, core, socket, random, threading, time -config.reload() - -PRIVATE_API_VERSION = 0 -PUBLIC_API_VERSION = 1 - -DEV_MODE = config.get('general.dev_mode') - -def getOpenPort(): - '''Get a random open port''' - p = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - p.bind(("127.0.0.1",0)) - p.listen(1) - port = p.getsockname()[1] - p.close() - return port - -def getRandomLocalIP(): - '''Get a random local ip address''' - hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))] - host = '.'.join(hostOctets) - return host - -class APIManager: - def __init__(self, coreInst): - assert isinstance(coreInst, core.Core) - self.core = coreInst - self.utils = coreInst._utils - self.crypto = coreInst._crypto - - # if this gets set to true, both the public and private apis will shutdown - self.shutdown = False - - publicIP = '127.0.0.1' - privateIP = '127.0.0.1' - if DEV_MODE: - # set private and local api servers bind IPs to random localhost (127.x.x.x), make sure not the same - privateIP = getRandomLocalIP() - while True: - publicIP = getRandomLocalIP() - if publicIP != privateIP: - break - - # Make official the IPs and Ports - self.publicIP = publicIP - self.privateIP = privateIP - self.publicPort = config.get('client.port', 59496) - self.privatePort = config.get('client.port', 59496) - - # Run the API servers in new threads - self.publicAPI = apipublic.APIPublic(self) - self.privateAPI = apiprivate.APIPrivate(self) - threading.Thread(target=self.publicAPI.run).start() - threading.Thread(target=self.privateAPI.run).start() - while not self.shutdown: - time.sleep(1) \ No newline at end of file diff --git a/onionr/apiprivate.py b/onionr/apiprivate.py deleted file mode 100644 index 7f62b9d5..00000000 --- a/onionr/apiprivate.py +++ /dev/null @@ -1,32 +0,0 @@ -''' - Onionr - P2P Anonymous Storage Network - - Handle incoming commands from the client. Intended for localhost use -''' -''' - 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 flask, apimanager -from flask import request, Response, abort, send_from_directory -from gevent.pywsgi import WSGIServer - -class APIPrivate: - def __init__(self, managerInst): - assert isinstance(managerInst, apimanager.APIManager) - self.app = flask.Flask(__name__) # The flask application, which recieves data from the greenlet wsgiserver - self.httpServer = WSGIServer((managerInst.privateIP, managerInst.privatePort), self.app, log=None) - - def run(self): - self.httpServer.serve_forever() - return \ No newline at end of file diff --git a/onionr/apipublic.py b/onionr/apipublic.py deleted file mode 100644 index 9308f50a..00000000 --- a/onionr/apipublic.py +++ /dev/null @@ -1,41 +0,0 @@ -''' - Onionr - P2P Anonymous Storage Network - - Handle incoming commands from other Onionr nodes, over HTTP -''' -''' - 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 flask, apimanager -from flask import request, Response, abort, send_from_directory -from gevent.pywsgi import WSGIServer - - -class APIPublic: - def __init__(self, managerInst): - assert isinstance(managerInst, apimanager.APIManager) - app = flask.Flask(__name__) - @app.route('/') - def banner(): - try: - with open('static-data/index.html', 'r') as html: - resp = Response(html.read(), mimetype='text/html') - except FileNotFoundError: - resp = Response("") - return resp - self.httpServer = WSGIServer((managerInst.publicIP, managerInst.publicPort), app) - - def run(self): - self.httpServer.serve_forever() - return diff --git a/onionr/core.py b/onionr/core.py index 92ffad2d..cafdf4de 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -48,8 +48,8 @@ class Core: self.queueDB = self.dataDir + 'queue.db' self.peerDB = self.dataDir + 'peers.db' self.blockDB = self.dataDir + 'blocks.db' - self.blockDataDB = self.dataDir + 'block-data.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' diff --git a/onionr/onionrstorage.py b/onionr/onionrstorage.py index fc460917..9bbb7207 100644 --- a/onionr/onionrstorage.py +++ b/onionr/onionrstorage.py @@ -17,15 +17,51 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import core -class OnionrStorage: - def __init__(self, coreInst): - assert isinstance(coreInst, core.Core) - self._core = coreInst - return +import core, sys, sqlite3, os + +DB_ENTRY_SIZE_LIMIT = 10000 # Will be a config option + +def _dbInsert(coreInst, blockHash, data): + assert isinstance(core, core.Core) + conn = sqlite3.connect(coreInst.blockDataDB, timeout=10) + c = conn.cursor() + data = (blockHash, data) + c.execute('INSERT INTO blockData (hash, data) VALUES(?, ?);', data) + conn.commit() + conn.close() + +def _dbFetch(coreInst, blockHash): + conn = sqlite3.connect(coreInst.blockDataDB, timeout=10) + c = conn.cursor() + for i in c.execute('SELECT data from blockData where hash = ?', (blockHash,)): + return i[0] + conn.commit() + conn.close() + return None + +def store(coreInst, blockHash, data): + assert isinstance(coreInst, core.Core) + assert self._core._utils.validateHash(blockHash) + assert self._core._crypto.sha3Hash(data) == blockHash - def store(self, hash, data): - return - - def getData(self, hash): - return \ No newline at end of file + if DB_ENTRY_SIZE_LIMIT >= sys.getsizeof(data): + _dbInsert(coreInst, blockHash, data) + else: + with open('%s/%s.dat' % (coreInst.blockDataLocation, blockHash), 'w') as blockFile: + blockFile.write(data) + +def getData(coreInst, bHash): + assert isinstance(coreInst, core.Core) + assert self._core._utils.validateHash(blockHash) + + # First check DB for data entry by hash + # if no entry, check disk + # If no entry in either, raise an exception + retData = '' + fileLocation = '%s/%s.dat' % (coreInst.blockDataLocation, bHash) + if os.path.exists(fileLocation): + with open(fileLocation, 'r') as block: + retData = block.read() + else: + retData = _dbFetch(coreInst, bHash) + return \ No newline at end of file diff --git a/onionr/static-data/www/ui/dist/js/main.js b/onionr/static-data/www/ui/dist/js/main.js index 6fc7c3d1..0ddf141e 100644 --- a/onionr/static-data/www/ui/dist/js/main.js +++ b/onionr/static-data/www/ui/dist/js/main.js @@ -704,7 +704,7 @@ if(tt !== null && tt !== undefined) { if(getWebPassword() === null) { var password = ""; while(password.length != 64) { - password = prompt("Please enter the web password (run `./RUN-LINUX.sh --get-password`)"); + password = prompt("Please enter the web password (run `./RUN-LINUX.sh --details`)"); } setWebPassword(password); From 84fdb23b1cf5ed0d6f4431e3005a5bce224e00e5 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 5 Jan 2019 16:16:36 -0600 Subject: [PATCH 22/94] dbstorage largely complete --- onionr/communicator2.py | 4 ++- onionr/core.py | 12 ++++--- onionr/dbcreator.py | 10 +++--- onionr/onionrblockapi.py | 13 ++++---- onionr/onionrstorage.py | 31 +++++++++++++------ .../static-data/default-plugins/flow/main.py | 7 +++-- 6 files changed, 49 insertions(+), 28 deletions(-) diff --git a/onionr/communicator2.py b/onionr/communicator2.py index 8e10e8b1..bf99ff56 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -273,7 +273,9 @@ class OnionrCommunicatorDaemon: pass # Punish peer for sharing invalid block (not always malicious, but is bad regardless) onionrpeers.PeerProfiles(peerUsed, self._core).addScore(-50) - logger.warn('Block hash validation failed for ' + blockHash + ' got ' + tempHash) + 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) if removeFromQueue: try: self.blockQueue.remove(blockHash) # remove from block queue both if success or false diff --git a/onionr/core.py b/onionr/core.py index cafdf4de..a4783d08 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -22,7 +22,7 @@ from onionrblockapi import Block import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions, onionrvalues import onionrblacklist, onionrchat, onionrusers -import dbcreator +import dbcreator, onionrstorage if sys.version_info < (3, 6): try: @@ -278,6 +278,7 @@ class Core: Simply return the data associated to a hash ''' + ''' try: # logger.debug('Opening %s' % (str(self.blockDataLocation) + str(hash) + '.dat')) dataFile = open(self.blockDataLocation + hash + '.dat', 'rb') @@ -285,6 +286,8 @@ class Core: dataFile.close() except FileNotFoundError: data = False + ''' + data = onionrstorage.getData(self, hash) return data @@ -309,9 +312,10 @@ class Core: #raise Exception("Data is already set for " + dataHash) else: if self._utils.storageCounter.addBytes(dataSize) != False: - blockFile = open(blockFileName, 'wb') - blockFile.write(data) - blockFile.close() + #blockFile = open(blockFileName, 'wb') + #blockFile.write(data) + #blockFile.close() + onionrstorage.store(self, data, blockHash=dataHash) conn = sqlite3.connect(self.blockDB, timeout=10) c = conn.cursor() c.execute("UPDATE hashes SET dataSaved=1 WHERE hash = ?;", (dataHash,)) diff --git a/onionr/dbcreator.py b/onionr/dbcreator.py index cde4fd83..19de9e84 100644 --- a/onionr/dbcreator.py +++ b/onionr/dbcreator.py @@ -92,11 +92,11 @@ class DBCreator: expire int - block expire date in epoch ''' if os.path.exists(self.core.blockDB): - raise Exception("Block database already exists") + raise FileExistsError("Block database already exists") conn = sqlite3.connect(self.core.blockDB) c = conn.cursor() c.execute('''CREATE TABLE hashes( - hash text distinct not null, + hash text not null, dateReceived int, decrypted int, dataType text, @@ -114,11 +114,11 @@ class DBCreator: def createBlockDataDB(self): if os.path.exists(self.core.blockDataDB): - raise Exception("Block data database already exists") + raise FileExistsError("Block data database already exists") conn = sqlite3.connect(self.core.blockDataDB) c = conn.cursor() c.execute('''CREATE TABLE blockData( - hash text distinct not null, + hash text not null, data blob not null ); ''') @@ -130,7 +130,7 @@ class DBCreator: Create the forward secrecy key db (*for *OUR* keys*) ''' if os.path.exists(self.core.forwardKeysFile): - raise Exception("Block database already exists") + raise FileExistsError("Block database already exists") conn = sqlite3.connect(self.core.forwardKeysFile) c = conn.cursor() c.execute('''CREATE TABLE myForwardKeys( diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py index 93bc3201..39af3cee 100644 --- a/onionr/onionrblockapi.py +++ b/onionr/onionrblockapi.py @@ -19,7 +19,7 @@ ''' import core as onionrcore, logger, config, onionrexceptions, nacl.exceptions, onionrusers -import json, os, sys, datetime, base64 +import json, os, sys, datetime, base64, onionrstorage class Block: blockCacheOrder = list() # NEVER write your own code that writes to this! @@ -164,13 +164,13 @@ class Block: filelocation = self.core.dataDir + 'blocks/%s.dat' % self.getHash() if readfile: - with open(filelocation, 'rb') as f: - blockdata = f.read().decode() + blockdata = onionrstorage.getData(self.core, self.getHash()).decode() + #with open(filelocation, 'rb') as f: + #blockdata = f.read().decode() self.blockFile = filelocation else: self.blockFile = None - # parse block self.raw = str(blockdata) self.bheader = json.loads(self.getRaw()[:self.getRaw().index('\n')]) @@ -241,8 +241,9 @@ class Block: try: if self.isValid() is True: if (not self.getBlockFile() is None) and (recreate is True): - with open(self.getBlockFile(), 'wb') as blockFile: - blockFile.write(self.getRaw().encode()) + onionrstorage.store(self.core, self.getRaw().encode()) + #with open(self.getBlockFile(), 'wb') as blockFile: + # blockFile.write(self.getRaw().encode()) else: self.hash = self.getCore().insertBlock(self.getContent(), header = self.getType(), sign = sign, meta = self.getMetadata(), expire = self.getExpire()) if self.hash != False: diff --git a/onionr/onionrstorage.py b/onionr/onionrstorage.py index 9bbb7207..2a2c44ad 100644 --- a/onionr/onionrstorage.py +++ b/onionr/onionrstorage.py @@ -17,12 +17,19 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import core, sys, sqlite3, os +import core, sys, sqlite3, os, dbcreator DB_ENTRY_SIZE_LIMIT = 10000 # Will be a config option +def dbCreate(coreInst): + try: + dbcreator.DBCreator(coreInst).createBlockDataDB() + except FileExistsError: + pass + def _dbInsert(coreInst, blockHash, data): - assert isinstance(core, core.Core) + assert isinstance(coreInst, core.Core) + dbCreate(coreInst) conn = sqlite3.connect(coreInst.blockDataDB, timeout=10) c = conn.cursor() data = (blockHash, data) @@ -31,6 +38,8 @@ def _dbInsert(coreInst, blockHash, data): conn.close() def _dbFetch(coreInst, blockHash): + assert isinstance(coreInst, core.Core) + dbCreate(coreInst) conn = sqlite3.connect(coreInst.blockDataDB, timeout=10) c = conn.cursor() for i in c.execute('SELECT data from blockData where hash = ?', (blockHash,)): @@ -39,20 +48,24 @@ def _dbFetch(coreInst, blockHash): conn.close() return None -def store(coreInst, blockHash, data): +def store(coreInst, data, blockHash=''): assert isinstance(coreInst, core.Core) - assert self._core._utils.validateHash(blockHash) - assert self._core._crypto.sha3Hash(data) == blockHash + assert coreInst._utils.validateHash(blockHash) + ourHash = coreInst._crypto.sha3Hash(data) + if blockHash != '': + assert ourHash == blockHash + else: + blockHash = ourHash if DB_ENTRY_SIZE_LIMIT >= sys.getsizeof(data): _dbInsert(coreInst, blockHash, data) else: - with open('%s/%s.dat' % (coreInst.blockDataLocation, blockHash), 'w') as blockFile: + with open('%s/%s.dat' % (coreInst.blockDataLocation, blockHash), 'wb') as blockFile: blockFile.write(data) def getData(coreInst, bHash): assert isinstance(coreInst, core.Core) - assert self._core._utils.validateHash(blockHash) + assert coreInst._utils.validateHash(bHash) # First check DB for data entry by hash # if no entry, check disk @@ -60,8 +73,8 @@ def getData(coreInst, bHash): retData = '' fileLocation = '%s/%s.dat' % (coreInst.blockDataLocation, bHash) if os.path.exists(fileLocation): - with open(fileLocation, 'r') as block: + with open(fileLocation, 'rb') as block: retData = block.read() else: retData = _dbFetch(coreInst, bHash) - return \ No newline at end of file + 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 6f430e82..8ef0f5f8 100644 --- a/onionr/static-data/default-plugins/flow/main.py +++ b/onionr/static-data/default-plugins/flow/main.py @@ -54,9 +54,10 @@ class OnionrFlow: self.flowRunning = False expireTime = self.myCore._utils.getEpoch() + 43200 if len(message) > 0: - insertBL = Block(content = message, type = 'txt', expire=expireTime, core = self.myCore) - insertBL.setMetadata('ch', self.channel) - insertBL.save() + self.myCore.insertBlock(message, header='txt', expire=expireTime) + #insertBL = Block(content = message, type = 'txt', expire=expireTime, core = self.myCore) + #insertBL.setMetadata('ch', self.channel) + #insertBL.save() logger.info("Flow is exiting, goodbye") return From aeb9a6e77554aa9cd4236c1bb938a95268efe008 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 6 Jan 2019 23:50:20 -0600 Subject: [PATCH 23/94] work on gui, dbstorage, daemon queue responses --- onionr/api.py | 17 +++++++++- onionr/communicator2.py | 8 +++-- onionr/config.py | 2 +- onionr/core.py | 18 ++++++---- onionr/dbcreator.py | 3 +- onionr/onionr.py | 11 ++++-- onionr/onionrblockapi.py | 11 ++++-- onionr/onionrgui.py | 34 +++++++++++++++---- onionr/onionrstorage.py | 4 ++- onionr/onionrutils.py | 8 +++-- .../static-data/default-plugins/flow/main.py | 9 +++-- 11 files changed, 95 insertions(+), 30 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 75f02a13..62467631 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -109,7 +109,7 @@ class PublicAPI: data = name if clientAPI._utils.validateHash(data): if data not in self.hideBlocks: - if os.path.exists(clientAPI._core.dataDir + 'blocks/' + data + '.dat'): + if data in clientAPI._core.getBlockList(): block = Block(hash=data.encode(), core=clientAPI._core) resp = base64.b64encode(block.getRaw().encode()).decode() if len(resp) == 0: @@ -244,6 +244,8 @@ class API: self.host = setBindIP(self._core.privateApiHostFile) logger.info('Running api on %s:%s' % (self.host, self.bindPort)) self.httpServer = '' + + self.queueResponse = {} onionrInst.setClientAPIInst(self) @app.before_request @@ -287,6 +289,19 @@ class API: abort(403) return send_from_directory(config.get('www.private.path', 'static-data/www/private/'), path) + @app.route('/queueResponseAdd/', methods=['post']) + def queueResponseAdd(name): + self.queueResponse[name] = request.form['data'] + return Response('success') + + @app.route('/queueResponse/') + def queueResponse(name): + try: + res = self.queueResponse[name] + except KeyError: + resp = '' + return Response(resp) + @app.route('/ping') def ping(): return Response("pong!") diff --git a/onionr/communicator2.py b/onionr/communicator2.py index bf99ff56..6adb1f2c 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -472,7 +472,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': @@ -486,7 +486,7 @@ class OnionrCommunicatorDaemon: logger.debug('Status check; looks good.') open(self._core.dataDir + '.runcheck', 'w+').close() elif cmd[0] == 'connectedPeers': - self.printOnlinePeers() + response = '\n'.join(list(self.onlinePeers)).strip() elif cmd[0] == 'pex': for i in self.timers: if i.timerFunction.__name__ == 'lookupAdders': @@ -507,6 +507,10 @@ class OnionrCommunicatorDaemon: else: logger.info('Recieved daemonQueue command:' + cmd[0]) + if cmd[4] != '': + if response != '': + self._core._utils.localCommand('queueResponseAdd', data='/' + cmd[4], post=True, postData=response) + self.decrementThreadCount('daemonCommands') def uploadBlock(self): diff --git a/onionr/config.py b/onionr/config.py index 7c986b83..3a358233 100644 --- a/onionr/config.py +++ b/onionr/config.py @@ -129,7 +129,7 @@ def reload(): with open(get_config_file(), 'r', encoding="utf8") as configfile: set_config(json.loads(configfile.read())) except: - logger.warn('Failed to parse configuration file.') + logger.debug('Failed to parse configuration file.') def get_config(): ''' diff --git a/onionr/core.py b/onionr/core.py index a4783d08..ce29a0a2 100644 --- 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, math, base64, tarfile, nacl, logger, json, netcontroller, math, config +import sqlite3, os, sys, time, math, base64, tarfile, nacl, logger, json, netcontroller, math, config, uuid from onionrblockapi import Block import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions, onionrvalues @@ -342,7 +342,7 @@ class Core: conn = sqlite3.connect(self.queueDB, timeout=10) c = conn.cursor() try: - for row in c.execute('SELECT command, data, date, min(ID) FROM commands group by id'): + for row in c.execute('SELECT command, data, date, min(ID), responseID FROM commands group by id'): retData = row break except sqlite3.OperationalError: @@ -357,21 +357,20 @@ class Core: return retData - def daemonQueueAdd(self, command, data=''): + def daemonQueueAdd(self, command, data='', responseID=''): ''' Add a command to the daemon queue, used by the communication daemon (communicator.py) ''' retData = True - # Intended to be used by the web server date = self._utils.getEpoch() conn = sqlite3.connect(self.queueDB, timeout=10) c = conn.cursor() - t = (command, data, date) + t = (command, data, date, responseID) try: - c.execute('INSERT INTO commands (command, data, date) VALUES(?, ?, ?)', t) + c.execute('INSERT INTO commands (command, data, date, responseID) VALUES(?, ?, ?, ?)', t) conn.commit() conn.close() except sqlite3.OperationalError: @@ -379,6 +378,13 @@ class Core: self.daemonQueue() events.event('queue_push', data = {'command': command, 'data': data}, onionr = None) return retData + + 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', data='/' + responseID, post=True) def clearDaemonQueue(self): ''' diff --git a/onionr/dbcreator.py b/onionr/dbcreator.py index 19de9e84..f728254a 100644 --- a/onionr/dbcreator.py +++ b/onionr/dbcreator.py @@ -152,7 +152,6 @@ class DBCreator: conn = sqlite3.connect(self.core.queueDB, timeout=10) c = conn.cursor() # Create table - c.execute('''CREATE TABLE commands - (id integer primary key autoincrement, command text, data text, date text)''') + c.execute('''CREATE TABLE commands (id integer primary key autoincrement, command text, data text, date text, responseID text)''') conn.commit() conn.close() \ No newline at end of file diff --git a/onionr/onionr.py b/onionr/onionr.py index 04bee60a..979ad189 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -25,7 +25,7 @@ if sys.version_info[0] == 2 or sys.version_info[1] < 5: print('Error, Onionr requires Python 3.5+') sys.exit(1) import os, base64, random, getpass, shutil, subprocess, requests, time, platform, datetime, re, json, getpass, sqlite3 -import webbrowser +import webbrowser, uuid from threading import Thread import api, core, config, logger, onionrplugins as plugins, onionrevents as events import onionrutils @@ -394,7 +394,14 @@ class Onionr: return def listConn(self): - self.onionrCore.daemonQueueAdd('connectedPeers') + randID = str(uuid.uuid4()) + self.onionrCore.daemonQueueAdd('connectedPeers', responseID=randID) + while True: + time.sleep(1) + peers = self.onionrCore.daemonQueueGetResponse(randID) + if peers not in ('', None): + print(peers) + break def listPeers(self): logger.info('Peer transport address list:') diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py index 39af3cee..c76934a5 100644 --- a/onionr/onionrblockapi.py +++ b/onionr/onionrblockapi.py @@ -240,12 +240,14 @@ class Block: try: if self.isValid() is True: + ''' if (not self.getBlockFile() is None) and (recreate is True): onionrstorage.store(self.core, self.getRaw().encode()) #with open(self.getBlockFile(), 'wb') as blockFile: # blockFile.write(self.getRaw().encode()) else: - self.hash = self.getCore().insertBlock(self.getContent(), header = self.getType(), sign = sign, meta = self.getMetadata(), expire = self.getExpire()) + ''' + self.hash = self.getCore().insertBlock(self.getRaw(), header = self.getType(), sign = sign, meta = self.getMetadata(), expire = self.getExpire()) if self.hash != False: self.update() @@ -751,13 +753,16 @@ class Block: # no input data? scrap it. if hash is None: return False - + ''' if type(hash) == Block: blockfile = hash.getBlockFile() else: blockfile = onionrcore.Core().dataDir + 'blocks/%s.dat' % hash + ''' - return os.path.exists(blockfile) and os.path.isfile(blockfile) + ret = isinstance(onionrstorage.getData(onionrcore.Core(), hash.getHash()), type(None)) + + return not ret def getCache(hash = None): # give a list of the hashes of the cached blocks diff --git a/onionr/onionrgui.py b/onionr/onionrgui.py index 510ea594..41ff8fc0 100755 --- a/onionr/onionrgui.py +++ b/onionr/onionrgui.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import threading, time from tkinter import * import core class OnionrGUI: @@ -11,8 +12,7 @@ class OnionrGUI: # create a pulldown menu, and add it to the menu bar filemenu = Menu(menubar, tearoff=0) - filemenu.add_command(label="Open", command=None) - filemenu.add_command(label="Save", command=None) + #filemenu.add_command(label="Open", command=None) filemenu.add_separator() filemenu.add_command(label="Exit", command=self.root.quit) menubar.add_cascade(label="File", menu=filemenu) @@ -26,16 +26,13 @@ class OnionrGUI: self.root.config(menu=menubar) self.menuFrame = Frame(self.root) - self.mainButton = Button(self.menuFrame, text="Main View") - self.mainButton.grid(row=0, column=0, padx=0, pady=2, sticky=N+W) - self.tabButton1 = Button(self.menuFrame, text="Mail") + self.tabButton1 = Button(self.menuFrame, text="Mail", command=self.openMail) self.tabButton1.grid(row=0, column=1, padx=0, pady=2, sticky=N+W) self.tabButton2 = Button(self.menuFrame, text="Message Flow") self.tabButton2.grid(row=0, column=3, padx=0, pady=2, sticky=N+W) self.menuFrame.grid(row=0, column=0, padx=2, pady=0, sticky=N+W) - self.idFrame = Frame(self.root) self.ourIDLabel = Label(self.idFrame, text="ID: ") @@ -48,11 +45,34 @@ class OnionrGUI: self.syncStatus = Label(self.root, text="Sync Status: 15/100") self.syncStatus.place(relx=1.0, rely=1.0, anchor=S+E) - self.peerCount = Label(self.root, text="Connected Peers: 3") + self.peerCount = Label(self.root, text="Connected Peers: ") self.peerCount.place(relx=0.0, rely=1.0, anchor='sw') self.root.wm_title("Onionr") + threading.Thread(target=self.updateStats) self.root.mainloop() return + def updateStats(self): + #self.core._utils.localCommand() + self.peerCount.config(text='Connected Peers: %s' % ()) + time.sleep(1) + return + def openMail(self): + MailWindow(self) + + +class MailWindow: + def __init__(self, mainGUI): + assert isinstance(mainGUI, OnionrGUI) + self.core = mainGUI.core + self.mailRoot = Toplevel() + + self.inboxFrame = Frame(self.mailRoot) + self.sentboxFrame = Frame(self.mailRoot) + self.composeFrame = Frame(self.mailRoot) + + + self.mailRoot.mainloop() + OnionrGUI() \ No newline at end of file diff --git a/onionr/onionrstorage.py b/onionr/onionrstorage.py index 2a2c44ad..39dfd323 100644 --- a/onionr/onionrstorage.py +++ b/onionr/onionrstorage.py @@ -67,10 +67,12 @@ def getData(coreInst, bHash): assert isinstance(coreInst, core.Core) assert coreInst._utils.validateHash(bHash) + bHash = coreInst._utils.bytesToStr(bHash) + # First check DB for data entry by hash # if no entry, check disk # If no entry in either, raise an exception - retData = '' + retData = None fileLocation = '%s/%s.dat' % (coreInst.blockDataLocation, bHash) if os.path.exists(fileLocation): with open(fileLocation, 'rb') as block: diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 95c243f1..18b3e5ca 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -151,7 +151,7 @@ class OnionrUtils: logger.error('Failed to read my address.', error = error) return None - def localCommand(self, command, data='', silent = True): + def localCommand(self, command, data='', silent = True, post=False, postData = {}): ''' Send a command to the local http API server, securely. Intended for local clients, DO NOT USE for remote peers. ''' @@ -173,8 +173,12 @@ class OnionrUtils: if data != '': data = '&data=' + urllib.parse.quote_plus(data) payload = 'http://%s:%s/%s%s' % (hostname, config.get('client.client.port'), command, data) + try: - retData = requests.get(payload, headers={'token': config.get('client.webpassword')}).text + if post: + retData = requests.post(payload, data=postData, headers={'token': config.get('client.webpassword')}).text + else: + retData = requests.get(payload, headers={'token': config.get('client.webpassword')}).text except Exception as error: if not silent: logger.error('Failed to make local request (command: %s):%s' % (command, error)) diff --git a/onionr/static-data/default-plugins/flow/main.py b/onionr/static-data/default-plugins/flow/main.py index 8ef0f5f8..09826b79 100644 --- a/onionr/static-data/default-plugins/flow/main.py +++ b/onionr/static-data/default-plugins/flow/main.py @@ -54,7 +54,7 @@ class OnionrFlow: self.flowRunning = False expireTime = self.myCore._utils.getEpoch() + 43200 if len(message) > 0: - self.myCore.insertBlock(message, header='txt', expire=expireTime) + self.myCore.insertBlock(message, header='txt', expire=expireTime, meta={'ch': self.channel}) #insertBL = Block(content = message, type = 'txt', expire=expireTime, core = self.myCore) #insertBL.setMetadata('ch', self.channel) #insertBL.save() @@ -67,10 +67,13 @@ class OnionrFlow: time.sleep(1) try: while self.flowRunning: - for block in Block.getBlocks(type = 'txt', core = self.myCore): + 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 @@ -80,7 +83,7 @@ class OnionrFlow: 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) + time.sleep(5) except KeyboardInterrupt: self.flowRunning = False From c5a0b299889a33d395c7e142887e7966c72abc93 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 7 Jan 2019 15:09:58 -0600 Subject: [PATCH 24/94] communicator db responses probably finished --- onionr/api.py | 7 +++++-- onionr/communicator2.py | 5 +++-- onionr/core.py | 3 ++- onionr/onionr.py | 12 ++++++++---- onionr/onionrutils.py | 1 - 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 62467631..48c70d37 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -296,10 +296,13 @@ class API: @app.route('/queueResponse/') def queueResponse(name): + resp = '' try: - res = self.queueResponse[name] + resp = self.queueResponse[name] except KeyError: - resp = '' + pass + else: + del self.queueResponse[name] return Response(resp) @app.route('/ping') diff --git a/onionr/communicator2.py b/onionr/communicator2.py index 6adb1f2c..cf04a755 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -507,9 +507,10 @@ class OnionrCommunicatorDaemon: else: logger.info('Recieved daemonQueue command:' + cmd[0]) - if cmd[4] != '': + if cmd[4] != '' and cmd[0] not in ('', None): if response != '': - self._core._utils.localCommand('queueResponseAdd', data='/' + cmd[4], post=True, postData=response) + self._core._utils.localCommand('queueResponseAdd/' + cmd[4], post=True, postData={'data': response}) + response = '' self.decrementThreadCount('daemonCommands') diff --git a/onionr/core.py b/onionr/core.py index ce29a0a2..c41537a5 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -384,7 +384,8 @@ class Core: Get a response sent by communicator to the API, by requesting to the API ''' assert len(responseID) > 0 - resp = self._utils.localCommand('queueResponse', data='/' + responseID, post=True) + resp = self._utils.localCommand('queueResponse/' + responseID) + return resp def clearDaemonQueue(self): ''' diff --git a/onionr/onionr.py b/onionr/onionr.py index 979ad189..ab184a1f 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -397,11 +397,15 @@ class Onionr: randID = str(uuid.uuid4()) self.onionrCore.daemonQueueAdd('connectedPeers', responseID=randID) while True: - time.sleep(1) - peers = self.onionrCore.daemonQueueGetResponse(randID) - if peers not in ('', None): - print(peers) + try: + time.sleep(3) + peers = self.onionrCore.daemonQueueGetResponse(randID) + except KeyboardInterrupt: break + if not type(peers) is None: + if peers not in ('', None): + print(peers) + break def listPeers(self): logger.info('Peer transport address list:') diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 18b3e5ca..883b9756 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -173,7 +173,6 @@ class OnionrUtils: if data != '': data = '&data=' + urllib.parse.quote_plus(data) payload = 'http://%s:%s/%s%s' % (hostname, config.get('client.client.port'), command, data) - try: if post: retData = requests.post(payload, data=postData, headers={'token': config.get('client.webpassword')}).text From 8c72242eaf57c3f2b37c5dc527c5eff320724184 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 7 Jan 2019 16:30:47 -0600 Subject: [PATCH 25/94] fixed broken forward secrecy (not sharing new keys) --- onionr/core.py | 11 ++++++----- onionr/onionrusers.py | 4 +++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/onionr/core.py b/onionr/core.py index c41537a5..3a09f758 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -182,7 +182,7 @@ class Core: return True else: - logger.debug('Invalid ID: %s' % address) + #logger.debug('Invalid ID: %s' % address) return False def removeAddress(self, address): @@ -739,10 +739,11 @@ class Core: data = forwardEncrypted[0] meta['forwardEnc'] = True except onionrexceptions.InvalidPubkey: - onionrusers.OnionrUser(self, asymPeer).generateForwardKey() - onionrusers.OnionrUser(self, asymPeer).generateForwardKey() - fsKey = onionrusers.OnionrUser(self, asymPeer).getGeneratedForwardKeys()[0] - meta['newFSKey'] = fsKey[0] + pass + #onionrusers.OnionrUser(self, asymPeer).generateForwardKey() + fsKey = onionrusers.OnionrUser(self, asymPeer).generateForwardKey() + #fsKey = onionrusers.OnionrUser(self, asymPeer).getGeneratedForwardKeys().reverse() + meta['newFSKey'] = fsKey jsonMeta = json.dumps(meta) if sign: signature = self._crypto.edSign(jsonMeta.encode() + data, key=self._crypto.privKey, encodeResult=True) diff --git a/onionr/onionrusers.py b/onionr/onionrusers.py index 5267112e..9671c5db 100644 --- a/onionr/onionrusers.py +++ b/onionr/onionrusers.py @@ -169,7 +169,9 @@ class OnionrUser: def addForwardKey(self, newKey, expire=604800): if not self._core._utils.validatePubKey(newKey): - raise onionrexceptions.InvalidPubkey + raise onionrexceptions.InvalidPubkey(newKey) + if newKey in self._getForwardKeys(): + return False # Add a forward secrecy key for the peer conn = sqlite3.connect(self._core.peerDB, timeout=10) c = conn.cursor() From 75c8abd9e08f36a92214926e81566bd5692a3e86 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 8 Jan 2019 01:25:56 -0600 Subject: [PATCH 26/94] check for oserror when binding local ips --- onionr/api.py | 14 ++++++++++++-- onionr/onionrblockapi.py | 10 ++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 48c70d37..fa7833fe 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -20,7 +20,7 @@ import flask, cgi from flask import request, Response, abort, send_from_directory from gevent.pywsgi import WSGIServer -import sys, random, threading, hmac, hashlib, base64, time, math, os, json +import sys, random, threading, hmac, hashlib, base64, time, math, os, json, socket import core from onionrblockapi import Block import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config, onionr @@ -47,9 +47,19 @@ def setBindIP(filePath): '''Set a random localhost IP to a specified file (intended for private or public API localhost IPs)''' hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))] data = '.'.join(hostOctets) - + + # Try to bind IP. Some platforms like Mac block non normal 127.x.x.x + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.bind((data, 0)) + except OSError: + logger.warn('Your platform appears to not support random local host addresses 127.x.x.x. Falling back to 127.0.0.1.') + data = '127.0.0.1' + s.close() + with open(filePath, 'w') as bindFile: bindFile.write(data) + return data class PublicAPI: diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py index c76934a5..04dcf65a 100644 --- a/onionr/onionrblockapi.py +++ b/onionr/onionrblockapi.py @@ -737,7 +737,7 @@ class Block: return (blocks[-1], blocks) return blocks[-1] - def exists(hash): + def exists(bHash): ''' Checks if a block is saved to file or not @@ -751,7 +751,7 @@ class Block: ''' # no input data? scrap it. - if hash is None: + if bHash is None: return False ''' if type(hash) == Block: @@ -759,8 +759,10 @@ class Block: else: blockfile = onionrcore.Core().dataDir + 'blocks/%s.dat' % hash ''' - - ret = isinstance(onionrstorage.getData(onionrcore.Core(), hash.getHash()), type(None)) + if isinstance(bHash, Block): + bHash = bHash.getHash() + + ret = isinstance(onionrstorage.getData(onionrcore.Core(), bHash), type(None)) return not ret From d6eabe9f12ad4f9289ed7f6ed9f7e7c63454e6ca Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 9 Jan 2019 10:54:35 -0600 Subject: [PATCH 27/94] catch sigterm properly --- onionr/communicator2.py | 2 +- onionr/onionr.py | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/onionr/communicator2.py b/onionr/communicator2.py index cf04a755..d63bb9db 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -95,7 +95,7 @@ class OnionrCommunicatorDaemon: peerPoolTimer = OnionrCommunicatorTimers(self, self.getOnlinePeers, 60, maxThreads=1) OnionrCommunicatorTimers(self, self.runCheck, 1) 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) + OnionrCommunicatorTimers(self, self.getBlocks, self._core.config.get('timers.getBlocks'), requiresPeer=True, maxThreads=2) OnionrCommunicatorTimers(self, self.clearOfflinePeer, 58) OnionrCommunicatorTimers(self, self.daemonTools.cleanOldBlocks, 65) OnionrCommunicatorTimers(self, self.lookupAdders, 60, requiresPeer=True) diff --git a/onionr/onionr.py b/onionr/onionr.py index ab184a1f..e4f4b9f1 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -25,7 +25,7 @@ if sys.version_info[0] == 2 or sys.version_info[1] < 5: print('Error, Onionr requires Python 3.5+') sys.exit(1) import os, base64, random, getpass, shutil, subprocess, requests, time, platform, datetime, re, json, getpass, sqlite3 -import webbrowser, uuid +import webbrowser, uuid, signal from threading import Thread import api, core, config, logger, onionrplugins as plugins, onionrevents as events import onionrutils @@ -51,6 +51,7 @@ class Onionr: In general, external programs and plugins should not use this class. ''' self.userRunDir = os.getcwd() # Directory user runs the program from + self.killed = False try: os.chdir(sys.path[0]) except FileNotFoundError: @@ -73,6 +74,8 @@ class Onionr: self.clientAPIInst = '' # Client http api instance self.publicAPIInst = '' # Public http api instance + signal.signal(signal.SIGTERM, self.exitSigterm) + # Handle commands self.debug = False # Whole application debugging @@ -252,6 +255,9 @@ class Onionr: self.execute(command) return + + def exitSigterm(self, signum, frame): + self.killed = True ''' THIS SECTION HANDLES THE COMMANDS @@ -748,17 +754,20 @@ class Onionr: events.event('daemon_start', onionr = self) try: while True: - time.sleep(5) - + time.sleep(3) # Break if communicator process ends, so we don't have left over processes if communicatorProc.poll() is not None: break + if self.killed: + break # Break out if sigterm for clean exit except KeyboardInterrupt: + pass + finally: self.onionrCore.daemonQueueAdd('shutdown') self.onionrUtils.localCommand('shutdown') + net.killTor() time.sleep(3) self.deleteRunFiles() - net.killTor() return def killDaemon(self): From f82e7bfb590277bab98ff58ee6549554c16dd2df Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 11 Jan 2019 16:59:21 -0600 Subject: [PATCH 28/94] work on better peer connections --- onionr/api.py | 4 ++-- onionr/communicator2.py | 2 +- onionr/core.py | 11 ++++++++++- onionr/onionrpeers.py | 19 +++++++++++++++++-- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index fa7833fe..a10a9163 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -143,9 +143,9 @@ class PublicAPI: @app.route('/pex') def peerExchange(): - response = ','.join(clientAPI._core.listAdders()) + response = ','.join(clientAPI._core.listAdders(recent=3600)) if len(response) == 0: - response = 'none' + response = '' return Response(response) @app.route('/announce', methods=['post']) diff --git a/onionr/communicator2.py b/onionr/communicator2.py index d63bb9db..0458aa74 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -107,7 +107,7 @@ class OnionrCommunicatorDaemon: netCheckTimer = OnionrCommunicatorTimers(self, self.daemonTools.netCheck, 600) if config.get('general.security_level') == 0: - announceTimer = OnionrCommunicatorTimers(self, self.daemonTools.announceNode, 86400, requiresPeer=True, maxThreads=1) + announceTimer = OnionrCommunicatorTimers(self, self.daemonTools.announceNode, 3600, requiresPeer=True, maxThreads=1) announceTimer.count = (announceTimer.frequency - 120) else: logger.debug('Will not announce node.') diff --git a/onionr/core.py b/onionr/core.py index 3a09f758..35e9bb11 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -405,7 +405,7 @@ class Core: return - def listAdders(self, randomOrder=True, i2p=True): + def listAdders(self, randomOrder=True, i2p=True, recent=0): ''' Return a list of addresses ''' @@ -417,8 +417,17 @@ class Core: 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 def listPeers(self, randomOrder=True, getPow=False, trust=0): diff --git a/onionr/onionrpeers.py b/onionr/onionrpeers.py index e4793dfb..e0a509c4 100644 --- a/onionr/onionrpeers.py +++ b/onionr/onionrpeers.py @@ -28,6 +28,7 @@ class PeerProfiles: self.friendSigCount = 0 self.success = 0 self.failure = 0 + self.connectTime = None if not isinstance(coreInst, core.Core): raise TypeError("coreInst must be a type of core.Core") @@ -35,6 +36,7 @@ class PeerProfiles: assert isinstance(self.coreInst, core.Core) self.loadScore() + self.getConnectTime() return def loadScore(self): @@ -44,7 +46,13 @@ class PeerProfiles: except (TypeError, ValueError) as e: self.success = 0 self.score = self.success - + + def getConnectTime(self): + try: + self.connectTime = self.coreInst.getAddressInfo(self.address, 'lastConnect') + except KeyError: + pass + def saveScore(self): '''Save the node's score to the database''' self.coreInst.setAddressInfo(self.address, 'success', self.score) @@ -61,14 +69,21 @@ def getScoreSortedPeerList(coreInst): peerList = coreInst.listAdders() peerScores = {} + peerTimes = {} for address in peerList: # Load peer's profiles into a list profile = PeerProfiles(address, coreInst) peerScores[address] = profile.score + if not isinstance(profile.connectTime, type(None)): + peerTimes[address] = profile.connectTime + else: + peerTimes[address] = 9000 - # Sort peers by their score, greatest to least + # Sort peers by their score, greatest to least, and then last connected time peerList = sorted(peerScores, key=peerScores.get, reverse=True) + peerList = sorted(peerTimes, key=peerTimes.get, reverse=True) + print(peerList) return peerList def peerCleanup(coreInst): From 22cece2b2c773f48cb1e38cee693b962b19d29bd Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 13 Jan 2019 16:20:10 -0600 Subject: [PATCH 29/94] work on serialization and communication, misc work on web, run files --- CONTRIBUTING.md | 2 +- Makefile | 8 +++---- onionr-daemon-linux | 2 -- onionr/api.py | 22 ++++++++++++++++--- onionr/communicator2.py | 5 ++++- onionr/core.py | 21 +++++++++++++++++- onionr/onionr.py | 21 +++++++++++++++--- onionr/onionrpeers.py | 5 ++--- onionr/onionrutils.py | 21 +++++++++++++----- .../static-data/default-plugins/cliui/main.py | 2 +- onionr/static-data/www/board/index.html | 1 + run-linux | 4 ---- 12 files changed, 86 insertions(+), 28 deletions(-) delete mode 100644 onionr-daemon-linux delete mode 100755 run-linux diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91a0e227..f5a31c5d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,7 @@ And most importantly, please be patient. Onionr is an open source project done b ## Asking Questions -If you need help with Onionr, you can ask in our +If you need help with Onionr, you can contact the devs (be polite and remember this is a volunteer-driven non-profit project). ## Contributing Code diff --git a/Makefile b/Makefile index 7e235b4f..c3616e3d 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ uninstall: rm -f $(DESTDIR)$(PREFIX)/bin/onionr test: - @./run-linux stop + @./onionr.sh stop @sleep 1 @rm -rf onionr/data-backup @mv onionr/data onionr/data-backup | true > /dev/null 2>&1 @@ -29,15 +29,15 @@ test: soft-reset: @echo "Soft-resetting Onionr..." rm -f onionr/data/blocks/*.dat onionr/data/*.db onionr/data/block-nonces.dat | true > /dev/null 2>&1 - @./run-linux version | grep -v "Failed" --color=always + @./onionr.sh version | grep -v "Failed" --color=always reset: @echo "Hard-resetting Onionr..." rm -rf onionr/data/ | true > /dev/null 2>&1 cd onionr/static-data/www/ui/; rm -rf ./dist; python compile.py - #@./RUN-LINUX.sh version | grep -v "Failed" --color=always + #@./onionr.sh.sh version | grep -v "Failed" --color=always plugins-reset: @echo "Resetting plugins..." rm -rf onionr/data/plugins/ | true > /dev/null 2>&1 - @./run-linux version | grep -v "Failed" --color=always + @./onionr.sh version | grep -v "Failed" --color=always diff --git a/onionr-daemon-linux b/onionr-daemon-linux deleted file mode 100644 index d72ac015..00000000 --- a/onionr-daemon-linux +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/sh -nohup ./run-linux start & disown diff --git a/onionr/api.py b/onionr/api.py index a10a9163..358b64ad 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -17,9 +17,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' +from gevent.pywsgi import WSGIServer +import gevent.monkey +gevent.monkey.patch_socket() import flask, cgi from flask import request, Response, abort, send_from_directory -from gevent.pywsgi import WSGIServer import sys, random, threading, hmac, hashlib, base64, time, math, os, json, socket import core from onionrblockapi import Block @@ -238,6 +240,7 @@ class API: self.debug = debug self._privateDelayTime = 3 self._core = core.Core() + self.startTime = self._core._utils.getEpoch() self._crypto = onionrcrypto.OnionrCrypto(self._core) self._utils = onionrutils.OnionrUtils(self._core) app = flask.Flask(__name__) @@ -301,18 +304,20 @@ class API: @app.route('/queueResponseAdd/', methods=['post']) def queueResponseAdd(name): + print('added',name) self.queueResponse[name] = request.form['data'] return Response('success') @app.route('/queueResponse/') def queueResponse(name): - resp = '' + resp = 'failure' try: resp = self.queueResponse[name] except KeyError: pass else: del self.queueResponse[name] + print(name, resp) return Response(resp) @app.route('/ping') @@ -321,7 +326,7 @@ class API: @app.route('/', endpoint='onionrhome') def hello(): - return Response("Welcome to Onionr") + return send_from_directory('static-data/www/private/', 'index.html') @app.route('/getblocksbytype/') def getBlocksByType(name): @@ -375,6 +380,14 @@ class API: except AttributeError: pass return Response("bye") + + @app.route('/getstats') + def getStats(): + return Response(self._core.serializer.getStats()) + + @app.route('/getuptime') + def showUptime(): + return Response(str(self.getUptime())) self.httpServer = WSGIServer((self.host, bindPort), app, log=None) self.httpServer.serve_forever() @@ -397,3 +410,6 @@ class API: return True except TypeError: return False + + def getUptime(self): + return self._utils.getEpoch() - self.startTime \ No newline at end of file diff --git a/onionr/communicator2.py b/onionr/communicator2.py index 0458aa74..95c84b32 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -486,7 +486,10 @@ class OnionrCommunicatorDaemon: logger.debug('Status check; looks good.') open(self._core.dataDir + '.runcheck', 'w+').close() elif cmd[0] == 'connectedPeers': + print('yup') response = '\n'.join(list(self.onlinePeers)).strip() + if response == '': + response = 'none' elif cmd[0] == 'pex': for i in self.timers: if i.timerFunction.__name__ == 'lookupAdders': @@ -507,7 +510,7 @@ class OnionrCommunicatorDaemon: else: logger.info('Recieved daemonQueue command:' + cmd[0]) - if cmd[4] != '' and cmd[0] not in ('', None): + if cmd[0] not in ('', None): if response != '': self._core._utils.localCommand('queueResponseAdd/' + cmd[4], post=True, postData={'data': response}) response = '' diff --git a/onionr/core.py b/onionr/core.py index 35e9bb11..987a6940 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -22,7 +22,7 @@ from onionrblockapi import Block import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions, onionrvalues import onionrblacklist, onionrchat, onionrusers -import dbcreator, onionrstorage +import dbcreator, onionrstorage, serializeddata if sys.version_info < (3, 6): try: @@ -101,6 +101,7 @@ class Core: # 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) @@ -386,6 +387,24 @@ class Core: 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) + print(resp) + 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) def clearDaemonQueue(self): ''' diff --git a/onionr/onionr.py b/onionr/onionr.py index e4f4b9f1..c213c0f9 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -180,6 +180,9 @@ class Onionr: 'add-site': self.addWebpage, 'addsite': self.addWebpage, + 'openhome': self.openHome, + 'open-home': self.openHome, + 'get-file': self.getFile, 'getfile': self.getFile, @@ -240,7 +243,8 @@ class Onionr: 'introduce': 'Introduce your node to the public Onionr network', 'friend': '[add|remove] [public key/id]', 'add-id': 'Generate a new ID (key pair)', - 'change-id': 'Change active ID' + 'change-id': 'Change active ID', + 'open-home': 'Open your node\'s home/info screen' } # initialize plugins @@ -274,6 +278,14 @@ class Onionr: 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) + def openHome(self): + try: + url = self.onionrUtils.getClientAPIServer() + except FileNotFoundError: + logger.error('Onionr seems to not be running (could not get api host)') + else: + webbrowser.open_new_tab('http://%s/#%s' % (url, config.get('client.webpassword'))) + def addID(self): try: sys.argv[2] @@ -409,8 +421,11 @@ class Onionr: except KeyboardInterrupt: break if not type(peers) is None: - if peers not in ('', None): - print(peers) + if peers not in ('', 'failure', None): + if peers != False: + print(peers) + else: + print('Daemon probably not running. Unable to list connected peers.') break def listPeers(self): diff --git a/onionr/onionrpeers.py b/onionr/onionrpeers.py index e0a509c4..8a88d649 100644 --- a/onionr/onionrpeers.py +++ b/onionr/onionrpeers.py @@ -49,8 +49,8 @@ class PeerProfiles: def getConnectTime(self): try: - self.connectTime = self.coreInst.getAddressInfo(self.address, 'lastConnect') - except KeyError: + self.connectTime = int(self.coreInst.getAddressInfo(self.address, 'lastConnect')) + except (KeyError, ValueError, TypeError) as e: pass def saveScore(self): @@ -83,7 +83,6 @@ def getScoreSortedPeerList(coreInst): # Sort peers by their score, greatest to least, and then last connected time peerList = sorted(peerScores, key=peerScores.get, reverse=True) peerList = sorted(peerTimes, key=peerTimes.get, reverse=True) - print(peerList) return peerList def peerCleanup(coreInst): diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 883b9756..7207756e 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -150,6 +150,17 @@ class OnionrUtils: except Exception as error: logger.error('Failed to read my address.', error = error) return None + + 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 = {}): ''' @@ -163,8 +174,7 @@ class OnionrUtils: waited = 0 while hostname == '': try: - with open(self._core.privateApiHostFile, 'r') as host: - hostname = host.read() + hostname = self.getClientAPIServer() except FileNotFoundError: time.sleep(1) waited += 1 @@ -172,12 +182,13 @@ class OnionrUtils: return False if data != '': data = '&data=' + urllib.parse.quote_plus(data) - payload = 'http://%s:%s/%s%s' % (hostname, config.get('client.client.port'), command, data) + payload = 'http://%s/%s%s' % (hostname, command, data) + print(payload,config.get('client.webpassword')) try: if post: - retData = requests.post(payload, data=postData, headers={'token': config.get('client.webpassword')}).text + retData = requests.post(payload, data=postData, headers={'token': config.get('client.webpassword')}, timeout=(15, 30)).text else: - retData = requests.get(payload, headers={'token': config.get('client.webpassword')}).text + retData = requests.get(payload, headers={'token': config.get('client.webpassword')}, timeout=(15, 30)).text except Exception as error: if not silent: logger.error('Failed to make local request (command: %s):%s' % (command, error)) diff --git a/onionr/static-data/default-plugins/cliui/main.py b/onionr/static-data/default-plugins/cliui/main.py index 323b9419..c7c88acf 100644 --- a/onionr/static-data/default-plugins/cliui/main.py +++ b/onionr/static-data/default-plugins/cliui/main.py @@ -19,7 +19,7 @@ ''' # Imports some useful libraries -import logger, config, threading, time, uuid, subprocess +import logger, config, threading, time, uuid, subprocess, sys from onionrblockapi import Block plugin_name = 'cliui' diff --git a/onionr/static-data/www/board/index.html b/onionr/static-data/www/board/index.html index df48e912..ffc47159 100644 --- a/onionr/static-data/www/board/index.html +++ b/onionr/static-data/www/board/index.html @@ -2,6 +2,7 @@ + OnionrBoard diff --git a/run-linux b/run-linux deleted file mode 100755 index 286a0f7f..00000000 --- a/run-linux +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -cd "$(dirname "$0")" -cd onionr/ -./onionr.py "$@" From 0e6ab04996a6fe9e555e0be1ad5162731e545639 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 14 Jan 2019 00:14:02 -0600 Subject: [PATCH 30/94] more work on serialization and communication, misc work on web, run files --- onionr.sh | 4 + onionr/proofofmemory.py | 29 ++++ onionr/serializeddata.py | 42 ++++++ onionr/static-data/www/private/index.html | 24 ++++ onionr/static-data/www/shared/main/stats.js | 30 +++++ onionr/static-data/www/shared/main/style.css | 126 ++++++++++++++++++ onionr/static-data/www/shared/misc.js | 12 ++ onionr/static-data/www/shared/onionr-icon.png | Bin 0 -> 5176 bytes onionr/static-data/www/shared/onionrblocks.js | 7 + start-daemon.sh | 5 + 10 files changed, 279 insertions(+) create mode 100755 onionr.sh create mode 100644 onionr/proofofmemory.py create mode 100644 onionr/serializeddata.py create mode 100644 onionr/static-data/www/private/index.html create mode 100644 onionr/static-data/www/shared/main/stats.js create mode 100644 onionr/static-data/www/shared/main/style.css create mode 100644 onionr/static-data/www/shared/misc.js create mode 100644 onionr/static-data/www/shared/onionr-icon.png create mode 100644 onionr/static-data/www/shared/onionrblocks.js create mode 100755 start-daemon.sh diff --git a/onionr.sh b/onionr.sh new file mode 100755 index 00000000..286a0f7f --- /dev/null +++ b/onionr.sh @@ -0,0 +1,4 @@ +#!/bin/sh +cd "$(dirname "$0")" +cd onionr/ +./onionr.py "$@" diff --git a/onionr/proofofmemory.py b/onionr/proofofmemory.py new file mode 100644 index 00000000..4b0b0fa7 --- /dev/null +++ b/onionr/proofofmemory.py @@ -0,0 +1,29 @@ +''' + Onionr - P2P Anonymous Storage Network + + This file handles proof of memory functionality +''' +''' + 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 . +''' + +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 diff --git a/onionr/serializeddata.py b/onionr/serializeddata.py new file mode 100644 index 00000000..a7ff2e80 --- /dev/null +++ b/onionr/serializeddata.py @@ -0,0 +1,42 @@ +''' + Onionr - P2P Anonymous Storage Network + + This module serializes various data pieces for use in other modules, in particular the web 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 core, api, uuid, json + +class SerializedData: + def __init__(self, coreInst): + ''' + Serialized data is in JSON format: + { + 'success': bool, + 'foo': 'bar', + etc + } + ''' + assert isinstance(coreInst, core.Core) + self._core = coreInst + + def getStats(self): + '''Return statistics about our node''' + stats = {} + stats['uptime'] = self._core._utils.localCommand('getuptime') + stats['connectedNodes'] = self._core.daemonQueueSimple('connectedPeers') + stats['blockCount'] = len(self._core.getBlockList()) + return json.dumps(stats) diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html new file mode 100644 index 00000000..58c14e3f --- /dev/null +++ b/onionr/static-data/www/private/index.html @@ -0,0 +1,24 @@ + + + + + + Onionr + + + + + + Onionr Web Control Panel +
+ Shutdown node +

Stats

+

Uptime:

+

Stored Blocks:

+

Connected nodes:

+

+        
+ + + + \ 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 new file mode 100644 index 00000000..4e856e5a --- /dev/null +++ b/onionr/static-data/www/shared/main/stats.js @@ -0,0 +1,30 @@ +/* + + Onionr - P2P Anonymous Storage Network + + This file loads stats to show on the main node web page + + 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 +*/ + +uptimeDisplay = document.getElementById('uptime') +connectedDisplay = document.getElementById('connectedNodes') +storedBlockDisplay = document.getElementById('storedBlocks') + +pass = window.location.hash.replace('#', '') + +stats = JSON.parse(httpGet('getstats', pass)) +uptimeDisplay.innerText = stats['uptime'] + ' seconds' +connectedDisplay.innerText = stats['connectedNodes'] +storedBlockDisplay.innerText = stats['blockCount'] \ No newline at end of file diff --git a/onionr/static-data/www/shared/main/style.css b/onionr/static-data/www/shared/main/style.css new file mode 100644 index 00000000..7b8f3c2e --- /dev/null +++ b/onionr/static-data/www/shared/main/style.css @@ -0,0 +1,126 @@ +body{ + background-color: #2c2b3f; + color: white; +} + +a, a:visited{ + color: white; +} +.center{ + text-align: center; +} +footer{ + margin-top: 2em; + margin-bottom: 0.5em; +} + +body{ + margin-left: 3em; + padding: 1em; +} +.onionrMenu{ + max-width: 25%; + margin-left: 2%; + margin-right: 10%; + font-family: sans-serif; +} +.onionrMenu li{ + list-style-type: none; + margin-top: 3px; + font-size: 125%; +} +.onionrMenu li:hover{ + color: red; +} +.box { + display: flex; + align-items:center; + } + .logo{ + max-width: 25%; + vertical-align: middle; + } + .logoText{ + font-family: sans-serif; + font-size: 2em; + margin-top: 1em; + margin-left: 1%; + } + .main{ + min-height: 500px; + } + +.content{ + margin-top: 3em; + margin-left: 0%; + margin-right: 40%; + background-color: white; + color: black; + padding-right: 5%; + padding-left: 3%; + padding-bottom: 2em; + padding-top: 0.5em; + border: 1px solid black; + border-radius: 10px; + min-height: 300px; +} +.content p{ + text-align: justify; +} +.content img{ + max-width: 35%; +} +.content a, .content a:visited{ + color: black; +} + +.stats{ + margin-top: 1em; + background-color: #0c1049; + padding: 5px; + margin-right: 45%; + font-family: sans-serif; +} +.statDesc{ + background-color: black; + padding: 5px; + margin-right: 1%; + margin-left: -5px; +} + +.stats noscript{ + color: blue; +} + +.statItem{ + padding-left: 10px; + float: right; + margin-right: 5px; +} + +.warn{ + color: orangered; +} + +@media only screen and (max-width: 640px) { + .onionrMenu{ + margin-left: 0%; + } + body{ + margin-left: 0em; + } + .content{ + margin-left: 1%; + margin-right: 2%; + } + .content img{ + max-width: 85%; + } + .stats{ + margin-right: 1%; + } + .statItem{ + float: initial; + display: block; + } +} diff --git a/onionr/static-data/www/shared/misc.js b/onionr/static-data/www/shared/misc.js new file mode 100644 index 00000000..1b7d6092 --- /dev/null +++ b/onionr/static-data/www/shared/misc.js @@ -0,0 +1,12 @@ +function httpGet(theUrl, webpass) { + var xmlHttp = new XMLHttpRequest() + xmlHttp.open( "GET", theUrl, false ) // false for synchronous request + xmlHttp.setRequestHeader('token', webpass) + xmlHttp.send( null ) + if (xmlHttp.status == 200){ + return xmlHttp.responseText + } + else{ + return ""; + } +} \ No newline at end of file diff --git a/onionr/static-data/www/shared/onionr-icon.png b/onionr/static-data/www/shared/onionr-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6662210da9ad16105c2c57a7662864fb9611fd09 GIT binary patch literal 5176 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 m9mFYB8@wGvU=V?#2>c&c /dev/null From 1ebed8d6068e40adf2c5fd82d5029002cf7ad4ee Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 15 Jan 2019 23:57:47 -0600 Subject: [PATCH 31/94] improved block list syncing, added forgotten stats.js file --- onionr/api.py | 11 +++-- onionr/communicator2.py | 28 +++++++++-- onionr/core.py | 22 +++++---- onionr/onionr.py | 18 ++----- onionr/onionrchat.py | 50 -------------------- onionr/onionrutils.py | 1 - onionr/static-data/default_config.json | 4 +- 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 | 14 ++++++ onionr/static-data/www/shared/misc.js | 22 +++++++-- onionr/static-data/www/shared/panel.js | 6 +++ 12 files changed, 100 insertions(+), 94 deletions(-) delete mode 100644 onionr/onionrchat.py create mode 100644 onionr/static-data/www/shared/panel.js diff --git a/onionr/api.py b/onionr/api.py index 358b64ad..ab8438bd 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -109,7 +109,8 @@ class PublicAPI: @app.route('/getblocklist') def getBlockList(): - bList = clientAPI._core.getBlockList() + dateAdjust = request.args.get('date') + bList = clientAPI._core.getBlockList(dateRec=dateAdjust) for b in self.hideBlocks: if b in bList: bList.remove(b) @@ -304,7 +305,6 @@ class API: @app.route('/queueResponseAdd/', methods=['post']) def queueResponseAdd(name): - print('added',name) self.queueResponse[name] = request.form['data'] return Response('success') @@ -317,7 +317,6 @@ class API: pass else: del self.queueResponse[name] - print(name, resp) return Response(resp) @app.route('/ping') @@ -380,6 +379,12 @@ class API: except AttributeError: pass return Response("bye") + + @app.route('/shutdownclean') + def shutdownClean(): + # good for calling from other clients + self._core.daemonQueueAdd('shutdown') + return Response("bye") @app.route('/getstats') def getStats(): diff --git a/onionr/communicator2.py b/onionr/communicator2.py index 95c84b32..70c32988 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -21,7 +21,7 @@ ''' import sys, os, core, config, json, requests, time, logger, threading, base64, onionr, uuid import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block -import onionrdaemontools, onionrsockets, onionrchat, onionr, onionrproofs +import onionrdaemontools, onionrsockets, onionr, onionrproofs, proofofmemory import binascii from dependencies import secrets from defusedxml import minidom @@ -74,6 +74,9 @@ class OnionrCommunicatorDaemon: # timestamp when the last online node was seen self.lastNodeSeen = None + # Dict of time stamps for peer's block list lookup times, to avoid downloading full lists all the time + self.dbTimestamps = {} + # Clear the daemon queue for any dead messages if os.path.exists(self._core.queueDB): self._core.clearDaemonQueue() @@ -81,11 +84,12 @@ class OnionrCommunicatorDaemon: # Loads in and starts the enabled plugins plugins.reload() + self.proofofmemory = proofofmemory.ProofOfMemory(self) + # daemon tools are misc daemon functions, e.g. announce to online peers # intended only for use by OnionrCommunicatorDaemon self.daemonTools = onionrdaemontools.DaemonTools(self) - self._chat = onionrchat.OnionrChat(self) if debug or developmentMode: OnionrCommunicatorTimers(self, self.heartbeat, 30) @@ -164,7 +168,9 @@ class OnionrCommunicatorDaemon: 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: @@ -186,11 +192,21 @@ class OnionrCommunicatorDaemon: triedPeers.append(peer) if newDBHash != self._core.getAddressInfo(peer, 'DBHash'): self._core.setAddressInfo(peer, 'DBHash', newDBHash) + # 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, 'getblocklist') # 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'): @@ -224,8 +240,8 @@ class OnionrCommunicatorDaemon: if self._core._utils.storageCounter.isFull(): break self.currentDownloading.append(blockHash) # So we can avoid concurrent downloading in other threads of same block - logger.info("Attempting to download %s..." % blockHash) peerUsed = self.pickOnlinePeer() + 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: @@ -247,7 +263,7 @@ class OnionrCommunicatorDaemon: 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) + logger.info('Attempting to save block %s...' % blockHash[:12]) try: self._core.setData(content) except onionrexceptions.DiskAllocationReached: @@ -403,6 +419,10 @@ class OnionrCommunicatorDaemon: del self.connectTimes[peer] except KeyError: pass + try: + del self.dbTimestamps[peer] + except KeyError: + pass try: self.onlinePeers.remove(peer) except ValueError: diff --git a/onionr/core.py b/onionr/core.py index 987a6940..b2ec0dc6 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -21,7 +21,7 @@ import sqlite3, os, sys, time, math, base64, tarfile, nacl, logger, json, netcon from onionrblockapi import Block import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions, onionrvalues -import onionrblacklist, onionrchat, onionrusers +import onionrblacklist, onionrusers import dbcreator, onionrstorage, serializeddata if sys.version_info < (3, 6): @@ -373,11 +373,11 @@ class Core: try: c.execute('INSERT INTO commands (command, data, date, responseID) VALUES(?, ?, ?, ?)', t) conn.commit() - conn.close() except sqlite3.OperationalError: retData = False self.daemonQueue() events.event('queue_push', data = {'command': command, 'data': data}, onionr = None) + conn.close() return retData def daemonQueueGetResponse(self, responseID=''): @@ -602,24 +602,26 @@ class Core: return - def getBlockList(self, unsaved = False): # TODO: Use unsaved?? + def getBlockList(self, dateRec = None, unsaved = False): ''' Get list of our blocks ''' + if dateRec == None: + dateRec = 0 conn = sqlite3.connect(self.blockDB, timeout=10) c = conn.cursor() - if unsaved: - execute = 'SELECT hash FROM hashes WHERE dataSaved != 1 ORDER BY RANDOM();' - else: - execute = 'SELECT hash FROM hashes ORDER BY dateReceived ASC;' - + # if unsaved: + # execute = 'SELECT hash FROM hashes WHERE dataSaved != 1 ORDER BY RANDOM();' + # else: + # execute = 'SELECT hash FROM hashes ORDER BY dateReceived ASC;' + execute = 'SELECT hash FROM hashes WHERE dateReceived >= ? ORDER BY dateReceived ASC;' + args = (dateRec,) rows = list() - for row in c.execute(execute): + for row in c.execute(execute, args): for i in row: rows.append(i) - return rows def getBlockDate(self, blockHash): diff --git a/onionr/onionr.py b/onionr/onionr.py index c213c0f9..e54e90bb 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -21,8 +21,9 @@ along with this program. If not, see . ''' import sys -if sys.version_info[0] == 2 or sys.version_info[1] < 5: - print('Error, Onionr requires Python 3.5+') +MIN_PY_VERSION = 6 +if sys.version_info[0] == 2 or sys.version_info[1] < MIN_PY_VERSION: + print('Error, Onionr requires Python 3.%s+' % (MIN_PY_VERSION,)) sys.exit(1) import os, base64, random, getpass, shutil, subprocess, requests, time, platform, datetime, re, json, getpass, sqlite3 import webbrowser, uuid, signal @@ -198,7 +199,6 @@ class Onionr: 'ui' : self.openUI, 'gui' : self.openUI, - 'chat': self.startChat, 'getpassword': self.printWebPassword, 'get-password': self.printWebPassword, @@ -209,8 +209,6 @@ class Onionr: 'getpasswd': self.printWebPassword, 'get-passwd': self.printWebPassword, - 'chat': self.startChat, - 'friend': self.friendCmd, 'add-id': self.addID, 'change-id': self.changeID @@ -328,14 +326,6 @@ class Onionr: else: logger.error('Invalid key %s' % (key,)) - def startChat(self): - try: - data = json.dumps({'peer': sys.argv[2], 'reason': 'chat'}) - except IndexError: - logger.error('Must specify peer to chat with.') - else: - self.onionrCore.daemonQueueAdd('startSocket', data) - def getCommands(self): return self.cmds @@ -811,7 +801,7 @@ class Onionr: try: # define stats messages here - totalBlocks = len(Block.getBlocks()) + totalBlocks = len(self.onionrCore.getBlockList()) signedBlocks = len(Block.getBlocks(signed = True)) messages = { # info about local client diff --git a/onionr/onionrchat.py b/onionr/onionrchat.py deleted file mode 100644 index 84483295..00000000 --- a/onionr/onionrchat.py +++ /dev/null @@ -1,50 +0,0 @@ -''' - Onionr - P2P Anonymous Storage Network - - Onionr Chat 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 logger, time - -class OnionrChat: - def __init__(self, communicatorInst): - '''OnionrChat uses onionrsockets (handled by the communicator) to exchange direct chat messages''' - self.communicator = communicatorInst - self._core = self.communicator._core - self._utils = self._core._utils - - self.chats = {} # {'peer': {'date': date, message': message}} - self.chatSend = {} - - def chatHandler(self): - while not self.communicator.shutdown: - for peer in self._core.socketServerConnData: - try: - assert self._core.socketReasons[peer] == "chat" - except (AssertionError, KeyError) as e: - logger.warn('Peer is not for chat') - continue - else: - self.chats[peer] = {'date': self._core.socketServerConnData[peer]['date'], 'data': self._core.socketServerConnData[peer]['data']} - logger.info("CHAT MESSAGE RECIEVED: %s" % self.chats[peer]['data']) - for peer in self.communicator.socketClient.sockets: - try: - logger.info(self.communicator.socketClient.connPool[peer]['data']) - self.communicator.socketClient.sendData(peer, "lol") - except: - pass - - time.sleep(2) diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 7207756e..332d6a70 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -183,7 +183,6 @@ class OnionrUtils: if data != '': data = '&data=' + urllib.parse.quote_plus(data) payload = 'http://%s/%s%s' % (hostname, command, data) - print(payload,config.get('client.webpassword')) try: if post: retData = requests.post(payload, data=postData, headers={'token': config.get('client.webpassword')}, timeout=(15, 30)).text diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index ed9270db..2ef339bc 100644 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -2,8 +2,8 @@ "general" : { "dev_mode" : true, "display_header" : false, - "minimum_block_pow": 3, - "minimum_send_pow": 3, + "minimum_block_pow": 1, + "minimum_send_pow": 1, "socket_servers": false, "security_level": 0, "max_block_age": 2678400, diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index 58c14e3f..12f05b85 100644 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -5,20 +5,27 @@ Onionr + +
+
+

Your node will shutdown. Thank you for using Onionr.

+
+
Onionr Web Control Panel
- Shutdown node +

Stats

Uptime:

Stored Blocks:

Connected nodes:


         
+ + + - - \ 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 4e856e5a..6796f2b7 100644 --- a/onionr/static-data/www/shared/main/stats.js +++ b/onionr/static-data/www/shared/main/stats.js @@ -17,14 +17,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see */ - uptimeDisplay = document.getElementById('uptime') connectedDisplay = document.getElementById('connectedNodes') storedBlockDisplay = document.getElementById('storedBlocks') -pass = window.location.hash.replace('#', '') - -stats = JSON.parse(httpGet('getstats', pass)) +stats = JSON.parse(httpGet('getstats', webpass)) uptimeDisplay.innerText = stats['uptime'] + ' seconds' connectedDisplay.innerText = stats['connectedNodes'] storedBlockDisplay.innerText = stats['blockCount'] \ No newline at end of file diff --git a/onionr/static-data/www/shared/main/style.css b/onionr/static-data/www/shared/main/style.css index 7b8f3c2e..69e1a407 100644 --- a/onionr/static-data/www/shared/main/style.css +++ b/onionr/static-data/www/shared/main/style.css @@ -124,3 +124,17 @@ body{ display: block; } } + +/*https://stackoverflow.com/a/16778646*/ +.overlay { + visibility: hidden; + position: absolute; + left: 0px; + top: 0px; + width:100%; + opacity: 0.9; + height:100%; + text-align:center; + z-index: 1000; + background-color: black; + } diff --git a/onionr/static-data/www/shared/misc.js b/onionr/static-data/www/shared/misc.js index 1b7d6092..ae0c1fb0 100644 --- a/onionr/static-data/www/shared/misc.js +++ b/onionr/static-data/www/shared/misc.js @@ -1,4 +1,16 @@ -function httpGet(theUrl, webpass) { +webpass = document.location.hash.replace('#', '') +if (typeof webpass == "undefined"){ + webpass = localStorage['webpass'] +} +else{ + localStorage['webpass'] = webpass + document.location.hash = '' +} +if (typeof webpass == "undefined" || webpass == ""){ + alert('Web password was not found in memory or URL') +} + +function httpGet(theUrl) { var xmlHttp = new XMLHttpRequest() xmlHttp.open( "GET", theUrl, false ) // false for synchronous request xmlHttp.setRequestHeader('token', webpass) @@ -7,6 +19,10 @@ function httpGet(theUrl, webpass) { return xmlHttp.responseText } else{ - return ""; + return "" } -} \ No newline at end of file +} +function overlay(overlayID) { + el = document.getElementById(overlayID) + el.style.visibility = (el.style.visibility == "visible") ? "hidden" : "visible" + } diff --git a/onionr/static-data/www/shared/panel.js b/onionr/static-data/www/shared/panel.js new file mode 100644 index 00000000..c79241c0 --- /dev/null +++ b/onionr/static-data/www/shared/panel.js @@ -0,0 +1,6 @@ +shutdownBtn = document.getElementById('shutdownNode') + +shutdownBtn.onclick = function(){ + httpGet('shutdownclean') + overlay('shutdownNotice') +} From 9429afba182dce8b36992fcf25018a54b0aa4658 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 16 Jan 2019 23:31:56 -0600 Subject: [PATCH 32/94] fixed file bug, removed username setting --- onionr/api.py | 4 +++ onionr/communicator2.py | 6 ++-- onionr/core.py | 8 +++-- onionr/logger.py | 7 ++-- onionr/onionrblockapi.py | 2 ++ onionr/onionrutils.py | 7 ++-- .../static-data/default-plugins/cliui/main.py | 33 +++++++------------ .../default-plugins/metadataprocessor/main.py | 24 +------------- onionr/static-data/www/shared/misc.js | 2 ++ onionr/static-data/www/shared/panel.js | 6 ++-- 10 files changed, 40 insertions(+), 59 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index ab8438bd..cec7288a 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -82,6 +82,9 @@ class PublicAPI: @app.before_request def validateRequest(): '''Validate request has the correct hostname''' + # If high security level, deny requests to public + if config.get('general.security_level', default=0) > 0: + abort(403) if type(self.torAdder) is None and type(self.i2pAdder) is None: # abort if our hs addresses are not known abort(403) @@ -248,6 +251,7 @@ class API: bindPort = int(config.get('client.client.port', 59496)) self.bindPort = bindPort + # Be extremely mindful of this self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'board', 'boardContent', 'sharedContent') self.clientToken = config.get('client.webpassword') diff --git a/onionr/communicator2.py b/onionr/communicator2.py index 70c32988..c3540e61 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -201,7 +201,7 @@ class OnionrCommunicatorDaemon: else: listLookupCommand += '?date=%s' % (lastLookupTime,) try: - newBlocks = self.peerAction(peer, 'getblocklist') # get list of new block hashes + 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 @@ -241,7 +241,8 @@ class OnionrCommunicatorDaemon: break self.currentDownloading.append(blockHash) # So we can avoid concurrent downloading in other threads of same block peerUsed = self.pickOnlinePeer() - logger.info("Attempting to download %s from %s..." % (blockHash[:12], peerUsed)) + 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: @@ -506,7 +507,6 @@ class OnionrCommunicatorDaemon: logger.debug('Status check; looks good.') open(self._core.dataDir + '.runcheck', 'w+').close() elif cmd[0] == 'connectedPeers': - print('yup') response = '\n'.join(list(self.onlinePeers)).strip() if response == '': response = 'none' diff --git a/onionr/core.py b/onionr/core.py index b2ec0dc6..c427307c 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -598,7 +598,7 @@ class Core: else: c.execute('UPDATE adders SET ' + key + ' = ? WHERE address=?', command) conn.commit() - conn.close() + conn.close() return @@ -622,6 +622,7 @@ class Core: for row in c.execute(execute, args): for i in row: rows.append(i) + conn.close() return rows def getBlockDate(self, blockHash): @@ -637,7 +638,7 @@ class Core: for row in c.execute(execute, args): for i in row: return int(i) - + conn.close() return None def getBlocksByType(self, blockType, orderDate=True): @@ -659,7 +660,7 @@ class Core: for row in c.execute(execute, args): for i in row: rows.append(i) - + conn.close() return rows def getExpiredBlocks(self): @@ -674,6 +675,7 @@ class Core: for row in c.execute(execute): for i in row: rows.append(i) + conn.close() return rows def setBlockType(self, hash, blockType): diff --git a/onionr/logger.py b/onionr/logger.py index 7a8172b3..7cb409a1 100644 --- a/onionr/logger.py +++ b/onionr/logger.py @@ -132,8 +132,11 @@ def raw(data, fd = sys.stdout, sensitive = False): if get_settings() & OUTPUT_TO_CONSOLE: ts = fd.write('%s\n' % data) if get_settings() & OUTPUT_TO_FILE and not sensitive: - with open(_outputfile, "a+") as f: - f.write(colors.filter(data) + '\n') + 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): ''' diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py index 04dcf65a..f60e3b0f 100644 --- a/onionr/onionrblockapi.py +++ b/onionr/onionrblockapi.py @@ -647,6 +647,7 @@ class Block: buffer += contents.decode() else: file.write(contents) + file.close() return (None if not file is None else buffer) @@ -735,6 +736,7 @@ class Block: # return different things depending on verbosity if verbose: return (blocks[-1], blocks) + file.close() return blocks[-1] def exists(bHash): diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 332d6a70..4f936d93 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -162,7 +162,7 @@ class OnionrUtils: retData += '%s:%s' % (hostname, config.get('client.client.port')) return retData - def localCommand(self, command, data='', silent = True, post=False, postData = {}): + def localCommand(self, command, data='', silent = True, post=False, postData = {}, maxWait=10): ''' Send a command to the local http API server, securely. Intended for local clients, DO NOT USE for remote peers. ''' @@ -170,7 +170,6 @@ class OnionrUtils: self.getTimeBypassToken() # TODO: URL encode parameters, just as an extra measure. May not be needed, but should be added regardless. hostname = '' - maxWait = 5 waited = 0 while hostname == '': try: @@ -185,9 +184,9 @@ class OnionrUtils: payload = 'http://%s/%s%s' % (hostname, command, data) try: if post: - retData = requests.post(payload, data=postData, headers={'token': config.get('client.webpassword')}, timeout=(15, 30)).text + retData = requests.post(payload, data=postData, headers={'token': config.get('client.webpassword')}, timeout=(maxWait, 30)).text else: - retData = requests.get(payload, headers={'token': config.get('client.webpassword')}, timeout=(15, 30)).text + retData = requests.get(payload, headers={'token': config.get('client.webpassword')}, timeout=(maxWait, 30)).text except Exception as error: if not silent: logger.error('Failed to make local request (command: %s):%s' % (command, error)) diff --git a/onionr/static-data/default-plugins/cliui/main.py b/onionr/static-data/default-plugins/cliui/main.py index c7c88acf..59b59b0c 100644 --- a/onionr/static-data/default-plugins/cliui/main.py +++ b/onionr/static-data/default-plugins/cliui/main.py @@ -31,11 +31,14 @@ class OnionrCLIUI: self.myCore = apiInst.get_core() return - def subCommand(self, command): + 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) - subprocess.call(['./onionr.py', command]) + if args != None: + subprocess.call(['./onionr.py', command, args]) + else: + subprocess.call(['./onionr.py', command]) except KeyboardInterrupt: pass @@ -48,12 +51,11 @@ class OnionrCLIUI: isOnline = 'No' firstRun = True choice = '' - - if self.myCore._utils.localCommand('ping') == 'pong': + if self.myCore._utils.localCommand('ping', maxWait=10) == 'pong!': firstRun = False while showMenu: - if self.myCore._utils.localCommand('ping') == 'pong': + if self.myCore._utils.localCommand('ping', maxWait=2) == 'pong!': isOnline = "Yes" else: isOnline = "No" @@ -62,8 +64,7 @@ class OnionrCLIUI: 1. Flow (Anonymous public chat, use at your own risk) 2. Mail (Secure email-like service) 3. File Sharing -4. User Settings -5. Quit (Does not shutdown daemon) +4. Quit (Does not shutdown daemon) ''') try: choice = input(">").strip().lower() @@ -75,13 +76,9 @@ class OnionrCLIUI: elif choice in ("2", "mail"): self.subCommand("mail") elif choice in ("3", "file sharing", "file"): - print("Not supported yet") - elif choice in ("4", "user settings", "settings"): - try: - self.setName() - except (KeyboardInterrupt, EOFError) as e: - pass - elif choice in ("5", "quit"): + filename = input("Enter full path to file: ").strip() + self.subCommand("addfile", filename) + elif choice in ("4", "quit"): showMenu = False elif choice == "": pass @@ -89,14 +86,6 @@ class OnionrCLIUI: logger.error("Invalid choice") return - def setName(self): - try: - name = input("Enter your name: ") - if name != "": - self.myCore.insertBlock("userInfo-" + str(uuid.uuid1()), sign=True, header='userInfo', meta={'name': name}) - except KeyboardInterrupt: - pass - return def on_init(api, data = None): ''' diff --git a/onionr/static-data/default-plugins/metadataprocessor/main.py b/onionr/static-data/default-plugins/metadataprocessor/main.py index c0d3d38d..166249be 100644 --- a/onionr/static-data/default-plugins/metadataprocessor/main.py +++ b/onionr/static-data/default-plugins/metadataprocessor/main.py @@ -28,24 +28,6 @@ plugin_name = 'metadataprocessor' # event listeners -def _processUserInfo(api, newBlock): - ''' - Set the username for a particular user, from a signed block by them - ''' - myBlock = newBlock - peerName = myBlock.getMetadata('name') - try: - if len(peerName) > 20: - raise onionrexceptions.InvalidMetdata('Peer name specified is too large') - except TypeError: - pass - except onionrexceptions.InvalidMetadata: - pass - else: - if signer in self.api.get_core().listPeers(): - api.get_core().setPeerInfo(signer, 'name', peerName) - logger.info('%s is now using the name %s.' % (signer, api.get_utils().escapeAnsi(peerName))) - def _processForwardKey(api, myBlock): ''' Get the forward secrecy key specified by the user for us to use @@ -67,12 +49,8 @@ def on_processblocks(api): # Process specific block types - # userInfo blocks, such as for setting username - if blockType == 'userInfo': - if api.data['validSig'] == True: # we use == True for type safety - _processUserInfo(api, myBlock) # forwardKey blocks, add a new forward secrecy key for a peer - elif blockType == 'forwardKey': + if blockType == 'forwardKey': if api.data['validSig'] == True: _processForwardKey(api, myBlock) # socket blocks diff --git a/onionr/static-data/www/shared/misc.js b/onionr/static-data/www/shared/misc.js index ae0c1fb0..cd3dbba1 100644 --- a/onionr/static-data/www/shared/misc.js +++ b/onionr/static-data/www/shared/misc.js @@ -1,4 +1,5 @@ webpass = document.location.hash.replace('#', '') +nowebpass = false if (typeof webpass == "undefined"){ webpass = localStorage['webpass'] } @@ -8,6 +9,7 @@ else{ } if (typeof webpass == "undefined" || webpass == ""){ alert('Web password was not found in memory or URL') + nowebpass = true } function httpGet(theUrl) { diff --git a/onionr/static-data/www/shared/panel.js b/onionr/static-data/www/shared/panel.js index c79241c0..ea35696c 100644 --- a/onionr/static-data/www/shared/panel.js +++ b/onionr/static-data/www/shared/panel.js @@ -1,6 +1,8 @@ shutdownBtn = document.getElementById('shutdownNode') shutdownBtn.onclick = function(){ - httpGet('shutdownclean') - overlay('shutdownNotice') + if (! nowebpass){ + httpGet('shutdownclean') + overlay('shutdownNotice') + } } From e2c5fa3744cb0f4fce433a53c50092e890b847a0 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 17 Jan 2019 00:52:08 -0600 Subject: [PATCH 33/94] Work on gui, removed daemon queue print debug --- onionr/core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/onionr/core.py b/onionr/core.py index c427307c..00b68986 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -393,7 +393,6 @@ class Core: while resp == 'failure': resp = self.daemonQueueGetResponse(responseID) time.sleep(1) - print(resp) return resp def daemonQueueSimple(self, command, data='', checkFreqSecs=1): From ea5f18d4e107ad33b53035b79a0059e00d28302e Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 17 Jan 2019 19:14:26 -0600 Subject: [PATCH 34/94] removed gui --- onionr/core.py | 8 ++-- onionr/onionrblacklist.py | 4 +- onionr/onionrgui.py | 78 --------------------------------------- 3 files changed, 6 insertions(+), 84 deletions(-) delete mode 100755 onionr/onionrgui.py diff --git a/onionr/core.py b/onionr/core.py index 00b68986..5281f4c6 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -379,7 +379,7 @@ class Core: events.event('queue_push', data = {'command': command, 'data': data}, onionr = None) conn.close() return retData - + def daemonQueueGetResponse(self, responseID=''): ''' Get a response sent by communicator to the API, by requesting to the API @@ -387,14 +387,14 @@ class Core: 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 @@ -591,7 +591,7 @@ class Core: c = conn.cursor() command = (data, address) - + if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'DBHash', 'failure', 'powValue', 'lastConnect', 'lastConnectAttempt', 'trust', 'introduced'): raise Exception("Got invalid database key when setting address info") else: diff --git a/onionr/onionrblacklist.py b/onionr/onionrblacklist.py index 8736f78f..a87163f3 100644 --- a/onionr/onionrblacklist.py +++ b/onionr/onionrblacklist.py @@ -39,7 +39,7 @@ class OnionrBlackList: for i in self._dbExecute("SELECT * FROM blacklist WHERE hash = ?", (hashed,)): retData = True # this only executes if an entry is present by that hash break - + return retData def _dbExecute(self, toExec, params = ()): @@ -82,7 +82,7 @@ class OnionrBlackList: return def clearDB(self): - self._dbExecute('''DELETE FROM blacklist;);''') + self._dbExecute('''DELETE FROM blacklist;''') def getList(self): data = self._dbExecute('SELECT * FROM blacklist') diff --git a/onionr/onionrgui.py b/onionr/onionrgui.py deleted file mode 100755 index 41ff8fc0..00000000 --- a/onionr/onionrgui.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python3 -import threading, time -from tkinter import * -import core -class OnionrGUI: - def __init__(self): - self.dataDir = "/programming/onionr/data/" - self.root = Tk() - self.root.geometry("450x250") - self.core = core.Core() - menubar = Menu(self.root) - - # create a pulldown menu, and add it to the menu bar - filemenu = Menu(menubar, tearoff=0) - #filemenu.add_command(label="Open", command=None) - filemenu.add_separator() - filemenu.add_command(label="Exit", command=self.root.quit) - menubar.add_cascade(label="File", menu=filemenu) - - settingsmenu = Menu(menubar, tearoff=0) - menubar.add_cascade(label="Settings", menu=settingsmenu) - - helpmenu = Menu(menubar, tearoff=0) - menubar.add_cascade(label="Help", menu=helpmenu) - - self.root.config(menu=menubar) - - self.menuFrame = Frame(self.root) - self.tabButton1 = Button(self.menuFrame, text="Mail", command=self.openMail) - self.tabButton1.grid(row=0, column=1, padx=0, pady=2, sticky=N+W) - self.tabButton2 = Button(self.menuFrame, text="Message Flow") - self.tabButton2.grid(row=0, column=3, padx=0, pady=2, sticky=N+W) - - self.menuFrame.grid(row=0, column=0, padx=2, pady=0, sticky=N+W) - - self.idFrame = Frame(self.root) - - self.ourIDLabel = Label(self.idFrame, text="ID: ") - self.ourIDLabel.grid(row=2, column=0, padx=1, pady=1, sticky=N+W) - self.ourID = Entry(self.idFrame) - self.ourID.insert(0, self.core._crypto.pubKey) - self.ourID.grid(row=2, column=1, padx=1, pady=1, sticky=N+W) - self.ourID.config(state='readonly') - self.idFrame.grid(row=1, column=0, padx=2, pady=2, sticky=N+W) - - self.syncStatus = Label(self.root, text="Sync Status: 15/100") - self.syncStatus.place(relx=1.0, rely=1.0, anchor=S+E) - self.peerCount = Label(self.root, text="Connected Peers: ") - self.peerCount.place(relx=0.0, rely=1.0, anchor='sw') - - self.root.wm_title("Onionr") - threading.Thread(target=self.updateStats) - self.root.mainloop() - return - - def updateStats(self): - #self.core._utils.localCommand() - self.peerCount.config(text='Connected Peers: %s' % ()) - time.sleep(1) - return - def openMail(self): - MailWindow(self) - - -class MailWindow: - def __init__(self, mainGUI): - assert isinstance(mainGUI, OnionrGUI) - self.core = mainGUI.core - self.mailRoot = Toplevel() - - self.inboxFrame = Frame(self.mailRoot) - self.sentboxFrame = Frame(self.mailRoot) - self.composeFrame = Frame(self.mailRoot) - - - self.mailRoot.mainloop() - -OnionrGUI() \ No newline at end of file From 11d904754886a771ad502ff3c56a74c92bf2281e Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 17 Jan 2019 23:34:13 -0600 Subject: [PATCH 35/94] added tor check and fixed fd exhaustion --- onionr/api.py | 2 ++ onionr/communicator2.py | 3 +++ onionr/netcontroller.py | 9 +++++++++ onionr/onionr.py | 10 +++++++++- onionr/onionrutils.py | 10 +++++----- onionr/static-data/default_config.json | 1 + 6 files changed, 29 insertions(+), 6 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index cec7288a..e24e21a6 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -99,6 +99,7 @@ class PublicAPI: resp.headers['X-Frame-Options'] = 'deny' resp.headers['X-Content-Type-Options'] = "nosniff" resp.headers['X-API'] = onionr.API_VERSION + resp.headers['Connection'] = "close" return resp @app.route('/') @@ -288,6 +289,7 @@ class API: resp.headers['X-API'] = onionr.API_VERSION 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/communicator2.py b/onionr/communicator2.py index c3540e61..e258c8c8 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -140,6 +140,9 @@ class OnionrCommunicatorDaemon: break i.processTimer() time.sleep(self.delay) + # Debug to print out used FDs (regular and net) + #proc = psutil.Process() + #print(proc.open_files(), len(psutil.net_connections())) except KeyboardInterrupt: self.shutdown = True pass diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py index c87b5e46..c415c1a9 100644 --- a/onionr/netcontroller.py +++ b/onionr/netcontroller.py @@ -22,6 +22,7 @@ import subprocess, os, random, sys, logger, time, signal, config, base64, socket from stem.control import Controller from onionrblockapi import Block from dependencies import secrets +from shutil import which def getOpenPort(): # taken from (but modified) https://stackoverflow.com/a/2838309 @@ -31,6 +32,14 @@ def getOpenPort(): port = s.getsockname()[1] s.close() return port + +def torBinary(): + '''Return tor binary path or none if not exists''' + torPath = './tor' + if not os.path.exists(torPath): + torPath = which('tor') + return torPath + class NetController: ''' This class handles hidden service setup on Tor and I2P diff --git a/onionr/onionr.py b/onionr/onionr.py index e54e90bb..6ec90725 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -68,6 +68,10 @@ class Onionr: # Load global configuration data data_exists = Onionr.setupConfig(self.dataDir, self = self) + if netcontroller.torBinary() is None: + logger.error('Tor is not installed') + sys.exit(1) + self.onionrCore = core.Core() #self.deleteRunFiles() self.onionrUtils = onionrutils.OnionrUtils(self.onionrCore) @@ -292,7 +296,8 @@ class Onionr: newID = self.onionrCore._crypto.keyManager.addKey()[0] else: logger.warn('Deterministic keys require random and long passphrases.') - logger.warn('If a good password is not used, your key can be easily stolen.') + 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/') pass1 = getpass.getpass(prompt='Enter at least %s characters: ' % (self.onionrCore._crypto.deterministicRequirement,)) pass2 = getpass.getpass(prompt='Confirm entry: ') if self.onionrCore._crypto.safeCompare(pass1, pass2): @@ -760,6 +765,9 @@ class Onionr: try: while True: time.sleep(3) + # Debug to print out used FDs (regular and net) + #proc = psutil.Process() + #print('api-files:',proc.open_files(), len(psutil.net_connections())) # Break if communicator process ends, so we don't have left over processes if communicatorProc.poll() is not None: break diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 4f936d93..3260a42b 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -184,9 +184,9 @@ class OnionrUtils: payload = 'http://%s/%s%s' % (hostname, command, data) try: if post: - retData = requests.post(payload, data=postData, headers={'token': config.get('client.webpassword')}, timeout=(maxWait, 30)).text + retData = requests.post(payload, data=postData, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, 30)).text else: - retData = requests.get(payload, headers={'token': config.get('client.webpassword')}, timeout=(maxWait, 30)).text + retData = requests.get(payload, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, 30)).text except Exception as error: if not silent: logger.error('Failed to make local request (command: %s):%s' % (command, error)) @@ -624,7 +624,7 @@ class OnionrUtils: proxies = {'http': 'http://127.0.0.1:4444'} else: return - headers = {'user-agent': 'PyOnionr'} + 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)) @@ -649,11 +649,11 @@ class OnionrUtils: proxies = {'http': 'http://127.0.0.1:4444'} else: return - headers = {'user-agent': 'PyOnionr'} + 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)) + 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: diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index 2ef339bc..93f3a49a 100644 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -7,6 +7,7 @@ "socket_servers": false, "security_level": 0, "max_block_age": 2678400, + "bypass_tor_check": false, "public_key": "" }, From 403150300ed55ab18407acf4b39008f281ad23d4 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 19 Jan 2019 20:23:26 -0600 Subject: [PATCH 36/94] hopefully fully fixed FDs now --- onionr/api.py | 25 +++++++++++++++++++------ onionr/communicator2.py | 2 +- onionr/onionrdaemontools.py | 2 +- onionr/static-data/default_config.json | 2 +- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index e24e21a6..88f5f69f 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -17,9 +17,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -from gevent.pywsgi import WSGIServer -import gevent.monkey -gevent.monkey.patch_socket() +from gevent.pywsgi import WSGIServer, WSGIHandler +from gevent import Timeout +#import gevent.monkey +#gevent.monkey.patch_socket() import flask, cgi from flask import request, Response, abort, send_from_directory import sys, random, threading, hmac, hashlib, base64, time, math, os, json, socket @@ -27,6 +28,17 @@ import core from onionrblockapi import Block import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config, onionr +class FDSafeHandler(WSGIHandler): + def handle(self): + timeout = Timeout(10, exception=Exception) + timeout.start() + + #timeout = gevent.Timeout.start_new(3) + try: + WSGIHandler.handle(self) + except Timeout as ex: + raise + def guessMime(path): ''' Guesses the mime type of a file from the input filename @@ -221,7 +233,7 @@ class PublicAPI: clientAPI._core.refreshFirstStartVars() self.torAdder = clientAPI._core.hsAddress time.sleep(1) - self.httpServer = WSGIServer((self.host, self.bindPort), app, log=None) + self.httpServer = WSGIServer((self.host, self.bindPort), app, log=None, handler_class=FDSafeHandler) self.httpServer.serve_forever() class API: @@ -394,13 +406,14 @@ class API: @app.route('/getstats') def getStats(): - return Response(self._core.serializer.getStats()) + return Response("disabled") + #return Response(self._core.serializer.getStats()) @app.route('/getuptime') def showUptime(): return Response(str(self.getUptime())) - self.httpServer = WSGIServer((self.host, bindPort), app, log=None) + self.httpServer = WSGIServer((self.host, bindPort), app, log=None, handler_class=FDSafeHandler) self.httpServer.serve_forever() def setPublicAPIInstance(self, inst): diff --git a/onionr/communicator2.py b/onionr/communicator2.py index e258c8c8..de55a4fc 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -97,7 +97,7 @@ class OnionrCommunicatorDaemon: # Set timers, function reference, seconds # 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, 1) + OnionrCommunicatorTimers(self, self.runCheck, 2, maxThreads=1) 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.clearOfflinePeer, 58) diff --git a/onionr/onionrdaemontools.py b/onionr/onionrdaemontools.py index 9f1ab2a2..38db5d5b 100644 --- a/onionr/onionrdaemontools.py +++ b/onionr/onionrdaemontools.py @@ -74,7 +74,7 @@ class DaemonTools: retData = True self.daemon._core.setAddressInfo(peer, 'introduced', 1) self.daemon._core.setAddressInfo(peer, 'powValue', data['random']) - self.daemon.decrementThreadCount('announceNode') + self.daemon.decrementThreadCount('announceNode') return retData def netCheck(self): diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index 93f3a49a..cb7b37b1 100644 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -50,7 +50,7 @@ "file": { "output": true, - "path": "data/output.log" + "path": "output.log" }, "console" : { From 6b25a9301cfbc09a9235b7e5a620e4659e728dbc Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 19 Jan 2019 23:39:25 -0600 Subject: [PATCH 37/94] fixed broken deterministic addresses made it so we do not use forward secrecy when sending to self --- onionr/core.py | 23 ++++++++++++----------- onionr/onionrcrypto.py | 9 +++------ 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/onionr/core.py b/onionr/core.py index 5281f4c6..9c57503f 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -718,7 +718,7 @@ class Core: return True - def insertBlock(self, data, header='txt', sign=False, encryptType='', symKey='', asymPeer='', meta = {}, expire=None): + def insertBlock(self, data, header='txt', sign=False, encryptType='', symKey='', asymPeer='', meta = {}, expire=None, disableForward=False): ''' Inserts a block into the network encryptType must be specified to encrypt a block @@ -765,16 +765,17 @@ class Core: pass if encryptType == 'asym': - try: - forwardEncrypted = onionrusers.OnionrUser(self, asymPeer).forwardEncrypt(data) - data = forwardEncrypted[0] - meta['forwardEnc'] = True - except onionrexceptions.InvalidPubkey: - pass - #onionrusers.OnionrUser(self, asymPeer).generateForwardKey() - fsKey = onionrusers.OnionrUser(self, asymPeer).generateForwardKey() - #fsKey = onionrusers.OnionrUser(self, asymPeer).getGeneratedForwardKeys().reverse() - meta['newFSKey'] = fsKey + if not disableForward and asymPeer != self._crypto.pubKey: + try: + forwardEncrypted = onionrusers.OnionrUser(self, asymPeer).forwardEncrypt(data) + data = forwardEncrypted[0] + meta['forwardEnc'] = True + except onionrexceptions.InvalidPubkey: + pass + #onionrusers.OnionrUser(self, asymPeer).generateForwardKey() + fsKey = onionrusers.OnionrUser(self, asymPeer).generateForwardKey() + #fsKey = onionrusers.OnionrUser(self, asymPeer).getGeneratedForwardKeys().reverse() + meta['newFSKey'] = fsKey jsonMeta = json.dumps(meta) if sign: signature = self._crypto.edSign(jsonMeta.encode() + data, key=self._crypto.privKey, encodeResult=True) diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py index 03da9213..8c15f144 100644 --- a/onionr/onionrcrypto.py +++ b/onionr/onionrcrypto.py @@ -210,12 +210,9 @@ class OnionrCrypto: ops = nacl.pwhash.argon2id.OPSLIMIT_SENSITIVE mem = nacl.pwhash.argon2id.MEMLIMIT_SENSITIVE - key = kdf(nacl.secret.SecretBox.KEY_SIZE, passphrase, salt, opslimit=ops, memlimit=mem) - key = nacl.public.PrivateKey(key, nacl.encoding.RawEncoder()) - publicKey = key.public_key - - return (publicKey.encode(encoder=nacl.encoding.Base32Encoder()), - key.encode(encoder=nacl.encoding.Base32Encoder())) + key = kdf(32, passphrase, salt, opslimit=ops, memlimit=mem) # Generate seed for ed25519 key + key = nacl.signing.SigningKey(key) + return (key.verify_key.encode(nacl.encoding.Base32Encoder).decode(), key.encode(nacl.encoding.Base32Encoder).decode()) def pubKeyHashID(self, pubkey=''): '''Accept a ed25519 public key, return a truncated result of X many sha3_256 hash rounds''' From d2e7ced776f00708387648da068b3327bbd48421 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 20 Jan 2019 12:09:53 -0600 Subject: [PATCH 38/94] changed communicator to be in the same process --- onionr/api.py | 4 ++-- onionr/communicator2.py | 37 +++++++++++++------------------------ onionr/onionr.py | 13 ++++++++++--- onionr/onionrusers.py | 2 +- onionr/serializeddata.py | 2 +- 5 files changed, 27 insertions(+), 31 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 88f5f69f..c6047dee 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -406,8 +406,8 @@ class API: @app.route('/getstats') def getStats(): - return Response("disabled") - #return Response(self._core.serializer.getStats()) + #return Response("disabled") + return Response(self._core.serializer.getStats()) @app.route('/getuptime') def showUptime(): diff --git a/onionr/communicator2.py b/onionr/communicator2.py index de55a4fc..873e40c7 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -25,11 +25,13 @@ import onionrdaemontools, onionrsockets, onionr, onionrproofs, proofofmemory import binascii from dependencies import secrets from defusedxml import minidom - +config.reload() class OnionrCommunicatorDaemon: - def __init__(self, debug, developmentMode): + def __init__(self, onionrInst, proxyPort, developmentMode=config.get('general.dev_mode', False)): + onionrInst.communicatorInst = self # configure logger and stuff onionr.Onionr.setupConfig('data/', self = self) + self.proxyPort = proxyPort self.isOnline = True # Assume we're connected to the internet @@ -37,7 +39,7 @@ class OnionrCommunicatorDaemon: self.timers = [] # initalize core with Tor socks port being 3rd argument - self.proxyPort = sys.argv[2] + self.proxyPort = proxyPort self._core = core.Core(torPort=self.proxyPort) # intalize NIST beacon salt and time @@ -49,9 +51,6 @@ class OnionrCommunicatorDaemon: # loop time.sleep delay in seconds self.delay = 1 - # time app started running for info/statistics purposes - self.startTime = self._core._utils.getEpoch() - # lists of connected peers and peers we know we can't reach currently self.onlinePeers = [] self.offlinePeers = [] @@ -90,8 +89,11 @@ class OnionrCommunicatorDaemon: # 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() - if debug or developmentMode: + if developmentMode: + print('enabling heartbeat') OnionrCommunicatorTimers(self, self.heartbeat, 30) # Set timers, function reference, seconds @@ -129,8 +131,6 @@ class OnionrCommunicatorDaemon: self.socketServer.start() self.socketClient = onionrsockets.OnionrSocketClient(self._core) - # Loads chat messages into memory - threading.Thread(target=self._chat.chatHandler).start() # Main daemon loop, mainly for calling timers, don't do any complex operations here to avoid locking try: @@ -513,6 +513,8 @@ class OnionrCommunicatorDaemon: 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': @@ -643,18 +645,5 @@ class OnionrCommunicatorTimers: self.count = -1 # negative 1 because its incremented at bottom self.count += 1 -shouldRun = False -debug = True -developmentMode = False -if config.get('general.dev_mode', True): - developmentMode = True -try: - if sys.argv[1] == 'run': - shouldRun = True -except IndexError: - pass -if shouldRun: - try: - OnionrCommunicatorDaemon(debug, developmentMode) - except Exception as e: - logger.error('Error occured in Communicator', error = e, timestamp = False) +def startCommunicator(onionrInst, proxyPort): + OnionrCommunicatorDaemon(onionrInst, proxyPort) \ No newline at end of file diff --git a/onionr/onionr.py b/onionr/onionr.py index 6ec90725..8523ec77 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -33,7 +33,7 @@ import onionrutils import netcontroller from netcontroller import NetController from onionrblockapi import Block -import onionrproofs, onionrexceptions, onionrusers +import onionrproofs, onionrexceptions, onionrusers, communicator2 try: from urllib3.contrib.socks import SOCKSProxyManager @@ -72,6 +72,7 @@ class Onionr: logger.error('Tor is not installed') sys.exit(1) + self.communicatorInst = None self.onionrCore = core.Core() #self.deleteRunFiles() self.onionrUtils = onionrutils.OnionrUtils(self.onionrCore) @@ -749,7 +750,13 @@ class Onionr: time.sleep(1) # TODO: make runable on windows - communicatorProc = subprocess.Popen([communicatorDaemon, 'run', str(net.socksPort)]) + #communicatorProc = subprocess.Popen([communicatorDaemon, 'run', str(net.socksPort)]) + + communicatorThread = Thread(target=communicator2.startCommunicator, args=(self, str(net.socksPort))) + communicatorThread.start() + + while self.communicatorInst is None: + time.sleep(0.1) # print nice header thing :) if config.get('general.display_header', True): @@ -769,7 +776,7 @@ class Onionr: #proc = psutil.Process() #print('api-files:',proc.open_files(), len(psutil.net_connections())) # Break if communicator process ends, so we don't have left over processes - if communicatorProc.poll() is not None: + if self.communicatorInst.shutdown: break if self.killed: break # Break out if sigterm for clean exit diff --git a/onionr/onionrusers.py b/onionr/onionrusers.py index 9671c5db..c5bf41d1 100644 --- a/onionr/onionrusers.py +++ b/onionr/onionrusers.py @@ -83,7 +83,7 @@ class OnionrUser: if self._core._utils.validatePubKey(forwardKey): retData = self._core._crypto.pubKeyEncrypt(data, forwardKey, encodedData=True, anonymous=True) else: - raise onionrexceptions.InvalidPubkey("No valid forward key available for this user") + raise onionrexceptions.InvalidPubkey("No valid forward secrecy key available for this user") #self.generateForwardKey() return (retData, forwardKey) diff --git a/onionr/serializeddata.py b/onionr/serializeddata.py index a7ff2e80..79143374 100644 --- a/onionr/serializeddata.py +++ b/onionr/serializeddata.py @@ -36,7 +36,7 @@ class SerializedData: def getStats(self): '''Return statistics about our node''' stats = {} - stats['uptime'] = self._core._utils.localCommand('getuptime') + stats['uptime'] = int(self._core.daemonQueueSimple('localCommand', 'getuptime')) stats['connectedNodes'] = self._core.daemonQueueSimple('connectedPeers') stats['blockCount'] = len(self._core.getBlockList()) return json.dumps(stats) From a9e61e28271b3b6a777804a1b1a03ab65c24e476 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 20 Jan 2019 16:54:04 -0600 Subject: [PATCH 39/94] improved api-communicator integration, panel --- onionr/api.py | 2 +- onionr/communicator2.py | 9 +++++---- onionr/core.py | 1 + onionr/onionr.py | 3 ++- onionr/serializeddata.py | 4 ++-- onionr/static-data/www/private/index.html | 2 +- onionr/static-data/www/shared/main/stats.js | 11 +++++++---- onionr/static-data/www/shared/panel.js | 6 +++++- 8 files changed, 24 insertions(+), 14 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index c6047dee..3a340dc3 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -256,7 +256,7 @@ class API: self.debug = debug self._privateDelayTime = 3 - self._core = core.Core() + self._core = onionrInst.onionrCore self.startTime = self._core._utils.getEpoch() self._crypto = onionrcrypto.OnionrCrypto(self._core) self._utils = onionrutils.OnionrUtils(self._core) diff --git a/onionr/communicator2.py b/onionr/communicator2.py index 873e40c7..d131e332 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -40,7 +40,7 @@ class OnionrCommunicatorDaemon: # initalize core with Tor socks port being 3rd argument self.proxyPort = proxyPort - self._core = core.Core(torPort=self.proxyPort) + self._core = onionrInst.onionrCore # intalize NIST beacon salt and time self.nistSaltTimestamp = 0 @@ -93,7 +93,6 @@ class OnionrCommunicatorDaemon: self.startTime = self._core._utils.getEpoch() if developmentMode: - print('enabling heartbeat') OnionrCommunicatorTimers(self, self.heartbeat, 30) # Set timers, function reference, seconds @@ -485,10 +484,12 @@ class OnionrCommunicatorDaemon: retData = onionrpeers.PeerProfiles(peer, self._core) return retData + def getUptime(self): + return self._core._utils.getEpoch() - self.startTime + def heartbeat(self): '''Show a heartbeat debug message''' - currentTime = self._core._utils.getEpoch() - self.startTime - logger.debug('Heartbeat. Node running for %s.' % self.daemonTools.humanReadableTime(currentTime)) + logger.debug('Heartbeat. Node running for %s.' % self.daemonTools.humanReadableTime(self.getUptime())) self.decrementThreadCount('heartbeat') def daemonCommands(self): diff --git a/onionr/core.py b/onionr/core.py index 9c57503f..e7646396 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -45,6 +45,7 @@ class Core: self.dataDir = 'data/' try: + self.onionrInst = None self.queueDB = self.dataDir + 'queue.db' self.peerDB = self.dataDir + 'peers.db' self.blockDB = self.dataDir + 'blocks.db' diff --git a/onionr/onionr.py b/onionr/onionr.py index 8523ec77..4f054a3b 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -74,6 +74,7 @@ class Onionr: self.communicatorInst = None self.onionrCore = core.Core() + self.onionrCore.onionrInst = self #self.deleteRunFiles() self.onionrUtils = onionrutils.OnionrUtils(self.onionrCore) @@ -751,7 +752,7 @@ class Onionr: # TODO: make runable on windows #communicatorProc = subprocess.Popen([communicatorDaemon, 'run', str(net.socksPort)]) - + self.onionrCore.torPort = net.socksPort communicatorThread = Thread(target=communicator2.startCommunicator, args=(self, str(net.socksPort))) communicatorThread.start() diff --git a/onionr/serializeddata.py b/onionr/serializeddata.py index 79143374..c4063909 100644 --- a/onionr/serializeddata.py +++ b/onionr/serializeddata.py @@ -36,7 +36,7 @@ class SerializedData: def getStats(self): '''Return statistics about our node''' stats = {} - stats['uptime'] = int(self._core.daemonQueueSimple('localCommand', 'getuptime')) - stats['connectedNodes'] = self._core.daemonQueueSimple('connectedPeers') + stats['uptime'] = self._core.onionrInst.communicatorInst.getUptime() + stats['connectedNodes'] = '\n'.join(self._core.onionrInst.communicatorInst.onlinePeers) stats['blockCount'] = len(self._core.getBlockList()) return json.dumps(stats) diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index 12f05b85..d78140a5 100644 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -17,7 +17,7 @@ Onionr Web Control Panel
- +

Stats

Uptime:

Stored Blocks:

diff --git a/onionr/static-data/www/shared/main/stats.js b/onionr/static-data/www/shared/main/stats.js index 6796f2b7..bd6dcb18 100644 --- a/onionr/static-data/www/shared/main/stats.js +++ b/onionr/static-data/www/shared/main/stats.js @@ -21,7 +21,10 @@ uptimeDisplay = document.getElementById('uptime') connectedDisplay = document.getElementById('connectedNodes') storedBlockDisplay = document.getElementById('storedBlocks') -stats = JSON.parse(httpGet('getstats', webpass)) -uptimeDisplay.innerText = stats['uptime'] + ' seconds' -connectedDisplay.innerText = stats['connectedNodes'] -storedBlockDisplay.innerText = stats['blockCount'] \ No newline at end of file +function getStats(){ + stats = JSON.parse(httpGet('getstats', webpass)) + uptimeDisplay.innerText = stats['uptime'] + ' seconds' + connectedDisplay.innerText = stats['connectedNodes'] + storedBlockDisplay.innerText = stats['blockCount'] +} +getStats() \ No newline at end of file diff --git a/onionr/static-data/www/shared/panel.js b/onionr/static-data/www/shared/panel.js index ea35696c..665904e8 100644 --- a/onionr/static-data/www/shared/panel.js +++ b/onionr/static-data/www/shared/panel.js @@ -1,8 +1,12 @@ shutdownBtn = document.getElementById('shutdownNode') - +refreshStatsBtn = document.getElementById('refreshStats') shutdownBtn.onclick = function(){ if (! nowebpass){ httpGet('shutdownclean') overlay('shutdownNotice') } } + +refreshStatsBtn.onclick = function(){ + getStats() +} \ No newline at end of file From 10bed5f9c8a52b01f29826af8015cb9d562a3b09 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 21 Jan 2019 00:28:51 -0600 Subject: [PATCH 40/94] work on whitepaper and web gui --- README.md | 5 ++-- docs/whitepaper.md | 27 +++++++++------------ onionr/serializeddata.py | 1 + onionr/static-data/www/board/index.html | 2 +- onionr/static-data/www/private/index.html | 1 + onionr/static-data/www/shared/main/stats.js | 2 ++ 6 files changed, 18 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 5d1aa37b..03ec0de5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![Onionr logo](./docs/onionr-logo.png) -(***experimental, not safe or easy to use yet***) +(***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/) @@ -11,7 +11,6 @@ Anonymous P2P platform, using Tor & I2P. **The main repo for this software is at https://gitlab.com/beardog/Onionr/** - # Summary Onionr is a decentralized, peer-to-peer data storage network, designed to be anonymous and resistant to (meta)data analysis and spam. @@ -28,7 +27,7 @@ Check the [Gitlab Project](https://gitlab.com/beardog/Onionr/milestones/1) to se * [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 -* [ ] Metadata analysis resistance (being improved) +* [X] Metadata analysis resistance ## Other features diff --git a/docs/whitepaper.md b/docs/whitepaper.md index e791b83c..c96739ac 100644 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -5,7 +5,7 @@ # Introduction -The most important thing in the modern world is information. The ability to communicate freely with others. The internet has provided humanity with the ability to spread information globally, but there are many people who try (and sometimes succeed) to stifle the flow of information. +One of the most important things in the modern world is information. The ability to communicate freely with others is crucial for maintaining personal liberties. The internet has provided humanity with the ability to spread information globally, but there are many people who try (and sometimes succeed) to stifle the flow of information. Internet censorship comes in many forms, state censorship, corporate consolidation of media, threats of violence, network exploitation (e.g. denial of service attacks). @@ -14,25 +14,22 @@ To prevent censorship or loss of information, these measures must be in place: * Resistance to censorship of underlying infrastructure or of network hosts * Anonymization of users by default - * The Inability to violently coerce human users (personal threats/"doxxing", or totalitarian regime censorship) + * The Inability to coerce human users (personal threats/"doxxing", or totalitarian regime censorship) -* Economic availability. A system should not rely on a single device to be constantly online, and should not be overly expensive to use. The majority of people in the world own cell phones, but comparatively few own personal computers, particularly in developing countries. +* Economic availability. A system should not rely on a single device to be constantly online, and should not be overly expensive to use. The majority of people in the world own cell phones, but comparatively few own personal computers, particularly in developing countries. Internet connectivity can be slow or spotty in many areas. There are many great projects that tackle decentralization and privacy issues, but there are none which tackle all of the above issues. Some of the existing networks have also not worked well in practice, or are more complicated than they need to be. # Onionr Design Goals -When designing Onionr we had these goals in mind: +When designing Onionr we had these main goals in mind: * Anonymous Blocks - - * Difficult to determine block creator or users regardless of transport used -* Default Anonymous Transport Layer - * Tor and I2P + * Difficult to determine block creator or users regardless of transport used +* Node-anonymity * Transport agnosticism -* Default global sync, but can configure what blocks to seed +* Default global sync, but configurable * Spam resistance -* Encrypted blocks # Onionr Design @@ -40,9 +37,9 @@ When designing Onionr we had these goals in mind: ## General Overview -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 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. +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. +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 reciept or transport mechanism. ## User IDs @@ -52,11 +49,11 @@ Public keys can be generated deterministicly with a password using a key derivat ## Nodes -Although Onionr is transport agnostic, the only supported transports in the reference implemetation are Tor .onion services and I2P hidden services. Nodes announce their address on creation. +Although Onionr is transport agnostic, the only supported transports in the reference implemetation are Tor .onion services and I2P hidden services. Nodes announce their address on creation by connecting to peers specified in a bootstrap file. Peers in the bootstrap file have no special permissions aside from being default peers. ### 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 reliabilty of a node, and reduces 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 amount of successful block transfers, speed, and reliabilty 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. ## Block Format @@ -93,5 +90,3 @@ This can be done either by the creator of the block prior to generation, or by a In addition, randomness beacons such as the one operated by [NIST](https://beacon.nist.gov/home) or the hash of the latest blocks in a cryptocurrency network could be used to affirm that a block was at least not *created* before a given time. # Direct Connections - -We propose a system to \ No newline at end of file diff --git a/onionr/serializeddata.py b/onionr/serializeddata.py index c4063909..1587e74e 100644 --- a/onionr/serializeddata.py +++ b/onionr/serializeddata.py @@ -39,4 +39,5 @@ class SerializedData: stats['uptime'] = self._core.onionrInst.communicatorInst.getUptime() stats['connectedNodes'] = '\n'.join(self._core.onionrInst.communicatorInst.onlinePeers) stats['blockCount'] = len(self._core.getBlockList()) + stats['blockQueueCount'] = len(self._core.onionrInst.communicatorInst.blockQueue) return json.dumps(stats) diff --git a/onionr/static-data/www/board/index.html b/onionr/static-data/www/board/index.html index ffc47159..7486a910 100644 --- a/onionr/static-data/www/board/index.html +++ b/onionr/static-data/www/board/index.html @@ -12,7 +12,7 @@ diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index d78140a5..8907195b 100644 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -21,6 +21,7 @@

Stats

Uptime:

Stored Blocks:

+

Blocks in queue:

Connected nodes:


         
diff --git a/onionr/static-data/www/shared/main/stats.js b/onionr/static-data/www/shared/main/stats.js index bd6dcb18..b7d05776 100644 --- a/onionr/static-data/www/shared/main/stats.js +++ b/onionr/static-data/www/shared/main/stats.js @@ -20,11 +20,13 @@ uptimeDisplay = document.getElementById('uptime') connectedDisplay = document.getElementById('connectedNodes') storedBlockDisplay = document.getElementById('storedBlocks') +queuedBlockDisplay = document.getElementById('blockQueue') function getStats(){ stats = JSON.parse(httpGet('getstats', webpass)) uptimeDisplay.innerText = stats['uptime'] + ' seconds' connectedDisplay.innerText = stats['connectedNodes'] storedBlockDisplay.innerText = stats['blockCount'] + queuedBlockDisplay.innerText = stats['blockQueueCount'] } getStats() \ No newline at end of file From 4d5e0aeb74050325ca1d2820335cfe6320d82fd4 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 21 Jan 2019 20:26:56 -0600 Subject: [PATCH 41/94] bug fixes and performence improvements --- docs/whitepaper.md | 10 +++--- onionr/config.py | 2 +- onionr/core.py | 42 +++++++++++++------------- onionr/static-data/bootstrap-nodes.txt | 1 + onionr/static-data/default_config.json | 2 +- 5 files changed, 30 insertions(+), 27 deletions(-) diff --git a/docs/whitepaper.md b/docs/whitepaper.md index c96739ac..a32a1f1d 100644 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -39,21 +39,21 @@ 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 reciept 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). -Public keys can be generated deterministicly 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. +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. ## Nodes -Although Onionr is transport agnostic, the only supported transports in the reference implemetation are Tor .onion services and I2P hidden services. Nodes announce their address on creation by connecting to peers specified in a bootstrap file. Peers in the bootstrap file have no special permissions aside from being default peers. +Although Onionr is transport agnostic, the only supported transports in the reference implementation are Tor .onion services and I2P hidden services. Nodes announce their address on creation by connecting to peers specified in a bootstrap file. Peers in the bootstrap file have no special permissions aside from being default peers. ### 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 reliabilty 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 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. ## Block Format @@ -90,3 +90,5 @@ This can be done either by the creator of the block prior to generation, or by a In addition, randomness beacons such as the one operated by [NIST](https://beacon.nist.gov/home) or the hash of the latest blocks in a cryptocurrency network could be used to affirm that a block was at least not *created* before a given time. # 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, and in addition neither party knows the other's .onion or .i2p hostname, let alone IP address. 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 message brokering instead of a standard socket. \ No newline at end of file diff --git a/onionr/config.py b/onionr/config.py index 3a358233..0e1fd6a1 100644 --- a/onionr/config.py +++ b/onionr/config.py @@ -105,7 +105,7 @@ def check(): open(get_config_file(), 'a', encoding="utf8").close() save() except: - logger.warn('Failed to check configuration file.') + logger.debug('Failed to check configuration file.') def save(): ''' diff --git a/onionr/core.py b/onionr/core.py index e7646396..07762236 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -130,7 +130,7 @@ class Core: events.event('pubkey_add', data = {'key': peerID}, onionr = None) - conn = sqlite3.connect(self.peerDB, timeout=10) + conn = sqlite3.connect(self.peerDB, timeout=30) hashID = self._crypto.pubKeyHashID(peerID) c = conn.cursor() t = (peerID, name, 'unknown', hashID, 0) @@ -160,7 +160,7 @@ class Core: if type(address) is None or len(address) == 0: return False if self._utils.validateID(address): - conn = sqlite3.connect(self.addressDB, timeout=10) + 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 @@ -193,7 +193,7 @@ class Core: ''' if self._utils.validateID(address): - conn = sqlite3.connect(self.addressDB, timeout=10) + conn = sqlite3.connect(self.addressDB, timeout=30) c = conn.cursor() t = (address,) c.execute('Delete from adders where address=?;', t) @@ -213,7 +213,7 @@ class Core: ''' if self._utils.validateHash(block): - conn = sqlite3.connect(self.blockDB, timeout=10) + conn = sqlite3.connect(self.blockDB, timeout=30) c = conn.cursor() t = (block,) c.execute('Delete from hashes where hash=?;', t) @@ -261,7 +261,7 @@ class Core: raise Exception('Block db does not exist') if self._utils.hasBlock(newHash): return - conn = sqlite3.connect(self.blockDB, timeout=10) + conn = sqlite3.connect(self.blockDB, timeout=30) c = conn.cursor() currentTime = self._utils.getEpoch() + self._crypto.secrets.randbelow(301) if selfInsert or dataSaved: @@ -318,7 +318,7 @@ class Core: #blockFile.write(data) #blockFile.close() onionrstorage.store(self, data, blockHash=dataHash) - conn = sqlite3.connect(self.blockDB, timeout=10) + conn = sqlite3.connect(self.blockDB, timeout=30) c = conn.cursor() c.execute("UPDATE hashes SET dataSaved=1 WHERE hash = ?;", (dataHash,)) conn.commit() @@ -341,7 +341,7 @@ class Core: if not os.path.exists(self.queueDB): self.dbCreate.createDaemonDB() else: - conn = sqlite3.connect(self.queueDB, timeout=10) + 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'): @@ -367,7 +367,7 @@ class Core: retData = True date = self._utils.getEpoch() - conn = sqlite3.connect(self.queueDB, timeout=10) + conn = sqlite3.connect(self.queueDB, timeout=30) c = conn.cursor() t = (command, data, date, responseID) @@ -410,7 +410,7 @@ class Core: ''' Clear the daemon queue (somewhat dangerous) ''' - conn = sqlite3.connect(self.queueDB, timeout=10) + conn = sqlite3.connect(self.queueDB, timeout=30) c = conn.cursor() try: @@ -428,7 +428,7 @@ class Core: ''' Return a list of addresses ''' - conn = sqlite3.connect(self.addressDB, timeout=10) + conn = sqlite3.connect(self.addressDB, timeout=30) c = conn.cursor() if randomOrder: addresses = c.execute('SELECT * FROM adders ORDER BY RANDOM();') @@ -456,7 +456,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=10) + conn = sqlite3.connect(self.peerDB, timeout=30) c = conn.cursor() payload = '' @@ -505,7 +505,7 @@ class Core: trust int 4 hashID text 5 ''' - conn = sqlite3.connect(self.peerDB, timeout=10) + conn = sqlite3.connect(self.peerDB, timeout=30) c = conn.cursor() command = (peer,) @@ -531,7 +531,7 @@ class Core: Update a peer for a key ''' - conn = sqlite3.connect(self.peerDB, timeout=10) + conn = sqlite3.connect(self.peerDB, timeout=30) c = conn.cursor() command = (data, peer) @@ -563,7 +563,7 @@ class Core: introduced 10 ''' - conn = sqlite3.connect(self.addressDB, timeout=10) + conn = sqlite3.connect(self.addressDB, timeout=30) c = conn.cursor() command = (address,) @@ -588,7 +588,7 @@ class Core: Update an address for a key ''' - conn = sqlite3.connect(self.addressDB, timeout=10) + conn = sqlite3.connect(self.addressDB, timeout=30) c = conn.cursor() command = (data, address) @@ -609,7 +609,7 @@ class Core: if dateRec == None: dateRec = 0 - conn = sqlite3.connect(self.blockDB, timeout=10) + conn = sqlite3.connect(self.blockDB, timeout=30) c = conn.cursor() # if unsaved: @@ -630,7 +630,7 @@ class Core: Returns the date a block was received ''' - conn = sqlite3.connect(self.blockDB, timeout=10) + conn = sqlite3.connect(self.blockDB, timeout=30) c = conn.cursor() execute = 'SELECT dateReceived FROM hashes WHERE hash=?;' @@ -646,7 +646,7 @@ class Core: Returns a list of blocks by the type ''' - conn = sqlite3.connect(self.blockDB, timeout=10) + conn = sqlite3.connect(self.blockDB, timeout=30) c = conn.cursor() if orderDate: @@ -665,7 +665,7 @@ class Core: def getExpiredBlocks(self): '''Returns a list of expired blocks''' - conn = sqlite3.connect(self.blockDB, timeout=10) + conn = sqlite3.connect(self.blockDB, timeout=30) c = conn.cursor() date = int(self._utils.getEpoch()) @@ -683,7 +683,7 @@ class Core: Sets the type of block ''' - conn = sqlite3.connect(self.blockDB, timeout=10) + conn = sqlite3.connect(self.blockDB, timeout=30) c = conn.cursor() c.execute("UPDATE hashes SET dataType = ? WHERE hash = ?;", (blockType, hash)) conn.commit() @@ -710,7 +710,7 @@ class Core: if key not in ('dateReceived', 'decrypted', 'dataType', 'dataFound', 'dataSaved', 'sig', 'author', 'dateClaimed', 'expire'): return False - conn = sqlite3.connect(self.blockDB, timeout=10) + conn = sqlite3.connect(self.blockDB, timeout=30) c = conn.cursor() args = (data, hash) c.execute("UPDATE hashes SET " + key + " = ? where hash = ?;", args) diff --git a/onionr/static-data/bootstrap-nodes.txt b/onionr/static-data/bootstrap-nodes.txt index e69de29b..ad126465 100644 --- a/onionr/static-data/bootstrap-nodes.txt +++ b/onionr/static-data/bootstrap-nodes.txt @@ -0,0 +1 @@ +svlegnabtuh3dq6ncmzqmpnxzik5mita5x22up4tai2ekngzcgqbnbqd.onion \ No newline at end of file diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index cb7b37b1..5080ac5d 100644 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -79,7 +79,7 @@ "peers" : { "minimum_score" : -100, "max_stored_peers" : 5000, - "max_connect" : 10 + "max_connect" : 50 }, "timers" : { From aef6d5d8e640783e50be5e83a17603ac76ac6a22 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 21 Jan 2019 21:29:29 -0600 Subject: [PATCH 42/94] bug fixes and performence improvements --- onionr/communicator2.py | 5 ++++- onionr/static-data/default-plugins/pms/main.py | 17 +++++++++++------ onionr/static-data/default_config.json | 2 +- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/onionr/communicator2.py b/onionr/communicator2.py index d131e332..632142d4 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -218,7 +218,8 @@ class OnionrCommunicatorDaemon: # if block does not exist on disk and is not already in block queue if i not in self.blockQueue and not self._core._blacklist.inBlacklist(i): if onionrproofs.hashMeetsDifficulty(i): - self.blockQueue.append(i) # add blocks to download queue + if len(self.blockQueue) <= 1000000: + self.blockQueue.append(i) # add blocks to download queue self.decrementThreadCount('lookupBlocks') return @@ -295,6 +296,8 @@ class OnionrCommunicatorDaemon: 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: self.blockQueue.remove(blockHash) # remove from block queue both if success or false diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py index 0cf7e2eb..01e078b2 100644 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -207,22 +207,27 @@ class OnionrMail: else: # if -q or ctrl-c/d, exit function here, otherwise we successfully got the public key return - - logger.info('Enter your message, stop by entering -q on a new line.') + + cancelEnter = False + logger.info('Enter your message, stop by entering -q on a new line. -c to cancel') while newLine != '-q': try: newLine = input() except (KeyboardInterrupt, EOFError): - pass + cancelEnter = True + if newLine == '-c': + cancelEnter = True + break if newLine == '-q': continue newLine += '\n' message += newLine - logger.info('Inserting encrypted message as Onionr block....') + if not cancelEnter: + logger.info('Inserting encrypted message as Onionr block....') - blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=True) - self.sentboxTools.addToSent(blockID, recip, message) + blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=True) + self.sentboxTools.addToSent(blockID, recip, message) def menu(self): choice = '' while True: diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index 5080ac5d..b6cb3145 100644 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -79,7 +79,7 @@ "peers" : { "minimum_score" : -100, "max_stored_peers" : 5000, - "max_connect" : 50 + "max_connect" : 1000 }, "timers" : { From ee9023b15005d1e3421d795255bcaaa6cc69a12e Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 22 Jan 2019 11:40:27 -0600 Subject: [PATCH 43/94] start using very simple DHT --- onionr/communicator2.py | 31 ++++++++++++++++++++++--------- onionr/onionrcrypto.py | 17 ++++++++++++++++- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/onionr/communicator2.py b/onionr/communicator2.py index 632142d4..bda9e29a 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -65,7 +65,7 @@ class OnionrCommunicatorDaemon: self.shutdown = False # list of new blocks to download, added to when new block lists are fetched from peers - self.blockQueue = [] + self.blockQueue = {} # list of blocks currently downloading, avoid s self.currentDownloading = [] @@ -216,16 +216,24 @@ class OnionrCommunicatorDaemon: # 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 and not self._core._blacklist.inBlacklist(i): - if onionrproofs.hashMeetsDifficulty(i): + if i not in self.blockQueue: + if onionrproofs.hashMeetsDifficulty(i) and not self._core._blacklist.inBlacklist(i): if len(self.blockQueue) <= 1000000: - self.blockQueue.append(i) # add blocks to download queue + self.blockQueue[i] = [peer] # add blocks to download queue + else: + if peer not in self.blockQueue[i]: + self.blockQueue[i].append(peer) self.decrementThreadCount('lookupBlocks') return def getBlocks(self): '''download new blocks in queue''' - for blockHash in self.blockQueue: + 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 @@ -236,14 +244,19 @@ class OnionrCommunicatorDaemon: continue if blockHash in self._core.getBlockList(): logger.debug('Block %s is already saved.' % (blockHash,)) - self.blockQueue.remove(blockHash) + del self.blockQueue[blockHash] 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 - peerUsed = self.pickOnlinePeer() + if len(blockPeers) == 0: + peerUsed = self.pickOnlinePeer() + else: + blockPeers = self._core._crypto.randomShuffle(blockPeers) + peerUsed = blockPeers.pop(0) + 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) @@ -300,8 +313,8 @@ class OnionrCommunicatorDaemon: removeFromQueue = False # Don't remove from queue if 404 if removeFromQueue: try: - self.blockQueue.remove(blockHash) # remove from block queue both if success or false - except ValueError: + del self.blockQueue[blockHash] # remove from block queue both if success or false + except KeyError: pass self.currentDownloading.remove(blockHash) self.decrementThreadCount('getBlocks') diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py index 8c15f144..074c8aa1 100644 --- a/onionr/onionrcrypto.py +++ b/onionr/onionrcrypto.py @@ -281,5 +281,20 @@ class OnionrCrypto: return retData - def safeCompare(self, one, two): + @staticmethod + def safeCompare(one, two): return hmac.compare_digest(one, two) + + @staticmethod + def randomShuffle(theList): + myList = list(theList) + shuffledList = [] + myListLength = len(myList) + 1 + while myListLength > 0: + removed = secrets.randbelow(myListLength) + try: + shuffledList.append(myList.pop(removed)) + except IndexError: + pass + myListLength = len(myList) + return shuffledList \ No newline at end of file From 030244758842657f211b1f7f12416505a94bd043 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 22 Jan 2019 14:15:02 -0600 Subject: [PATCH 44/94] keyerror bugfix and increased api timeout --- onionr/api.py | 2 +- onionr/communicator2.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 3a340dc3..b0a6c8b6 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -30,7 +30,7 @@ import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents class FDSafeHandler(WSGIHandler): def handle(self): - timeout = Timeout(10, exception=Exception) + timeout = Timeout(60, exception=Exception) timeout.start() #timeout = gevent.Timeout.start_new(3) diff --git a/onionr/communicator2.py b/onionr/communicator2.py index bda9e29a..81e73f29 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -244,7 +244,10 @@ class OnionrCommunicatorDaemon: continue if blockHash in self._core.getBlockList(): logger.debug('Block %s is already saved.' % (blockHash,)) - del self.blockQueue[blockHash] + try: + del self.blockQueue[blockHash] + except KeyError: + pass continue if self._core._blacklist.inBlacklist(blockHash): continue From f91832a3e083d2bcae6b9c125fd28ddf5d22f51b Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 22 Jan 2019 23:33:09 -0600 Subject: [PATCH 45/94] whitepaper update --- docs/whitepaper.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/whitepaper.md b/docs/whitepaper.md index a32a1f1d..9447068a 100644 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -87,7 +87,7 @@ Onionr can provide evidence of when a block was inserted by requesting other use This can be done either by the creator of the block prior to generation, or by any node after insertion. -In addition, randomness beacons such as the one operated by [NIST](https://beacon.nist.gov/home) or the hash of the latest blocks in a cryptocurrency network could be used to affirm that a block was at least not *created* before a given time. +In addition, randomness beacons such as the one operated by [NIST](https://beacon.nist.gov/home), [Chile](https://beacon.clcert.cl/), or the hash of the latest blocks in a cryptocurrency network could be used to affirm that a block was at least not *created* before a given time. # Direct Connections From 0f4626a68c426eb11f959e11d4a3dba7e8e7ddb1 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 24 Jan 2019 11:56:46 -0600 Subject: [PATCH 46/94] do not announce when high security, more whitepaper --- docs/whitepaper.md | 4 +- onionr/onionrdaemontools.py | 78 ++++++++++++++++++------------------- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/docs/whitepaper.md b/docs/whitepaper.md index 9447068a..8c5d0f96 100644 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -91,4 +91,6 @@ 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, and in addition neither party knows the other's .onion or .i2p hostname, let alone IP address. 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 message brokering instead of a standard socket. \ No newline at end of file +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. + +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. \ No newline at end of file diff --git a/onionr/onionrdaemontools.py b/onionr/onionrdaemontools.py index 38db5d5b..01fce0b5 100644 --- a/onionr/onionrdaemontools.py +++ b/onionr/onionrdaemontools.py @@ -34,46 +34,46 @@ class DaemonTools: '''Announce our node to our peers''' retData = False announceFail = False - - # Announce to random online peers - for i in self.daemon.onlinePeers: - if not i in self.announceCache: - peer = i - break - else: - peer = self.daemon.pickOnlinePeer() - - ourID = self.daemon._core.hsAddress.strip() - - url = 'http://' + peer + '/announce' - data = {'node': ourID} - - combinedNodes = ourID + peer - existingRand = self.daemon._core.getAddressInfo(peer, 'powValue') - if type(existingRand) is type(None): - existingRand = '' - - if peer in self.announceCache: - data['random'] = self.announceCache[peer] - elif len(existingRand) > 0: - data['random'] = existingRand - else: - proof = onionrproofs.DataPOW(combinedNodes, forceDifficulty=4) - 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 + if config.get('general.security_level') == 0: + # Announce to random online peers + for i in self.daemon.onlinePeers: + if not i in self.announceCache: + peer = i + break 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']) + peer = self.daemon.pickOnlinePeer() + + ourID = self.daemon._core.hsAddress.strip() + + url = 'http://' + peer + '/announce' + data = {'node': ourID} + + combinedNodes = ourID + peer + existingRand = self.daemon._core.getAddressInfo(peer, 'powValue') + if type(existingRand) is type(None): + existingRand = '' + + if peer in self.announceCache: + data['random'] = self.announceCache[peer] + elif len(existingRand) > 0: + data['random'] = existingRand + else: + proof = onionrproofs.DataPOW(combinedNodes, forceDifficulty=4) + 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 From e60503771edfd6fc52ebe48e404f8ffd99a3008d Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 28 Jan 2019 00:06:20 -0600 Subject: [PATCH 47/94] renamed communicator, bug fixes and added work on onionfragment.py --- onionr/{communicator2.py => communicator.py} | 1 + onionr/core.py | 1 + onionr/onionr.py | 19 ++- onionr/onionrblockapi.py | 122 +++---------------- onionr/onionrdaemontools.py | 2 +- onionr/onionrexceptions.py | 3 + onionr/onionrfragment.py | 73 +++++++++++ onionr/onionrstorage.py | 8 ++ 8 files changed, 109 insertions(+), 120 deletions(-) rename onionr/{communicator2.py => communicator.py} (99%) create mode 100644 onionr/onionrfragment.py diff --git a/onionr/communicator2.py b/onionr/communicator.py similarity index 99% rename from onionr/communicator2.py rename to onionr/communicator.py index 81e73f29..8ba63c32 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator.py @@ -567,6 +567,7 @@ class OnionrCommunicatorDaemon: # 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): diff --git a/onionr/core.py b/onionr/core.py index 07762236..b14eee1f 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -99,6 +99,7 @@ class Core: logger.warn('Warning: address bootstrap file not found ' + self.bootstrapFileLocation) self._utils = onionrutils.OnionrUtils(self) + self.blockCache = onionrstorage.BlockCache() # Initialize the crypto object self._crypto = onionrcrypto.OnionrCrypto(self) self._blacklist = onionrblacklist.OnionrBlackList(self) diff --git a/onionr/onionr.py b/onionr/onionr.py index 4f054a3b..2733500a 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -33,7 +33,7 @@ import onionrutils import netcontroller from netcontroller import NetController from onionrblockapi import Block -import onionrproofs, onionrexceptions, onionrusers, communicator2 +import onionrproofs, onionrexceptions, onionrusers, communicator try: from urllib3.contrib.socks import SOCKSProxyManager @@ -710,7 +710,6 @@ class Onionr: ''' Starts the Onionr communication daemon ''' - communicatorDaemon = './communicator2.py' # remove runcheck if it exists if os.path.isfile('data/.runcheck'): @@ -750,10 +749,8 @@ class Onionr: logger.debug('Using public key: %s' % (logger.colors.underline + self.onionrCore._crypto.pubKey)) time.sleep(1) - # TODO: make runable on windows - #communicatorProc = subprocess.Popen([communicatorDaemon, 'run', str(net.socksPort)]) self.onionrCore.torPort = net.socksPort - communicatorThread = Thread(target=communicator2.startCommunicator, args=(self, str(net.socksPort))) + communicatorThread = Thread(target=communicator.startCommunicator, args=(self, str(net.socksPort))) communicatorThread.start() while self.communicatorInst is None: @@ -940,7 +937,8 @@ class Onionr: logger.error('Block hash is invalid') return - Block.mergeChain(bHash, fileName) + with open(fileName, 'wb') as myFile: + myFile.write(base64.b64decode(Block(bHash, core=self.onionrCore).bcontent)) return def addWebpage(self): @@ -963,12 +961,9 @@ class Onionr: return logger.info('Adding file... this might take a long time.') try: - if singleBlock: - with open(filename, 'rb') as singleFile: - blockhash = self.onionrCore.insertBlock(base64.b64encode(singleFile.read()), header=blockType) - else: - blockhash = Block.createChain(file = filename) - logger.info('File %s saved in block %s.' % (filename, blockhash)) + with open(filename, 'rb') as singleFile: + blockhash = self.onionrCore.insertBlock(base64.b64encode(singleFile.read()), header=blockType) + logger.info('File %s saved in block %s' % (filename, blockhash)) except: logger.error('Failed to save file in block.', timestamp = False) else: diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py index f60e3b0f..c8f12a54 100644 --- a/onionr/onionrblockapi.py +++ b/onionr/onionrblockapi.py @@ -1,7 +1,7 @@ ''' Onionr - P2P Anonymous Storage Network - This class contains the OnionrBlocks class which is a class for working with Onionr blocks + This file contains the OnionrBlocks class which is a class for working with Onionr blocks ''' ''' This program is free software: you can redistribute it and/or modify @@ -56,18 +56,7 @@ class Block: if self.getCore() is None: self.core = onionrcore.Core() - # update the blocks' contents if it exists - if not self.getHash() is None: - if not self.core._utils.validateHash(self.hash): - logger.debug('Block hash %s is invalid.' % self.getHash()) - raise onionrexceptions.InvalidHexHash('Block hash is invalid.') - elif not self.update(): - logger.debug('Failed to open block %s.' % self.getHash()) - else: - pass - #logger.debug('Did not update block.') - - # logic + self.update() def decrypt(self, anonymous = True, encodedData = True): ''' @@ -140,13 +129,15 @@ class Block: Outputs: - (bool): indicates whether or not the operation was successful ''' - try: # import from string blockdata = data # import from file if blockdata is None: + blockdata = onionrstorage.getData(self.core, self.getHash()).decode() + ''' + filelocation = file readfile = True @@ -169,6 +160,7 @@ class Block: #blockdata = f.read().decode() self.blockFile = filelocation + ''' else: self.blockFile = None # parse block @@ -588,7 +580,7 @@ class Block: return list() - def mergeChain(child, file = None, maximumFollows = 32, core = None): + def mergeChain(child, file = None, maximumFollows = 1000, core = None): ''' Follows a child Block to its root parent Block, merging content @@ -635,7 +627,7 @@ class Block: blocks.append(block.getHash()) - buffer = '' + buffer = b'' # combine block contents for hash in blocks: @@ -644,101 +636,17 @@ class Block: contents = base64.b64decode(contents.encode()) if file is None: - buffer += contents.decode() + try: + buffer += contents.encode() + except AttributeError: + buffer += contents else: file.write(contents) - file.close() + if file is not None: + file.close() return (None if not file is None else buffer) - def createChain(data = None, chunksize = 99800, file = None, type = 'chunk', sign = True, encrypt = False, verbose = False): - ''' - Creates a chain of blocks to store larger amounts of data - - The chunksize is set to 99800 because it provides the least amount of PoW for the most amount of data. - - Inputs: - - data (*): if `file` is None, the data to be stored in blocks - - file (file/str): the filename or file object to read from (or None to read `data` instead) - - chunksize (int): the number of bytes per block chunk - - type (str): the type header for each of the blocks - - sign (bool): whether or not to sign each block - - encrypt (str): the public key to encrypt to, or False to disable encryption - - verbose (bool): whether or not to return a tuple containing more info - - Outputs: - - if `verbose`: - - (tuple): - - (str): the child block hash - - (list): all block hashes associated with storing the file - - if not `verbose`: - - (str): the child block hash - ''' - - blocks = list() - - # initial datatype checks - if data is None and file is None: - return blocks - elif not (file is None or (isinstance(file, str) and os.path.exists(file))): - return blocks - elif isinstance(file, str): - file = open(file, 'rb') - if not isinstance(data, str): - data = str(data) - - if not file is None: - filesize = os.stat(file.name).st_size - offset = filesize % chunksize - maxtimes = int(filesize / chunksize) - - for times in range(0, maxtimes + 1): - # read chunksize bytes from the file (end -> beginning) - if times < maxtimes: - file.seek(- ((times + 1) * chunksize), 2) - content = file.read(chunksize) - else: - file.seek(0, 0) - content = file.read(offset) - - # encode it- python is really bad at handling certain bytes that - # are often present in binaries. - content = base64.b64encode(content).decode() - - # if it is the end of the file, exit - if not content: - break - - # create block - block = Block() - block.setType(type) - block.setContent(content) - block.setParent((blocks[-1] if len(blocks) != 0 else None)) - hash = block.save(sign = sign) - - # remember the hash in cache - blocks.append(hash) - elif not data is None: - for content in reversed([data[n:n + chunksize] for n in range(0, len(data), chunksize)]): - # encode chunk with base64 - content = base64.b64encode(content.encode()).decode() - - # create block - block = Block() - block.setType(type) - block.setContent(content) - block.setParent((blocks[-1] if len(blocks) != 0 else None)) - hash = block.save(sign = sign) - - # remember the hash in cache - blocks.append(hash) - - # return different things depending on verbosity - if verbose: - return (blocks[-1], blocks) - file.close() - return blocks[-1] - def exists(bHash): ''' Checks if a block is saved to file or not @@ -799,7 +707,7 @@ class Block: if block.getHash() in Block.getCache() and not override: return False - # dump old cached blocks if the size exeeds the maximum + # dump old cached blocks if the size exceeds the maximum if sys.getsizeof(Block.blockCacheOrder) >= config.get('allocations.block_cache_total', 50000000): # 50MB default cache size del Block.blockCache[blockCacheOrder.pop(0)] diff --git a/onionr/onionrdaemontools.py b/onionr/onionrdaemontools.py index 01fce0b5..7e7c8ec7 100644 --- a/onionr/onionrdaemontools.py +++ b/onionr/onionrdaemontools.py @@ -34,7 +34,7 @@ class DaemonTools: '''Announce our node to our peers''' retData = False announceFail = False - if config.get('general.security_level') == 0: + if self.daemon._core.config('general.security_level') == 0: # Announce to random online peers for i in self.daemon.onlinePeers: if not i in self.announceCache: diff --git a/onionr/onionrexceptions.py b/onionr/onionrexceptions.py index a0c468a3..6763a172 100644 --- a/onionr/onionrexceptions.py +++ b/onionr/onionrexceptions.py @@ -53,6 +53,9 @@ class BlacklistedBlock(Exception): class DataExists(Exception): pass +class NoDataAvailable(Exception): + pass + class InvalidHexHash(Exception): '''When a string is not a valid hex string of appropriate length for a hash value''' pass diff --git a/onionr/onionrfragment.py b/onionr/onionrfragment.py new file mode 100644 index 00000000..c8386465 --- /dev/null +++ b/onionr/onionrfragment.py @@ -0,0 +1,73 @@ +''' + 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/onionrstorage.py b/onionr/onionrstorage.py index 39dfd323..8e2aae41 100644 --- a/onionr/onionrstorage.py +++ b/onionr/onionrstorage.py @@ -21,6 +21,13 @@ import core, sys, sqlite3, os, dbcreator DB_ENTRY_SIZE_LIMIT = 10000 # Will be a config option +class BlockCache: + def __init__(self): + self.blocks = {} + def cleanCache(self): + while sys.getsizeof(self.blocks) > 100000000: + self.blocks.pop(list(self.blocks.keys())[0]) + def dbCreate(coreInst): try: dbcreator.DBCreator(coreInst).createBlockDataDB() @@ -62,6 +69,7 @@ def store(coreInst, data, blockHash=''): else: with open('%s/%s.dat' % (coreInst.blockDataLocation, blockHash), 'wb') as blockFile: blockFile.write(data) + coreInst.blockCache.cleanCache() def getData(coreInst, bHash): assert isinstance(coreInst, core.Core) From 4882a21b6a402c97d9d2cd2702f95b96e6f0945b Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 28 Jan 2019 16:49:04 -0600 Subject: [PATCH 48/94] improved uploading and fixed announce --- onionr/api.py | 7 ++++++- onionr/communicator.py | 3 +-- onionr/onionrdaemontools.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index b0a6c8b6..f5ba9c81 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -436,4 +436,9 @@ class API: return False def getUptime(self): - return self._utils.getEpoch() - self.startTime \ No newline at end of file + while True: + try: + return self._utils.getEpoch - startTime + except AttributeError: + # Don't error on race condition with startup + pass \ No newline at end of file diff --git a/onionr/communicator.py b/onionr/communicator.py index 8ba63c32..1b8dc7fd 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -574,7 +574,7 @@ class OnionrCommunicatorDaemon: logger.warn('Requested to upload invalid block') self.decrementThreadCount('uploadBlock') return - for i in range(max(len(self.onlinePeers), 2)): + for i in range(min(len(self.onlinePeers), 6)): peer = self.pickOnlinePeer() if peer in triedPeers: continue @@ -590,7 +590,6 @@ class OnionrCommunicatorDaemon: if not self._core._utils.doPostRequest(url, data=data, proxyType=proxyType) == False: self._core._utils.localCommand('waitforshare/' + bl) finishedUploads.append(bl) - break for x in finishedUploads: try: self.blocksToUpload.remove(x) diff --git a/onionr/onionrdaemontools.py b/onionr/onionrdaemontools.py index 7e7c8ec7..faea0070 100644 --- a/onionr/onionrdaemontools.py +++ b/onionr/onionrdaemontools.py @@ -34,7 +34,7 @@ class DaemonTools: '''Announce our node to our peers''' retData = False announceFail = False - if self.daemon._core.config('general.security_level') == 0: + 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: From f0382d24da462be9efe9cf71e4b15b26597d9d9d Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 30 Jan 2019 00:10:29 -0600 Subject: [PATCH 49/94] work on readme, added mail files, bugfixes --- README.md | 44 ++++++++++++++++++------- docs/Tor_Stinks_02.png | Bin 0 -> 71503 bytes onionr/api.py | 15 +++++++-- onionr/config.py | 6 ++-- onionr/onionr.py | 30 ++++++++++++++++- onionr/static-data/www/mail/index.html | 21 ++++++++++++ onionr/static-data/www/mail/mail.js | 0 onionr/storagecounter.py | 2 ++ 8 files changed, 101 insertions(+), 17 deletions(-) create mode 100644 docs/Tor_Stinks_02.png create mode 100644 onionr/static-data/www/mail/index.html create mode 100644 onionr/static-data/www/mail/mail.js diff --git a/README.md b/README.md index 03ec0de5..98297240 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ -![Onionr logo](./docs/onionr-logo.png) +

-(***experimental, not well tested or easy to use yet***) + + +

+ +(***pre-alpha quality & 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/) @@ -9,19 +13,20 @@ Anonymous P2P platform, using Tor & I2P.
-**The main repo for this software is at https://gitlab.com/beardog/Onionr/** +**The main repository for this software is at https://gitlab.com/beardog/Onionr/** # Summary Onionr is a decentralized, peer-to-peer data storage network, designed to be anonymous and resistant to (meta)data analysis and spam. +Onionr stores data in independent packages referred to as 'blocks' (not to be confused with a blockchain). 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). + +Users are identified by ed25519 public keys, which can be used to sign blocks (optional) or send encrypted data. + Onionr can be used for mail, as a social network, instant messenger, file sharing software, or for encrypted group discussion. -# Roadmap/features -Check the [Gitlab Project](https://gitlab.com/beardog/Onionr/milestones/1) to see progress towards the alpha release. - -## Core internal features +## Main Features * [X] Fully p2p/decentralized, no trackers or other single points of failure * [X] End to end encryption of user data @@ -29,14 +34,29 @@ Check the [Gitlab Project](https://gitlab.com/beardog/Onionr/milestones/1) to se * [X] Easy API system for integration to websites * [X] Metadata analysis resistance - -## Other features - **Onionr API and functionality is subject to non-backwards compatible change during pre-alpha development** +# Install and Run on Linux + +The following applies to Ubuntu Bionic. Other distros may have different package or command names. + +* Have python3.5+, 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 -r requirements.txt` + ## Help out -Everyone is welcome to help out. Please get in touch first if you are making non-trivial changes. If you can't help with programming, you can write documentation or guides. +Everyone is welcome to help out. Help is wanted for the following: + +* Development (Get in touch first) + * Creation of a shared object library for use from other languages and faster proof-of-work + * Onionr mobile app development + * Windows and Mac support + * General development +* Testing +* Running stable nodes +* Security review/audit Bitcoin/Bitcoin Cash: 1onion55FXzm6h8KQw3zFw2igpHcV7LPq @@ -44,4 +64,4 @@ Bitcoin/Bitcoin Cash: 1onion55FXzm6h8KQw3zFw2igpHcV7LPq The Tor Project, I2P developers, and anyone else do not own, create, or endorse this project, and are not otherwise involved. -The badges (besides travis-ci build) are by Maik Ellerbrock is licensed under a Creative Commons Attribution 4.0 International License. +The badges (besides travis-ci build) are by Maik Ellerbrock is licensed under a Creative Commons Attribution 4.0 International License. \ No newline at end of file diff --git a/docs/Tor_Stinks_02.png b/docs/Tor_Stinks_02.png new file mode 100644 index 0000000000000000000000000000000000000000..114a760cdf163431794d446ceaddc4e9105bd25c GIT binary patch literal 71503 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 +`r!SnQ3wb^kA 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 X>Of6cBf#$s@FyW6EBswhKj8lY#Mu)1 literal 0 HcmV?d00001 diff --git a/onionr/api.py b/onionr/api.py index f5ba9c81..029da837 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -265,7 +265,7 @@ class API: self.bindPort = bindPort # Be extremely mindful of this - self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'board', 'boardContent', 'sharedContent') + self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'board', 'boardContent', 'sharedContent', 'mail', 'mailindex') self.clientToken = config.get('client.webpassword') self.timeBypassToken = base64.b16encode(os.urandom(32)).decode() @@ -308,6 +308,13 @@ class API: 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/', '') + @app.route('/mail/', endpoint='mailindex') + def loadMailIndex(): + return send_from_directory('static-data/www/mail/', 'index.html') + @app.route('/board/', endpoint='boardContent') def boardContent(path): return send_from_directory('static-data/www/board/', path) @@ -407,7 +414,11 @@ class API: @app.route('/getstats') def getStats(): #return Response("disabled") - return Response(self._core.serializer.getStats()) + while True: + try: + return Response(self._core.serializer.getStats()) + except AttributeError: + pass @app.route('/getuptime') def showUptime(): diff --git a/onionr/config.py b/onionr/config.py index 0e1fd6a1..960286f6 100644 --- a/onionr/config.py +++ b/onionr/config.py @@ -105,7 +105,8 @@ def check(): open(get_config_file(), 'a', encoding="utf8").close() save() except: - logger.debug('Failed to check configuration file.') + pass + #logger.debug('Failed to check configuration file.') def save(): ''' @@ -129,7 +130,8 @@ def reload(): with open(get_config_file(), 'r', encoding="utf8") as configfile: set_config(json.loads(configfile.read())) except: - logger.debug('Failed to parse configuration file.') + pass + #logger.debug('Failed to parse configuration file.') def get_config(): ''' diff --git a/onionr/onionr.py b/onionr/onionr.py index 2733500a..e1c1c7f2 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -30,7 +30,7 @@ import webbrowser, uuid, signal from threading import Thread import api, core, config, logger, onionrplugins as plugins, onionrevents as events import onionrutils -import netcontroller +import netcontroller, onionrstorage from netcontroller import NetController from onionrblockapi import Block import onionrproofs, onionrexceptions, onionrusers, communicator @@ -190,6 +190,9 @@ class Onionr: 'openhome': self.openHome, 'open-home': self.openHome, + 'export-block': self.exportBlock, + 'exportblock': self.exportBlock, + 'get-file': self.getFile, 'getfile': self.getFile, @@ -271,6 +274,31 @@ class Onionr: THIS SECTION HANDLES THE 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] + try: + path = sys.argv[3] + except (IndexError): + if not os.path.exists(exportDir): + if os.path.exists(self.dataDir): + os.mkdir(exportDir) + else: + logger.error('Onionr not initialized') + sys.exit(1) + path = exportDir + data = onionrstorage.getData(self.onionrCore, bHash) + with open('%s/%s.dat' % (exportDir, bHash), 'wb') as exportFile: + exportFile.write(data) + + + def showDetails(self): details = { 'Node Address' : self.get_hostname(), diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html new file mode 100644 index 00000000..73481922 --- /dev/null +++ b/onionr/static-data/www/mail/index.html @@ -0,0 +1,21 @@ + + + + + + Onionr Mail + + + + + +
+
+ + Onionr Web Mail +
+ +
+ + + \ No newline at end of file diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js new file mode 100644 index 00000000..e69de29b diff --git a/onionr/storagecounter.py b/onionr/storagecounter.py index 863145f9..fc1a9d6b 100644 --- a/onionr/storagecounter.py +++ b/onionr/storagecounter.py @@ -42,6 +42,8 @@ class StorageCounter: retData = int(dataFile.read()) except FileNotFoundError: pass + except ValueError: + pass # Possibly happens when the file is empty return retData def getPercent(self): From 557ffa2f4a1324ae2bff4c2b9403229f51bf5e4b Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 1 Feb 2019 00:38:12 -0600 Subject: [PATCH 50/94] moved a couple files, work on mail interface, improvements to web + blockapi for block decryption --- README.md | 12 ++++--- onionr/api.py | 40 ++++++++++++++++++++--- onionr/core.py | 4 +-- onionr/{ => etc}/onionrvalues.py | 0 onionr/{ => etc}/pgpwords.py | 0 onionr/onionrblockapi.py | 7 +++- onionr/onionrutils.py | 3 +- onionr/static-data/bootstrap-nodes.txt | 2 +- onionr/static-data/www/mail/index.html | 6 ++-- onionr/static-data/www/mail/mail.js | 28 ++++++++++++++++ onionr/static-data/www/private/index.html | 1 + onionr/static-data/www/shared/misc.js | 16 ++++++++- 12 files changed, 101 insertions(+), 18 deletions(-) rename onionr/{ => etc}/onionrvalues.py (100%) rename onionr/{ => etc}/pgpwords.py (100%) diff --git a/README.md b/README.md index 98297240..c87b3544 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,14 @@

+

+ Anonymous P2P storage network +

+ (***pre-alpha quality & 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/) - -Anonymous P2P platform, using Tor & I2P. -
**The main repository for this software is at https://gitlab.com/beardog/Onionr/** @@ -19,9 +20,9 @@ Anonymous P2P platform, using Tor & I2P. Onionr is a decentralized, peer-to-peer data storage network, designed to be anonymous and resistant to (meta)data analysis and spam. -Onionr stores data in independent packages referred to as 'blocks' (not to be confused with a blockchain). 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). +Onionr stores data in independent packages referred to as 'blocks' (not to be confused with a blockchain). 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. -Users are identified by ed25519 public keys, which can be used to sign blocks (optional) or send encrypted data. +Users are identified by ed25519 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. @@ -33,6 +34,7 @@ Onionr can be used for mail, as a social network, instant messenger, file sharin * [X] Optional non-encrypted blocks, useful for blog posts or public file sharing * [X] Easy API system for integration to websites * [X] Metadata analysis resistance +* [X] Transport agnosticism **Onionr API and functionality is subject to non-backwards compatible change during pre-alpha development** diff --git a/onionr/api.py b/onionr/api.py index 029da837..dbd79321 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -139,8 +139,8 @@ class PublicAPI: if clientAPI._utils.validateHash(data): if data not in self.hideBlocks: if data in clientAPI._core.getBlockList(): - block = Block(hash=data.encode(), core=clientAPI._core) - resp = base64.b64encode(block.getRaw().encode()).decode() + block = self.clientAPI.getBlockData(data).encode() + resp = base64.b64encode(block).decode() if len(resp) == 0: abort(404) resp = "" @@ -310,7 +310,7 @@ class API: @app.route('/mail/', endpoint='mail') def loadMail(path): - return send_from_directory('static-data/www/mail/', '') + 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') @@ -358,7 +358,7 @@ class API: return Response(','.join(blocks)) @app.route('/gethtmlsafeblockdata/') - def getData(name): + def getSafeData(name): resp = '' if self._core._utils.validateHash(name): try: @@ -368,6 +368,21 @@ class API: else: abort(404) return Response(resp) + + @app.route('/getblockdata/') + def getData(name): + resp = "" + if self._core._utils.validateHash(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('/site/', endpoint='site') def site(name): @@ -452,4 +467,19 @@ class API: return self._utils.getEpoch - startTime except AttributeError: # Don't error on race condition with startup - pass \ No newline at end of file + pass + + def getBlockData(self, bHash, decrypt=False): + bl = Block(bHash, core=self._core) + if decrypt: + bl.decrypt() + if bl.isEncrypted and not bl.decrypted: + raise ValueError + + 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 + return json.dumps(retData) \ No newline at end of file diff --git a/onionr/core.py b/onionr/core.py index b14eee1f..8634ab1f 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -20,9 +20,10 @@ import sqlite3, os, sys, time, math, base64, tarfile, nacl, logger, json, netcontroller, math, config, uuid from onionrblockapi import Block -import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions, onionrvalues +import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions import onionrblacklist, onionrusers import dbcreator, onionrstorage, serializeddata +from etc import onionrvalues if sys.version_info < (3, 6): try: @@ -868,5 +869,4 @@ class Core: else: logger.error('Onionr daemon is not running.') return False - return diff --git a/onionr/onionrvalues.py b/onionr/etc/onionrvalues.py similarity index 100% rename from onionr/onionrvalues.py rename to onionr/etc/onionrvalues.py diff --git a/onionr/pgpwords.py b/onionr/etc/pgpwords.py similarity index 100% rename from onionr/pgpwords.py rename to onionr/etc/pgpwords.py diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py index c8f12a54..09dd77ea 100644 --- a/onionr/onionrblockapi.py +++ b/onionr/onionrblockapi.py @@ -25,7 +25,7 @@ class Block: blockCacheOrder = list() # NEVER write your own code that writes to this! blockCache = dict() # should never be accessed directly, look at Block.getCache() - def __init__(self, hash = None, core = None, type = None, content = None, expire=None): + def __init__(self, hash = None, core = None, type = None, content = None, expire=None, decrypt=False): # take from arguments # sometimes people input a bytes object instead of str in `hash` if (not hash is None) and isinstance(hash, bytes): @@ -51,6 +51,7 @@ class Block: self.decrypted = False self.signer = None self.validSig = False + self.autoDecrypt = decrypt # handle arguments if self.getCore() is None: @@ -80,6 +81,7 @@ class Block: self.bmetadata = json.loads(bmeta) self.signature = core._crypto.pubKeyDecrypt(self.signature, anonymous=anonymous, encodedData=encodedData) self.signer = core._crypto.pubKeyDecrypt(self.signer, anonymous=anonymous, encodedData=encodedData) + self.bheader['signer'] = self.signer.decode() self.signedData = json.dumps(self.bmetadata) + self.bcontent.decode() try: assert self.bmetadata['forwardEnc'] is True @@ -190,6 +192,9 @@ class Block: if len(self.getRaw()) <= config.get('allocations.blockCache', 500000): self.cache() + + if self.autoDecrypt: + self.decrypt() return True except Exception as e: diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 3260a42b..b34eb948 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -24,7 +24,8 @@ from onionrblockapi import Block import onionrexceptions from onionr import API_VERSION import onionrevents -import pgpwords, onionrusers, storagecounter +import onionrusers, storagecounter +from etc import pgpwords if sys.version_info < (3, 6): try: import sha3 diff --git a/onionr/static-data/bootstrap-nodes.txt b/onionr/static-data/bootstrap-nodes.txt index ad126465..93dec7ce 100644 --- a/onionr/static-data/bootstrap-nodes.txt +++ b/onionr/static-data/bootstrap-nodes.txt @@ -1 +1 @@ -svlegnabtuh3dq6ncmzqmpnxzik5mita5x22up4tai2ekngzcgqbnbqd.onion \ No newline at end of file +dd3llxdp5q6ak3zmmicoy3jnodmroouv2xr7whkygiwp3rl7nf23gdad.onion \ 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 73481922..a08dbf55 100644 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -12,10 +12,12 @@
- Onionr Web Mail + Onionr Mail
- + +
+ \ 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 e69de29b..bafbda6d 100644 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -0,0 +1,28 @@ +pms = '' +threadPart = document.getElementById('threads') +function getInbox(){ + for(var i = 0; i < pms.length; i++) { + fetch('/getblockdata/' + pms[i], { + headers: { + "token": webpass + }}) + .then((resp) => resp.json()) // Transform the data into json + .then(function(resp) { + var entry = document.createElement('div') + entry.innerHTML = resp['meta']['time'] + ' - ' + resp['meta']['signer'] + threadPart.appendChild(entry) + }) + } + +} + +fetch('/getblocksbytype/pm', { + headers: { + "token": webpass + }}) +.then((resp) => resp.text()) // Transform the data into json +.then(function(data) { + pms = data.split(',') + getInbox() + }) + diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index 8907195b..3e239eb0 100644 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -18,6 +18,7 @@ Onionr Web Control Panel
+

Mail

Stats

Uptime:

Stored Blocks:

diff --git a/onionr/static-data/www/shared/misc.js b/onionr/static-data/www/shared/misc.js index cd3dbba1..c9b3790e 100644 --- a/onionr/static-data/www/shared/misc.js +++ b/onionr/static-data/www/shared/misc.js @@ -5,7 +5,7 @@ if (typeof webpass == "undefined"){ } else{ localStorage['webpass'] = webpass - document.location.hash = '' + //document.location.hash = '' } if (typeof webpass == "undefined" || webpass == ""){ alert('Web password was not found in memory or URL') @@ -28,3 +28,17 @@ function overlay(overlayID) { el = document.getElementById(overlayID) el.style.visibility = (el.style.visibility == "visible") ? "hidden" : "visible" } + +var passLinks = document.getElementsByClassName("idLink") + for(var i = 0; i < passLinks.length; i++) { + passLinks[i].href += '#' + webpass + } + +var refreshLinks = document.getElementsByClassName("refresh") + +for(var i = 0; i < refreshLinks.length; i++) { + //Can't use .reload because of webpass + refreshLinks[i].onclick = function(){ + location.reload() + } +} From 13c2289096dbfc6d160fe1ef3d1c0c57c83e583a Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 1 Feb 2019 13:55:59 -0600 Subject: [PATCH 51/94] added subject line in mail, improved readme --- README.md | 24 ++++++++++-------- docs/Tor_Stinks_02.png | Bin 71503 -> 0 bytes docs/onionr-logo.png | Bin 7223 -> 193823 bytes .../static-data/default-plugins/pms/main.py | 15 +++++++++-- onionr/static-data/www/mail/index.html | 3 ++- onionr/static-data/www/mail/mail.js | 24 +++++++++++++++++- 6 files changed, 51 insertions(+), 15 deletions(-) delete mode 100644 docs/Tor_Stinks_02.png diff --git a/README.md b/README.md index c87b3544..5a346e5d 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@

- +

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

-(***pre-alpha quality & experimental, not well tested or easy to use yet***) +(***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/) @@ -18,14 +18,15 @@ # Summary -Onionr is a decentralized, peer-to-peer data storage network, designed to be anonymous and resistant to (meta)data analysis and spam. +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' (not to be confused with a blockchain). 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 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. Users are identified by ed25519 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. +![Tor stinks slide image](docs/tor-stinks-02.png) ## Main Features @@ -34,7 +35,7 @@ Onionr can be used for mail, as a social network, instant messenger, file sharin * [X] Optional non-encrypted blocks, useful for blog posts or public file sharing * [X] Easy API system for integration to websites * [X] Metadata analysis resistance -* [X] Transport agnosticism +* [X] Transport agnosticism (no internet required) **Onionr API and functionality is subject to non-backwards compatible change during pre-alpha development** @@ -52,18 +53,19 @@ The following applies to Ubuntu Bionic. Other distros may have different package Everyone is welcome to help out. Help is wanted for the following: * Development (Get in touch first) - * Creation of a shared object library for use from other languages and faster proof-of-work - * Onionr mobile app development + * Creation of a lib for use from other languages and faster proof-of-work + * Android and IOS development * Windows and Mac support - * General development + * General bug fixes and development of new features * Testing * Running stable nodes * Security review/audit -Bitcoin/Bitcoin Cash: 1onion55FXzm6h8KQw3zFw2igpHcV7LPq +Bitcoin: [1onion55FXzm6h8KQw3zFw2igpHcV7LPq](bitcoin:1onion55FXzm6h8KQw3zFw2igpHcV7LPq) +USD: [Ko-Fi](https://www.ko-fi.com/beardogkf) ## Disclaimer The Tor Project, I2P developers, and anyone else do not own, create, or endorse this project, and are not otherwise involved. -The badges (besides travis-ci build) are by Maik Ellerbrock is licensed under a Creative Commons Attribution 4.0 International License. \ No newline at end of file +The 'open source badge' is by Maik Ellerbrock and is licensed under a Creative Commons Attribution 4.0 International License. \ No newline at end of file diff --git a/docs/Tor_Stinks_02.png b/docs/Tor_Stinks_02.png deleted file mode 100644 index 114a760cdf163431794d446ceaddc4e9105bd25c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71503 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 +`r!SnQ3wb^kA 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 X>Of6cBf#$s@FyW6EBswhKj8lY#Mu)1 diff --git a/docs/onionr-logo.png b/docs/onionr-logo.png index b6c3c9b556ca0a650db683d0910b6d0c728f2722..31a9524137cc4ec5d173c16c7cebd30e113eed89 100644 GIT binary patch literal 193823 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%0DI;Ml^w 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

i45Z1XNWMx8M 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 h3>N%${ literal 7223 zcmZ8`byQT}7w-rRjnW`FG)RXcJ;c!6B{>q(-5oNdBK<{5q+@6V22cb95eDg&hM^mW zdgFV4{oY;op0n;*XP?-0_Wm50uC@vZAsrzA03cCQRno`Qdzdc~9~V<2JNXbAo5B;$U8DW>Wh)~G#}}{JRI^Y&<+S6iyVu6_k6i+nORHd zV_o;Fw(N7QnlT9pypALNrMsJD>#6$i3tljY6dwmG>VGOl;gcUqd516Xadx9iJ0Eo= zb2H0-tfKHAN|lMZY|il~!WmLf55Nx0xjK+Gub;*SSXL1k^b|akuKB~Il0@nPYr>(F zL!CwLf$1#RNK4;RvrOmNXw5#*ch!K84Za=Qee`ps_N%Oe&qRwQ45@G>)xrO8#jW-S ze-C4?AtDA)QaXk@{+?uw0?E)?)s%;o=hj;jTe)s!VHVVXYU52kXm?*PKIPLH71=ZptV~~I$!xe*$d^Jw=xsB zLfyX!C%W0zw88jqHLFPW`I?@L+M5*cyKrLOf1Lbg=U44o&r8~@M9gbkI6P|kJBD{B zG3*@}sUTEi`k{e%R1qR>iVPO2GmU{IJqKf}Il;g-#28{|viRMk;FdYC7Pl|aC=e0_ zV(3e3Vd0S@xN0OjO~f-F_h++k++U+9h1tI(a7Y!BfTZ>a_=S%G@tx7DEtzXU0q!F->sZg{R9CeX-2mY6^0)B4#tD zrY?*o`z)i|RnDauK3b;VyDGL`rgLMXCjA)8{lWDOF!Wrt14>RuDX7r2X*c z@{SKXv%XO4j!3@4;&K0&P)Rc$UpNHc2>C`R*_0!R2Nm*EfGGUrX!(a+cI6zS@hv&w zRlwKd#zP_C24%B8NR>IBtUBE-3C;BA>+`|+)egZBwh3ojB4Z#f!+z{|;RQsyRx8j@ zzPeAyF7tr;*Hvawa`dqFj!&tKs(pi1L5Z`;L9d^+RZKn|i=q79cOg3?2I@$sB)*@{ z{Sim)bZ*s*w#!q8BC8Rt)+tQo-Ym`pMoy;4XyIhj8-99~gpwr&82P#MIPOEs-m{!L z5%8lA*5BZb?zF9K^czK&f4?(?S@Mz6GWI1<0oV=p8A#&PY4yjcWVAP;bc-f(U6bc0 zD9p57Mh@#;Y3!()Et5Hn=?inb1?PmjYNQ(9)Ls`dB9NP!k7r&^EpqaXa}we>_X+LB z18khLfe|hKRN;VkM$ys3sqoHUJ3h@a?4y?7zt+owlxw4@lZvIQzc3bVvw?nin+xxN z9ZeCT%chOF6wZ0zcmBj2)3QVZq(UOOrlh2*pv4KX>BkZ|y|+F-4XL7nq9HsP%C29$|#-Ogo%-rDdkG#5Ed&rl{|kq#SaH=a8e1tCxdTjPa2k zr~CKuM;ud`N1SWkQn9Q>k(x?v+x^Jeb$+rEAkiZr}e<~b{0`9*=K37%T?E7t38uTG%dD^%hBql&9#&UshM;(t$HPOK+|pi~<3 zY=*sTb}Z)CNAA+xN=6mS!Psbh<^z*$`{Qam0b`^zJJf06A>h%}L<&3SQwazH8F2DS z@5n;(G6*GO@oB-pWhJK1j`niGy zV`RJ=-4}PLp6>05!8jy%<_)UazwAYWHES_oyi@d3tNqwf!KeGAi^XirO#OwU6n0dl z`R&QBM=}`61Z;mv6W^Q_-uH;GS#~)>V4A${T{wsg+|PTBhZuP)>#I^9%!61b)ZPWJ zRZ$kJRH$e1SLt5e9!j8O$8QyYfQSCz{f;1srutXsqnh6=x<~-G*_JP*k7~1W40)X7 zcP?=|-Gk##-PJWm2ovRO1Hu9p;W_$+sVBCHphV)3`b*m8`}+ic)GkWHm_j-Hy!5y( zI-~e!`{R3CqFNrAfq+$R1sSmrqT0C-hDn=hPZ6~L*IpVfNOQpU&SyivoAx?e_w2Zm z00;c9g!<8StSh7SFUy_Tqidq;dbw5wne)y#Yk5QqbRYEwZHQg!M5H`6^|8*GRStid z?fTGn$B!g&%e-xHZ>a z+wHk-_`}ah3nM&TLW5S4I1i;lM*+s0P^bGmjHPd(-q>#%c<7A{y}`*Z;B8f)Bv3Eo z-^#EyxYV-J37hhZRA-qRf%^?9K5TP-?;7|j=UQB6zYWHgSA8LxC{_jgr{qRya{l)C z1a}T@B7P4Jk&nlM|D1?B9W1X~4w_4D2$9$f`Vm!<#RUTu-aW6NCp3<)htG-ca-byr zQ}@VQ3l4Oz%})~>55BkoFRZK5a-BIAe!;Cjrac;4DG&0PK2~&T|J_^nU-r1o$MPX9 zj%0Lb`1hci;`C1ec*Mbw5S7?;DHDT;7SXLxaXl!;c@0n=)~!e@#*F@8u~04?&9jqu zdE@=0@x(-cEE&B$LAL#G&0*VXA*vcLL~9s*gaX;Db1e@66akXOnmGzf62X-XafE=3 zlGfYKgls&IeDw@Vycv#AIrefoWsJ4@-Uw314vaAP%*kgz@w?HnoAJnUW*W((`_v8a zUpi0T3ep@S+q^P-YDd)3yv`t1XRR70H|fbyh6IxyQfxP@$M*}*kEFsTBYcd{(DzAo zKa?0UOVY`d&b?f($XmkPnJouLZO4vGJb_$a`Z1F9VRSqi7g%daUoi1byJ-eF0H^ER z-pQ15Dc|j-m97ew?>w8WAAJke`X=C&VK?*ZJ+9nseN%hl1KulHhR{$Sh4-s5X&z+- zM}proX~}(qb-f3lEpg%QA!g;m?9Ex{rQA)G*3C@i!yfH-=~=)>&Ti#1QbOd{ySDD= zZl|;}QmiS?$}H`tmwLjoa0R~DF_lB-gaQ@YHCE)p*lMMG6SrSzvR4z7y?9EC-eS}H z{M+O2HT-J0yi0Rw#X3RCNZ52+{%=e@vW(15Vj>AA;B+T~%>E)1W)PcPzJ>_e z7gu=5Ymve8j;nE@PzEkF^96R!`mD^&29H;jdtMrqRIYZieKu4PA*;2IIF*ME=+KsO zKmD}QsqX>UG_ZCvZd>d5L0y&Zrg-_TTKD#Es0ixpFMB7Pi=C)*dqale?J;-%s5QUZ zFUL#{D9L{6+St|y-iX36j3g1Zqhk&#iB8-}I(v=6{lPmhaUPu(;800X1jQ4RljDK* zfy(cEY?!fdJyiMB!v7?FP&+9m$vR8|I*tMy3t#cwpbke_Cj{n@=(q*1}QA zx7TXFoAaMUwmZ_av$lJIc*qHjmnp`P-ARY6kOd~Ttkiw2-&ie7TYDyT^m_w`AEqox z#M;JE1AkrcTjM;hk;mA+T&_yr&oWHn6 zuh1PaO7DeE`@W3mn4chVJABUdJMOk0X!2sJ{n&QSLAu*fSLYDR#c&*qvTI%~J&2tB zWWK+5WpzZYoLBAk_Sp2xSQbbgmcOvqWE-5dFdni^pvn$`c#)1NzF;zZvLK6=p+HPe z7hM?SM9IFY{!B+!;k9}^;UOMklkz+iHl>)rV_cU3<~S9(P&u#l0Y)afNC{i7oZV1O z23_Ce*vq4cdM_@__R$ymTiJ4JFA&@($8N%b*fkf3=j0D zYICZKn8**10N*^*7JCRY^QY~5@w`9SY1FvNJ0Ww5ga~zBrQXVjJ*`xBbI=}6cCp70 z0emI9-yY6Vn}1V|?<3SQg!JQ|rdaqb@L_K;?8A(Bngl8oaPmijQA%>o9DlmQ%VwZ# zE6lgzy_KPe%hjw{Kb`NHR#=Wt9}xa9KI1nX$U=K>wK1H}C@xSITrDmJ#(pM{3E2{U z5)#14lK5DM(oj#BKlFZlG0Ov@Ekxk+(>Vs~bHE@Cy9;d6i1Wo#*;{N;apCPu`+g&_ z{^3s2#osK_?Hf|%f=Md2u0>q|Ye^Xc5|5oD(J>>(U6r-6NAtc+@Udv&5r907^4M%* zRKaruKO>4Lwq#eS$vEqfriD64TBt zNY|EZX1;q4>SP_6FYdfv6!*yl?|Ss)2s)VO$VpD1<-I3e&mn&&zu$SpA$i?WSqgEF zA}3ME!QMjuRAV{0+@f>xB>z6CYE?xz#jNyed3_&L(ia_VG~X%f6l^Ro-k^Y#lv(R- zSyh_~IeMk?VK>uq@Z0v|8-b1wLLUf9diO`YlPQ7*Zj>W)pg(erw>);ymkh&)W-hl~ z?u@2Vn<9=ThH1>59KE5!H9im5CPySdm!}FG)Ihd-S*|j9jztoOWtOkZ zFP0^So#5Zgc%z3;ix}+OcyI+AO@%SqQ^Fbg{l9Wo!}uLR0Ju`>p5$%d=wrnNo6am%KnX+!)_4LbV#aQ}#^Fr*iL&{~BHsh7L~s?HEmq`{D*2 zAR{?_yPf-`SK55)LufIi=8txI2!Y~CC_4w!V8XezYyo#o9I4vCa+2FbRNm~~cY5S4 zL{s{+=9;RsN8*j)^>9JA#z1C##q_bA1lY3;w&}mape)(k>(Pb`U;Wn*J+n*_O~EZOT!bzPg5CuTM@_| zDq~j|7m#kcYtkTuao$cpi{~s@tIwU~StEYLG`$d|(NZBD?Bg^K-x(d>ufpYMt1qpA zP^9yF70(63w^={L)$-n;F}aw+iWkK&b7&my8(K5l*rJD~hM!4qgnjqec&_U1vQpSa zoI2GT_NXrpNmIrR?J-V0)+nxsXXH17q+HvcDfDS3i$xiQ##`>!LUuO8K0+>tX{Je& zCyCr5XnrDV>)p+f+~<3itJokE=dY@TG{zoFC5=N<2qqEy{Mo(`bFv^Z(6mGwk9y}n zryPcezeb{O{9Xs|em+|3#ILT_#(wr@SjXmH%J6!92HEH(h+Jxu)!0jGbWs|b-XIn~ zcg-aid^+QAhszYx;=YAlB`h34kZZqBTay;rxX`g1@~qm*Ut*-#Lq=IS>_mli;V&ng zqnaT1u2V=n@fm%OazG{|tNhI54EEfZWW_0Gp(!OE9dm3n_06Y|Zdo+DL_jg4hLbo| z?^AYmKA8zu#Istm%YuVB4;-Uw!RbC96qfXgH8Iex|ww<;pLZLSQk z>AU*c1iqMsoE*DN#x}86PfXjL^9~d2;LAw}in;YnvWFZ+@A!-8N-bRWB&-tENKG)5 z;Cl&pw3*e#Ga&hPN9~(AjfJoqc1k%s>*EP>br`PR+ncmpAg{{c_Xz-qW&afzg=`lu zV8RlNvFa|&k(j}2g8L53_iT>yYV2^#iIVZfLwH>1T^`A6Cc!rxRY+{a2Kr}7Ml<%1 zh>_vDrJ#L803&~>Ok-8p#38rf=94b+`QlfO;ks&zTSk{lP}v@RySs>qm&F{@fbJf{wHoj! zoOOL(oBQ6d6UhL8T27Fq{~{hL0XODg(r2+NFK1A75&Z`7-sTzQkHG_TAD1V+ARmQl;7QNDuz_bH3(&B*bU!`qI< z*hp^j5jRocHRgS^J&7le^I%HS7wTw>&J9}(B4azV!B0Bsny(g*Q0dGGDTU8I{BW84 zJGI*$pl{;6PrD4Yo({+Wi?x}gd4E&F9@BT$s;91~bK0FW*g8VV!)Pc=_3YP# zxtz;pR~z>cCH;*2=2eg8o74ky_#@hPNMuWbO1v?Soz`nWZF4SAAetmz@H*a|Tr{-64*=^uymN}39^>q|y zcSnzK4ddA5a$9Og&!%cwLSqvK&*ENOHTPk%R?@w=_VV-(M9klhX@S(Vtcfa$?*F7k zs8LqsJHPht3>vJe3D(Dw7x_c&GQJ?%j->_oQdk>V;^3{qx4IZ>XWPjP-IKFbEKx6{{BjQ*XT_h>;-9O6+=+v-Fr_c_e zz%MD*s8MXy+*X&vDLImm*k2liamaaF=limnX^nLB<2ksMEjQ)EC^t^mu%keb9qrp@ z|5fIYVC`36pnc48@d6VORgHSv6V~FmX)SC!fTz=;vAKy?bcjS&i-7=I!1?0@5tcn^ zw%5V!6!|4n2_GiQk6OyL_`{fwh?9I0#_U6MV+ht15vkivc;v!nK*76>fCwI-<2xmb zp?X56>*cY!KpqZv?Dda6H~>|g%j$U*J}i$S-$kP0uBEMtBqasdDP+MxXu%s383tFL z=hS&unPGRnHRdM3;bF(nX=ONta#>vY+ic#xfZt?oiv2k+Rc_9nm} zU);+4zljUTalic3O@d!cnD-%;#O>CNTClc@wU$;saGfxdbNm(Lgu~?EXB- z#OM1vM~g03vA62+Ubbj%y^TFQR~oYW_9KlA+W7g z_ktm(0fzf#N%u}(*9iEYT~*!b`9S1zG<8V!a!Ve%3U%(`!=g_JbOt%&w)vcsS9Ppc z_g6y~a?kQBOEyw7#g-3k1eiRxlMC#f1ti{EteJ? zRjt-mrG%{WVPIuRwT{(;F9#B+o_DhXo=Wcp@Zv>uuRFTcVJUOsuVpTbrs7%`G=$~~ zbD)$FbYE_DKu!YscA*^h#{XEPpCpWdg`l0l#w8rqpYKMBhY1Wn>xP9RK@j97=8Zv} za9k|^+ujXPC#ndHv@9I~{9k-_#keI$M*Wj52DRI2P6HzuN*D11QmxcK9PH*{50!xa z`7YD2deW+1Qn4f!W6cHdUXKJ3N@JPzB!h))Hl(@lPl`dDDloppj6}p7rt%3yehO6P zV%EvJhYN zCTmTGHPS_C)J-a<5fp8_v^mI6ic02xfrG_nuNt!??#xK?5`jM%v{op}Q49^4vyEDV z7afxb-?2K)HFIMNMErS8S8)NpfRu@mL*mn&V=$0&7|mc04E{e?X#abm_W%DyKH%AA XytiM$meax91On6`+Ddf_HqrkF%>eJr diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py index 01e078b2..35b85519 100644 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -74,6 +74,7 @@ class OnionrMail: logger.info('Decrypting messages...') choice = '' displayList = [] + subject = '' # this could use a lot of memory if someone has recieved a lot of messages for blockHash in self.myCore.getBlocksByType('pm'): @@ -97,7 +98,12 @@ class OnionrMail: senderDisplay = senderKey blockDate = pmBlocks[blockHash].getDate().strftime("%m/%d %H:%M") - displayList.append('%s. %s - %s: %s' % (blockCount, blockDate, senderDisplay[:12], blockHash)) + try: + subject = pmBlocks[blockHash].bmetadata['subject'] + except KeyError: + subject = '' + + 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) @@ -188,6 +194,7 @@ class OnionrMail: def draftMessage(self, recip=''): message = '' newLine = '' + subject = '' entering = False if len(recip) == 0: entering = True @@ -207,6 +214,10 @@ class OnionrMail: else: # if -q or ctrl-c/d, exit function here, otherwise we successfully got the public key return + try: + subject = logger.readline('Message subject: ') + except (KeyboardInterrupt, EOFError): + pass cancelEnter = False logger.info('Enter your message, stop by entering -q on a new line. -c to cancel') @@ -226,7 +237,7 @@ class OnionrMail: if not cancelEnter: logger.info('Inserting encrypted message as Onionr block....') - blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=True) + blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=True, meta={'subject': subject}) self.sentboxTools.addToSent(blockID, recip, message) def menu(self): choice = '' diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index a08dbf55..6e8ce037 100644 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -7,6 +7,7 @@ +
@@ -15,7 +16,7 @@ Onionr Mail
-
+
diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index bafbda6d..c07d9534 100644 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -9,8 +9,30 @@ function getInbox(){ .then((resp) => resp.json()) // Transform the data into json .then(function(resp) { var entry = document.createElement('div') - entry.innerHTML = resp['meta']['time'] + ' - ' + resp['meta']['signer'] + var bHash = pms[i].substring(0, 10) + var bHashDisplay = document.createElement('span') + var senderInput = document.createElement('input') + var subjectLine = document.createElement('span') + var dateStr = document.createElement('span') + var humanDate = new Date(0) + humanDate.setUTCSeconds(resp['meta']['time']) + senderInput.value = resp['meta']['signer'] + bHashDisplay.innerText = bHash + senderInput.readOnly = true + dateStr.innerText = humanDate.toString() + if (resp['metadata']['subject'] === undefined || resp['metadata']['subject'] === null) { + subjectLine.innerText = '()' + } + else{ + subjectLine.innerText = '(' + resp['metadata']['subject'] + ')' + } + //entry.innerHTML = 'sender ' + resp['meta']['signer'] + ' - ' + resp['meta']['time'] threadPart.appendChild(entry) + entry.appendChild(bHashDisplay) + entry.appendChild(senderInput) + entry.appendChild(subjectLine) + entry.appendChild(dateStr) + }) } From 158154184a6eeb3fc287536099a5218310f8e65f Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 1 Feb 2019 13:56:22 -0600 Subject: [PATCH 52/94] added subject line in mail, improved readme --- docs/onionr-logo.png~ | Bin 0 -> 195499 bytes docs/tor-stinks-02.png | Bin 0 -> 28565 bytes onionr/static-data/www/mail/mail.css | 6 ++++++ 3 files changed, 6 insertions(+) create mode 100644 docs/onionr-logo.png~ create mode 100644 docs/tor-stinks-02.png create mode 100644 onionr/static-data/www/mail/mail.css diff --git a/docs/onionr-logo.png~ b/docs/onionr-logo.png~ new file mode 100644 index 0000000000000000000000000000000000000000..5e15d42f73b97d6dd0368883938d64ca53aa3945 GIT binary patch literal 195499 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 literal 0 HcmV?d00001 diff --git a/docs/tor-stinks-02.png b/docs/tor-stinks-02.png new file mode 100644 index 0000000000000000000000000000000000000000..ca35b23c5987248bcf9f9892d9eb83e45e26f614 GIT binary patch literal 28565 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 Date: Fri, 1 Feb 2019 21:15:28 -0600 Subject: [PATCH 53/94] fixed broken getcontent, more work on mail --- onionr/api.py | 21 ++++++++++++--------- onionr/static-data/www/mail/mail.css | 3 ++- onionr/static-data/www/mail/mail.js | 12 +++++++----- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index dbd79321..352dc329 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -139,7 +139,7 @@ class PublicAPI: if clientAPI._utils.validateHash(data): if data not in self.hideBlocks: if data in clientAPI._core.getBlockList(): - block = self.clientAPI.getBlockData(data).encode() + block = clientAPI.getBlockData(data, raw=True).encode() resp = base64.b64encode(block).decode() if len(resp) == 0: abort(404) @@ -469,17 +469,20 @@ class API: # Don't error on race condition with startup pass - def getBlockData(self, bHash, decrypt=False): + def getBlockData(self, bHash, decrypt=False, raw=False): bl = Block(bHash, core=self._core) if decrypt: bl.decrypt() if bl.isEncrypted and not bl.decrypted: raise ValueError - 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 - return json.dumps(retData) \ No newline at end of file + if not raw: + 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 + return json.dumps(retData) + else: + return bl.raw \ No newline at end of file diff --git a/onionr/static-data/www/mail/mail.css b/onionr/static-data/www/mail/mail.css index 17ea217f..7a807d3e 100644 --- a/onionr/static-data/www/mail/mail.css +++ b/onionr/static-data/www/mail/mail.css @@ -2,5 +2,6 @@ padding-top: 1em; } .threads div span{ - padding-left: 1em; + padding-left: 0.5em; + padding-right: 0.5em; } \ 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 c07d9534..f392f569 100644 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -8,16 +8,18 @@ function getInbox(){ }}) .then((resp) => resp.json()) // Transform the data into json .then(function(resp) { + var entry = document.createElement('div') - var bHash = pms[i].substring(0, 10) - var bHashDisplay = document.createElement('span') + + var bHashDisplay = document.createElement('a') var senderInput = document.createElement('input') var subjectLine = document.createElement('span') var dateStr = document.createElement('span') var humanDate = new Date(0) humanDate.setUTCSeconds(resp['meta']['time']) senderInput.value = resp['meta']['signer'] - bHashDisplay.innerText = bHash + bHashDisplay.innerText = pms[i - 1].substring(0, 10) + bHashDisplay.setAttribute('hash', pms[i - 1]); senderInput.readOnly = true dateStr.innerText = humanDate.toString() if (resp['metadata']['subject'] === undefined || resp['metadata']['subject'] === null) { @@ -33,7 +35,7 @@ function getInbox(){ entry.appendChild(subjectLine) entry.appendChild(dateStr) - }) + }.bind([pms, i])) } } @@ -45,6 +47,6 @@ fetch('/getblocksbytype/pm', { .then((resp) => resp.text()) // Transform the data into json .then(function(data) { pms = data.split(',') - getInbox() + getInbox(pms) }) From 2bb7246fbe81383de9037de75d91b3ea48e828aa Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 1 Feb 2019 21:38:24 -0600 Subject: [PATCH 54/94] fixed merge conflicts --- onionr/static-data/default_config.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index 4949c809..b6cb3145 100644 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -7,13 +7,8 @@ "socket_servers": false, "security_level": 0, "max_block_age": 2678400, -<<<<<<< HEAD - "public_key": "", - "use_new_api_server": false -======= "bypass_tor_check": false, "public_key": "" ->>>>>>> pom }, "www" : { From 6687b2a843f635cd45266a8df6174c781991a9f9 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 2 Feb 2019 17:10:04 -0600 Subject: [PATCH 55/94] changed permisisons --- .dockerignore | 0 .gitignore | 0 .gitmodules | 0 CODE_OF_CONDUCT.md | 0 CONTRIBUTING.md | 0 Dockerfile | 0 ISSUE_TEMPLATE.md | 0 LICENSE.txt | 0 Makefile | 0 README.md | 0 docs/api.md | 0 docs/onionr-logo.png | Bin docs/onionr-logo.png~ | Bin docs/onionr-web.png | Bin docs/tor-stinks-02.png | Bin docs/whitepaper.md | 0 onionr/apimanager.py | 75 ------------------ onionr/apiprivate.py | 32 -------- onionr/apipublic.py | 41 ---------- onionr/blockimporter.py | 0 onionr/config.py | 0 onionr/core.py | 2 +- onionr/dbcreator.py | 0 onionr/dependencies/secrets.py | 0 onionr/etc/onionrvalues.py | 0 onionr/etc/pgpwords.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/onionrdaemontools.py | 66 ++++++++------- 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/onionrsockets.py | 0 onionr/onionrstorage.py | 0 onionr/onionrusers.py | 0 onionr/onionrutils.py | 0 onionr/serializeddata.py | 0 onionr/static-data/bootstrap-nodes.txt | 0 onionr/static-data/connect-check.txt | 0 .../default-plugins/cliui/info.json | 0 .../static-data/default-plugins/cliui/main.py | 0 .../default-plugins/encrypt/info.json | 0 .../default-plugins/encrypt/main.py | 0 .../default-plugins/flow/info.json | 0 .../static-data/default-plugins/flow/main.py | 0 .../metadataprocessor/info.json | 0 .../default-plugins/metadataprocessor/main.py | 0 .../default-plugins/pluginmanager/.gitignore | 0 .../default-plugins/pluginmanager/LICENSE | 0 .../default-plugins/pluginmanager/README.md | 0 .../default-plugins/pluginmanager/info.json | 0 .../default-plugins/pluginmanager/main.py | 0 .../static-data/default-plugins/pms/info.json | 0 .../static-data/default-plugins/pms/main.py | 6 +- .../default-plugins/pms/sentboxdb.py | 0 onionr/static-data/default_config.json | 3 +- onionr/static-data/default_plugin.py | 0 onionr/static-data/header.txt | 0 onionr/static-data/index.html | 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/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/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/ui/README.md | 0 .../www/ui/common/default-icon.html | 0 onionr/static-data/www/ui/common/footer.html | 0 onionr/static-data/www/ui/common/header.html | 0 .../www/ui/common/onionr-reply-creator.html | 0 .../www/ui/common/onionr-timeline-post.html | 0 .../common/onionr-timeline-reply-creator.html | 0 .../www/ui/common/onionr-timeline-reply.html | 0 onionr/static-data/www/ui/config.json | 0 onionr/static-data/www/ui/dist/css/main.css | 0 .../www/ui/dist/css/themes/dark.css | 0 .../static-data/www/ui/dist/img/default.png | Bin onionr/static-data/www/ui/dist/index.html | 0 onionr/static-data/www/ui/dist/js/main.js | 0 onionr/static-data/www/ui/dist/js/timeline.js | 0 onionr/static-data/www/ui/lang.json | 0 onionr/static-data/www/ui/src/css/main.css | 0 .../www/ui/src/css/themes/dark.css | 0 onionr/static-data/www/ui/src/img/default.png | Bin onionr/static-data/www/ui/src/index.html | 0 onionr/static-data/www/ui/src/js/main.js | 0 onionr/static-data/www/ui/src/js/timeline.js | 0 onionr/storagecounter.py | 0 requirements.txt | 0 run-windows.bat | 0 104 files changed, 43 insertions(+), 182 deletions(-) mode change 100644 => 100755 .dockerignore mode change 100644 => 100755 .gitignore mode change 100644 => 100755 .gitmodules mode change 100644 => 100755 CODE_OF_CONDUCT.md mode change 100644 => 100755 CONTRIBUTING.md mode change 100644 => 100755 Dockerfile mode change 100644 => 100755 ISSUE_TEMPLATE.md mode change 100644 => 100755 LICENSE.txt mode change 100644 => 100755 Makefile mode change 100644 => 100755 README.md mode change 100644 => 100755 docs/api.md mode change 100644 => 100755 docs/onionr-logo.png mode change 100644 => 100755 docs/onionr-logo.png~ mode change 100644 => 100755 docs/onionr-web.png mode change 100644 => 100755 docs/tor-stinks-02.png mode change 100644 => 100755 docs/whitepaper.md delete mode 100644 onionr/apimanager.py delete mode 100644 onionr/apiprivate.py delete mode 100644 onionr/apipublic.py mode change 100644 => 100755 onionr/blockimporter.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/dependencies/secrets.py mode change 100644 => 100755 onionr/etc/onionrvalues.py mode change 100644 => 100755 onionr/etc/pgpwords.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/onionrcrypto.py mode change 100644 => 100755 onionr/onionrdaemontools.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/onionrsockets.py mode change 100644 => 100755 onionr/onionrstorage.py mode change 100644 => 100755 onionr/onionrusers.py mode change 100644 => 100755 onionr/onionrutils.py mode change 100644 => 100755 onionr/serializeddata.py mode change 100644 => 100755 onionr/static-data/bootstrap-nodes.txt mode change 100644 => 100755 onionr/static-data/connect-check.txt mode change 100644 => 100755 onionr/static-data/default-plugins/cliui/info.json mode change 100644 => 100755 onionr/static-data/default-plugins/cliui/main.py mode change 100644 => 100755 onionr/static-data/default-plugins/encrypt/info.json mode change 100644 => 100755 onionr/static-data/default-plugins/encrypt/main.py mode change 100644 => 100755 onionr/static-data/default-plugins/flow/info.json mode change 100644 => 100755 onionr/static-data/default-plugins/flow/main.py mode change 100644 => 100755 onionr/static-data/default-plugins/metadataprocessor/info.json mode change 100644 => 100755 onionr/static-data/default-plugins/metadataprocessor/main.py mode change 100644 => 100755 onionr/static-data/default-plugins/pluginmanager/.gitignore mode change 100644 => 100755 onionr/static-data/default-plugins/pluginmanager/LICENSE mode change 100644 => 100755 onionr/static-data/default-plugins/pluginmanager/README.md mode change 100644 => 100755 onionr/static-data/default-plugins/pluginmanager/info.json mode change 100644 => 100755 onionr/static-data/default-plugins/pluginmanager/main.py mode change 100644 => 100755 onionr/static-data/default-plugins/pms/info.json mode change 100644 => 100755 onionr/static-data/default-plugins/pms/main.py mode change 100644 => 100755 onionr/static-data/default-plugins/pms/sentboxdb.py mode change 100644 => 100755 onionr/static-data/default_config.json mode change 100644 => 100755 onionr/static-data/default_plugin.py mode change 100644 => 100755 onionr/static-data/header.txt mode change 100644 => 100755 onionr/static-data/index.html 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/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/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/ui/README.md mode change 100644 => 100755 onionr/static-data/www/ui/common/default-icon.html mode change 100644 => 100755 onionr/static-data/www/ui/common/footer.html mode change 100644 => 100755 onionr/static-data/www/ui/common/header.html mode change 100644 => 100755 onionr/static-data/www/ui/common/onionr-reply-creator.html mode change 100644 => 100755 onionr/static-data/www/ui/common/onionr-timeline-post.html mode change 100644 => 100755 onionr/static-data/www/ui/common/onionr-timeline-reply-creator.html mode change 100644 => 100755 onionr/static-data/www/ui/common/onionr-timeline-reply.html mode change 100644 => 100755 onionr/static-data/www/ui/config.json mode change 100644 => 100755 onionr/static-data/www/ui/dist/css/main.css mode change 100644 => 100755 onionr/static-data/www/ui/dist/css/themes/dark.css mode change 100644 => 100755 onionr/static-data/www/ui/dist/img/default.png mode change 100644 => 100755 onionr/static-data/www/ui/dist/index.html mode change 100644 => 100755 onionr/static-data/www/ui/dist/js/main.js mode change 100644 => 100755 onionr/static-data/www/ui/dist/js/timeline.js mode change 100644 => 100755 onionr/static-data/www/ui/lang.json mode change 100644 => 100755 onionr/static-data/www/ui/src/css/main.css mode change 100644 => 100755 onionr/static-data/www/ui/src/css/themes/dark.css mode change 100644 => 100755 onionr/static-data/www/ui/src/img/default.png mode change 100644 => 100755 onionr/static-data/www/ui/src/index.html mode change 100644 => 100755 onionr/static-data/www/ui/src/js/main.js mode change 100644 => 100755 onionr/static-data/www/ui/src/js/timeline.js mode change 100644 => 100755 onionr/storagecounter.py mode change 100644 => 100755 requirements.txt mode change 100644 => 100755 run-windows.bat diff --git a/.dockerignore b/.dockerignore old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/.gitmodules b/.gitmodules old mode 100644 new mode 100755 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md old mode 100644 new mode 100755 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md old mode 100644 new mode 100755 diff --git a/Dockerfile b/Dockerfile old mode 100644 new mode 100755 diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md old mode 100644 new mode 100755 diff --git a/LICENSE.txt b/LICENSE.txt old mode 100644 new mode 100755 diff --git a/Makefile b/Makefile old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/docs/api.md b/docs/api.md 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/onionr-logo.png~ b/docs/onionr-logo.png~ old mode 100644 new mode 100755 diff --git a/docs/onionr-web.png b/docs/onionr-web.png 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/docs/whitepaper.md b/docs/whitepaper.md old mode 100644 new mode 100755 diff --git a/onionr/apimanager.py b/onionr/apimanager.py deleted file mode 100644 index 44737097..00000000 --- a/onionr/apimanager.py +++ /dev/null @@ -1,75 +0,0 @@ -''' - Onionr - P2P Anonymous Storage Network - - Handles api data exchange, interfaced by both public and client 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 config, apipublic, apiprivate, core, socket, random, threading, time -config.reload() - -PRIVATE_API_VERSION = 0 -PUBLIC_API_VERSION = 1 - -DEV_MODE = config.get('general.dev_mode') - -def getOpenPort(): - '''Get a random open port''' - p = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - p.bind(("127.0.0.1",0)) - p.listen(1) - port = p.getsockname()[1] - p.close() - return port - -def getRandomLocalIP(): - '''Get a random local ip address''' - hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))] - host = '.'.join(hostOctets) - return host - -class APIManager: - def __init__(self, coreInst): - assert isinstance(coreInst, core.Core) - self.core = coreInst - self.utils = coreInst._utils - self.crypto = coreInst._crypto - - # if this gets set to true, both the public and private apis will shutdown - self.shutdown = False - - publicIP = '127.0.0.1' - privateIP = '127.0.0.1' - if DEV_MODE: - # set private and local api servers bind IPs to random localhost (127.x.x.x), make sure not the same - privateIP = getRandomLocalIP() - while True: - publicIP = getRandomLocalIP() - if publicIP != privateIP: - break - - # Make official the IPs and Ports - self.publicIP = publicIP - self.privateIP = privateIP - self.publicPort = config.get('client.port', 59496) - self.privatePort = config.get('client.port', 59496) - - # Run the API servers in new threads - self.publicAPI = apipublic.APIPublic(self) - self.privateAPI = apiprivate.APIPrivate(self) - threading.Thread(target=self.publicAPI.run).start() - threading.Thread(target=self.privateAPI.run).start() - while not self.shutdown: - time.sleep(1) \ No newline at end of file diff --git a/onionr/apiprivate.py b/onionr/apiprivate.py deleted file mode 100644 index 7f62b9d5..00000000 --- a/onionr/apiprivate.py +++ /dev/null @@ -1,32 +0,0 @@ -''' - Onionr - P2P Anonymous Storage Network - - Handle incoming commands from the client. Intended for localhost use -''' -''' - 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 flask, apimanager -from flask import request, Response, abort, send_from_directory -from gevent.pywsgi import WSGIServer - -class APIPrivate: - def __init__(self, managerInst): - assert isinstance(managerInst, apimanager.APIManager) - self.app = flask.Flask(__name__) # The flask application, which recieves data from the greenlet wsgiserver - self.httpServer = WSGIServer((managerInst.privateIP, managerInst.privatePort), self.app, log=None) - - def run(self): - self.httpServer.serve_forever() - return \ No newline at end of file diff --git a/onionr/apipublic.py b/onionr/apipublic.py deleted file mode 100644 index 9308f50a..00000000 --- a/onionr/apipublic.py +++ /dev/null @@ -1,41 +0,0 @@ -''' - Onionr - P2P Anonymous Storage Network - - Handle incoming commands from other Onionr nodes, over HTTP -''' -''' - 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 flask, apimanager -from flask import request, Response, abort, send_from_directory -from gevent.pywsgi import WSGIServer - - -class APIPublic: - def __init__(self, managerInst): - assert isinstance(managerInst, apimanager.APIManager) - app = flask.Flask(__name__) - @app.route('/') - def banner(): - try: - with open('static-data/index.html', 'r') as html: - resp = Response(html.read(), mimetype='text/html') - except FileNotFoundError: - resp = Response("") - return resp - self.httpServer = WSGIServer((managerInst.publicIP, managerInst.publicPort), app) - - def run(self): - self.httpServer.serve_forever() - return diff --git a/onionr/blockimporter.py b/onionr/blockimporter.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 index 8634ab1f..47f0e1ad --- a/onionr/core.py +++ b/onionr/core.py @@ -56,7 +56,7 @@ class Core: self.privateApiHostFile = self.dataDir + 'private-host.txt' self.addressDB = self.dataDir + 'address.db' self.hsAddress = '' - self.i2pAddress = config.get('i2p.ownAddr', None) + self.i2pAddress = config.get('i2p.own_addr', None) self.bootstrapFileLocation = 'static-data/bootstrap-nodes.txt' self.bootstrapList = [] self.requirements = onionrvalues.OnionrValues() diff --git a/onionr/dbcreator.py b/onionr/dbcreator.py old mode 100644 new mode 100755 diff --git a/onionr/dependencies/secrets.py b/onionr/dependencies/secrets.py old mode 100644 new mode 100755 diff --git a/onionr/etc/onionrvalues.py b/onionr/etc/onionrvalues.py old mode 100644 new mode 100755 diff --git a/onionr/etc/pgpwords.py b/onionr/etc/pgpwords.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 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/onionrcrypto.py b/onionr/onionrcrypto.py old mode 100644 new mode 100755 diff --git a/onionr/onionrdaemontools.py b/onionr/onionrdaemontools.py old mode 100644 new mode 100755 index faea0070..d64a69ff --- a/onionr/onionrdaemontools.py +++ b/onionr/onionrdaemontools.py @@ -43,37 +43,43 @@ class DaemonTools: else: peer = self.daemon.pickOnlinePeer() - ourID = self.daemon._core.hsAddress.strip() - - url = 'http://' + peer + '/announce' - data = {'node': ourID} - - combinedNodes = ourID + peer - existingRand = self.daemon._core.getAddressInfo(peer, 'powValue') - if type(existingRand) is type(None): - existingRand = '' - - if peer in self.announceCache: - data['random'] = self.announceCache[peer] - elif len(existingRand) > 0: - data['random'] = existingRand - else: - proof = onionrproofs.DataPOW(combinedNodes, forceDifficulty=4) - 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 + 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: - 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']) + 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.getAddressInfo(peer, 'powValue') + if type(existingRand) is type(None): + existingRand = '' + + if peer in self.announceCache: + data['random'] = self.announceCache[peer] + elif len(existingRand) > 0: + data['random'] = existingRand + else: + proof = onionrproofs.DataPOW(combinedNodes, forceDifficulty=4) + 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 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/onionrsockets.py b/onionr/onionrsockets.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.py b/onionr/onionrusers.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/static-data/bootstrap-nodes.txt b/onionr/static-data/bootstrap-nodes.txt old mode 100644 new mode 100755 diff --git a/onionr/static-data/connect-check.txt b/onionr/static-data/connect-check.txt old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/cliui/info.json b/onionr/static-data/default-plugins/cliui/info.json old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/cliui/main.py b/onionr/static-data/default-plugins/cliui/main.py old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/encrypt/info.json b/onionr/static-data/default-plugins/encrypt/info.json old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/encrypt/main.py b/onionr/static-data/default-plugins/encrypt/main.py old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/flow/info.json b/onionr/static-data/default-plugins/flow/info.json old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/flow/main.py b/onionr/static-data/default-plugins/flow/main.py old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/metadataprocessor/info.json b/onionr/static-data/default-plugins/metadataprocessor/info.json old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/metadataprocessor/main.py b/onionr/static-data/default-plugins/metadataprocessor/main.py old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/pluginmanager/.gitignore b/onionr/static-data/default-plugins/pluginmanager/.gitignore old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/pluginmanager/LICENSE b/onionr/static-data/default-plugins/pluginmanager/LICENSE old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/pluginmanager/README.md b/onionr/static-data/default-plugins/pluginmanager/README.md old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/pluginmanager/info.json b/onionr/static-data/default-plugins/pluginmanager/info.json old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/pluginmanager/main.py b/onionr/static-data/default-plugins/pluginmanager/main.py old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/pms/info.json b/onionr/static-data/default-plugins/pms/info.json old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py old mode 100644 new mode 100755 index 35b85519..7c971df3 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -141,7 +141,11 @@ class OnionrMail: logger.warn('This message has an INVALID signature. ANYONE could have sent this message.') cancel = logger.readline('Press enter to continue to message, or -q to not open the message (recommended).') if cancel != '-q': - print(draw_border(self.myCore._utils.escapeAnsi(readBlock.bcontent.decode().strip()))) + 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.') + pass reply = logger.readline("Press enter to continue, or enter %s to reply" % ("-r",)) if reply == "-r": self.draftMessage(self.myCore._utils.bytesToStr(readBlock.signer,)) diff --git a/onionr/static-data/default-plugins/pms/sentboxdb.py b/onionr/static-data/default-plugins/pms/sentboxdb.py old mode 100644 new mode 100755 diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json old mode 100644 new mode 100755 index b6cb3145..eeefe49a --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -21,8 +21,7 @@ "private" : { "run" : true, "path" : "static-data/www/private/", - "guess_mime" : true, - "timing_protection" : true + "guess_mime" : true }, "ui" : { diff --git a/onionr/static-data/default_plugin.py b/onionr/static-data/default_plugin.py old mode 100644 new mode 100755 diff --git a/onionr/static-data/header.txt b/onionr/static-data/header.txt old mode 100644 new mode 100755 diff --git a/onionr/static-data/index.html b/onionr/static-data/index.html 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/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/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/ui/README.md b/onionr/static-data/www/ui/README.md old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/common/default-icon.html b/onionr/static-data/www/ui/common/default-icon.html old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/common/footer.html b/onionr/static-data/www/ui/common/footer.html old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/common/header.html b/onionr/static-data/www/ui/common/header.html old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/common/onionr-reply-creator.html b/onionr/static-data/www/ui/common/onionr-reply-creator.html old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/common/onionr-timeline-post.html b/onionr/static-data/www/ui/common/onionr-timeline-post.html old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/common/onionr-timeline-reply-creator.html b/onionr/static-data/www/ui/common/onionr-timeline-reply-creator.html old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/common/onionr-timeline-reply.html b/onionr/static-data/www/ui/common/onionr-timeline-reply.html old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/config.json b/onionr/static-data/www/ui/config.json old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/dist/css/main.css b/onionr/static-data/www/ui/dist/css/main.css old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/dist/css/themes/dark.css b/onionr/static-data/www/ui/dist/css/themes/dark.css old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/dist/img/default.png b/onionr/static-data/www/ui/dist/img/default.png old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/dist/index.html b/onionr/static-data/www/ui/dist/index.html 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/static-data/www/ui/dist/js/timeline.js b/onionr/static-data/www/ui/dist/js/timeline.js old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/lang.json b/onionr/static-data/www/ui/lang.json old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/src/css/main.css b/onionr/static-data/www/ui/src/css/main.css old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/src/css/themes/dark.css b/onionr/static-data/www/ui/src/css/themes/dark.css old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/src/img/default.png b/onionr/static-data/www/ui/src/img/default.png old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/src/index.html b/onionr/static-data/www/ui/src/index.html old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/src/js/main.js b/onionr/static-data/www/ui/src/js/main.js old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/src/js/timeline.js b/onionr/static-data/www/ui/src/js/timeline.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/requirements.txt b/requirements.txt old mode 100644 new mode 100755 diff --git a/run-windows.bat b/run-windows.bat old mode 100644 new mode 100755 From 0a8b31ff6e40bec51121218b1363bc5e7581c4e2 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 3 Feb 2019 12:19:50 -0600 Subject: [PATCH 56/94] work on mail, storagecounter bugfix --- README.md | 1 + onionr/api.py | 2 -- onionr/onionr.py | 3 ++- onionr/static-data/bootstrap-nodes.txt | 2 +- onionr/static-data/www/mail/index.html | 8 ++++++- onionr/static-data/www/mail/mail.css | 32 +++++++++++++++++++++++--- onionr/static-data/www/mail/mail.js | 16 +++++++++++-- onionr/storagecounter.py | 4 ++-- 8 files changed, 56 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 5a346e5d..6be34104 100755 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Everyone is welcome to help out. Help is wanted for the following: * Testing * Running stable nodes * Security review/audit +* Automatic I2P setup Bitcoin: [1onion55FXzm6h8KQw3zFw2igpHcV7LPq](bitcoin:1onion55FXzm6h8KQw3zFw2igpHcV7LPq) USD: [Ko-Fi](https://www.ko-fi.com/beardogkf) diff --git a/onionr/api.py b/onionr/api.py index 57f34ac0..19f3db97 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -19,8 +19,6 @@ ''' from gevent.pywsgi import WSGIServer, WSGIHandler from gevent import Timeout -#import gevent.monkey -#gevent.monkey.patch_socket() import flask, cgi from flask import request, Response, abort, send_from_directory import sys, random, threading, hmac, hashlib, base64, time, math, os, json, socket diff --git a/onionr/onionr.py b/onionr/onionr.py index 110aa6f2..89215dc5 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -124,6 +124,7 @@ 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) @@ -774,7 +775,7 @@ class Onionr: Onionr.setupConfig('data/', self = self) if self._developmentMode: - logger.warn('DEVELOPMENT MODE ENABLED (LESS SECURE)', timestamp = False) + logger.warn('DEVELOPMENT MODE ENABLED (NOT RECOMMENDED)', timestamp = False) net = NetController(config.get('client.public.port', 59497), apiServerIP=apiHost) logger.debug('Tor is starting...') if not net.startTor(): diff --git a/onionr/static-data/bootstrap-nodes.txt b/onionr/static-data/bootstrap-nodes.txt index 93dec7ce..6fb8319b 100755 --- a/onionr/static-data/bootstrap-nodes.txt +++ b/onionr/static-data/bootstrap-nodes.txt @@ -1 +1 @@ -dd3llxdp5q6ak3zmmicoy3jnodmroouv2xr7whkygiwp3rl7nf23gdad.onion \ No newline at end of file +i7dgbnouzyl7gv75b3eaqfz7x236abkn6nkjdpun273sydkbwcoidrid.onion \ 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 6e8ce037..a811eaa7 100755 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -16,7 +16,13 @@ Onionr Mail
-
+

+
+ +
+
+
Nothing here yet 😞
+
diff --git a/onionr/static-data/www/mail/mail.css b/onionr/static-data/www/mail/mail.css index 7a807d3e..304e6803 100755 --- a/onionr/static-data/www/mail/mail.css +++ b/onionr/static-data/www/mail/mail.css @@ -2,6 +2,32 @@ padding-top: 1em; } .threads div span{ - padding-left: 0.5em; - padding-right: 0.5em; -} \ No newline at end of file + padding-left: 0.1em; + padding-right: 0.1em; +} + +#threadPlaceholder{ + display: none; + margin-top: 1em; + font-size: 2em; +} + +input{ + background-color: white; + color: black; +} + +.btn-group button { + border: 1px solid black; + padding: 10px 24px; /* Some padding */ + cursor: pointer; /* Pointer/hand icon */ + float: left; /* Float the buttons side by side */ + } + + .btn-group button:hover { + background-color: darkgray; + } + + .btn-group { + margin-bottom: 2em; + } \ 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 f392f569..900ec14c 100755 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -1,7 +1,16 @@ pms = '' threadPart = document.getElementById('threads') +threadPlaceholder = document.getElementById('threadPlaceholder') function getInbox(){ + var showed = false for(var i = 0; i < pms.length; i++) { + if (pms[i].trim().length == 0){ + continue + } + else{ + threadPlaceholder.style.display = 'none' + showed = true + } fetch('/getblockdata/' + pms[i], { headers: { "token": webpass @@ -11,7 +20,7 @@ function getInbox(){ var entry = document.createElement('div') - var bHashDisplay = document.createElement('a') + var bHashDisplay = document.createElement('span') var senderInput = document.createElement('input') var subjectLine = document.createElement('span') var dateStr = document.createElement('span') @@ -30,13 +39,16 @@ function getInbox(){ } //entry.innerHTML = 'sender ' + resp['meta']['signer'] + ' - ' + resp['meta']['time'] threadPart.appendChild(entry) - entry.appendChild(bHashDisplay) + //entry.appendChild(bHashDisplay) entry.appendChild(senderInput) entry.appendChild(subjectLine) entry.appendChild(dateStr) }.bind([pms, i])) } + if (! showed){ + threadPlaceholder.style.display = 'block' + } } diff --git a/onionr/storagecounter.py b/onionr/storagecounter.py index fc1a9d6b..4647a084 100755 --- a/onionr/storagecounter.py +++ b/onionr/storagecounter.py @@ -18,7 +18,7 @@ along with this program. If not, see . ''' import config - +config.reload() class StorageCounter: def __init__(self, coreInst): self._core = coreInst @@ -27,7 +27,7 @@ class StorageCounter: def isFull(self): retData = False - if self._core.config.get('allocations.disk') <= (self.getAmount() + 1000): + if self._core.config.get('allocations.disk', 2000000000) <= (self.getAmount() + 1000): retData = True return retData From 6ed731fbe9ca7570e0f55a1b4e1c36532aa81bbc Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 3 Feb 2019 18:31:03 -0600 Subject: [PATCH 57/94] more work on mail --- onionr/api.py | 8 +++ onionr/static-data/www/mail/index.html | 6 +- onionr/static-data/www/mail/mail.css | 10 +++ onionr/static-data/www/mail/mail.js | 71 +++++++++++++++++++- onionr/static-data/www/shared/main/style.css | 2 +- 5 files changed, 91 insertions(+), 6 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 19f3db97..be793772 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -436,6 +436,14 @@ class API: @app.route('/getuptime') def showUptime(): return Response(str(self.getUptime())) + + @app.route('/getActivePubkey') + def getActivePubkey(): + return Response(self._core._crypto.pubKey) + + @app.route('/getHumanReadable/') + def getHumanReadable(name): + return Response(self._core._utils.getHumanReadableID(name)) self.httpServer = WSGIServer((self.host, bindPort), app, log=None, handler_class=FDSafeHandler) self.httpServer.serve_forever() diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index a811eaa7..2723677a 100755 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -15,10 +15,10 @@ Onionr Mail
- +
Current Used Identity:


-
- +
+
Nothing here yet 😞
diff --git a/onionr/static-data/www/mail/mail.css b/onionr/static-data/www/mail/mail.css index 304e6803..50b86b49 100755 --- a/onionr/static-data/www/mail/mail.css +++ b/onionr/static-data/www/mail/mail.css @@ -30,4 +30,14 @@ input{ .btn-group { margin-bottom: 2em; + } + +#tabBtns{ + margin-bottom: 3em; + display: block; +} + + .activeTab{ + color: black; + background-color: gray; } \ 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 900ec14c..8199e35f 100755 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -1,6 +1,47 @@ +/* + Onionr - P2P Anonymous Storage Network + + This file handles the mail 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 . +*/ + pms = '' threadPart = document.getElementById('threads') threadPlaceholder = document.getElementById('threadPlaceholder') +tabBtns = document.getElementById('tabBtns') + +myPub = httpGet('/getActivePubkey') + +function setActiveTab(tabName){ + threadPart.innerHTML = "" + switch(tabName){ + case 'inbox': + getInbox(); + break + case 'sentbox': + console.log(tabName) + break + case 'drafts': + console.log(tabName) + break + case 'send message': + console.log(tabName) + break + } +} + function getInbox(){ var showed = false for(var i = 0; i < pms.length; i++) { @@ -26,7 +67,7 @@ function getInbox(){ var dateStr = document.createElement('span') var humanDate = new Date(0) humanDate.setUTCSeconds(resp['meta']['time']) - senderInput.value = resp['meta']['signer'] + senderInput.value = httpGet('/getHumanReadable/' + resp['meta']['signer']) bHashDisplay.innerText = pms[i - 1].substring(0, 10) bHashDisplay.setAttribute('hash', pms[i - 1]); senderInput.readOnly = true @@ -52,6 +93,7 @@ function getInbox(){ } + fetch('/getblocksbytype/pm', { headers: { "token": webpass @@ -59,6 +101,31 @@ fetch('/getblocksbytype/pm', { .then((resp) => resp.text()) // Transform the data into json .then(function(data) { pms = data.split(',') - getInbox(pms) + setActiveTab('inbox') }) +tabBtns.onclick = function(event){ + var children = tabBtns.children + for (var i = 0; i < children.length; i++) { + var btn = children[i] + btn.classList.remove('activeTab') + } + event.target.classList.add('activeTab') + setActiveTab(event.target.innerText.toLowerCase()) +} + + +var idStrings = document.getElementsByClassName('myPub') +var myHumanReadable = httpGet('/getHumanReadable/' + myPub) +for (var i = 0; i < idStrings.length; i++){ + if (idStrings[i].tagName.toLowerCase() == 'input'){ + idStrings[i].value = myHumanReadable + } + else{ + idStrings[i].innerText = myHumanReadable + } +} + +for (var i = 0; i < document.getElementsByClassName('refresh').length; i++){ + document.getElementsByClassName('refresh')[i].style.float = 'right' +} \ No newline at end of file diff --git a/onionr/static-data/www/shared/main/style.css b/onionr/static-data/www/shared/main/style.css index 69e1a407..d2ff44e3 100755 --- a/onionr/static-data/www/shared/main/style.css +++ b/onionr/static-data/www/shared/main/style.css @@ -37,7 +37,7 @@ body{ align-items:center; } .logo{ - max-width: 25%; + max-width: 20%; vertical-align: middle; } .logoText{ From 66900627b74b9887e74fbcdad261643e2d8c0f3e Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 4 Feb 2019 17:48:21 -0600 Subject: [PATCH 58/94] more work on mail --- onionr/api.py | 31 ++++--- onionr/static-data/www/mail/index.html | 3 + onionr/static-data/www/mail/mail.css | 2 +- onionr/static-data/www/mail/mail.js | 90 ++++++++++++-------- onionr/static-data/www/shared/base64.min.js | 10 +++ onionr/static-data/www/shared/main/style.css | 1 + onionr/static-data/www/shared/misc.js | 5 ++ 7 files changed, 94 insertions(+), 48 deletions(-) create mode 100644 onionr/static-data/www/shared/base64.min.js diff --git a/onionr/api.py b/onionr/api.py index be793772..69f775c7 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -355,12 +355,13 @@ class API: blocks = self._core.getBlocksByType(name) return Response(','.join(blocks)) - @app.route('/gethtmlsafeblockdata/') - def getSafeData(name): + @app.route('/getblockbody/') + def getBlockBodyData(name): resp = '' if self._core._utils.validateHash(name): try: - resp = cgi.escape(Block(name).bcontent, quote=True) + resp = Block(name, decrypt=True).bcontent + #resp = cgi.escape(Block(name, decrypt=True).bcontent, quote=True) except TypeError: pass else: @@ -382,6 +383,11 @@ class API: abort(404) return Response(resp) + @app.route('/getblockheader/') + def getBlockHeader(name): + resp = self.getBlockData(name, decrypt=True, headerOnly=True) + return Response(resp) + @app.route('/site/', endpoint='site') def site(name): bHash = name @@ -475,7 +481,8 @@ class API: # Don't error on race condition with startup pass - def getBlockData(self, bHash, decrypt=False, raw=False): + def getBlockData(self, bHash, decrypt=False, raw=False, headerOnly=False): + assert self._core._utils.validateHash(bHash) bl = Block(bHash, core=self._core) if decrypt: bl.decrypt() @@ -483,12 +490,16 @@ class API: raise ValueError if not raw: - 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 + 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: + bl.bheader['meta'] = '' + retData = {'meta': bl.bheader, 'metadata': bl.bmetadata} return json.dumps(retData) else: return bl.raw diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index 2723677a..dc7e6c65 100755 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -5,6 +5,7 @@ Onionr Mail + @@ -23,7 +24,9 @@
Nothing here yet 😞
+
+ diff --git a/onionr/static-data/www/mail/mail.css b/onionr/static-data/www/mail/mail.css index 50b86b49..fc39509b 100755 --- a/onionr/static-data/www/mail/mail.css +++ b/onionr/static-data/www/mail/mail.css @@ -3,7 +3,7 @@ } .threads div span{ padding-left: 0.1em; - padding-right: 0.1em; + padding-right: 0.2em; } #threadPlaceholder{ diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index 8199e35f..5f26f694 100755 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -21,9 +21,16 @@ pms = '' threadPart = document.getElementById('threads') threadPlaceholder = document.getElementById('threadPlaceholder') tabBtns = document.getElementById('tabBtns') - +threadContent = {} myPub = httpGet('/getActivePubkey') +function openThread(bHash, sender, date){ + var messageDisplay = document.getElementById('threadDisplay') + stuff = httpGet('/getblockbody/' + bHash) + messageDisplay.innerText = stuff + overlay('messageDisplay') +} + function setActiveTab(tabName){ threadPart.innerHTML = "" switch(tabName){ @@ -42,8 +49,52 @@ function setActiveTab(tabName){ } } +function loadInboxEntrys(bHash){ + fetch('/getblockheader/' + bHash, { + headers: { + "token": webpass + }}) + .then((resp) => resp.json()) // Transform the data into json + .then(function(resp) { + console.log(resp) + var entry = document.createElement('div') + var bHashDisplay = document.createElement('span') + var senderInput = document.createElement('input') + var subjectLine = document.createElement('span') + var dateStr = document.createElement('span') + var humanDate = new Date(0) + var metadata = resp['metadata'] + humanDate.setUTCSeconds(resp['meta']['time']) + senderInput.value = httpGet('/getHumanReadable/' + resp['meta']['signer']) + bHashDisplay.innerText = bHash.substring(0, 10) + entry.setAttribute('hash', bHash); + senderInput.readOnly = true + dateStr.innerText = humanDate.toString() + if (metadata['subject'] === undefined || metadata['subject'] === null) { + subjectLine.innerText = '()' + } + else{ + subjectLine.innerText = '(' + metadata['subject'] + ')' + } + //entry.innerHTML = 'sender ' + resp['meta']['signer'] + ' - ' + resp['meta']['time'] + threadPart.appendChild(entry) + entry.appendChild(bHashDisplay) + entry.appendChild(senderInput) + entry.appendChild(subjectLine) + entry.appendChild(dateStr) + entry.classList.add('threadEntry') + + entry.onclick = function(){ + openThread(entry.getAttribute('hash'), senderInput.value, dateStr.innerText) + } + + }.bind(bHash)) +} + + function getInbox(){ var showed = false + var requested = '' for(var i = 0; i < pms.length; i++) { if (pms[i].trim().length == 0){ continue @@ -52,40 +103,7 @@ function getInbox(){ threadPlaceholder.style.display = 'none' showed = true } - fetch('/getblockdata/' + pms[i], { - headers: { - "token": webpass - }}) - .then((resp) => resp.json()) // Transform the data into json - .then(function(resp) { - - var entry = document.createElement('div') - - var bHashDisplay = document.createElement('span') - var senderInput = document.createElement('input') - var subjectLine = document.createElement('span') - var dateStr = document.createElement('span') - var humanDate = new Date(0) - humanDate.setUTCSeconds(resp['meta']['time']) - senderInput.value = httpGet('/getHumanReadable/' + resp['meta']['signer']) - bHashDisplay.innerText = pms[i - 1].substring(0, 10) - bHashDisplay.setAttribute('hash', pms[i - 1]); - senderInput.readOnly = true - dateStr.innerText = humanDate.toString() - if (resp['metadata']['subject'] === undefined || resp['metadata']['subject'] === null) { - subjectLine.innerText = '()' - } - else{ - subjectLine.innerText = '(' + resp['metadata']['subject'] + ')' - } - //entry.innerHTML = 'sender ' + resp['meta']['signer'] + ' - ' + resp['meta']['time'] - threadPart.appendChild(entry) - //entry.appendChild(bHashDisplay) - entry.appendChild(senderInput) - entry.appendChild(subjectLine) - entry.appendChild(dateStr) - - }.bind([pms, i])) + loadInboxEntrys(pms[i]) } if (! showed){ threadPlaceholder.style.display = 'block' @@ -93,7 +111,6 @@ function getInbox(){ } - fetch('/getblocksbytype/pm', { headers: { "token": webpass @@ -114,7 +131,6 @@ tabBtns.onclick = function(event){ setActiveTab(event.target.innerText.toLowerCase()) } - var idStrings = document.getElementsByClassName('myPub') var myHumanReadable = httpGet('/getHumanReadable/' + myPub) for (var i = 0; i < idStrings.length; i++){ diff --git a/onionr/static-data/www/shared/base64.min.js b/onionr/static-data/www/shared/base64.min.js new file mode 100644 index 00000000..7b118f6c --- /dev/null +++ b/onionr/static-data/www/shared/base64.min.js @@ -0,0 +1,10 @@ +/* + * base64.js + * + * Licensed under the BSD 3-Clause License. + * http://opensource.org/licenses/BSD-3-Clause + * + * References: + * http://en.wikipedia.org/wiki/Base64 + */ +(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory(global):typeof define==="function"&&define.amd?define(factory):factory(global)})(typeof self!=="undefined"?self:typeof window!=="undefined"?window:typeof global!=="undefined"?global:this,function(global){"use strict";global=global||{};var _Base64=global.Base64;var version="2.5.1";var buffer;if(typeof module!=="undefined"&&module.exports){try{buffer=eval("require('buffer').Buffer")}catch(err){buffer=undefined}}var b64chars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";var b64tab=function(bin){var t={};for(var i=0,l=bin.length;i>>6)+fromCharCode(128|cc&63):fromCharCode(224|cc>>>12&15)+fromCharCode(128|cc>>>6&63)+fromCharCode(128|cc&63)}else{var cc=65536+(c.charCodeAt(0)-55296)*1024+(c.charCodeAt(1)-56320);return fromCharCode(240|cc>>>18&7)+fromCharCode(128|cc>>>12&63)+fromCharCode(128|cc>>>6&63)+fromCharCode(128|cc&63)}};var re_utob=/[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;var utob=function(u){return u.replace(re_utob,cb_utob)};var cb_encode=function(ccc){var padlen=[0,2,1][ccc.length%3],ord=ccc.charCodeAt(0)<<16|(ccc.length>1?ccc.charCodeAt(1):0)<<8|(ccc.length>2?ccc.charCodeAt(2):0),chars=[b64chars.charAt(ord>>>18),b64chars.charAt(ord>>>12&63),padlen>=2?"=":b64chars.charAt(ord>>>6&63),padlen>=1?"=":b64chars.charAt(ord&63)];return chars.join("")};var btoa=global.btoa?function(b){return global.btoa(b)}:function(b){return b.replace(/[\s\S]{1,3}/g,cb_encode)};var _encode=buffer?buffer.from&&Uint8Array&&buffer.from!==Uint8Array.from?function(u){return(u.constructor===buffer.constructor?u:buffer.from(u)).toString("base64")}:function(u){return(u.constructor===buffer.constructor?u:new buffer(u)).toString("base64")}:function(u){return btoa(utob(u))};var encode=function(u,urisafe){return!urisafe?_encode(String(u)):_encode(String(u)).replace(/[+\/]/g,function(m0){return m0=="+"?"-":"_"}).replace(/=/g,"")};var encodeURI=function(u){return encode(u,true)};var re_btou=new RegExp(["[À-ß][€-¿]","[à-ï][€-¿]{2}","[ð-÷][€-¿]{3}"].join("|"),"g");var cb_btou=function(cccc){switch(cccc.length){case 4:var cp=(7&cccc.charCodeAt(0))<<18|(63&cccc.charCodeAt(1))<<12|(63&cccc.charCodeAt(2))<<6|63&cccc.charCodeAt(3),offset=cp-65536;return fromCharCode((offset>>>10)+55296)+fromCharCode((offset&1023)+56320);case 3:return fromCharCode((15&cccc.charCodeAt(0))<<12|(63&cccc.charCodeAt(1))<<6|63&cccc.charCodeAt(2));default:return fromCharCode((31&cccc.charCodeAt(0))<<6|63&cccc.charCodeAt(1))}};var btou=function(b){return b.replace(re_btou,cb_btou)};var cb_decode=function(cccc){var len=cccc.length,padlen=len%4,n=(len>0?b64tab[cccc.charAt(0)]<<18:0)|(len>1?b64tab[cccc.charAt(1)]<<12:0)|(len>2?b64tab[cccc.charAt(2)]<<6:0)|(len>3?b64tab[cccc.charAt(3)]:0),chars=[fromCharCode(n>>>16),fromCharCode(n>>>8&255),fromCharCode(n&255)];chars.length-=[0,0,2,1][padlen];return chars.join("")};var _atob=global.atob?function(a){return global.atob(a)}:function(a){return a.replace(/\S{1,4}/g,cb_decode)};var atob=function(a){return _atob(String(a).replace(/[^A-Za-z0-9\+\/]/g,""))};var _decode=buffer?buffer.from&&Uint8Array&&buffer.from!==Uint8Array.from?function(a){return(a.constructor===buffer.constructor?a:buffer.from(a,"base64")).toString()}:function(a){return(a.constructor===buffer.constructor?a:new buffer(a,"base64")).toString()}:function(a){return btou(_atob(a))};var decode=function(a){return _decode(String(a).replace(/[-_]/g,function(m0){return m0=="-"?"+":"/"}).replace(/[^A-Za-z0-9\+\/]/g,""))};var noConflict=function(){var Base64=global.Base64;global.Base64=_Base64;return Base64};global.Base64={VERSION:version,atob:atob,btoa:btoa,fromBase64:decode,toBase64:encode,utob:utob,encode:encode,encodeURI:encodeURI,btou:btou,decode:decode,noConflict:noConflict,__buffer__:buffer};if(typeof Object.defineProperty==="function"){var noEnum=function(v){return{value:v,enumerable:false,writable:true,configurable:true}};global.Base64.extendString=function(){Object.defineProperty(String.prototype,"fromBase64",noEnum(function(){return decode(this)}));Object.defineProperty(String.prototype,"toBase64",noEnum(function(urisafe){return encode(this,urisafe)}));Object.defineProperty(String.prototype,"toBase64URI",noEnum(function(){return encode(this,true)}))}}if(global["Meteor"]){Base64=global.Base64}if(typeof module!=="undefined"&&module.exports){module.exports.Base64=global.Base64}else if(typeof define==="function"&&define.amd){define([],function(){return global.Base64})}return{Base64:global.Base64}}); diff --git a/onionr/static-data/www/shared/main/style.css b/onionr/static-data/www/shared/main/style.css index d2ff44e3..0c220f40 100755 --- a/onionr/static-data/www/shared/main/style.css +++ b/onionr/static-data/www/shared/main/style.css @@ -137,4 +137,5 @@ body{ text-align:center; z-index: 1000; background-color: black; + color: white; } diff --git a/onionr/static-data/www/shared/misc.js b/onionr/static-data/www/shared/misc.js index c9b3790e..10c1e129 100755 --- a/onionr/static-data/www/shared/misc.js +++ b/onionr/static-data/www/shared/misc.js @@ -1,5 +1,6 @@ webpass = document.location.hash.replace('#', '') nowebpass = false + if (typeof webpass == "undefined"){ webpass = localStorage['webpass'] } @@ -12,6 +13,10 @@ if (typeof webpass == "undefined" || webpass == ""){ nowebpass = true } +function arrayContains(needle, arrhaystack) { + return (arrhaystack.indexOf(needle) > -1); +} + function httpGet(theUrl) { var xmlHttp = new XMLHttpRequest() xmlHttp.open( "GET", theUrl, false ) // false for synchronous request From b58f8e416a7c41bceb35b74ecf81f6652ada6a87 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 5 Feb 2019 00:29:06 -0600 Subject: [PATCH 59/94] more work on mail --- onionr/static-data/www/mail/index.html | 10 +++++++++- onionr/static-data/www/mail/mail.css | 12 ++++++++++++ onionr/static-data/www/mail/mail.js | 15 +++++++++++---- onionr/static-data/www/shared/main/style.css | 19 ++++++++++++++++--- 4 files changed, 48 insertions(+), 8 deletions(-) diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index dc7e6c65..01584f09 100755 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -24,7 +24,15 @@
Nothing here yet 😞
-
+
+ +
+
From: +
+
+
+
+
diff --git a/onionr/static-data/www/mail/mail.css b/onionr/static-data/www/mail/mail.css index fc39509b..c3da2296 100755 --- a/onionr/static-data/www/mail/mail.css +++ b/onionr/static-data/www/mail/mail.css @@ -40,4 +40,16 @@ input{ .activeTab{ color: black; background-color: gray; + } + + .overlayContent{ + background-color: lightgray; + border: 3px solid black; + border-radius: 3px; + opacity: 1.0; + color: black; + font-family: Verdana, Geneva, Tahoma, sans-serif; + min-height: 100%; + padding: 1em; + margin: 1em; } \ 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 5f26f694..d39c0380 100755 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -26,8 +26,9 @@ myPub = httpGet('/getActivePubkey') function openThread(bHash, sender, date){ var messageDisplay = document.getElementById('threadDisplay') - stuff = httpGet('/getblockbody/' + bHash) - messageDisplay.innerText = stuff + blockContent = httpGet('/getblockbody/' + bHash) + document.getElementById('fromUser').value = sender + messageDisplay.innerText = blockContent overlay('messageDisplay') } @@ -91,7 +92,6 @@ function loadInboxEntrys(bHash){ }.bind(bHash)) } - function getInbox(){ var showed = false var requested = '' @@ -144,4 +144,11 @@ for (var i = 0; i < idStrings.length; i++){ for (var i = 0; i < document.getElementsByClassName('refresh').length; i++){ document.getElementsByClassName('refresh')[i].style.float = 'right' -} \ No newline at end of file +} + +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' + } +} + diff --git a/onionr/static-data/www/shared/main/style.css b/onionr/static-data/www/shared/main/style.css index 0c220f40..c481b7a0 100755 --- a/onionr/static-data/www/shared/main/style.css +++ b/onionr/static-data/www/shared/main/style.css @@ -132,10 +132,23 @@ body{ left: 0px; top: 0px; width:100%; - opacity: 0.9; + opacity: 0.95; height:100%; - text-align:center; + text-align:left; z-index: 1000; - background-color: black; + 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 From bec8ecdc12bbb452f76f218dde7e3b77d5a49d94 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 5 Feb 2019 12:47:11 -0600 Subject: [PATCH 60/94] added unsigned mail messages --- onionr/onionrutils.py | 2 +- .../static-data/default-plugins/pms/main.py | 45 +++++++++++++------ onionr/static-data/www/mail/index.html | 2 +- 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index b34eb948..1929a398 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -221,7 +221,7 @@ class OnionrUtils: if pub == '': pub = self._core._crypto.pubKey pub = base64.b16encode(base64.b32decode(pub)).decode() - return '-'.join(pgpwords.wordify(pub)) + return ' '.join(pgpwords.wordify(pub)) def getBlockMetadataFromData(self, blockData): ''' diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py index 7c971df3..bc16fa49 100755 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -48,14 +48,14 @@ class MailStrings: self.mailInstance = mailInstance self.programTag = 'OnionrMail v%s' % (PLUGIN_VERSION) - choices = ['view inbox', 'view sentbox', 'send message', 'quit'] + choices = ['view inbox', 'view sentbox', 'send message', 'toggle pseudonymity', 'quit'] self.mainMenuChoices = choices - self.mainMenu = '''\n ------------------ -1. %s -2. %s -3. %s -4. %s''' % (choices[0], choices[1], choices[2], choices[3]) + self.mainMenu = '''----------------- + 1. %s + 2. %s + 3. %s + 4. %s + 5. %s''' % (choices[0], choices[1], choices[2], choices[3], choices[4]) class OnionrMail: def __init__(self, pluginapi): @@ -65,6 +65,7 @@ class OnionrMail: self.sentboxTools = sentboxdb.SentBox(self.myCore) self.sentboxList = [] self.sentMessages = {} + self.doSigs = True return def inbox(self): @@ -133,12 +134,14 @@ class OnionrMail: else: cancel = '' readBlock.verifySig() - - logger.info('Message recieved from %s' % (self.myCore._utils.bytesToStr(readBlock.signer,))) + 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) if not readBlock.validSig: - logger.warn('This message has an INVALID signature. ANYONE could have sent this message.') + logger.warn('This message has an INVALID/NO signature. ANYONE could have sent this message.') cancel = logger.readline('Press enter to continue to message, or -q to not open the message (recommended).') if cancel != '-q': try: @@ -147,6 +150,7 @@ class OnionrMail: logger.warn('Error presenting message. This is usually due to a malformed or blank message.') pass reply = logger.readline("Press enter to continue, or enter %s to reply" % ("-r",)) + print('') if reply == "-r": self.draftMessage(self.myCore._utils.bytesToStr(readBlock.signer,)) return @@ -241,14 +245,27 @@ class OnionrMail: if not cancelEnter: logger.info('Inserting encrypted message as Onionr block....') - blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=True, meta={'subject': subject}) + blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=self.doSigs, meta={'subject': subject}) self.sentboxTools.addToSent(blockID, recip, message) + + def toggleSigning(self): + self.doSigs = not self.doSigs + def menu(self): choice = '' while True: + sigMsg = 'Message Signing: %s' - logger.info(self.strings.programTag + '\n\nOur ID: ' + self.myCore._crypto.pubKey + self.strings.mainMenu.title()) # print out main menu - + logger.info(self.strings.programTag + '\n\nUser ID: ' + self.myCore._crypto.pubKey) + if self.doSigs: + sigMsg = sigMsg % ('enabled',) + else: + sigMsg = sigMsg % ('disabled (Your messages cannot be trusted)',) + if self.doSigs: + logger.info(sigMsg) + else: + logger.warn(sigMsg) + logger.info(self.strings.mainMenu.title()) # print out main menu try: choice = logger.readline('Enter 1-%s:\n' % (len(self.strings.mainMenuChoices))).lower().strip() except (KeyboardInterrupt, EOFError): @@ -261,6 +278,8 @@ class OnionrMail: elif choice in (self.strings.mainMenuChoices[2], '3'): self.draftMessage() elif choice in (self.strings.mainMenuChoices[3], '4'): + self.toggleSigning() + elif choice in (self.strings.mainMenuChoices[4], '5'): logger.info('Goodbye.') break elif choice == '': diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index 01584f09..dd48b4f3 100755 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -25,8 +25,8 @@
Nothing here yet 😞
-
+
From:
From 06048fe442dc68fc2acbf61d4343680efd8a854a Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 5 Feb 2019 17:20:36 -0600 Subject: [PATCH 61/94] added signature validity display to web ui mail --- onionr/api.py | 6 ++++++ onionr/static-data/www/mail/index.html | 4 +++- onionr/static-data/www/mail/mail.js | 13 +++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/onionr/api.py b/onionr/api.py index 69f775c7..54adbc47 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -498,6 +498,12 @@ class API: except AttributeError: pass 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 bl.bheader['meta'] = '' retData = {'meta': bl.bheader, 'metadata': bl.bmetadata} return json.dumps(retData) diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index dd48b4f3..18b2ddfc 100755 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -27,8 +27,10 @@
-
From: +
+ From:
+
diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index d39c0380..fa6a87f0 100755 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -63,10 +63,22 @@ function loadInboxEntrys(bHash){ var senderInput = document.createElement('input') var subjectLine = document.createElement('span') var dateStr = document.createElement('span') + var validSig = document.createElement('span') var humanDate = new Date(0) var metadata = resp['metadata'] humanDate.setUTCSeconds(resp['meta']['time']) senderInput.value = httpGet('/getHumanReadable/' + resp['meta']['signer']) + alert(resp['meta']['validSig']) + if (resp['meta']['validSig']){ + validSig.innerText = 'Signature Validity: Good' + } + else{ + validSig.innerText = 'Signature Validity: Bad' + validSig.style.color = 'red'; + } + if (senderInput.value == ''){ + senderInput.value = 'Anonymous' + } bHashDisplay.innerText = bHash.substring(0, 10) entry.setAttribute('hash', bHash); senderInput.readOnly = true @@ -81,6 +93,7 @@ function loadInboxEntrys(bHash){ threadPart.appendChild(entry) entry.appendChild(bHashDisplay) entry.appendChild(senderInput) + entry.appendChild(validSig) entry.appendChild(subjectLine) entry.appendChild(dateStr) entry.classList.add('threadEntry') From 0e3fb419127d3c05bffbd9f8d3a9f3103b9ddacf Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 6 Feb 2019 19:03:31 -0600 Subject: [PATCH 62/94] no longer use b64 transport (was a crutch/temp fix), mail work, dont name all files 'txt' --- onionr/api.py | 8 +++++--- onionr/communicator.py | 7 ++++--- onionr/onionr.py | 4 ++-- onionr/onionrblockapi.py | 3 ++- onionr/onionrutils.py | 3 ++- onionr/static-data/bootstrap-nodes.txt | 1 - onionr/static-data/www/mail/index.html | 2 +- onionr/static-data/www/mail/mail.css | 16 ++++++++++++++-- onionr/static-data/www/mail/mail.js | 19 +++++++++++++++---- 9 files changed, 45 insertions(+), 18 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 54adbc47..d9b9e920 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -138,11 +138,13 @@ class PublicAPI: if data not in self.hideBlocks: if data in clientAPI._core.getBlockList(): block = clientAPI.getBlockData(data, raw=True).encode() - resp = base64.b64encode(block).decode() + block = clientAPI._core._utils.strToBytes(block) + resp = block + #resp = base64.b64encode(block).decode() if len(resp) == 0: abort(404) resp = "" - return Response(resp) + return Response(resp, mimetype='application/octet-stream') @app.route('/www/') def wwwPublic(path): @@ -500,7 +502,7 @@ class API: else: validSig = False signer = self._core._utils.bytesToStr(bl.signer) - print(signer, bl.isSigned(), self._core._utils.validatePubKey(signer), bl.isSigner(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/communicator.py b/onionr/communicator.py index 1b8dc7fd..6104fd2c 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -222,7 +222,8 @@ class OnionrCommunicatorDaemon: self.blockQueue[i] = [peer] # add blocks to download queue else: if peer not in self.blockQueue[i]: - self.blockQueue[i].append(peer) + if len(self.blockQueue[i]) < 10: + self.blockQueue[i].append(peer) self.decrementThreadCount('lookupBlocks') return @@ -240,10 +241,10 @@ class OnionrCommunicatorDaemon: 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) + #logger.debug('Already downloading block %s...' % blockHash) continue if blockHash in self._core.getBlockList(): - logger.debug('Block %s is already saved.' % (blockHash,)) + #logger.debug('Block %s is already saved.' % (blockHash,)) try: del self.blockQueue[blockHash] except KeyError: diff --git a/onionr/onionr.py b/onionr/onionr.py index 89215dc5..06aaa694 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -40,7 +40,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.VoidNet.Tech' +ONIONR_TAGLINE = 'Anonymous P2P Platform - GPLv3 - https://Onionr.net' ONIONR_VERSION = '0.5.0' # for debugging and stuff ONIONR_VERSION_TUPLE = tuple(ONIONR_VERSION.split('.')) # (MAJOR, MINOR, VERSION) API_VERSION = '5' # increments of 1; only change when something fundemental about how the API works changes. This way other nodes know how to communicate without learning too much information about you. @@ -986,7 +986,7 @@ class Onionr: ''' self.addFile(singleBlock=True, blockType='html') - def addFile(self, singleBlock=False, blockType='txt'): + def addFile(self, singleBlock=False, blockType='bin'): ''' Adds a file to the onionr network ''' diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py index 09dd77ea..9f38b785 100755 --- a/onionr/onionrblockapi.py +++ b/onionr/onionrblockapi.py @@ -94,7 +94,8 @@ class Block: logger.error(str(e)) pass except nacl.exceptions.CryptoError: - logger.debug('Could not decrypt block. Either invalid key or corrupted data') + pass + #logger.debug('Could not decrypt block. Either invalid key or corrupted data') else: retData = True self.decrypted = True diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 1929a398..85cfc441 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -309,7 +309,8 @@ class OnionrUtils: else: self._core.updateBlockInfo(blockHash, 'expire', expireTime) else: - logger.debug('Not processing metadata on encrypted block we cannot decrypt.') + pass + #logger.debug('Not processing metadata on encrypted block we cannot decrypt.') def escapeAnsi(self, line): ''' diff --git a/onionr/static-data/bootstrap-nodes.txt b/onionr/static-data/bootstrap-nodes.txt index 6fb8319b..e69de29b 100755 --- a/onionr/static-data/bootstrap-nodes.txt +++ b/onionr/static-data/bootstrap-nodes.txt @@ -1 +0,0 @@ -i7dgbnouzyl7gv75b3eaqfz7x236abkn6nkjdpun273sydkbwcoidrid.onion \ 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 18b2ddfc..051e280b 100755 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -28,7 +28,7 @@
- From: + From: Signature:
diff --git a/onionr/static-data/www/mail/mail.css b/onionr/static-data/www/mail/mail.css index c3da2296..c7bd8ed8 100755 --- a/onionr/static-data/www/mail/mail.css +++ b/onionr/static-data/www/mail/mail.css @@ -2,7 +2,7 @@ padding-top: 1em; } .threads div span{ - padding-left: 0.1em; + padding-left: 0.2em; padding-right: 0.2em; } @@ -52,4 +52,16 @@ input{ min-height: 100%; padding: 1em; margin: 1em; - } \ No newline at end of file + } + +.danger{ + color: red; +} + +.warn{ + color: orange; +} + +.good{ + color: greenyellow; +} \ 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 fa6a87f0..71e592e9 100755 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -24,11 +24,23 @@ tabBtns = document.getElementById('tabBtns') threadContent = {} myPub = httpGet('/getActivePubkey') -function openThread(bHash, sender, date){ +function openThread(bHash, sender, date, sigBool){ var messageDisplay = document.getElementById('threadDisplay') - blockContent = httpGet('/getblockbody/' + bHash) + var blockContent = httpGet('/getblockbody/' + bHash) document.getElementById('fromUser').value = sender messageDisplay.innerText = blockContent + var sigEl = document.getElementById('sigValid') + var sigMsg = 'signature' + + if (sigBool){ + sigMsg = 'Good ' + sigMsg + sigEl.classList.remove('danger') + } + else{ + sigMsg = 'Bad/no ' + sigMsg + ' (message could be fake)' + sigEl.classList.add('danger') + } + sigEl.innerText = sigMsg overlay('messageDisplay') } @@ -68,7 +80,6 @@ function loadInboxEntrys(bHash){ var metadata = resp['metadata'] humanDate.setUTCSeconds(resp['meta']['time']) senderInput.value = httpGet('/getHumanReadable/' + resp['meta']['signer']) - alert(resp['meta']['validSig']) if (resp['meta']['validSig']){ validSig.innerText = 'Signature Validity: Good' } @@ -99,7 +110,7 @@ function loadInboxEntrys(bHash){ entry.classList.add('threadEntry') entry.onclick = function(){ - openThread(entry.getAttribute('hash'), senderInput.value, dateStr.innerText) + openThread(entry.getAttribute('hash'), senderInput.value, dateStr.innerText, resp['meta']['validSig']) } }.bind(bHash)) From b038d758b9cf455b7038925b65bc575fb79199f9 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 7 Feb 2019 12:12:04 -0600 Subject: [PATCH 63/94] proof of work adjustments, bugfixes, added connection check url --- onionr/api.py | 6 ++++-- onionr/communicator.py | 5 +---- onionr/onionr.py | 3 ++- onionr/onionrproofs.py | 6 +++--- onionr/static-data/connect-check.txt | 2 +- onionr/static-data/default_config.json | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index d9b9e920..e6754edd 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -294,8 +294,10 @@ class API: @app.after_request def afterReq(resp): - #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'" - 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'" + 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['X-API'] = onionr.API_VERSION diff --git a/onionr/communicator.py b/onionr/communicator.py index 6104fd2c..336258d4 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -269,10 +269,7 @@ class OnionrCommunicatorDaemon: content = content.encode() except AttributeError: pass - try: - content = base64.b64decode(content) # content is base64 encoded in transport - except binascii.Error: - pass + realHash = self._core._crypto.sha3Hash(content) try: realHash = realHash.decode() # bytes on some versions for some reason diff --git a/onionr/onionr.py b/onionr/onionr.py index 06aaa694..d767140d 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -1002,7 +1002,8 @@ class Onionr: try: with open(filename, 'rb') as singleFile: blockhash = self.onionrCore.insertBlock(base64.b64encode(singleFile.read()), header=blockType) - logger.info('File %s saved in block %s' % (filename, blockhash)) + if len(blockhash) > 0: + logger.info('File %s saved in block %s' % (filename, blockhash)) except: logger.error('Failed to save file in block.', timestamp = False) else: diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index f1645a49..2c479a8a 100755 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -41,9 +41,9 @@ def getDifficultyModifier(coreOrUtilsInst=None): if percentUse >= 0.50: retData += 1 elif percentUse >= 0.75: - retData += 2 + retData += 2 elif percentUse >= 0.95: - retData += 3 + retData += 3 return retData @@ -68,7 +68,7 @@ def getDifficultyForNewBlock(data, ourBlock=True): else: minDifficulty = config.get('general.minimum_block_pow') - retData = max(minDifficulty, math.floor(dataSize / 1000000)) + getDifficultyModifier() + retData = max(minDifficulty, math.floor(dataSize / 100000)) + getDifficultyModifier() return retData def getHashDifficulty(h): diff --git a/onionr/static-data/connect-check.txt b/onionr/static-data/connect-check.txt index 009a2a9a..7be85aa0 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,https://onionr.voidnet.tech/ +https://3g2upl4pq6kufc4m.onion/robots.txt,http://expyuzz4wqqyqhjn.onion/robots.txt,http://archivecaslytosk.onion/robots.txt diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index eeefe49a..ba5e5566 100755 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -2,8 +2,8 @@ "general" : { "dev_mode" : true, "display_header" : false, - "minimum_block_pow": 1, - "minimum_send_pow": 1, + "minimum_block_pow": 4, + "minimum_send_pow": 4, "socket_servers": false, "security_level": 0, "max_block_age": 2678400, From 64be7ebff38ba093567dd0785b8c28d7b14171ae Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 8 Feb 2019 00:19:05 -0600 Subject: [PATCH 64/94] fixed broken waitforshare, work on mail, work on new plugin api endpoint --- onionr/api.py | 14 +++++++++++++- onionr/communicator.py | 2 +- onionr/core.py | 2 +- onionr/static-data/www/mail/index.html | 2 +- onionr/static-data/www/mail/mail.css | 10 ++++++++++ 5 files changed, 26 insertions(+), 4 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index e6754edd..ac1a7eaa 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -410,7 +410,7 @@ class API: return Response(resp) @app.route('/waitforshare/', methods=['post']) - def waitforshare(): + def waitforshare(name): assert name.isalnum() if name in self.publicAPI.hideBlocks: self.publicAPI.hideBlocks.remove(name) @@ -454,6 +454,18 @@ class API: @app.route('/getHumanReadable/') def getHumanReadable(name): return Response(self._core._utils.getHumanReadableID(name)) + + @app.route('/apipoints/') + def pluginEndpoints(subpath=''): + # TODO have a variable for the plugin to set data to that we can use for the response + if len(subpath) > 1: + data = subpath.split('/') + if len(data) > 1: + plName = data[0] + events.event('pluginRequest', plName, subpath) + else: + abort(404) + return Response('Success') self.httpServer = WSGIServer((self.host, bindPort), app, log=None, handler_class=FDSafeHandler) self.httpServer.serve_forever() diff --git a/onionr/communicator.py b/onionr/communicator.py index 336258d4..52f41871 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -586,7 +586,7 @@ class OnionrCommunicatorDaemon: proxyType = 'i2p' logger.info("Uploading block to " + peer) if not self._core._utils.doPostRequest(url, data=data, proxyType=proxyType) == False: - self._core._utils.localCommand('waitforshare/' + bl) + self._core._utils.localCommand('waitforshare/' + bl, post=True) finishedUploads.append(bl) for x in finishedUploads: try: diff --git a/onionr/core.py b/onionr/core.py index 47f0e1ad..90e44f5e 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -832,7 +832,7 @@ 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) + self._utils.localCommand('/waitforshare/' + retData, post=True) self.addToBlockDB(retData, selfInsert=True, dataSaved=True) #self.setBlockType(retData, meta['type']) self._utils.processBlockMetadata(retData) diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index 051e280b..9830725b 100755 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -31,7 +31,7 @@ From: Signature:
-
+
diff --git a/onionr/static-data/www/mail/mail.css b/onionr/static-data/www/mail/mail.css index c7bd8ed8..ea1cf169 100755 --- a/onionr/static-data/www/mail/mail.css +++ b/onionr/static-data/www/mail/mail.css @@ -64,4 +64,14 @@ input{ .good{ color: greenyellow; +} + +.pre{ + padding-top: 1em; + word-wrap: break-word; + font-family: monospace; + white-space: pre; +} +.messageContent{ + font-size: 1.5em; } \ No newline at end of file From 9d5aec1b78c062a0122df3ff1e941f20c82e6dbc Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 8 Feb 2019 12:53:28 -0600 Subject: [PATCH 65/94] plugins can now respond to api --- onionr/api.py | 21 +++++++++++++--- onionr/onionrevents.py | 3 +-- .../static-data/default-plugins/pms/main.py | 25 ++++++++++++------- onionr/static-data/www/mail/mail.js | 15 +++++++++-- 4 files changed, 48 insertions(+), 16 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index ac1a7eaa..1aaed6b5 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -19,7 +19,7 @@ ''' from gevent.pywsgi import WSGIServer, WSGIHandler from gevent import Timeout -import flask, cgi +import flask, cgi, uuid from flask import request, Response, abort, send_from_directory import sys, random, threading, hmac, hashlib, base64, time, math, os, json, socket import core @@ -276,6 +276,7 @@ class API: logger.info('Running api on %s:%s' % (self.host, self.bindPort)) self.httpServer = '' + self.pluginResponses = {} self.queueResponse = {} onionrInst.setClientAPIInst(self) @@ -458,14 +459,28 @@ class API: @app.route('/apipoints/') def pluginEndpoints(subpath=''): # 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 = 5 + startTime = self._core._utils.getEpoch() if len(subpath) > 1: data = subpath.split('/') if len(data) > 1: plName = data[0] - events.event('pluginRequest', plName, subpath) + events.event('pluginRequest', {'name': plName, 'path': subpath, 'pluginResponse': pluginResponseCode}, 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('Success') + return Response(resp) self.httpServer = WSGIServer((self.host, bindPort), app, log=None, handler_class=FDSafeHandler) self.httpServer.serve_forever() diff --git a/onionr/onionrevents.py b/onionr/onionrevents.py index 26fdc093..0a2c48f1 100755 --- a/onionr/onionrevents.py +++ b/onionr/onionrevents.py @@ -61,10 +61,9 @@ def call(plugin, event_name, data = None, pluginapi = None): try: attribute = 'on_' + str(event_name).lower() - # TODO: Use multithreading perhaps? if hasattr(plugin, attribute): #logger.debug('Calling event ' + str(event_name)) - getattr(plugin, attribute)(pluginapi) + getattr(plugin, attribute)(pluginapi, data) return True except Exception as e: diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py index bc16fa49..099e442c 100755 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -22,7 +22,7 @@ import logger, config, threading, time, readline, datetime from onionrblockapi import Block import onionrexceptions, onionrusers -import locale, sys, os +import locale, sys, os, json locale.setlocale(locale.LC_ALL, '') @@ -161,7 +161,7 @@ class OnionrMail: ''' entering = True while entering: - self.getSentList() + self.get_sent_list() logger.info('Enter a block number or -q to return') try: choice = input('>') @@ -188,18 +188,19 @@ class OnionrMail: return - def getSentList(self): + def get_sent_list(self, display=True): count = 1 self.sentboxList = [] self.sentMessages = {} for i in self.sentboxTools.listSent(): self.sentboxList.append(i['hash']) self.sentMessages[i['hash']] = (i['message'], i['peer']) - - logger.info('%s. %s - %s - %s' % (count, i['hash'], i['peer'][:12], i['date'])) + if display: + logger.info('%s. %s - %s - %s' % (count, i['hash'], i['peer'][:12], i['date'])) count += 1 + return json.dumps(self.sentMessages) - def draftMessage(self, recip=''): + def draft_message(self, recip=''): message = '' newLine = '' subject = '' @@ -248,7 +249,7 @@ class OnionrMail: blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=self.doSigs, meta={'subject': subject}) self.sentboxTools.addToSent(blockID, recip, message) - def toggleSigning(self): + def toggle_signing(self): self.doSigs = not self.doSigs def menu(self): @@ -276,9 +277,9 @@ class OnionrMail: elif choice in (self.strings.mainMenuChoices[1], '2'): self.sentbox() elif choice in (self.strings.mainMenuChoices[2], '3'): - self.draftMessage() + self.draft_message() elif choice in (self.strings.mainMenuChoices[3], '4'): - self.toggleSigning() + self.toggle_signing() elif choice in (self.strings.mainMenuChoices[4], '5'): logger.info('Goodbye.') break @@ -288,6 +289,12 @@ class OnionrMail: logger.warn('Invalid choice.') return +def on_pluginrequest(api, data=None): + if data['name'] == 'mail': + path = data['path'] + if path.split('/')[1] == 'sentbox': + api.get_onionr().clientAPIInst.pluginResponses[data['pluginResponse']] = OnionrMail(api).get_sent_list(display=False) + return def on_init(api, data = None): ''' diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index 71e592e9..daf9e115 100755 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -18,6 +18,7 @@ */ pms = '' +sentbox = '' threadPart = document.getElementById('threads') threadPlaceholder = document.getElementById('threadPlaceholder') tabBtns = document.getElementById('tabBtns') @@ -48,10 +49,10 @@ function setActiveTab(tabName){ threadPart.innerHTML = "" switch(tabName){ case 'inbox': - getInbox(); + getInbox() break case 'sentbox': - console.log(tabName) + getSentbox() break case 'drafts': console.log(tabName) @@ -132,7 +133,17 @@ function getInbox(){ if (! showed){ threadPlaceholder.style.display = 'block' } +} +function getSentbox(){ + fetch('/apipoints/mail/sentbox', { + headers: { + "token": webpass + }}) + .then((resp) => resp.text()) // Transform the data into json + .then(function(data) { + sentbox = data + }) } fetch('/getblocksbytype/pm', { From 2dbe2e9be58b6a91a250966d10d357b4d4c57ece Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 9 Feb 2019 00:32:11 -0600 Subject: [PATCH 66/94] finished sentbox, started message compose --- onionr/static-data/www/mail/index.html | 20 +++++++- onionr/static-data/www/mail/mail.css | 22 ++++++++- onionr/static-data/www/mail/mail.js | 49 +++++++++++++++----- onionr/static-data/www/shared/main/style.css | 1 - 4 files changed, 77 insertions(+), 15 deletions(-) diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index 9830725b..c2a2e74f 100755 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -14,12 +14,12 @@
- Onionr Mail + Onionr Mail ✉️
Current Used Identity:


- +
Nothing here yet 😞
@@ -35,6 +35,22 @@
+
+
+ + To: +
+
+
+
+
+
+ + To: + + +
+
diff --git a/onionr/static-data/www/mail/mail.css b/onionr/static-data/www/mail/mail.css index ea1cf169..a8d27120 100755 --- a/onionr/static-data/www/mail/mail.css +++ b/onionr/static-data/www/mail/mail.css @@ -46,7 +46,6 @@ input{ background-color: lightgray; border: 3px solid black; border-radius: 3px; - opacity: 1.0; color: black; font-family: Verdana, Geneva, Tahoma, sans-serif; min-height: 100%; @@ -74,4 +73,25 @@ input{ } .messageContent{ font-size: 1.5em; +} + +#draftText{ + margin-top: 1em; + margin-bottom: 1em; + display: block; + width: 50%; + height: 75%; + min-width: 2%; + min-height: 5%; + background: white; + color: black; +} + +.successBtn{ + background-color: #28a745; + border-radius: 3px; + padding: 5px; + color: black; + font-size: 1.5em; + width: 10%; } \ 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 daf9e115..aba8f493 100755 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -54,11 +54,8 @@ function setActiveTab(tabName){ case 'sentbox': getSentbox() break - case 'drafts': - console.log(tabName) - break case 'send message': - console.log(tabName) + overlay('sendMessage') break } } @@ -70,7 +67,7 @@ function loadInboxEntrys(bHash){ }}) .then((resp) => resp.json()) // Transform the data into json .then(function(resp) { - console.log(resp) + //console.log(resp) var entry = document.createElement('div') var bHashDisplay = document.createElement('span') var senderInput = document.createElement('input') @@ -86,13 +83,13 @@ function loadInboxEntrys(bHash){ } else{ validSig.innerText = 'Signature Validity: Bad' - validSig.style.color = 'red'; + validSig.style.color = 'red' } if (senderInput.value == ''){ senderInput.value = 'Anonymous' } bHashDisplay.innerText = bHash.substring(0, 10) - entry.setAttribute('hash', bHash); + entry.setAttribute('hash', bHash) senderInput.readOnly = true dateStr.innerText = humanDate.toString() if (metadata['subject'] === undefined || metadata['subject'] === null) { @@ -140,10 +137,40 @@ function getSentbox(){ headers: { "token": webpass }}) - .then((resp) => resp.text()) // Transform the data into json - .then(function(data) { - sentbox = data - }) + .then((resp) => resp.json()) // Transform the data into json + .then(function(resp) { + var keys = []; + var entry = document.createElement('div') + var entryUsed; + for(var k in resp) keys.push(k); + for (var i = 0; i < keys.length; i++){ + var entry = document.createElement('div') + var obj = resp[i]; + var toLabel = document.createElement('span') + toLabel.innerText = 'To: ' + var toEl = document.createElement('input') + var preview = document.createElement('span') + toEl.readOnly = true + toEl.value = resp[keys[i]][1] + preview.innerText = resp[keys[i]][0].split('\n')[0]; + entry.appendChild(toLabel) + entry.appendChild(toEl) + entry.appendChild(preview) + entryUsed = resp[keys[i]] + entry.onclick = function(){ + console.log(resp) + showSentboxWindow(toEl.value, entryUsed[0]) + } + threadPart.appendChild(entry) + } + threadPart.appendChild(entry) + }.bind(threadPart)) +} + +function showSentboxWindow(to, content){ + document.getElementById('toID').value = to + document.getElementById('sentboxDisplayText').innerText = content + overlay('sentboxDisplay') } fetch('/getblocksbytype/pm', { diff --git a/onionr/static-data/www/shared/main/style.css b/onionr/static-data/www/shared/main/style.css index c481b7a0..96cb5e88 100755 --- a/onionr/static-data/www/shared/main/style.css +++ b/onionr/static-data/www/shared/main/style.css @@ -132,7 +132,6 @@ body{ left: 0px; top: 0px; width:100%; - opacity: 0.95; height:100%; text-align:left; z-index: 1000; From 898085887cbfd501c828d104c5ae3b27d5bea3ea Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 9 Feb 2019 20:21:36 -0600 Subject: [PATCH 67/94] convert human readable keys back to base32, work on mail sending from web ui --- onionr/api.py | 18 ++++++++++++--- onionr/etc/pgpwords.py | 18 ++++++++++++++- onionr/onionrutils.py | 4 ++++ .../static-data/default-plugins/pms/main.py | 23 ++++++++++++++++--- onionr/static-data/www/mail/index.html | 12 ++++++---- onionr/static-data/www/mail/mail.js | 4 +++- onionr/static-data/www/shared/base64.min.js | 10 -------- 7 files changed, 66 insertions(+), 23 deletions(-) delete mode 100644 onionr/static-data/www/shared/base64.min.js diff --git a/onionr/api.py b/onionr/api.py index 1aaed6b5..306d8194 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -455,19 +455,31 @@ class API: @app.route('/getHumanReadable/') def getHumanReadable(name): return Response(self._core._utils.getHumanReadableID(name)) + + @app.route('/insertblock', methods=['POST']) + def insertBlock(): + bData = request.get_json(force=True) + message = bData['message'] + to = bData['to'] + subject = 'temp' + return Response(self._core.insertBlock(message, header='pm', encryptType='asym', sign=True, asymPeer=to, meta={'subject': subject})) - @app.route('/apipoints/') + @app.route('/apipoints/', methods=['POST', 'GET']) def pluginEndpoints(subpath=''): # 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 = 5 + 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}, onionr=onionrInst) + + events.event('pluginRequest', {'name': plName, 'path': subpath, 'pluginResponse': pluginResponseCode, 'postData': postData}, onionr=onionrInst) while True: try: resp = self.pluginResponses[pluginResponseCode] diff --git a/onionr/etc/pgpwords.py b/onionr/etc/pgpwords.py index 6183eba9..16aeda1c 100755 --- a/onionr/etc/pgpwords.py +++ b/onionr/etc/pgpwords.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- (because 0xFF, even : "Yucatán") -import os, re, sys +import os, re, sys, binascii, base64 _words = [ ["aardvark", "adroitness"], @@ -278,6 +278,22 @@ def wordify(seq): ret.append(_words[int(seq[i:i+2], 16)][(i//2)%2]) return ret +def hexify(seq, delim=' '): + ret = b'' + sentence = seq + try: + sentence = seq.split(delim) + except AttributeError: + pass + count = 0 + for word in sentence: + count = 0 + for wordPair in _words: + if word in wordPair: + ret += bytes([(count)]) + count += 1 + return binascii.hexlify(ret) + def usage(): print("Usage:") print(" {0} [fingerprint...]".format(os.path.basename(sys.argv[0]))) diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 85cfc441..655605fd 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -222,6 +222,10 @@ class OnionrUtils: 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''' + return base64.b32encode(binascii.unhexlify(pgpwords.hexify(pub))) def getBlockMetadataFromData(self, blockData): ''' diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py index 099e442c..b393f6eb 100755 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -290,11 +290,28 @@ class OnionrMail: return def on_pluginrequest(api, data=None): + resp = '' + subject = '' + recip = '' + message = '' + postData = {} + blockID = '' + sentboxTools = sentboxdb.SentBox(api.get_core()) if data['name'] == 'mail': path = data['path'] - if path.split('/')[1] == 'sentbox': - api.get_onionr().clientAPIInst.pluginResponses[data['pluginResponse']] = OnionrMail(api).get_sent_list(display=False) - return + cmd = path.split('/')[1] + if cmd == 'sentbox': + resp = OnionrMail(api).get_sent_list(display=False) + elif cmd == 'send': + print(data['postData']) + postData = json.loads(data['postData']) + message = postData['message'] + recip = postData['to'] + subject = 'temp' + blockID = api.get_core().insertBlock(message, header='pm', encryptType='asym', sign=True, asymPeer=recip, meta={'subject': subject}) + sentboxTools.addToSent(blockID, recip, message) + if resp != '': + api.get_onionr().clientAPIInst.pluginResponses[data['pluginResponse']] = resp def on_init(api, data = None): ''' diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index c2a2e74f..99f0dba0 100755 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -45,15 +45,17 @@
- - To: - - +
+ + To: + + +
- + \ 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 aba8f493..76fc9024 100755 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -77,7 +77,9 @@ function loadInboxEntrys(bHash){ var humanDate = new Date(0) var metadata = resp['metadata'] humanDate.setUTCSeconds(resp['meta']['time']) - senderInput.value = httpGet('/getHumanReadable/' + resp['meta']['signer']) + if (resp['meta']['signer'] != ''){ + senderInput.value = httpGet('/getHumanReadable/' + resp['meta']['signer']) + } if (resp['meta']['validSig']){ validSig.innerText = 'Signature Validity: Good' } diff --git a/onionr/static-data/www/shared/base64.min.js b/onionr/static-data/www/shared/base64.min.js deleted file mode 100644 index 7b118f6c..00000000 --- a/onionr/static-data/www/shared/base64.min.js +++ /dev/null @@ -1,10 +0,0 @@ -/* - * base64.js - * - * Licensed under the BSD 3-Clause License. - * http://opensource.org/licenses/BSD-3-Clause - * - * References: - * http://en.wikipedia.org/wiki/Base64 - */ -(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory(global):typeof define==="function"&&define.amd?define(factory):factory(global)})(typeof self!=="undefined"?self:typeof window!=="undefined"?window:typeof global!=="undefined"?global:this,function(global){"use strict";global=global||{};var _Base64=global.Base64;var version="2.5.1";var buffer;if(typeof module!=="undefined"&&module.exports){try{buffer=eval("require('buffer').Buffer")}catch(err){buffer=undefined}}var b64chars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";var b64tab=function(bin){var t={};for(var i=0,l=bin.length;i>>6)+fromCharCode(128|cc&63):fromCharCode(224|cc>>>12&15)+fromCharCode(128|cc>>>6&63)+fromCharCode(128|cc&63)}else{var cc=65536+(c.charCodeAt(0)-55296)*1024+(c.charCodeAt(1)-56320);return fromCharCode(240|cc>>>18&7)+fromCharCode(128|cc>>>12&63)+fromCharCode(128|cc>>>6&63)+fromCharCode(128|cc&63)}};var re_utob=/[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;var utob=function(u){return u.replace(re_utob,cb_utob)};var cb_encode=function(ccc){var padlen=[0,2,1][ccc.length%3],ord=ccc.charCodeAt(0)<<16|(ccc.length>1?ccc.charCodeAt(1):0)<<8|(ccc.length>2?ccc.charCodeAt(2):0),chars=[b64chars.charAt(ord>>>18),b64chars.charAt(ord>>>12&63),padlen>=2?"=":b64chars.charAt(ord>>>6&63),padlen>=1?"=":b64chars.charAt(ord&63)];return chars.join("")};var btoa=global.btoa?function(b){return global.btoa(b)}:function(b){return b.replace(/[\s\S]{1,3}/g,cb_encode)};var _encode=buffer?buffer.from&&Uint8Array&&buffer.from!==Uint8Array.from?function(u){return(u.constructor===buffer.constructor?u:buffer.from(u)).toString("base64")}:function(u){return(u.constructor===buffer.constructor?u:new buffer(u)).toString("base64")}:function(u){return btoa(utob(u))};var encode=function(u,urisafe){return!urisafe?_encode(String(u)):_encode(String(u)).replace(/[+\/]/g,function(m0){return m0=="+"?"-":"_"}).replace(/=/g,"")};var encodeURI=function(u){return encode(u,true)};var re_btou=new RegExp(["[À-ß][€-¿]","[à-ï][€-¿]{2}","[ð-÷][€-¿]{3}"].join("|"),"g");var cb_btou=function(cccc){switch(cccc.length){case 4:var cp=(7&cccc.charCodeAt(0))<<18|(63&cccc.charCodeAt(1))<<12|(63&cccc.charCodeAt(2))<<6|63&cccc.charCodeAt(3),offset=cp-65536;return fromCharCode((offset>>>10)+55296)+fromCharCode((offset&1023)+56320);case 3:return fromCharCode((15&cccc.charCodeAt(0))<<12|(63&cccc.charCodeAt(1))<<6|63&cccc.charCodeAt(2));default:return fromCharCode((31&cccc.charCodeAt(0))<<6|63&cccc.charCodeAt(1))}};var btou=function(b){return b.replace(re_btou,cb_btou)};var cb_decode=function(cccc){var len=cccc.length,padlen=len%4,n=(len>0?b64tab[cccc.charAt(0)]<<18:0)|(len>1?b64tab[cccc.charAt(1)]<<12:0)|(len>2?b64tab[cccc.charAt(2)]<<6:0)|(len>3?b64tab[cccc.charAt(3)]:0),chars=[fromCharCode(n>>>16),fromCharCode(n>>>8&255),fromCharCode(n&255)];chars.length-=[0,0,2,1][padlen];return chars.join("")};var _atob=global.atob?function(a){return global.atob(a)}:function(a){return a.replace(/\S{1,4}/g,cb_decode)};var atob=function(a){return _atob(String(a).replace(/[^A-Za-z0-9\+\/]/g,""))};var _decode=buffer?buffer.from&&Uint8Array&&buffer.from!==Uint8Array.from?function(a){return(a.constructor===buffer.constructor?a:buffer.from(a,"base64")).toString()}:function(a){return(a.constructor===buffer.constructor?a:new buffer(a,"base64")).toString()}:function(a){return btou(_atob(a))};var decode=function(a){return _decode(String(a).replace(/[-_]/g,function(m0){return m0=="-"?"+":"/"}).replace(/[^A-Za-z0-9\+\/]/g,""))};var noConflict=function(){var Base64=global.Base64;global.Base64=_Base64;return Base64};global.Base64={VERSION:version,atob:atob,btoa:btoa,fromBase64:decode,toBase64:encode,utob:utob,encode:encode,encodeURI:encodeURI,btou:btou,decode:decode,noConflict:noConflict,__buffer__:buffer};if(typeof Object.defineProperty==="function"){var noEnum=function(v){return{value:v,enumerable:false,writable:true,configurable:true}};global.Base64.extendString=function(){Object.defineProperty(String.prototype,"fromBase64",noEnum(function(){return decode(this)}));Object.defineProperty(String.prototype,"toBase64",noEnum(function(urisafe){return encode(this,urisafe)}));Object.defineProperty(String.prototype,"toBase64URI",noEnum(function(){return encode(this,true)}))}}if(global["Meteor"]){Base64=global.Base64}if(typeof module!=="undefined"&&module.exports){module.exports.Base64=global.Base64}else if(typeof define==="function"&&define.amd){define([],function(){return global.Base64})}return{Base64:global.Base64}}); From 30604fa23c25fdf8494d2095bbb6b648af84e322 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 10 Feb 2019 12:43:45 -0600 Subject: [PATCH 68/94] more mail ui work --- onionr/api.py | 28 +++++++++++++++++-- onionr/communicator.py | 2 +- onionr/core.py | 8 +++++- onionr/etc/pgpwords.py | 2 +- onionr/onionrproofs.py | 4 +-- onionr/onionrutils.py | 8 +++--- .../static-data/default-plugins/pms/main.py | 17 ++++++----- 7 files changed, 49 insertions(+), 20 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 306d8194..3b3cc7e3 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -458,11 +458,35 @@ class API: @app.route('/insertblock', methods=['POST']) def insertBlock(): + encrypt = False bData = request.get_json(force=True) message = bData['message'] - to = bData['to'] subject = 'temp' - return Response(self._core.insertBlock(message, header='pm', encryptType='asym', sign=True, asymPeer=to, meta={'subject': subject})) + 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 + return Response(self._core.insertBlock(message, header=bType, encryptType=encryptType, sign=sign, asymPeer=to, meta=meta)) @app.route('/apipoints/', methods=['POST', 'GET']) def pluginEndpoints(subpath=''): diff --git a/onionr/communicator.py b/onionr/communicator.py index 52f41871..3cb455dd 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -603,7 +603,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(5): + for i in range(8): if self._core._utils.localCommand('ping') in ('pong', 'pong!'): break # break for loop time.sleep(1) diff --git a/onionr/core.py b/onionr/core.py index 90e44f5e..a4852dba 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -731,6 +731,7 @@ class Core: logger.error(allocationReachedMessage) return False retData = False + # check nonce dataNonce = self._utils.bytesToStr(self._crypto.sha3Hash(data)) try: @@ -746,6 +747,11 @@ class Core: if type(data) is bytes: data = data.decode() data = str(data) + plaintext = data + + # Convert asym peer human readable key to base32 if set + if ' ' in asymPeer.strip(): + asymPeer = self._utils.convertHumanReadableID(asymPeer) retData = '' signature = '' @@ -839,7 +845,7 @@ class Core: self.daemonQueueAdd('uploadBlock', retData) if retData != False: - events.event('insertBlock', onionr = None, threaded = False) + events.event('insertblock', {'content': plaintext, 'meta': jsonMeta, 'hash': retData, 'peer': self._utils.bytesToStr(asymPeer)}, onionr = self.onionrInst, threaded = False) return retData def introduceNode(self): diff --git a/onionr/etc/pgpwords.py b/onionr/etc/pgpwords.py index 16aeda1c..a1fc7c6b 100755 --- a/onionr/etc/pgpwords.py +++ b/onionr/etc/pgpwords.py @@ -259,7 +259,7 @@ _words = [ ["wayside", "Wilmington"], ["willow", "Wyoming"], ["woodlark", "yesteryear"], - ["Zulu", "Yucatán"]] + ["Zulu", "Yucatan"]] hexre = re.compile("[a-fA-F0-9]+") diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index 2c479a8a..202500f1 100755 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -192,7 +192,7 @@ class DataPOW: if not self.hashing: break else: - time.sleep(2) + time.sleep(1) except KeyboardInterrupt: self.shutdown() logger.warn('Got keyboard interrupt while waiting for POW result, stopping') @@ -299,7 +299,7 @@ class POW: if not self.hashing: break else: - time.sleep(2) + time.sleep(1) except KeyboardInterrupt: self.shutdown() logger.warn('Got keyboard interrupt while waiting for POW result, stopping') diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 655605fd..4181ba4b 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -163,7 +163,7 @@ class OnionrUtils: retData += '%s:%s' % (hostname, config.get('client.client.port')) return retData - def localCommand(self, command, data='', silent = True, post=False, postData = {}, maxWait=10): + 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. ''' @@ -185,9 +185,9 @@ class OnionrUtils: 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, 30)).text + 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, 30)).text + 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)) @@ -225,7 +225,7 @@ class OnionrUtils: def convertHumanReadableID(self, pub): '''Convert a human readable pubkey id to base32''' - return base64.b32encode(binascii.unhexlify(pgpwords.hexify(pub))) + return self.bytesToStr(base64.b32encode(binascii.unhexlify(pgpwords.hexify(pub.strip())))) def getBlockMetadataFromData(self, blockData): ''' diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py index b393f6eb..f2421ca5 100755 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -194,7 +194,7 @@ class OnionrMail: self.sentMessages = {} for i in self.sentboxTools.listSent(): self.sentboxList.append(i['hash']) - self.sentMessages[i['hash']] = (i['message'], i['peer']) + self.sentMessages[i['hash']] = (self.myCore._utils.bytesToStr(i['message']), i['peer']) if display: logger.info('%s. %s - %s - %s' % (count, i['hash'], i['peer'][:12], i['date'])) count += 1 @@ -289,6 +289,13 @@ class OnionrMail: logger.warn('Invalid choice.') return +def on_insertblock(api, data={}): + print(data) + sentboxTools = sentboxdb.SentBox(api.get_core()) + meta = json.dumps(data['meta']) + print('on_insertblock', data) + sentboxTools.addToSent(data['hash'], data['peer'], data['content']) + def on_pluginrequest(api, data=None): resp = '' subject = '' @@ -302,14 +309,6 @@ def on_pluginrequest(api, data=None): cmd = path.split('/')[1] if cmd == 'sentbox': resp = OnionrMail(api).get_sent_list(display=False) - elif cmd == 'send': - print(data['postData']) - postData = json.loads(data['postData']) - message = postData['message'] - recip = postData['to'] - subject = 'temp' - blockID = api.get_core().insertBlock(message, header='pm', encryptType='asym', sign=True, asymPeer=recip, meta={'subject': subject}) - sentboxTools.addToSent(blockID, recip, message) if resp != '': api.get_onionr().clientAPIInst.pluginResponses[data['pluginResponse']] = resp From 3cf5f4c04df2e87143fe3b398f628a7e856691f9 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 10 Feb 2019 16:26:47 -0600 Subject: [PATCH 69/94] better support human public keys, do not use forward secrecy when not signing --- onionr/core.py | 6 ++++-- onionr/onionrusers.py | 3 +++ onionr/static-data/default-plugins/pms/main.py | 2 -- onionr/static-data/www/mail/index.html | 1 + onionr/static-data/www/shared/misc.js | 1 + 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/onionr/core.py b/onionr/core.py index a4852dba..6102d892 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -748,6 +748,7 @@ class Core: data = data.decode() data = str(data) plaintext = data + plaintextMeta = {} # Convert asym peer human readable key to base32 if set if ' ' in asymPeer.strip(): @@ -774,7 +775,7 @@ class Core: pass if encryptType == 'asym': - if not disableForward and asymPeer != self._crypto.pubKey: + if not disableForward and sign and asymPeer != self._crypto.pubKey: try: forwardEncrypted = onionrusers.OnionrUser(self, asymPeer).forwardEncrypt(data) data = forwardEncrypted[0] @@ -786,6 +787,7 @@ class Core: #fsKey = onionrusers.OnionrUser(self, asymPeer).getGeneratedForwardKeys().reverse() meta['newFSKey'] = fsKey jsonMeta = json.dumps(meta) + plaintextMeta = jsonMeta if sign: signature = self._crypto.edSign(jsonMeta.encode() + data, key=self._crypto.privKey, encodeResult=True) signer = self._crypto.pubKey @@ -845,7 +847,7 @@ class Core: self.daemonQueueAdd('uploadBlock', retData) if retData != False: - events.event('insertblock', {'content': plaintext, 'meta': jsonMeta, 'hash': retData, 'peer': self._utils.bytesToStr(asymPeer)}, onionr = self.onionrInst, threaded = False) + events.event('insertblock', {'content': plaintext, 'meta': plaintextMeta, 'hash': retData, 'peer': self._utils.bytesToStr(asymPeer)}, onionr = self.onionrInst, threaded = True) return retData def introduceNode(self): diff --git a/onionr/onionrusers.py b/onionr/onionrusers.py index c5bf41d1..41bc7b6e 100755 --- a/onionr/onionrusers.py +++ b/onionr/onionrusers.py @@ -40,6 +40,9 @@ 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) + self.trust = 0 self._core = coreInst self.publicKey = publicKey diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py index f2421ca5..75480ce2 100755 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -290,10 +290,8 @@ class OnionrMail: return def on_insertblock(api, data={}): - print(data) sentboxTools = sentboxdb.SentBox(api.get_core()) meta = json.dumps(data['meta']) - print('on_insertblock', data) sentboxTools.addToSent(data['hash'], data['peer'], data['content']) def on_pluginrequest(api, data=None): diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index 99f0dba0..3ea92f1a 100755 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -48,6 +48,7 @@
To: + Subject:
diff --git a/onionr/static-data/www/shared/misc.js b/onionr/static-data/www/shared/misc.js index 10c1e129..03d2f3da 100755 --- a/onionr/static-data/www/shared/misc.js +++ b/onionr/static-data/www/shared/misc.js @@ -32,6 +32,7 @@ function httpGet(theUrl) { function overlay(overlayID) { el = document.getElementById(overlayID) el.style.visibility = (el.style.visibility == "visible") ? "hidden" : "visible" + scroll(0,0) } var passLinks = document.getElementsByClassName("idLink") From 1d32b3daa17efa9f2a93be2792884fa64bd51b88 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 10 Feb 2019 16:43:42 -0600 Subject: [PATCH 70/94] added forgotten file for mail --- onionr/static-data/www/mail/sendmail.js | 45 +++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 onionr/static-data/www/mail/sendmail.js diff --git a/onionr/static-data/www/mail/sendmail.js b/onionr/static-data/www/mail/sendmail.js new file mode 100644 index 00000000..945c7792 --- /dev/null +++ b/onionr/static-data/www/mail/sendmail.js @@ -0,0 +1,45 @@ +/* + Onionr - P2P Anonymous Storage Network + + This file handles the mail 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 sendbutton = document.getElementById('sendMail') + +function sendMail(to, message, subject){ + //postData = {"postData": '{"to": "' + to + '", "message": "' + message + '"}'} // galaxy brain + postData = {'message': message, 'to': to, 'type': 'pm', 'encrypt': true, 'meta': JSON.stringify({'subject': subject})} + postData = JSON.stringify(postData) + 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) { + }) +} + +sendForm.onsubmit = function(){ + var messageContent = document.getElementById('draftText') + var to = document.getElementById('draftID') + var subject = document.getElementById('draftSubject') + + sendMail(to.value, messageContent.value, subject.value) + return false; +} From b09dae276c736fb9b75f07beaa0a03dd9c87ca6a Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 11 Feb 2019 16:36:43 -0600 Subject: [PATCH 71/94] mail fixes --- onionr/api.py | 23 ++++--------------- .../static-data/default-plugins/pms/main.py | 9 ++++---- .../default-plugins/pms/sentboxdb.py | 9 ++++---- onionr/static-data/www/mail/index.html | 4 ++-- onionr/static-data/www/mail/mail.js | 2 +- onionr/static-data/www/shared/misc.js | 19 +++++++++++++++ 6 files changed, 35 insertions(+), 31 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 3b3cc7e3..6a53ef19 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, onionr class FDSafeHandler(WSGIHandler): + '''Our WSGI handler. Doesn't do much non-default except timeouts''' def handle(self): timeout = Timeout(60, exception=Exception) timeout.start() @@ -37,24 +38,6 @@ class FDSafeHandler(WSGIHandler): except Timeout as ex: raise -def guessMime(path): - ''' - Guesses the mime type of a file from the input filename - ''' - mimetypes = { - 'html' : 'text/html', - 'js' : 'application/javascript', - 'css' : 'text/css', - 'png' : 'image/png', - 'jpg' : 'image/jpeg' - } - - for mimetype in mimetypes: - if path.endswith('.%s' % mimetype): - return mimetypes[mimetype] - - return 'text/plain' - def setBindIP(filePath): '''Set a random localhost IP to a specified file (intended for private or public API localhost IPs)''' hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))] @@ -65,6 +48,7 @@ def setBindIP(filePath): try: s.bind((data, 0)) except OSError: + # if mac/non-bindable, show warning and default to 127.0.0.1 logger.warn('Your platform appears to not support random local host addresses 127.x.x.x. Falling back to 127.0.0.1.') data = '127.0.0.1' s.close() @@ -486,7 +470,8 @@ class API: meta = json.loads(bData['meta']) except KeyError: pass - return Response(self._core.insertBlock(message, header=bType, encryptType=encryptType, sign=sign, asymPeer=to, meta=meta)) + 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=''): diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py index 75480ce2..55cd9a7c 100755 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -194,9 +194,9 @@ 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']) + self.sentMessages[i['hash']] = (self.myCore._utils.bytesToStr(i['message']), i['peer'], i['subject']) if display: - logger.info('%s. %s - %s - %s' % (count, i['hash'], i['peer'][:12], i['date'])) + logger.info('%s. %s - %s - (%s) - %s' % (count, i['hash'], i['peer'][:12], i['subject'], i['date'])) count += 1 return json.dumps(self.sentMessages) @@ -247,7 +247,6 @@ class OnionrMail: logger.info('Inserting encrypted message as Onionr block....') blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=self.doSigs, meta={'subject': subject}) - self.sentboxTools.addToSent(blockID, recip, message) def toggle_signing(self): self.doSigs = not self.doSigs @@ -291,8 +290,8 @@ class OnionrMail: def on_insertblock(api, data={}): sentboxTools = sentboxdb.SentBox(api.get_core()) - meta = json.dumps(data['meta']) - sentboxTools.addToSent(data['hash'], data['peer'], data['content']) + meta = json.loads(data['meta']) + sentboxTools.addToSent(data['hash'], data['peer'], data['content'], meta['subject']) def on_pluginrequest(api, data=None): resp = '' diff --git a/onionr/static-data/default-plugins/pms/sentboxdb.py b/onionr/static-data/default-plugins/pms/sentboxdb.py index 2d6207e6..f00accb3 100755 --- a/onionr/static-data/default-plugins/pms/sentboxdb.py +++ b/onionr/static-data/default-plugins/pms/sentboxdb.py @@ -37,6 +37,7 @@ class SentBox: hash id not null, peer text not null, message text not null, + subject text not null, date int not null ); ''') @@ -46,12 +47,12 @@ class SentBox: def listSent(self): retData = [] for entry in self.cursor.execute('SELECT * FROM sent;'): - retData.append({'hash': entry[0], 'peer': entry[1], 'message': entry[2], 'date': entry[3]}) + retData.append({'hash': entry[0], 'peer': entry[1], 'message': entry[2], 'subject': entry[3], 'date': entry[4]}) return retData - def addToSent(self, blockID, peer, message): - args = (blockID, peer, message, self.core._utils.getEpoch()) - self.cursor.execute('INSERT INTO sent VALUES(?, ?, ?, ?)', args) + def addToSent(self, blockID, peer, message, subject=''): + args = (blockID, peer, message, subject, self.core._utils.getEpoch()) + self.cursor.execute('INSERT INTO sent VALUES(?, ?, ?, ?, ?)', args) self.conn.commit() return diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index 3ea92f1a..03b0b8ec 100755 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -13,9 +13,9 @@
- - Onionr Mail ✉️
+ + Onionr Mail ✉️
Current Used Identity:


diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index 76fc9024..4513ce9b 100755 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -154,7 +154,7 @@ function getSentbox(){ var preview = document.createElement('span') toEl.readOnly = true toEl.value = resp[keys[i]][1] - preview.innerText = resp[keys[i]][0].split('\n')[0]; + preview.innerText = '(' + resp[keys[i]][2] + ')' entry.appendChild(toLabel) entry.appendChild(toEl) entry.appendChild(preview) diff --git a/onionr/static-data/www/shared/misc.js b/onionr/static-data/www/shared/misc.js index 03d2f3da..d4322887 100755 --- a/onionr/static-data/www/shared/misc.js +++ b/onionr/static-data/www/shared/misc.js @@ -1,3 +1,22 @@ +/* + Onionr - P2P Anonymous Storage Network + + This file handles the mail 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 . +*/ + webpass = document.location.hash.replace('#', '') nowebpass = false From 944c76d2e95f53bdf3d8167131c47b3d3fd7b887 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 11 Feb 2019 17:44:39 -0600 Subject: [PATCH 72/94] code cleanup, defunct code removal, and some module splitting --- onionr/communicator.py | 5 +- onionr/core.py | 1 + onionr/etc/pgpwords.py | 44 +---- onionr/logger.py | 15 +- onionr/onionr.py | 20 +- onionr/onionrdaemontools.py | 8 +- onionr/onionrutils.py | 182 +----------------- .../default-plugins/metadataprocessor/main.py | 2 +- onionr/utils/netutils.py | 34 ++++ onionr/utils/networkmerger.py | 46 +++++ 10 files changed, 114 insertions(+), 243 deletions(-) create mode 100644 onionr/utils/netutils.py create mode 100644 onionr/utils/networkmerger.py diff --git a/onionr/communicator.py b/onionr/communicator.py index 3cb455dd..9723ed6f 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -25,6 +25,8 @@ import onionrdaemontools, onionrsockets, onionr, onionrproofs, proofofmemory import binascii from dependencies import secrets from defusedxml import minidom +from utils import networkmerger + config.reload() class OnionrCommunicatorDaemon: def __init__(self, onionrInst, proxyPort, developmentMode=config.get('general.dev_mode', False)): @@ -130,7 +132,6 @@ class OnionrCommunicatorDaemon: self.socketServer.start() self.socketClient = onionrsockets.OnionrSocketClient(self._core) - # Main daemon loop, mainly for calling timers, don't do any complex operations here to avoid locking try: while not self.shutdown: @@ -159,7 +160,7 @@ class OnionrCommunicatorDaemon: # Download new peer address list from random online peers peer = self.pickOnlinePeer() newAdders = self.peerAction(peer, action='pex') - self._core._utils.mergeAdders(newAdders) + networkmerger.mergeAdders(newAdders, self._core) self.decrementThreadCount('lookupAdders') def lookupBlocks(self): diff --git a/onionr/core.py b/onionr/core.py index 6102d892..c79ae86f 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -125,6 +125,7 @@ class Core: ''' Adds a public key to the key database (misleading function name) ''' + assert peerID not in self.listPeers() # This function simply adds a peer to the DB if not self._utils.validatePubKey(peerID): diff --git a/onionr/etc/pgpwords.py b/onionr/etc/pgpwords.py index a1fc7c6b..d9738f3f 100755 --- a/onionr/etc/pgpwords.py +++ b/onionr/etc/pgpwords.py @@ -1,7 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- (because 0xFF, even : "Yucatán") -import os, re, sys, binascii, base64 +'''This file is adapted from https://github.com/thblt/pgp-words by github user 'thblt' ('Thibault Polge), GPL v3 license''' + +import os, re, sys, binascii _words = [ ["aardvark", "adroitness"], @@ -275,7 +277,7 @@ def wordify(seq): ret = [] for i in range(0, len(seq), 2): - ret.append(_words[int(seq[i:i+2], 16)][(i//2)%2]) + ret.append(_words[int(seq[i:i+2], 16)][(i//2)%2].lower()) return ret def hexify(seq, delim=' '): @@ -292,40 +294,4 @@ def hexify(seq, delim=' '): if word in wordPair: ret += bytes([(count)]) count += 1 - return binascii.hexlify(ret) - -def usage(): - print("Usage:") - print(" {0} [fingerprint...]".format(os.path.basename(sys.argv[0]))) - print("") - print("If called with multiple arguments, they will be concatenated") - print("and treated as a single fingerprint.") - print("") - print("If called with no arguments, input is read from stdin,") - print("and each line is treated as a single fingerprint. In this") - print("mode, invalid values are silently ignored.") - exit(1) - -if __name__ == '__main__': - if 1 == len(sys.argv): - fps = sys.stdin.readlines() - else: - fps = [" ".join(sys.argv[1:])] - for fp in fps: - try: - words = wordify(fp) - print("\n{0}: ".format(fp.strip())) - sys.stdout.write("\t") - for i in range(0, len(words)): - sys.stdout.write(words[i] + " ") - if (not (i+1) % 4) and not i == len(words)-1: - sys.stdout.write("\n\t") - print("") - - except Exception as e: - if len(fps) == 1: - print (e) - usage() - - print("") - + return binascii.hexlify(ret) \ No newline at end of file diff --git a/onionr/logger.py b/onionr/logger.py index 7cb409a1..c9639b0d 100755 --- a/onionr/logger.py +++ b/onionr/logger.py @@ -18,7 +18,9 @@ along with this program. If not, see . ''' -import re, sys, time, traceback +import re, sys, time, traceback, os + +MAX_LOG_SIZE = 100000000 class colors: ''' @@ -132,11 +134,12 @@ def raw(data, fd = sys.stdout, sensitive = False): if get_settings() & OUTPUT_TO_CONSOLE: ts = fd.write('%s\n' % data) if get_settings() & OUTPUT_TO_FILE and not sensitive: - try: - with open(_outputfile, "a+") as f: - f.write(colors.filter(data) + '\n') - except OSError: - pass + if os.path.getsize(_outputfile) < MAX_LOG_SIZE: + 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): ''' diff --git a/onionr/onionr.py b/onionr/onionr.py index d767140d..aca17a1a 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -563,24 +563,12 @@ class Onionr: if self.onionrUtils.hasKey(newPeer): logger.info('We already have that key') return - if not '-' in newPeer: - logger.info('Since no POW token was supplied for that key, one is being generated') - proof = onionrproofs.DataPOW(newPeer) - while True: - result = proof.getResult() - if result == False: - time.sleep(0.5) - else: - break - newPeer += '-' + base64.b64encode(result[1]).decode() - logger.info(newPeer) - logger.info("Adding peer: " + logger.colors.underline + newPeer) - if self.onionrUtils.mergeKeys(newPeer): - logger.info('Successfully added key') - else: + try: + if self.onionrCore.addPeer(newPeer): + logger.info('Successfully added key') + except AssertionError: logger.error('Failed to add key') - return def addAddress(self): diff --git a/onionr/onionrdaemontools.py b/onionr/onionrdaemontools.py index d64a69ff..33e84b34 100755 --- a/onionr/onionrdaemontools.py +++ b/onionr/onionrdaemontools.py @@ -21,6 +21,7 @@ import onionrexceptions, onionrpeers, onionrproofs, logger, onionrusers import base64, sqlite3, os from dependencies import secrets +from utils import netutils class DaemonTools: ''' @@ -86,7 +87,7 @@ class DaemonTools: 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 self.daemon._core._utils.checkNetwork(torPort=self.daemon.proxyPort): + if not netutils.checkNetwork(self.daemon._core._utils, torPort=self.daemon.proxyPort): logger.warn('Network check failed, are you connected to the internet?') self.daemon.isOnline = False else: @@ -192,10 +193,11 @@ class DaemonTools: def insertDeniableBlock(self): '''Insert a fake block in order to make it more difficult to track real blocks''' - fakePeer = self.daemon._core._crypto.generatePubKey()[0] + fakePeer = '' chance = 10 if secrets.randbelow(chance) == (chance - 1): + fakePeer = self.daemon._core._crypto.generatePubKey()[0] data = secrets.token_hex(secrets.randbelow(500) + 1) - self.daemon._core.insertBlock(data, header='pm', encryptType='asym', asymPeer=fakePeer) + 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/onionrutils.py b/onionr/onionrutils.py index 4181ba4b..65a680b0 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -67,90 +67,6 @@ class OnionrUtils: ''' epoch = self.getEpoch() return epoch - (epoch % roundS) - - def mergeKeys(self, newKeyList): - ''' - Merge ed25519 key list to our database, comma seperated string - ''' - try: - retVal = False - if newKeyList != False: - for key in newKeyList.split(','): - key = key.split('-') - # Test if key is valid - try: - if len(key[0]) > 60 or len(key[1]) > 1000: - logger.warn('%s or its pow value is too large.' % key[0]) - continue - except IndexError: - logger.warn('No pow token') - continue - try: - value = base64.b64decode(key[1]) - except binascii.Error: - continue - # Load the pow token - hashedKey = self._core._crypto.blake2bHash(key[0]) - powHash = self._core._crypto.blake2bHash(value + hashedKey) - try: - powHash = powHash.encode() - except AttributeError: - pass - # if POW meets required difficulty, TODO make configurable/dynamic - if powHash.startswith(b'0000'): - # if we don't already have the key and its not our key, add it. - if not key[0] in self._core.listPeers(randomOrder=False) and type(key) != None and key[0] != self._core._crypto.pubKey: - if self._core.addPeer(key[0], key[1]): - # Check if the peer has a set username already - onionrusers.OnionrUser(self._core, key[0]).findAndSetID() - retVal = True - else: - logger.warn("Failed to add key") - else: - pass - #logger.debug('%s pow failed' % key[0]) - return retVal - except Exception as error: - logger.error('Failed to merge keys.', error=error) - return False - - - def mergeAdders(self, newAdderList): - ''' - Merge peer adders list to our database - ''' - try: - retVal = False - if newAdderList != False: - for adder in newAdderList.split(','): - adder = adder.strip() - if not adder in self._core.listAdders(randomOrder = False) and adder != self.getMyAddress() and not self._core._blacklist.inBlacklist(adder): - if not config.get('tor.v3onions') and len(adder) == 62: - continue - if self._core.addAddress(adder): - # Check if we have the maxmium amount of allowed stored peers - if config.get('peers.max_stored_peers') > len(self._core.listAdders()): - logger.info('Added %s to db.' % adder, timestamp = True) - retVal = True - else: - logger.warn('Reached the maximum amount of peers in the net database as allowed by your config.') - else: - pass - #logger.debug('%s is either our address or already in our DB' % adder) - return retVal - except Exception as error: - logger.error('Failed to merge adders.', error = error) - return False - - def getMyAddress(self): - try: - with open('./' + self._core.dataDir + 'hs/hostname', 'r') as hostname: - return hostname.read().strip() - except FileNotFoundError: - return "" - except Exception as error: - logger.error('Failed to read my address.', error = error) - return None def getClientAPIServer(self): retData = '' @@ -195,27 +111,6 @@ class OnionrUtils: return retData - def getPassword(self, message='Enter password: ', confirm = True): - ''' - Get a password without showing the users typing and confirm the input - ''' - # Get a password safely with confirmation and return it - while True: - print(message) - pass1 = getpass.getpass() - if confirm: - print('Confirm password: ') - pass2 = getpass.getpass() - if pass1 != pass2: - logger.error("Passwords do not match.") - logger.readline() - else: - break - else: - break - - return pass1 - def getHumanReadableID(self, pub=''): '''gets a human readable ID from a public key''' if pub == '': @@ -225,12 +120,14 @@ class OnionrUtils: 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). - + 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 = {} @@ -255,34 +152,6 @@ class OnionrUtils: meta = metadata['meta'] return (metadata, meta, data) - def checkPort(self, port, host=''): - ''' - Checks if a port is available, returns bool - ''' - # inspired by https://www.reddit.com/r/learnpython/comments/2i4qrj/how_to_write_a_python_script_that_checks_to_see/ckzarux/ - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - retVal = False - try: - sock.bind((host, port)) - except OSError as e: - if e.errno is 98: - retVal = True - finally: - sock.close() - - return retVal - - def checkIsIP(self, ip): - ''' - Check if a string is a valid IPv4 address - ''' - try: - socket.inet_aton(ip) - except: - return False - else: - return True - def processBlockMetadata(self, blockHash): ''' Read metadata from a block and cache it to the block database @@ -366,7 +235,7 @@ class OnionrUtils: def validateHash(self, data, length=64): ''' - Validate if a string is a valid hex formatted hash + 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: @@ -533,22 +402,6 @@ class OnionrUtils: except: return False - def getPeerByHashId(self, hash): - ''' - Return the pubkey of the user if known from the hash - ''' - if self._core._crypto.pubKeyHashID() == hash: - retData = self._core._crypto.pubKey - return retData - conn = sqlite3.connect(self._core.peerDB) - c = conn.cursor() - command = (hash,) - retData = '' - for row in c.execute('SELECT id FROM peers WHERE hashID = ?', command): - if row[0] != '': - retData = row[0] - return retData - def isCommunicatorRunning(self, timeout = 5, interval = 0.1): try: runcheck_file = self._core.dataDir + '.runcheck' @@ -569,13 +422,6 @@ class OnionrUtils: except: return False - def token(self, size = 32): - ''' - Generates a secure random hex encoded token - ''' - - return binascii.hexlify(os.urandom(size)) - def importNewBlocks(self, scanDir=''): ''' This function is intended to scan for new blocks ON THE DISK and import them @@ -697,22 +543,6 @@ class OnionrUtils: pass return data - def checkNetwork(self, torPort=0): - '''Check if we are connected to the internet (through Tor)''' - retData = False - connectURLs = [] - try: - with open('static-data/connect-check.txt', 'r') as connectTest: - connectURLs = connectTest.read().split(',') - - for url in connectURLs: - if self.doGetRequest(url, port=torPort, ignoreAPI=True) != False: - retData = True - break - except FileNotFoundError: - pass - return retData - def size(path='.'): ''' Returns the size of a folder's contents in bytes @@ -737,4 +567,4 @@ 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) + return "%.1f %s%s" % (num, 'Yi', suffix) \ 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 166249be..e7277c7d 100755 --- a/onionr/static-data/default-plugins/metadataprocessor/main.py +++ b/onionr/static-data/default-plugins/metadataprocessor/main.py @@ -41,7 +41,7 @@ def _processForwardKey(api, myBlock): else: raise onionrexceptions.InvalidPubkey("%s is nota valid pubkey key" % (key,)) -def on_processblocks(api): +def on_processblocks(api, data=None): # Generally fired by utils. myBlock = api.data['block'] blockType = api.data['type'] diff --git a/onionr/utils/netutils.py b/onionr/utils/netutils.py new file mode 100644 index 00000000..b6f16922 --- /dev/null +++ b/onionr/utils/netutils.py @@ -0,0 +1,34 @@ +''' + Onionr - P2P Microblogging Platform & Social network + + OnionrUtils offers various useful functions to Onionr networking. +''' +''' + 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 checkNetwork(utilsInst, torPort=0): + '''Check if we are connected to the internet (through Tor)''' + retData = False + connectURLs = [] + try: + with open('static-data/connect-check.txt', 'r') as connectTest: + connectURLs = connectTest.read().split(',') + + for url in connectURLs: + if utilsInst.doGetRequest(url, port=torPort, ignoreAPI=True) != False: + retData = True + break + except FileNotFoundError: + pass + return retData \ No newline at end of file diff --git a/onionr/utils/networkmerger.py b/onionr/utils/networkmerger.py new file mode 100644 index 00000000..fab10432 --- /dev/null +++ b/onionr/utils/networkmerger.py @@ -0,0 +1,46 @@ +''' + Onionr - P2P Microblogging Platform & Social network + + Merges peer and block 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 . +''' +import logger +def mergeAdders(newAdderList, coreInst): + ''' + Merge peer adders list to our database + ''' + try: + retVal = False + if newAdderList != False: + for adder in newAdderList.split(','): + adder = adder.strip() + if not adder in coreInst.listAdders(randomOrder = False) and adder != coreInst.hsAddress and not coreInst._blacklist.inBlacklist(adder): + if not config.get('tor.v3onions') and len(adder) == 62: + continue + if coreInst.addAddress(adder): + # Check if we have the maxmium amount of allowed stored peers + if config.get('peers.max_stored_peers') > len(coreInst.listAdders()): + logger.info('Added %s to db.' % adder, timestamp = True) + retVal = True + else: + logger.warn('Reached the maximum amount of peers in the net database as allowed by your config.') + else: + pass + #logger.debug('%s is either our address or already in our DB' % adder) + return retVal + except Exception as error: + logger.error('Failed to merge adders.', error = error) + return False \ No newline at end of file From 7c57829ec35b5cc7f0f96fe9c6ad308c7bfb2cbc Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 11 Feb 2019 23:30:56 -0600 Subject: [PATCH 73/94] code cleanup, defunct code removal --- onionr/api.py | 49 ++++++++++++++++++-------- onionr/communicator.py | 66 ++++++++++++++++------------------- onionr/core.py | 45 +++++------------------- onionr/dbcreator.py | 1 - onionr/onionr.py | 3 +- onionr/onionrblacklist.py | 5 ++- onionr/onionrstorage.py | 15 ++++++++ onionr/onionrutils.py | 33 +++++------------- onionr/utils/networkmerger.py | 4 +-- 9 files changed, 106 insertions(+), 115 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 6a53ef19..839b3bf3 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -31,8 +31,6 @@ class FDSafeHandler(WSGIHandler): def handle(self): timeout = Timeout(60, exception=Exception) timeout.start() - - #timeout = gevent.Timeout.start_new(3) try: WSGIHandler.handle(self) except Timeout as ex: @@ -76,28 +74,35 @@ class PublicAPI: @app.before_request def validateRequest(): '''Validate request has the correct hostname''' - # If high security level, deny requests to public + # 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: abort(403) if type(self.torAdder) is None and type(self.i2pAdder) is None: # abort if our hs addresses are not known abort(403) if request.host not in (self.i2pAdder, self.torAdder): + # Disallow connection if wrong HTTP hostname, in order to prevent DNS rebinding attacks abort(403) @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. + 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" + # Network API version resp.headers['X-API'] = onionr.API_VERSION + # Close connections to limit FD use resp.headers['Connection'] = "close" return resp @app.route('/') def banner(): + # Display a bit of information to people who visit a node address in their browser try: with open('static-data/index.html', 'r') as html: resp = Response(html.read(), mimetype='text/html') @@ -107,21 +112,28 @@ 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) 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/') def getBlockData(name): + # Share data for a block if we have it resp = '' data = name if clientAPI._utils.validateHash(data): if data not in self.hideBlocks: if data in clientAPI._core.getBlockList(): - block = clientAPI.getBlockData(data, raw=True).encode() + 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() @@ -132,18 +144,16 @@ class PublicAPI: @app.route('/www/') def wwwPublic(path): + # A way to share files directly over your .onion if not config.get("www.public.run", True): abort(403) return send_from_directory(config.get('www.public.path', 'static-data/www/public/'), path) @app.route('/ping') def ping(): + # Endpoint to test if nodes are up return Response("pong!") - @app.route('/getdbhash') - def getDBHash(): - return Response(clientAPI._utils.getBlockDBHash()) - @app.route('/pex') def peerExchange(): response = ','.join(clientAPI._core.listAdders(recent=3600)) @@ -191,6 +201,9 @@ class PublicAPI: @app.route('/upload', methods=['post']) def upload(): + '''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'] @@ -212,6 +225,7 @@ class PublicAPI: resp = Response(resp) return resp + # Set instances, then startup our public api server clientAPI.setPublicAPIInstance(self) while self.torAdder == '': clientAPI._core.refreshFirstStartVars() @@ -239,7 +253,6 @@ class API: onionr.Onionr.setupConfig('data/', self = self) self.debug = debug - self._privateDelayTime = 3 self._core = onionrInst.onionrCore self.startTime = self._core._utils.getEpoch() self._crypto = onionrcrypto.OnionrCrypto(self._core) @@ -248,7 +261,7 @@ class API: bindPort = int(config.get('client.client.port', 59496)) self.bindPort = bindPort - # Be extremely mindful of this + # Be extremely mindful of this. These are endpoints available without a password self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'board', 'boardContent', 'sharedContent', 'mail', 'mailindex') self.clientToken = config.get('client.webpassword') @@ -260,13 +273,14 @@ class API: logger.info('Running api on %s:%s' % (self.host, self.bindPort)) self.httpServer = '' - self.pluginResponses = {} + self.pluginResponses = {} # Responses for plugin endpoints self.queueResponse = {} onionrInst.setClientAPIInst(self) @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: @@ -279,13 +293,13 @@ class API: @app.after_request def afterReq(resp): + # Security headers 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['X-API'] = onionr.API_VERSION 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" @@ -317,11 +331,13 @@ class API: @app.route('/queueResponseAdd/', methods=['post']) def queueResponseAdd(name): + # Responses from the daemon. TODO: change to direct var access instead of http endpoint self.queueResponse[name] = request.form['data'] return Response('success') @app.route('/queueResponse/') def queueResponse(name): + # Fetch a daemon queue response resp = 'failure' try: resp = self.queueResponse[name] @@ -333,10 +349,12 @@ class API: @app.route('/ping') 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/') @@ -396,6 +414,7 @@ class API: @app.route('/waitforshare/', methods=['post']) def waitforshare(name): + '''Used to prevent the **public** api from sharing blocks we just created''' assert name.isalnum() if name in self.publicAPI.hideBlocks: self.publicAPI.hideBlocks.remove(name) @@ -421,6 +440,7 @@ class API: @app.route('/getstats') def getStats(): + # returns node stats #return Response("disabled") while True: try: @@ -475,6 +495,7 @@ class API: @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' @@ -512,7 +533,7 @@ class API: def validateToken(self, token): ''' - Validate that the client token matches the given token + Validate that the client token matches the given token. Used to prevent CSRF and data exfiltration ''' if len(self.clientToken) == 0: logger.error("client password needs to be set") diff --git a/onionr/communicator.py b/onionr/communicator.py index 9723ed6f..c76dd1f4 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -189,42 +189,38 @@ class OnionrCommunicatorDaemon: break else: continue - newDBHash = self.peerAction(peer, 'getdbhash') # get their db hash - if newDBHash == False or not self._core._utils.validateHash(newDBHash): - continue # if request failed, restart loop (peer is added to offline peers automatically) triedPeers.append(peer) - if newDBHash != self._core.getAddressInfo(peer, 'DBHash'): - self._core.setAddressInfo(peer, 'DBHash', newDBHash) - # 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) + + # 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 diff --git a/onionr/core.py b/onionr/core.py index c79ae86f..f3ac5218 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -222,18 +222,8 @@ class Core: c.execute('Delete from hashes where hash=?;', t) conn.commit() conn.close() - blockFile = self.dataDir + '/blocks/%s.dat' % block - dataSize = 0 - try: - ''' Get size of data when loaded as an object/var, rather than on disk, - to avoid conflict with getsizeof when saving blocks - ''' - with open(blockFile, 'r') as data: - dataSize = sys.getsizeof(data.read()) - self._utils.storageCounter.removeBytes(dataSize) - os.remove(blockFile) - except FileNotFoundError: - pass + dataSize = sys.getsizeof(onionrstorage.getData(self, block)) + self._utils.storageCounter.removeBytes(dataSize) def createAddressDB(self): ''' @@ -317,9 +307,6 @@ class Core: #raise Exception("Data is already set for " + dataHash) else: if self._utils.storageCounter.addBytes(dataSize) != False: - #blockFile = open(blockFileName, 'wb') - #blockFile.write(data) - #blockFile.close() onionrstorage.store(self, data, blockHash=dataHash) conn = sqlite3.connect(self.blockDB, timeout=30) c = conn.cursor() @@ -558,19 +545,18 @@ class Core: knownPeer text, 2 speed int, 3 success int, 4 - DBHash text, 5 - powValue 6 - failure int 7 - lastConnect 8 - trust 9 - introduced 10 + powValue 5 + failure int 6 + lastConnect 7 + 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, 'DBHash': 5, 'powValue': 6, 'failure': 7, 'lastConnect': 8, 'trust': 9, 'introduced': 10} + 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 = '' @@ -596,7 +582,7 @@ class Core: command = (data, address) - if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'DBHash', 'failure', 'powValue', 'lastConnect', 'lastConnectAttempt', 'trust', 'introduced'): + 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) @@ -681,19 +667,6 @@ class Core: conn.close() return rows - def setBlockType(self, hash, blockType): - ''' - Sets the type of block - ''' - - conn = sqlite3.connect(self.blockDB, timeout=30) - c = conn.cursor() - c.execute("UPDATE hashes SET dataType = ? WHERE hash = ?;", (blockType, hash)) - conn.commit() - conn.close() - - return - def updateBlockInfo(self, hash, key, data): ''' sets info associated with a block diff --git a/onionr/dbcreator.py b/onionr/dbcreator.py index f728254a..b84794d3 100755 --- a/onionr/dbcreator.py +++ b/onionr/dbcreator.py @@ -39,7 +39,6 @@ class DBCreator: knownPeer text, speed int, success int, - DBHash text, powValue text, failure int, lastConnect int, diff --git a/onionr/onionr.py b/onionr/onionr.py index aca17a1a..54f83616 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -43,7 +43,7 @@ except ImportError: ONIONR_TAGLINE = 'Anonymous P2P Platform - GPLv3 - https://Onionr.net' ONIONR_VERSION = '0.5.0' # for debugging and stuff ONIONR_VERSION_TUPLE = tuple(ONIONR_VERSION.split('.')) # (MAJOR, MINOR, VERSION) -API_VERSION = '5' # increments of 1; only change when something fundemental about how the API works changes. This way other nodes know how to communicate without learning too much information about you. +API_VERSION = '5' # 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): @@ -587,7 +587,6 @@ class Onionr: logger.info("Successfully added address.") else: logger.warn("Unable to add address.") - return def addMessage(self, header="txt"): diff --git a/onionr/onionrblacklist.py b/onionr/onionrblacklist.py index a87163f3..63ecbb6b 100755 --- a/onionr/onionrblacklist.py +++ b/onionr/onionrblacklist.py @@ -116,4 +116,7 @@ class OnionrBlackList: return insert = (hashed,) blacklistDate = self._core._utils.getEpoch() - self._dbExecute("INSERT INTO blacklist (hash, dataType, blacklistDate, expire) VALUES(?, ?, ?, ?);", (str(hashed), dataType, blacklistDate, expire)) + try: + self._dbExecute("INSERT INTO blacklist (hash, dataType, blacklistDate, expire) VALUES(?, ?, ?, ?);", (str(hashed), dataType, blacklistDate, expire)) + except sqlite3.IntegrityError: + pass diff --git a/onionr/onionrstorage.py b/onionr/onionrstorage.py index 8e2aae41..63aa150d 100755 --- a/onionr/onionrstorage.py +++ b/onionr/onionrstorage.py @@ -55,6 +55,21 @@ def _dbFetch(coreInst, blockHash): conn.close() return None +def deleteBlock(coreInst, blockHash): + # You should call core.removeBlock if you automatically want to remove storage byte count + assert isinstance(coreInst, core.Core) + if os.path.exists('%s/%s.dat' % (coreInst.blockDataLocation, blockHash)): + os.remove('%s/%s.dat' % (coreInst.blockDataLocation, blockHash)) + return True + dbCreate(coreInst) + conn = sqlite3.connect(coreInst.blockDataDB, timeout=10) + c = conn.cursor() + data = (blockHash,) + c.execute('DELETE FROM blockData where hash = ?', data) + conn.commit() + conn.close() + return True + def store(coreInst, data, blockHash=''): assert isinstance(coreInst, core.Core) assert coreInst._utils.validateHash(blockHash) diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 65a680b0..21c10e5f 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -194,21 +194,6 @@ class OnionrUtils: ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]') return ansi_escape.sub('', line) - def getBlockDBHash(self): - ''' - Return a sha3_256 hash of the blocks DB - ''' - try: - with open(self._core.blockDB, 'rb') as data: - data = data.read() - hasher = hashlib.sha3_256() - hasher.update(data) - dataHash = hasher.hexdigest() - - return dataHash - except Exception as error: - logger.error('Failed to get block DB hash.', error=error) - def hasBlock(self, hash): ''' Check for new block in the list @@ -334,15 +319,6 @@ class OnionrUtils: retVal = True return retVal - def isIntegerString(self, data): - '''Check if a string is a valid base10 integer (also returns true if already an int)''' - try: - int(data) - except ValueError: - return False - else: - return True - def validateID(self, id): ''' Validate if an address is a valid tor or i2p hidden service @@ -402,6 +378,15 @@ class OnionrUtils: except: return False + def isIntegerString(self, data): + '''Check if a string is a valid base10 integer (also returns true if already an int)''' + try: + int(data) + except ValueError: + return False + else: + return True + def isCommunicatorRunning(self, timeout = 5, interval = 0.1): try: runcheck_file = self._core.dataDir + '.runcheck' diff --git a/onionr/utils/networkmerger.py b/onionr/utils/networkmerger.py index fab10432..a074c62f 100644 --- a/onionr/utils/networkmerger.py +++ b/onionr/utils/networkmerger.py @@ -28,11 +28,11 @@ def mergeAdders(newAdderList, coreInst): for adder in newAdderList.split(','): adder = adder.strip() if not adder in coreInst.listAdders(randomOrder = False) and adder != coreInst.hsAddress and not coreInst._blacklist.inBlacklist(adder): - if not config.get('tor.v3onions') and len(adder) == 62: + if not coreInst.config.get('tor.v3onions') and len(adder) == 62: continue if coreInst.addAddress(adder): # Check if we have the maxmium amount of allowed stored peers - if config.get('peers.max_stored_peers') > len(coreInst.listAdders()): + if coreInst.config.get('peers.max_stored_peers') > len(coreInst.listAdders()): logger.info('Added %s to db.' % adder, timestamp = True) retVal = True else: From baf9d3a3c629bf27a6497e9e2ab651197c30313f Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 12 Feb 2019 13:18:08 -0600 Subject: [PATCH 74/94] better peer exchange --- onionr/api.py | 8 +++----- onionr/communicator.py | 37 +++++++++++++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 839b3bf3..8f180f3b 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -188,11 +188,9 @@ class PublicAPI: except AttributeError: pass if powHash.startswith('0000'): - try: - newNode = newNode.decode() - except AttributeError: - pass - if clientAPI._core.addAddress(newNode): + 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) diff --git a/onionr/communicator.py b/onionr/communicator.py index c76dd1f4..77de6839 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -40,11 +40,11 @@ class OnionrCommunicatorDaemon: # list of timer instances self.timers = [] - # initalize core with Tor socks port being 3rd argument + # initialize core with Tor socks port being 3rd argument self.proxyPort = proxyPort self._core = onionrInst.onionrCore - # intalize NIST beacon salt and time + # initialize NIST beacon salt and time self.nistSaltTimestamp = 0 self.powSalt = 0 @@ -59,11 +59,12 @@ class OnionrCommunicatorDaemon: self.cooldownPeer = {} 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 # amount of threads running by name, used to prevent too many self.threadCounts = {} - # set true when shutdown command recieved + # set true when shutdown command received self.shutdown = False # list of new blocks to download, added to when new block lists are fetched from peers @@ -156,11 +157,27 @@ class OnionrCommunicatorDaemon: '''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') - networkmerger.mergeAdders(newAdders, self._core) + try: + newPeers = newAdders.split(',') + except AttributeError: + pass + else: + # Validate new peers are good format and not already in queue + invalid = [] + for x in newPeers: + if not self._core._utils.validateID(x) or x in self.newPeers: + invalid.append(x) + for x in invalid: + newPeers.remove(x) + self.newPeers.extend(newPeers) self.decrementThreadCount('lookupAdders') def lookupBlocks(self): @@ -397,8 +414,18 @@ class OnionrCommunicatorDaemon: 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) @@ -413,6 +440,8 @@ class OnionrCommunicatorDaemon: if self.peerAction(address, 'ping') == 'pong!': logger.info('Connected to ' + address) time.sleep(0.1) + if address not in mainPeerList: + networkmerger.mergeAdders(address, self._core) if address not in self.onlinePeers: self.onlinePeers.append(address) self.connectTimes[address] = self._core._utils.getEpoch() From 59603deb6a9e3e0d5055af376507cfa0ea570527 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 12 Feb 2019 22:35:43 -0600 Subject: [PATCH 75/94] fixed first log --- onionr/logger.py | 13 +++++++------ onionr/onionr.py | 11 ----------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/onionr/logger.py b/onionr/logger.py index c9639b0d..896e2623 100755 --- a/onionr/logger.py +++ b/onionr/logger.py @@ -134,12 +134,13 @@ def raw(data, fd = sys.stdout, sensitive = False): if get_settings() & OUTPUT_TO_CONSOLE: ts = fd.write('%s\n' % data) if get_settings() & OUTPUT_TO_FILE and not sensitive: - if os.path.getsize(_outputfile) < MAX_LOG_SIZE: - try: - with open(_outputfile, "a+") as f: - f.write(colors.filter(data) + '\n') - except OSError: - pass + if os.path.getsize(_outputfile) >= MAX_LOG_SIZE: + return + 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): ''' diff --git a/onionr/onionr.py b/onionr/onionr.py index 54f83616..901f6ffe 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -207,9 +207,6 @@ class Onionr: 'connect': self.addAddress, 'pex': self.doPEX, - 'ui' : self.openUI, - 'gui' : self.openUI, - 'getpassword': self.printWebPassword, 'get-password': self.printWebPassword, 'getpwd': self.printWebPassword, @@ -298,8 +295,6 @@ class Onionr: with open('%s/%s.dat' % (exportDir, bHash), 'wb') as exportFile: exportFile.write(data) - - def showDetails(self): details = { 'Node Address' : self.get_hostname(), @@ -1062,12 +1057,6 @@ class Onionr: return data_exists - def openUI(self): - url = 'http://127.0.0.1:%s/ui/index.html?timingToken=%s' % (config.get('client.port', 59496), self.onionrUtils.getTimeBypassToken()) - - logger.info('Opening %s ...' % url) - webbrowser.open(url, new = 1, autoraise = True) - def header(self, message = logger.colors.fg.pink + logger.colors.bold + 'Onionr' + logger.colors.reset + logger.colors.fg.pink + ' has started.'): if os.path.exists('static-data/header.txt') and logger.get_level() <= logger.LEVEL_INFO: with open('static-data/header.txt', 'rb') as file: From 1243b4aea73fe46dc8e98ea0318d1eadbd2ecfca Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 12 Feb 2019 22:37:08 -0600 Subject: [PATCH 76/94] fixed first log --- onionr/logger.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/onionr/logger.py b/onionr/logger.py index 896e2623..3c923276 100755 --- a/onionr/logger.py +++ b/onionr/logger.py @@ -134,8 +134,11 @@ def raw(data, fd = sys.stdout, sensitive = False): if get_settings() & OUTPUT_TO_CONSOLE: ts = fd.write('%s\n' % data) if get_settings() & OUTPUT_TO_FILE and not sensitive: - if os.path.getsize(_outputfile) >= MAX_LOG_SIZE: - return + try: + if os.path.getsize(_outputfile) >= MAX_LOG_SIZE: + return + except FileNotFoundError: + pass try: with open(_outputfile, "a+") as f: f.write(colors.filter(data) + '\n') From 1be6bf1ec8bd18fce95a5f371955fe7c1a1a62ae Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 12 Feb 2019 22:47:11 -0600 Subject: [PATCH 77/94] fixed broken mail function call --- onionr/static-data/default-plugins/pms/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py index 55cd9a7c..bc261317 100755 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -152,7 +152,7 @@ class OnionrMail: reply = logger.readline("Press enter to continue, or enter %s to reply" % ("-r",)) print('') if reply == "-r": - self.draftMessage(self.myCore._utils.bytesToStr(readBlock.signer,)) + self.draft_message(self.myCore._utils.bytesToStr(readBlock.signer,)) return def sentbox(self): From 9ccf870e4dcbd8f04e06cf7827adc6c93c9b9ac8 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Tue, 12 Feb 2019 22:57:05 -0600 Subject: [PATCH 78/94] do not error on user save --- onionr/onionrusers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/onionr/onionrusers.py b/onionr/onionrusers.py index 41bc7b6e..53f89d44 100755 --- a/onionr/onionrusers.py +++ b/onionr/onionrusers.py @@ -48,7 +48,10 @@ class OnionrUser: self.publicKey = publicKey if saveUser: - self._core.addPeer(publicKey) + try: + self._core.addPeer(publicKey) + except AssertionError: + pass self.trust = self._core.getPeerInfo(self.publicKey, 'trust') return From 3357f93fc1780749113e9fada3bd270305decb83 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 14 Feb 2019 17:48:41 -0600 Subject: [PATCH 79/94] work on tests and various fixes --- .gitlab-ci.yml | 6 + Makefile | 14 +- onionr/api.py | 6 + onionr/communicator.py | 6 +- onionr/core.py | 13 +- onionr/logger.py | 8 +- onionr/onionr.py | 7 - onionr/onionrutils.py | 19 +- onionr/proofofmemory.py | 29 --- onionr/static-data/bootstrap-nodes.txt | 1 + onionr/static-data/www/private/index.html | 1 + onionr/static-data/www/shared/main/stats.js | 11 + onionr/tests.py | 243 -------------------- onionr/tests/test_database_creation.py | 70 ++++++ onionr/tests/test_stringvalidations.py | 58 +++++ 15 files changed, 176 insertions(+), 316 deletions(-) create mode 100644 .gitlab-ci.yml delete mode 100644 onionr/proofofmemory.py delete mode 100755 onionr/tests.py create mode 100644 onionr/tests/test_database_creation.py create mode 100644 onionr/tests/test_stringvalidations.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..d68521b7 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,6 @@ +test: + script: + - apt-get update -qy + - apt-get install -y python3-pip tor + - pip3 install -r requirements.txt + - make test diff --git a/Makefile b/Makefile index c3616e3d..4721e97c 100755 --- a/Makefile +++ b/Makefile @@ -18,26 +18,20 @@ uninstall: rm -f $(DESTDIR)$(PREFIX)/bin/onionr test: - @./onionr.sh stop - @sleep 1 - @rm -rf onionr/data-backup - @mv onionr/data onionr/data-backup | true > /dev/null 2>&1 - -@cd onionr; ./tests.py; - @rm -rf onionr/data - @mv onionr/data-backup onionr/data | true > /dev/null 2>&1 + ./run_tests.sh soft-reset: @echo "Soft-resetting Onionr..." - rm -f onionr/data/blocks/*.dat onionr/data/*.db onionr/data/block-nonces.dat | true > /dev/null 2>&1 + rm -f onionr/$(ONIONR_HOME)/blocks/*.dat onionr/data/*.db onionr/$(ONIONR_HOME)/block-nonces.dat | true > /dev/null 2>&1 @./onionr.sh version | grep -v "Failed" --color=always reset: @echo "Hard-resetting Onionr..." - rm -rf onionr/data/ | true > /dev/null 2>&1 + rm -rf onionr/$(ONIONR_HOME)/ | true > /dev/null 2>&1 cd onionr/static-data/www/ui/; rm -rf ./dist; python compile.py #@./onionr.sh.sh version | grep -v "Failed" --color=always plugins-reset: @echo "Resetting plugins..." - rm -rf onionr/data/plugins/ | true > /dev/null 2>&1 + rm -rf onionr/$(ONIONR_HOME)/plugins/ | true > /dev/null 2>&1 @./onionr.sh version | grep -v "Failed" --color=always diff --git a/onionr/api.py b/onionr/api.py index 8f180f3b..2e6ff46d 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -69,6 +69,7 @@ class PublicAPI: self.torAdder = clientAPI._core.hsAddress self.i2pAdder = clientAPI._core.i2pAddress self.bindPort = config.get('client.public.port') + self.lastRequest = 0 logger.info('Running public api on %s:%s' % (self.host, self.bindPort)) @app.before_request @@ -98,6 +99,7 @@ class PublicAPI: 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 @app.route('/') @@ -393,6 +395,10 @@ class API: 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 diff --git a/onionr/communicator.py b/onionr/communicator.py index 77de6839..b6847422 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -21,7 +21,7 @@ ''' import sys, os, core, config, json, requests, time, logger, threading, base64, onionr, uuid import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block -import onionrdaemontools, onionrsockets, onionr, onionrproofs, proofofmemory +import onionrdaemontools, onionrsockets, onionr, onionrproofs import binascii from dependencies import secrets from defusedxml import minidom @@ -86,8 +86,6 @@ class OnionrCommunicatorDaemon: # Loads in and starts the enabled plugins plugins.reload() - self.proofofmemory = proofofmemory.ProofOfMemory(self) - # daemon tools are misc daemon functions, e.g. announce to online peers # intended only for use by OnionrCommunicatorDaemon self.daemonTools = onionrdaemontools.DaemonTools(self) @@ -630,7 +628,7 @@ class OnionrCommunicatorDaemon: '''exit if the api server crashes/stops''' if self._core._utils.localCommand('ping', silent=False) not in ('pong', 'pong!'): for i in range(8): - if self._core._utils.localCommand('ping') in ('pong', 'pong!'): + if self._core._utils.localCommand('ping') in ('pong', 'pong!') or self.shutdown: break # break for loop time.sleep(1) else: diff --git a/onionr/core.py b/onionr/core.py index f3ac5218..0f7692de 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -85,6 +85,10 @@ class Core: 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: @@ -273,15 +277,6 @@ class Core: Simply return the data associated to a hash ''' - ''' - try: - # logger.debug('Opening %s' % (str(self.blockDataLocation) + str(hash) + '.dat')) - dataFile = open(self.blockDataLocation + hash + '.dat', 'rb') - data = dataFile.read() - dataFile.close() - except FileNotFoundError: - data = False - ''' data = onionrstorage.getData(self, hash) return data diff --git a/onionr/logger.py b/onionr/logger.py index 3c923276..2e7773a1 100755 --- a/onionr/logger.py +++ b/onionr/logger.py @@ -79,8 +79,12 @@ LEVEL_IMPORTANT = 6 _type = OUTPUT_TO_CONSOLE | USE_ANSI # the default settings for logging _level = LEVEL_DEBUG # the lowest level to log -_outputfile = './output.log' # the file to log to - +dataFolder = os.getenv('ONIONR_HOME') +if type(dataFolder) is type(None): + dataFolder = 'data/' +if not dataFolder.endswith('/'): + dataFolder += '/' +_outputfile = dataFolder + 'output.log' # the file to log to def set_settings(type): ''' Set the settings for the logger using bitwise operators diff --git a/onionr/onionr.py b/onionr/onionr.py index 901f6ffe..b47e6be3 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -110,12 +110,6 @@ class Onionr: except: plugins.disable(name, onionr = self, stop_event = False) - if not os.path.exists(self.onionrCore.peerDB): - self.onionrCore.createPeerDB() - pass - if not os.path.exists(self.onionrCore.addressDB): - self.onionrCore.createAddressDB() - # Get configuration if type(config.get('client.webpassword')) is type(None): config.set('client.webpassword', base64.b16encode(os.urandom(32)).decode('utf-8'), savefile=True) @@ -1014,7 +1008,6 @@ class Onionr: settings = settings | logger.OUTPUT_TO_CONSOLE if config.get('log.file.output', True): settings = settings | logger.OUTPUT_TO_FILE - logger.set_file(config.get('log.file.path', '/tmp/onionr.log').replace('data/', dataDir)) logger.set_settings(settings) if not self is None: diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 21c10e5f..85f26d66 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -18,7 +18,7 @@ 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 +import getpass, sys, requests, os, socket, hashlib, logger, sqlite3, config, binascii, time, base64, json, glob, shutil, math, json, re, urllib.parse, string import nacl.signing, nacl.encoding from onionrblockapi import Block import onionrexceptions @@ -309,6 +309,8 @@ class OnionrUtils: Validate if a string is a valid base32 encoded Ed25519 key ''' retVal = False + if type(key) is type(None): + return False try: nacl.signing.SigningKey(seed=key, encoder=nacl.encoding.Base32Encoder) except nacl.exceptions.ValueError: @@ -363,16 +365,9 @@ class OnionrUtils: retVal = False # Validate address is valid base32 (when capitalized and minus extension); v2/v3 onions and .b32.i2p use base32 - try: - base64.b32decode(idNoDomain.upper().encode()) - except binascii.Error: - retVal = False - - # Validate address is valid base32 (when capitalized and minus extension); v2/v3 onions and .b32.i2p use base32 - try: - base64.b32decode(idNoDomain.upper().encode()) - except binascii.Error: - retVal = False + for x in idNoDomain.upper(): + if x not in string.ascii_uppercase and x not in '234567': + retVal = False return retVal except: @@ -382,7 +377,7 @@ class OnionrUtils: '''Check if a string is a valid base10 integer (also returns true if already an int)''' try: int(data) - except ValueError: + except (ValueError, TypeError) as e: return False else: return True diff --git a/onionr/proofofmemory.py b/onionr/proofofmemory.py deleted file mode 100644 index 4b0b0fa7..00000000 --- a/onionr/proofofmemory.py +++ /dev/null @@ -1,29 +0,0 @@ -''' - Onionr - P2P Anonymous Storage Network - - This file handles proof of memory functionality -''' -''' - 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 . -''' - -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 diff --git a/onionr/static-data/bootstrap-nodes.txt b/onionr/static-data/bootstrap-nodes.txt index e69de29b..fb8679b7 100755 --- a/onionr/static-data/bootstrap-nodes.txt +++ b/onionr/static-data/bootstrap-nodes.txt @@ -0,0 +1 @@ +lc4mw2es4se22xsjrccumna3ih53difx64q2t2mgk5ijjk7d6aiacbqd.onion \ 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 3e239eb0..80da5cdc 100755 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -21,6 +21,7 @@

Mail

Stats

Uptime:

+

Last Received Connection: Unknown

Stored Blocks:

Blocks in queue:

Connected nodes:

diff --git a/onionr/static-data/www/shared/main/stats.js b/onionr/static-data/www/shared/main/stats.js index b7d05776..3a445acb 100755 --- a/onionr/static-data/www/shared/main/stats.js +++ b/onionr/static-data/www/shared/main/stats.js @@ -21,6 +21,7 @@ uptimeDisplay = document.getElementById('uptime') connectedDisplay = document.getElementById('connectedNodes') storedBlockDisplay = document.getElementById('storedBlocks') queuedBlockDisplay = document.getElementById('blockQueue') +lastIncoming = document.getElementById('lastIncoming') function getStats(){ stats = JSON.parse(httpGet('getstats', webpass)) @@ -28,5 +29,15 @@ function getStats(){ connectedDisplay.innerText = stats['connectedNodes'] storedBlockDisplay.innerText = stats['blockCount'] queuedBlockDisplay.innerText = stats['blockQueueCount'] + var lastConnect = httpGet('/lastconnect') + if (lastConnect > 0){ + var humanDate = new Date(0) + humanDate.setUTCSeconds(httpGet('/lastconnect')) + lastConnect = humanDate.toString() + } + else{ + lastConnect = 'Unknown' + } + lastIncoming.innerText = lastConnect } getStats() \ No newline at end of file diff --git a/onionr/tests.py b/onionr/tests.py deleted file mode 100755 index c23db1fa..00000000 --- a/onionr/tests.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/env python3 -''' - Onionr - P2P Microblogging Platform & Social 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 unittest, sys, os, base64, tarfile, shutil, logger - -class OnionrTests(unittest.TestCase): - def testPython3(self): - if sys.version_info.major != 3: - logger.debug('Python version: ' + sys.version_info.major) - self.assertTrue(False) - else: - self.assertTrue(True) - - def testNone(self): - logger.debug('-'*26 + '\n') - logger.info('Running simple program run test...') - - blank = os.system('./onionr.py --version') - if blank != 0: - self.assertTrue(False) - else: - self.assertTrue(True) - - def testPeer_a_DBCreation(self): - logger.debug('-'*26 + '\n') - logger.info('Running peer db creation test...') - - if os.path.exists('data/peers.db'): - os.remove('data/peers.db') - import core - myCore = core.Core() - myCore.createPeerDB() - if os.path.exists('data/peers.db'): - self.assertTrue(True) - else: - self.assertTrue(False) - - def testPeer_b_addPeerToDB(self): - logger.debug('-'*26 + '\n') - logger.info('Running peer db insertion test...') - - import core - myCore = core.Core() - if not os.path.exists('data/peers.db'): - myCore.createPeerDB() - if myCore.addPeer('6M5MXL237OK57ITHVYN5WGHANPGOMKS5C3PJLHBBNKFFJQOIDOJA====', '1cSix9Ao/yQSdo0sNif8cm2uTcYnSphb4JdZL/3WkN4=') and not myCore.addPeer('NFXHMYLMNFSAU===', '1cSix9Ao/yQSdo0sNif8cm2uTcYnSphb4JdZL/3WkN4='): - self.assertTrue(True) - else: - self.assertTrue(False) - - def testConfig(self): - logger.debug('-'*26 + '\n') - logger.info('Running simple configuration test...') - - import config - - config.check() - config.reload() - configdata = str(config.get_config()) - - config.set('testval', 1337) - if not config.get('testval', None) is 1337: - self.assertTrue(False) - - config.set('testval') - if not config.get('testval', None) is None: - self.assertTrue(False) - - config.save() - config.reload() - - if not str(config.get_config()) == configdata: - self.assertTrue(False) - - self.assertTrue(True) - ''' - def testBlockAPI(self): - logger.debug('-'*26 + '\n') - logger.info('Running BlockAPI test #1...') - - content = 'Onionr test block' - - from onionrblockapi import Block - hash = Block(type = 'test', content = content).save() - block = Block(hash) # test init - - if len(Block.getBlocks(type = 'test')) == 0: - logger.warn('Failed to find test block.') - self.assertTrue(False) - if not block.getContent() == content: - logger.warn('Test block content is invalid! (%s != %s)' % (block.getContent(), content)) - self.assertTrue(False) - - logger.debug('-'*26 + '\n') - logger.info('Running BlockAPI test #2...') - - original_content = 'onionr' - - logger.debug('original: %s' % original_content) - - blocks = Block.createChain(data = original_content, chunksize = 2, verbose = True) - - logger.debug(blocks[1]) - - child = blocks[0] - merged = Block.mergeChain(child) - - logger.debug('merged blocks (child: %s): %s' % (child, merged)) - - if merged != original_content: - self.assertTrue(False) - self.assertTrue(True) - - def testPluginReload(self): - logger.debug('-'*26 + '\n') - logger.info('Running simple plugin reload test...') - - import onionrplugins, os - - if not onionrplugins.exists('test'): - os.makedirs(onionrplugins.get_plugins_folder('test')) - with open(onionrplugins.get_plugins_folder('test') + '/main.py', 'a') as main: - main.write("print('Running')\n\ndef on_test(pluginapi, data = None):\n print('received test event!')\n return True\n\ndef on_start(pluginapi, data = None):\n print('start event called')\n\ndef on_stop(pluginapi, data = None):\n print('stop event called')\n\ndef on_enable(pluginapi, data = None):\n print('enable event called')\n\ndef on_disable(pluginapi, data = None):\n print('disable event called')\n") - onionrplugins.enable('test') - - try: - onionrplugins.reload('test') - self.assertTrue(True) - except: - self.assertTrue(False) - - def testPluginStopStart(self): - logger.debug('-'*26 + '\n') - logger.info('Running simple plugin restart test...') - - import onionrplugins, os - - if not onionrplugins.exists('test'): - os.makedirs(onionrplugins.get_plugins_folder('test')) - with open(onionrplugins.get_plugins_folder('test') + '/main.py', 'a') as main: - main.write("print('Running')\n\ndef on_test(pluginapi, data = None):\n print('received test event!')\n return True\n\ndef on_start(pluginapi, data = None):\n print('start event called')\n\ndef on_stop(pluginapi, data = None):\n print('stop event called')\n\ndef on_enable(pluginapi, data = None):\n print('enable event called')\n\ndef on_disable(pluginapi, data = None):\n print('disable event called')\n") - onionrplugins.enable('test') - - try: - onionrplugins.start('test') - onionrplugins.stop('test') - self.assertTrue(True) - except: - self.assertTrue(False) - - def testPluginEvent(self): - logger.debug('-'*26 + '\n') - logger.info('Running plugin event test...') - - import onionrplugins as plugins, onionrevents as events, os - - if not plugins.exists('test'): - os.makedirs(plugins.get_plugins_folder('test')) - with open(plugins.get_plugins_folder('test') + '/main.py', 'a') as main: - main.write("print('Running')\n\ndef on_test(pluginapi, data = None):\n print('received test event!')\n print('thread test started...')\n import time\n time.sleep(1)\n \n return True\n\ndef on_start(pluginapi, data = None):\n print('start event called')\n\ndef on_stop(pluginapi, data = None):\n print('stop event called')\n\ndef on_enable(pluginapi, data = None):\n print('enable event called')\n\ndef on_disable(pluginapi, data = None):\n print('disable event called')\n") - plugins.enable('test') - - - plugins.start('test') - if not events.call(plugins.get_plugin('test'), 'enable'): - self.assertTrue(False) - - logger.debug('preparing to start thread', timestamp = False) - thread = events.event('test', data = {'tests': self}) - logger.debug('thread running...', timestamp = False) - thread.join() - logger.debug('thread finished.', timestamp = False) - - self.assertTrue(True) - ''' - def testQueue(self): - logger.debug('-'*26 + '\n') - logger.info('Running daemon queue test...') - - # test if the daemon queue can read/write data - import core - myCore = core.Core() - if not os.path.exists('data/queue.db'): - myCore.daemonQueue() - while True: - command = myCore.daemonQueue() - if command == False: - logger.debug('The queue is empty (false)') - break - else: - logger.debug(command[0]) - myCore.daemonQueueAdd('testCommand', 'testData') - command = myCore.daemonQueue() - if command[0] == 'testCommand': - if myCore.daemonQueue() == False: - logger.info('Succesfully added and read command') - - def testHashValidation(self): - logger.debug('-'*26 + '\n') - logger.info('Running hash validation test...') - - import core - myCore = core.Core() - if not myCore._utils.validateHash("$324dfgfdg") and myCore._utils.validateHash("f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2") and not myCore._utils.validateHash("f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd$"): - self.assertTrue(True) - else: - self.assertTrue(False) - - def testAddAdder(self): - logger.debug('-'*26 + '\n') - logger.info('Running address add+remove test') - import core - myCore = core.Core() - if not os.path.exists('data/address.db'): - myCore.createAddressDB() - if myCore.addAddress('facebookcorewwwi.onion') and not myCore.removeAddress('invalid'): - if myCore.removeAddress('facebookcorewwwi.onion'): - self.assertTrue(True) - else: - self.assertTrue(False) - else: - self.assertTrue(False) # <- annoying :( - def testCrypto(self): - logger.info('running cryptotests') - if os.system('python3 cryptotests.py') == 0: - self.assertTrue(True) - else: - self.assertTrue(False) - -unittest.main() diff --git a/onionr/tests/test_database_creation.py b/onionr/tests/test_database_creation.py new file mode 100644 index 00000000..09da1aaa --- /dev/null +++ b/onionr/tests/test_database_creation.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +import sys, os +sys.path.append(".") +import unittest, uuid, sqlite3 +TEST_DIR = 'testdata/%s-%s' % (uuid.uuid4(), os.path.basename(__file__)) + '/' +print("Test directory:", TEST_DIR) +os.environ["ONIONR_HOME"] = TEST_DIR +from urllib.request import pathname2url +import core, onionr + +core.Core() + +class OnionrTests(unittest.TestCase): + + def test_peer_db_creation(self): + try: + dburi = 'file:{}?mode=rw'.format(pathname2url(TEST_DIR + 'peers.db')) + conn = sqlite3.connect(dburi, uri=True, timeout=30) + cursor = conn.cursor() + conn.close() + except sqlite3.OperationalError: + self.assertTrue(False) + else: + self.assertTrue(True) + + def test_block_db_creation(self): + try: + dburi = 'file:{}?mode=rw'.format(pathname2url(TEST_DIR + 'blocks.db')) + conn = sqlite3.connect(dburi, uri=True, timeout=30) + cursor = conn.cursor() + conn.close() + except sqlite3.OperationalError: + self.assertTrue(False) + else: + self.assertTrue(True) + + def test_forward_keys_db_creation(self): + try: + dburi = 'file:{}?mode=rw'.format(pathname2url(TEST_DIR + 'forward-keys.db')) + conn = sqlite3.connect(dburi, uri=True, timeout=30) + cursor = conn.cursor() + conn.close() + except sqlite3.OperationalError: + self.assertTrue(False) + else: + self.assertTrue(True) + + def test_address_db_creation(self): + try: + dburi = 'file:{}?mode=rw'.format(pathname2url(TEST_DIR + 'address.db')) + conn = sqlite3.connect(dburi, uri=True, timeout=30) + cursor = conn.cursor() + conn.close() + except sqlite3.OperationalError: + self.assertTrue(False) + else: + self.assertTrue(True) + + def blacklist_db_creation(self): + try: + dburi = 'file:{}?mode=rw'.format(pathname2url(TEST_DIR + 'blacklist.db')) + conn = sqlite3.connect(dburi, uri=True, timeout=30) + cursor = conn.cursor() + conn.close() + except sqlite3.OperationalError: + self.assertTrue(False) + else: + self.assertTrue(True) + +unittest.main() \ No newline at end of file diff --git a/onionr/tests/test_stringvalidations.py b/onionr/tests/test_stringvalidations.py new file mode 100644 index 00000000..9616c412 --- /dev/null +++ b/onionr/tests/test_stringvalidations.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +import sys, os +sys.path.append(".") +import unittest, uuid, sqlite3 +TEST_DIR = 'testdata/%s-%s' % (uuid.uuid4(), os.path.basename(__file__)) + '/' +print("Test directory:", TEST_DIR) +os.environ["ONIONR_HOME"] = TEST_DIR +from urllib.request import pathname2url +import core, onionr + +core.Core() + +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'] + + invalid = [None, 'dsfewjirji0ejipdfs', '', ' ', '\n', '\r\n', 'f$ce%^okc+rewwwi.onion'] + + for x in valid: + print('testing', x) + self.assertTrue(c._utils.validateID(x)) + + for x in invalid: + print('testing', x) + self.assertFalse(c._utils.validateID(x)) + + def test_pubkey_validator(self): + # Test ed25519 public key validity + valid = 'JZ5VE72GUS3C7BOHDRIYZX4B5U5EJMCMLKHLYCVBQQF3UKHYIRRQ====' + invalid = [None, '', ' ', 'dfsg', '\n', 'JZ5VE72GUS3C7BOHDRIYZX4B5U5EJMCMLKHLYCVBQQF3UKHYIR$Q===='] + c = core.Core() + print('testing', valid) + self.assertTrue(c._utils.validatePubKey(valid)) + + for x in invalid: + print('testing', x) + self.assertFalse(c._utils.validatePubKey(x)) + + 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)) + + for x in invalid: + print('testing', x) + self.assertFalse(c._utils.isIntegerString(x)) + + + +unittest.main() \ No newline at end of file From 398d8da3471992f5a709caa2ec942d63271860ae Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 14 Feb 2019 17:52:08 -0600 Subject: [PATCH 80/94] work on tests and various fixes --- run_tests.sh | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100755 run_tests.sh diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 00000000..b650ed95 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,7 @@ +#!/bin/bash +cd onionr; +mkdir testdata; +for f in tests/*.py; do + python3 "$f" || break # if needed +done +rm -rf testdata; \ No newline at end of file From 4afff79d2fe17ce48072e03f32fe792a1741947d Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 14 Feb 2019 21:28:41 -0600 Subject: [PATCH 81/94] more work on tests --- README.md | 1 + onionr/cryptotests.py | 35 -------------------------- onionr/onionrcrypto.py | 17 +++++++------ onionr/tests/test_stringvalidations.py | 11 +++----- 4 files changed, 15 insertions(+), 49 deletions(-) delete mode 100755 onionr/cryptotests.py diff --git a/README.md b/README.md index 6be34104..7cc1a597 100755 --- a/README.md +++ b/README.md @@ -11,6 +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/) +
diff --git a/onionr/cryptotests.py b/onionr/cryptotests.py deleted file mode 100755 index f7927fb8..00000000 --- a/onionr/cryptotests.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -''' - Onionr - P2P Microblogging Platform & Social 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 unittest, sys, os, core, onionrcrypto, logger - -class OnionrCryptoTests(unittest.TestCase): - def testSymmetric(self): - dataString = "this is a secret message" - dataBytes = dataString.encode() - myCore = core.Core() - crypto = onionrcrypto.OnionrCrypto(myCore) - key = key = b"tttttttttttttttttttttttttttttttt" - - logger.info("Encrypting: " + dataString, timestamp=True) - encrypted = crypto.symmetricEncrypt(dataString, key, returnEncoded=True) - logger.info(encrypted, timestamp=True) - logger.info('Decrypting encrypted string:', timestamp=True) - decrypted = crypto.symmetricDecrypt(encrypted, key, encodedMessage=True) - logger.info(decrypted, timestamp=True) - self.assertTrue(True) -if __name__ == "__main__": - unittest.main() diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py index 074c8aa1..883098a3 100755 --- a/onionr/onionrcrypto.py +++ b/onionr/onionrcrypto.py @@ -18,7 +18,7 @@ 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 onionrexceptions, keymanager +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 @@ -180,12 +180,6 @@ class OnionrCrypto: decrypted = base64.b64encode(decrypted) return decrypted - def generateSymmetricPeer(self, peer): - '''Generate symmetric key for a peer and save it to the peer database''' - key = self.generateSymmetric() - self._core.setPeerInfo(peer, 'forwardKey', key) - return - def generateSymmetric(self): '''Generate a symmetric key (bytes) and return it''' return binascii.hexlify(nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE)) @@ -283,6 +277,15 @@ class OnionrCrypto: @staticmethod def safeCompare(one, two): + # Do encode here to avoid spawning core + try: + one = one.encode() + except AttributeError: + pass + try: + two = two.encode() + except AttributeError: + pass return hmac.compare_digest(one, two) @staticmethod diff --git a/onionr/tests/test_stringvalidations.py b/onionr/tests/test_stringvalidations.py index 9616c412..0cc45eae 100644 --- a/onionr/tests/test_stringvalidations.py +++ b/onionr/tests/test_stringvalidations.py @@ -1,11 +1,10 @@ #!/usr/bin/env python3 import sys, os sys.path.append(".") -import unittest, uuid, sqlite3 +import unittest, uuid TEST_DIR = 'testdata/%s-%s' % (uuid.uuid4(), os.path.basename(__file__)) + '/' print("Test directory:", TEST_DIR) os.environ["ONIONR_HOME"] = TEST_DIR -from urllib.request import pathname2url import core, onionr core.Core() @@ -37,7 +36,7 @@ class OnionrValidations(unittest.TestCase): self.assertTrue(c._utils.validatePubKey(valid)) for x in invalid: - print('testing', x) + #print('testing', x) self.assertFalse(c._utils.validatePubKey(x)) def test_integer_string(self): @@ -46,13 +45,11 @@ class OnionrValidations(unittest.TestCase): c = core.Core() for x in valid: - print('testing', x) + #print('testing', x) self.assertTrue(c._utils.isIntegerString(x)) for x in invalid: - print('testing', x) + #print('testing', x) self.assertFalse(c._utils.isIntegerString(x)) - - unittest.main() \ No newline at end of file From 2e99b6b95c651e39e69b0ded241414ccc6adc1ae Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 15 Feb 2019 22:08:03 -0600 Subject: [PATCH 82/94] removed non-anonymous pubkey encryption, fixes, more tests --- onionr/core.py | 8 +-- onionr/onionrcrypto.py | 49 +++++++------------ onionr/onionrproofs.py | 4 +- onionr/onionrusers.py | 2 +- .../default-plugins/encrypt/main.py | 2 +- onionr/storagecounter.py | 4 +- run_tests.sh | 5 +- 7 files changed, 33 insertions(+), 41 deletions(-) diff --git a/onionr/core.py b/onionr/core.py index 0f7692de..1b803395 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -779,10 +779,10 @@ class Core: if self._utils.validatePubKey(asymPeer): # Encrypt block data with forward secrecy key first, but not meta jsonMeta = json.dumps(meta) - jsonMeta = self._crypto.pubKeyEncrypt(jsonMeta, asymPeer, encodedData=True, anonymous=True).decode() - data = self._crypto.pubKeyEncrypt(data, asymPeer, encodedData=True, anonymous=True).decode() - signature = self._crypto.pubKeyEncrypt(signature, asymPeer, encodedData=True, anonymous=True).decode() - signer = self._crypto.pubKeyEncrypt(signer, asymPeer, encodedData=True, anonymous=True).decode() + jsonMeta = self._crypto.pubKeyEncrypt(jsonMeta, asymPeer, encodedData=True).decode() + 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) else: raise onionrexceptions.InvalidPubkey(asymPeer + ' is not a valid base32 encoded ed25519 key') diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py index 883098a3..8f6f8e7a 100755 --- a/onionr/onionrcrypto.py +++ b/onionr/onionrcrypto.py @@ -94,52 +94,41 @@ class OnionrCrypto: retData = key.sign(data).signature return retData - def pubKeyEncrypt(self, data, pubkey, anonymous=True, encodedData=False): + def pubKeyEncrypt(self, data, pubkey, encodedData=False): '''Encrypt to a public key (Curve25519, taken from base32 Ed25519 pubkey)''' retVal = '' - try: - pubkey = pubkey.encode() - except AttributeError: - pass + box = None + data = self._core._utils.strToBytes(data) + + pubkey = nacl.signing.VerifyKey(pubkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_public_key() if encodedData: encoding = nacl.encoding.Base64Encoder else: encoding = nacl.encoding.RawEncoder + + box = nacl.public.SealedBox(pubkey) + retVal = box.encrypt(data, encoder=encoding) - if self.privKey != None and not anonymous: - ownKey = nacl.signing.SigningKey(seed=self.privKey, encoder=nacl.encoding.Base32Encoder).to_curve25519_private_key() - key = nacl.signing.VerifyKey(key=pubkey, encoder=nacl.encoding.Base32Encoder).to_curve25519_public_key() - ourBox = nacl.public.Box(ownKey, key) - retVal = ourBox.encrypt(data.encode(), encoder=encoding) - elif anonymous: - key = nacl.signing.VerifyKey(key=pubkey, encoder=nacl.encoding.Base32Encoder).to_curve25519_public_key() - anonBox = nacl.public.SealedBox(key) - try: - data = data.encode() - except AttributeError: - pass - retVal = anonBox.encrypt(data, encoder=encoding) return retVal - def pubKeyDecrypt(self, data, pubkey='', privkey='', anonymous=False, encodedData=False): + def pubKeyDecrypt(self, data, pubkey='', privkey='', encodedData=False): '''pubkey decrypt (Curve25519, taken from Ed25519 pubkey)''' decrypted = False if encodedData: encoding = nacl.encoding.Base64Encoder else: encoding = nacl.encoding.RawEncoder - ownKey = nacl.signing.SigningKey(seed=self.privKey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key() - if self.privKey != None and not anonymous: - ourBox = nacl.public.Box(ownKey, pubkey) - decrypted = ourBox.decrypt(data, encoder=encoding) - elif anonymous: - if self._core._utils.validatePubKey(privkey): - privkey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key() - anonBox = nacl.public.SealedBox(privkey) - else: - anonBox = nacl.public.SealedBox(ownKey) - decrypted = anonBox.decrypt(data, encoder=encoding) + if privkey == '': + privkey = self.privKey + ownKey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key() + + if self._core._utils.validatePubKey(privkey): + privkey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key() + anonBox = nacl.public.SealedBox(privkey) + else: + anonBox = nacl.public.SealedBox(ownKey) + decrypted = anonBox.decrypt(data, encoder=encoding) return decrypted def symmetricEncrypt(self, data, key, encodedKey=False, returnEncoded=True): diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index 202500f1..12e5615f 100755 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -64,9 +64,9 @@ def getDifficultyForNewBlock(data, ourBlock=True): else: raise ValueError('not Block, str, or int') if ourBlock: - minDifficulty = config.get('general.minimum_send_pow') + minDifficulty = config.get('general.minimum_send_pow', 4) else: - minDifficulty = config.get('general.minimum_block_pow') + minDifficulty = config.get('general.minimum_block_pow', 4) retData = max(minDifficulty, math.floor(dataSize / 100000)) + getDifficultyModifier() return retData diff --git a/onionr/onionrusers.py b/onionr/onionrusers.py index 53f89d44..b51e93cf 100755 --- a/onionr/onionrusers.py +++ b/onionr/onionrusers.py @@ -87,7 +87,7 @@ class OnionrUser: retData = '' forwardKey = self._getLatestForwardKey() if self._core._utils.validatePubKey(forwardKey): - retData = self._core._crypto.pubKeyEncrypt(data, forwardKey, encodedData=True, anonymous=True) + retData = self._core._crypto.pubKeyEncrypt(data, forwardKey, encodedData=True) else: raise onionrexceptions.InvalidPubkey("No valid forward secrecy key available for this user") #self.generateForwardKey() diff --git a/onionr/static-data/default-plugins/encrypt/main.py b/onionr/static-data/default-plugins/encrypt/main.py index 01eabd81..ac60cc69 100755 --- a/onionr/static-data/default-plugins/encrypt/main.py +++ b/onionr/static-data/default-plugins/encrypt/main.py @@ -69,7 +69,7 @@ class PlainEncryption: data['data'] = plaintext data = json.dumps(data) plaintext = data - encrypted = self.api.get_core()._crypto.pubKeyEncrypt(plaintext, pubkey, anonymous=True, encodedData=True) + 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,)) diff --git a/onionr/storagecounter.py b/onionr/storagecounter.py index 4647a084..76ebe16b 100755 --- a/onionr/storagecounter.py +++ b/onionr/storagecounter.py @@ -49,13 +49,13 @@ class StorageCounter: def getPercent(self): '''Return percent (decimal/float) of disk space we're using''' amount = self.getAmount() - return round(amount / self._core.config.get('allocations.disk'), 2) + return round(amount / self._core.config.get('allocations.disk', 2000000000), 2) def addBytes(self, amount): '''Record that we are now using more disk space, unless doing so would exceed configured max''' newAmount = amount + self.getAmount() retData = newAmount - if newAmount > self._core.config.get('allocations.disk'): + if newAmount > self._core.config.get('allocations.disk', 2000000000): retData = False else: self._update(newAmount) diff --git a/run_tests.sh b/run_tests.sh index b650ed95..f32868d3 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -1,7 +1,10 @@ #!/bin/bash cd onionr; mkdir testdata; +ran=0 for f in tests/*.py; do python3 "$f" || break # if needed + let "ran++" done -rm -rf testdata; \ No newline at end of file +rm -rf testdata; +echo "ran $ran test files successfully" \ No newline at end of file From 4827ef6def4cd371754f1ec4e33c9996a1720b4d Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 15 Feb 2019 22:08:26 -0600 Subject: [PATCH 83/94] removed non-anonymous pubkey encryption, fixes, more tests --- onionr/tests/test_blocks.py | 19 ++++ onionr/tests/test_highlevelcrypto.py | 130 +++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 onionr/tests/test_blocks.py create mode 100644 onionr/tests/test_highlevelcrypto.py diff --git a/onionr/tests/test_blocks.py b/onionr/tests/test_blocks.py new file mode 100644 index 00000000..257238b1 --- /dev/null +++ b/onionr/tests/test_blocks.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +import sys, os +sys.path.append(".") +import unittest, uuid, hashlib +import nacl.exceptions +import nacl.signing, nacl.hash, nacl.encoding +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 + +c = core.Core() + +class OnionrBlockTests(unittest.TestCase): + def test_plaintext_insert(self): + message = 'hello world' + c.insertBlock(message) + +unittest.main() \ No newline at end of file diff --git a/onionr/tests/test_highlevelcrypto.py b/onionr/tests/test_highlevelcrypto.py new file mode 100644 index 00000000..67236b45 --- /dev/null +++ b/onionr/tests/test_highlevelcrypto.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +import sys, os +sys.path.append(".") +import unittest, uuid, hashlib +import nacl.exceptions +import nacl.signing, nacl.hash, nacl.encoding +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 + +c = core.Core() +crypto = c._crypto +class OnionrCryptoTests(unittest.TestCase): + + def test_blake2b(self): + self.assertTrue(crypto.blake2bHash('test') == crypto.blake2bHash(b'test')) + self.assertTrue(crypto.blake2bHash(b'test') == crypto.blake2bHash(b'test')) + + self.assertFalse(crypto.blake2bHash('') == crypto.blake2bHash(b'test')) + try: + crypto.blake2bHash(None) + except nacl.exceptions.TypeError: + pass + else: + self.assertTrue(False) + + self.assertTrue(nacl.hash.blake2b(b'test') == crypto.blake2bHash(b'test')) + + def test_sha3256(self): + hasher = hashlib.sha3_256() + self.assertTrue(crypto.sha3Hash('test') == crypto.sha3Hash(b'test')) + self.assertTrue(crypto.sha3Hash(b'test') == crypto.sha3Hash(b'test')) + + self.assertFalse(crypto.sha3Hash('') == crypto.sha3Hash(b'test')) + try: + crypto.sha3Hash(None) + except TypeError: + pass + else: + self.assertTrue(False) + + hasher.update(b'test') + normal = hasher.hexdigest() + self.assertTrue(crypto.sha3Hash(b'test') == normal) + + def valid_default_id(self): + self.assertTrue(c._utils.validatePubKey(crypto.pubKey)) + + def test_human_readable_length(self): + human = c._utils.getHumanReadableID() + 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')) + self.assertFalse(crypto.safeCompare('test', 'test2')) + try: + crypto.safeCompare('test', None) + except TypeError: + pass + else: + self.assertTrue(False) + + def test_random_shuffle(self): + # Small chance that the randomized list will be same. Rerun test a couple times if it fails + startList = ['cat', 'dog', 'moose', 'rabbit', 'monkey', 'crab', 'human', 'dolphin', 'whale', 'etc'] * 10 + + self.assertFalse(startList == list(crypto.randomShuffle(startList))) + self.assertTrue(len(startList) == len(startList)) + + def test_asymmetric(self): + keyPair = crypto.generatePubKey() + keyPair2 = crypto.generatePubKey() + message = "hello world" + + self.assertTrue(len(crypto.pubKeyEncrypt(message, keyPair2[0], encodedData=True)) > 0) + encrypted = crypto.pubKeyEncrypt(message, keyPair2[0], encodedData=False) + decrypted = crypto.pubKeyDecrypt(encrypted, privkey=keyPair2[1], encodedData=False) + + self.assertTrue(decrypted.decode() == message) + try: + crypto.pubKeyEncrypt(None, keyPair2[0]) + except TypeError: + pass + else: + self.assertTrue(False) + + blankMessage = crypto.pubKeyEncrypt('', keyPair2[0]) + self.assertTrue('' == crypto.pubKeyDecrypt(blankMessage, privkey=keyPair2[1], encodedData=False).decode()) + # Try to encrypt arbitrary bytes + crypto.pubKeyEncrypt(os.urandom(32), keyPair2[0]) + + def test_symmetric(self): + dataString = "this is a secret message" + dataBytes = dataString.encode() + key = b"tttttttttttttttttttttttttttttttt" + invalidKey = b'tttttttttttttttttttttttttttttttb' + encrypted = crypto.symmetricEncrypt(dataString, key, returnEncoded=True) + decrypted = crypto.symmetricDecrypt(encrypted, key, encodedMessage=True) + self.assertTrue(dataString == decrypted.decode()) + + try: + crypto.symmetricDecrypt(encrypted, invalidKey, encodedMessage=True) + except nacl.exceptions.CryptoError: + pass + else: + self.assertFalse(True) + try: + crypto.symmetricEncrypt(None, key, returnEncoded=True) + except AttributeError: + pass + else: + self.assertFalse(True) + crypto.symmetricEncrypt("string", key, returnEncoded=True) + try: + crypto.symmetricEncrypt("string", None, returnEncoded=True) + except nacl.exceptions.TypeError: + pass + else: + self.assertFalse(True) + +unittest.main() \ No newline at end of file From 3fc623b8ee400dbf227418a7cf95388032c06cb2 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 16 Feb 2019 00:01:26 -0600 Subject: [PATCH 84/94] work on contact manager, removed old twitter-like ui for now --- CODE_OF_CONDUCT.md | 2 +- onionr/core.py | 3 +- onionr/onionr.py | 5 +- onionr/onionrblockapi.py | 3 +- onionr/onionrusers/contactmanager.py | 30 + onionr/{ => onionrusers}/onionrusers.py | 0 onionr/static-data/www/ui/README.md | 44 - .../www/ui/common/default-icon.html | 1 - onionr/static-data/www/ui/common/footer.html | 19 - onionr/static-data/www/ui/common/header.html | 30 - .../www/ui/common/onionr-reply-creator.html | 31 - .../www/ui/common/onionr-timeline-post.html | 32 - .../common/onionr-timeline-reply-creator.html | 30 - .../www/ui/common/onionr-timeline-reply.html | 31 - onionr/static-data/www/ui/compile.py | 130 --- onionr/static-data/www/ui/config.json | 4 - onionr/static-data/www/ui/dist/css/main.css | 122 --- .../www/ui/dist/css/themes/dark.css | 76 -- .../static-data/www/ui/dist/img/default.png | Bin 6758 -> 0 bytes onionr/static-data/www/ui/dist/index.html | 215 ----- onionr/static-data/www/ui/dist/js/main.js | 753 ------------------ onionr/static-data/www/ui/dist/js/timeline.js | 491 ------------ onionr/static-data/www/ui/lang.json | 65 -- onionr/static-data/www/ui/src/css/main.css | 122 --- .../www/ui/src/css/themes/dark.css | 76 -- onionr/static-data/www/ui/src/img/default.png | Bin 6758 -> 0 bytes onionr/static-data/www/ui/src/index.html | 136 ---- onionr/static-data/www/ui/src/js/main.js | 689 ---------------- onionr/static-data/www/ui/src/js/timeline.js | 460 ----------- 29 files changed, 38 insertions(+), 3562 deletions(-) create mode 100644 onionr/onionrusers/contactmanager.py rename onionr/{ => onionrusers}/onionrusers.py (100%) delete mode 100755 onionr/static-data/www/ui/README.md delete mode 100755 onionr/static-data/www/ui/common/default-icon.html delete mode 100755 onionr/static-data/www/ui/common/footer.html delete mode 100755 onionr/static-data/www/ui/common/header.html delete mode 100755 onionr/static-data/www/ui/common/onionr-reply-creator.html delete mode 100755 onionr/static-data/www/ui/common/onionr-timeline-post.html delete mode 100755 onionr/static-data/www/ui/common/onionr-timeline-reply-creator.html delete mode 100755 onionr/static-data/www/ui/common/onionr-timeline-reply.html delete mode 100755 onionr/static-data/www/ui/compile.py delete mode 100755 onionr/static-data/www/ui/config.json delete mode 100755 onionr/static-data/www/ui/dist/css/main.css delete mode 100755 onionr/static-data/www/ui/dist/css/themes/dark.css delete mode 100755 onionr/static-data/www/ui/dist/img/default.png delete mode 100755 onionr/static-data/www/ui/dist/index.html delete mode 100755 onionr/static-data/www/ui/dist/js/main.js delete mode 100755 onionr/static-data/www/ui/dist/js/timeline.js delete mode 100755 onionr/static-data/www/ui/lang.json delete mode 100755 onionr/static-data/www/ui/src/css/main.css delete mode 100755 onionr/static-data/www/ui/src/css/themes/dark.css delete mode 100755 onionr/static-data/www/ui/src/img/default.png delete mode 100755 onionr/static-data/www/ui/src/index.html delete mode 100755 onionr/static-data/www/ui/src/js/main.js delete mode 100755 onionr/static-data/www/ui/src/js/timeline.js diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 052ac851..ccd047ea 100755 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at beardog@firemail.cc. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at beardog at mailbox.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. diff --git a/onionr/core.py b/onionr/core.py index 1b803395..c1b743e4 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -21,7 +21,8 @@ import sqlite3, os, sys, time, math, base64, tarfile, nacl, logger, json, netcon from onionrblockapi import Block import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions -import onionrblacklist, onionrusers +import onionrblacklist +from onionrusers import onionrusers import dbcreator, onionrstorage, serializeddata from etc import onionrvalues diff --git a/onionr/onionr.py b/onionr/onionr.py index b47e6be3..c79b6488 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -25,7 +25,7 @@ MIN_PY_VERSION = 6 if sys.version_info[0] == 2 or sys.version_info[1] < MIN_PY_VERSION: print('Error, Onionr requires Python 3.%s+' % (MIN_PY_VERSION,)) sys.exit(1) -import os, base64, random, getpass, shutil, subprocess, requests, time, platform, datetime, re, json, getpass, sqlite3 +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 @@ -33,7 +33,8 @@ import onionrutils import netcontroller, onionrstorage from netcontroller import NetController from onionrblockapi import Block -import onionrproofs, onionrexceptions, onionrusers, communicator +import onionrproofs, onionrexceptions, communicator +from onionrusers import onionrusers try: from urllib3.contrib.socks import SOCKSProxyManager diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py index 9f38b785..732071d3 100755 --- a/onionr/onionrblockapi.py +++ b/onionr/onionrblockapi.py @@ -18,8 +18,9 @@ along with this program. If not, see . ''' -import core as onionrcore, logger, config, onionrexceptions, nacl.exceptions, onionrusers +import core as onionrcore, logger, config, onionrexceptions, nacl.exceptions import json, os, sys, datetime, base64, onionrstorage +from onionrusers import onionrusers class Block: blockCacheOrder = list() # NEVER write your own code that writes to this! diff --git a/onionr/onionrusers/contactmanager.py b/onionr/onionrusers/contactmanager.py new file mode 100644 index 00000000..c44952b3 --- /dev/null +++ b/onionr/onionrusers/contactmanager.py @@ -0,0 +1,30 @@ +''' + Onionr - P2P Anonymous Storage Network + + Sets more abstract information related to a peer. Can be thought of as traditional 'contact' 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 . +''' + +import onionrusers + +class ContactManager(onionrusers.OnionrUser): + def set_info(self, key, value): + return + def add_contact(self): + return + def delete_contact(self): + return + \ No newline at end of file diff --git a/onionr/onionrusers.py b/onionr/onionrusers/onionrusers.py similarity index 100% rename from onionr/onionrusers.py rename to onionr/onionrusers/onionrusers.py diff --git a/onionr/static-data/www/ui/README.md b/onionr/static-data/www/ui/README.md deleted file mode 100755 index 451b08ed..00000000 --- a/onionr/static-data/www/ui/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Onionr UI - -## About - -The default GUI for Onionr - -## Setup - -To compile the application, simply execute the following: - -``` -python3 compile.py -``` - -If you are wanting to compile Onionr UI for another language, execute the following, replacing `[lang]` with the target language (supported languages include `eng` for English, `spa` para español, and `zho`为中国人): - -``` -python3 compile.py [lang] -``` - -## FAQ -### Why "compile" anyway? -This web application is compiled for a few reasons: -1. To make it easier to update; this way, we do not have to update the header in every file if we want to change something about it. -2. To make the application smaller in size; there is less duplicated code when the code like the header and footer can be stored in an individual file rather than every file. -3. For multi-language support; with the Python "tags" feature, we can reference strings by variable name, and based on a language file, they can be dynamically inserted into the page on compilation. -4. For compile-time customizations. - -### What exactly happens when you compile? -Upon compilation, files from the `src/` directory will be copied to `dist/` directory, header and footers will be injected in the proper places, and Python "tags" will be interpreted. - - -### How do Python "tags" work? -There are two types of Python "tags": -1. Logic tags (`<$ logic $>`): These tags allow you to perform logic at compile time. Example: `<$ import datetime; lastUpdate = datetime.datetime.now() $>`: This gets the current time while compiling, then stores it in `lastUpdate`. -2. Data tags (`<$= data $>`): These tags take whatever the return value of the statement in the tags is, and write it directly to the page. Example: `<$= 'This application was compiled at %s.' % lastUpdate $>`: This will write the message in the string in the tags to the page. - -**Note:** Logic tags take a higher priority and will always be interpreted first. - -### How does the language feature work? -When you use a data tag to write a string to the page (e.g. `<$= LANG.HELLO_WORLD $>`), the language feature simply takes dictionary of the language that is currently being used from the language map file (`lang.json`), then searches for the key (being the variable name after the characters `LANG.` in the data tag, like `HELLO_WORLD` from the example before). It then writes that string to the page. Language variables are always prefixed with `LANG.` and should always be uppercase (as they are a constant). - -### I changed a few things in the application and tried to view the updates in my browser, but nothing changed! -You most likely forgot to compile. Try running `python3 compile.py` and check again. If you are still having issues, [open up an issue](https://gitlab.com/beardog/Onionr/issues/new?issue[title]=Onionr UI not updating after compiling). \ No newline at end of file diff --git a/onionr/static-data/www/ui/common/default-icon.html b/onionr/static-data/www/ui/common/default-icon.html deleted file mode 100755 index 86ccf773..00000000 --- a/onionr/static-data/www/ui/common/default-icon.html +++ /dev/null @@ -1 +0,0 @@ -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 diff --git a/onionr/static-data/www/ui/common/footer.html b/onionr/static-data/www/ui/common/footer.html deleted file mode 100755 index 0143c2d8..00000000 --- a/onionr/static-data/www/ui/common/footer.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - diff --git a/onionr/static-data/www/ui/common/header.html b/onionr/static-data/www/ui/common/header.html deleted file mode 100755 index 2a2b4f56..00000000 --- a/onionr/static-data/www/ui/common/header.html +++ /dev/null @@ -1,30 +0,0 @@ -<$= LANG.ONIONR_TITLE $> - - - - - - - - - - diff --git a/onionr/static-data/www/ui/common/onionr-reply-creator.html b/onionr/static-data/www/ui/common/onionr-reply-creator.html deleted file mode 100755 index aafc8557..00000000 --- a/onionr/static-data/www/ui/common/onionr-reply-creator.html +++ /dev/null @@ -1,31 +0,0 @@ - -
-
-
-
-
- -
-
- - - - -
- - -
-
-
-
-
- -
-
-
- diff --git a/onionr/static-data/www/ui/common/onionr-timeline-post.html b/onionr/static-data/www/ui/common/onionr-timeline-post.html deleted file mode 100755 index 67ec158c..00000000 --- a/onionr/static-data/www/ui/common/onionr-timeline-post.html +++ /dev/null @@ -1,32 +0,0 @@ - -
-
-
-
- -
-
-
- - -
- -
-
- -
- $content -
- -
- $liked - <$= LANG.POST_REPLY $> -
-
-
-
-
- diff --git a/onionr/static-data/www/ui/common/onionr-timeline-reply-creator.html b/onionr/static-data/www/ui/common/onionr-timeline-reply-creator.html deleted file mode 100755 index 4cb95b02..00000000 --- a/onionr/static-data/www/ui/common/onionr-timeline-reply-creator.html +++ /dev/null @@ -1,30 +0,0 @@ - -
-
-
-
-
-
- -
-
- - - - -
- - -
-
-
- -
-
-
- diff --git a/onionr/static-data/www/ui/common/onionr-timeline-reply.html b/onionr/static-data/www/ui/common/onionr-timeline-reply.html deleted file mode 100755 index cc8a312e..00000000 --- a/onionr/static-data/www/ui/common/onionr-timeline-reply.html +++ /dev/null @@ -1,31 +0,0 @@ - -
-
-
-
- -
-
-
- - -
- -
-
- -
- $content -
- -
- $liked - <$= LANG.POST_REPLY $> -
-
-
-
-
- diff --git a/onionr/static-data/www/ui/compile.py b/onionr/static-data/www/ui/compile.py deleted file mode 100755 index e991af08..00000000 --- a/onionr/static-data/www/ui/compile.py +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/python3 - -import shutil, os, re, json, traceback - -# get user's config -settings = {} -with open('config.json', 'r') as file: - settings = json.loads(file.read()) - -# "hardcoded" config, not for user to mess with -HEADER_FILE = 'common/header.html' -FOOTER_FILE = 'common/footer.html' -SRC_DIR = 'src/' -DST_DIR = 'dist/' -HEADER_STRING = '
' -FOOTER_STRING = '