diff --git a/.gitignore b/.gitignore index 6edc23ff..6fcdd586 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ onionr/data-encrypted.dat onionr/.onionr-lock core .vscode/* +venv/* +onionr/fs* diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index f2ab3397..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "onionr/bitpeer"] - path = onionr/bitpeer - url = https://github.com/beardog108/bitpeer.py diff --git a/Dockerfile b/Dockerfile index c83de87d..e6132726 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ FROM ubuntu:bionic ENV HOME /root #Install needed packages -RUN apt update && apt install -y python3 python3-dev python3-pip tor locales nano +RUN apt update && apt install -y python3 python3-dev python3-pip tor locales nano sqlite3 RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ locale-gen diff --git a/RUN-LINUX.sh b/RUN-LINUX.sh index 8f9a4b37..286a0f7f 100755 --- a/RUN-LINUX.sh +++ b/RUN-LINUX.sh @@ -1,3 +1,4 @@ #!/bin/sh +cd "$(dirname "$0")" cd onionr/ ./onionr.py "$@" diff --git a/onionr/api.py b/onionr/api.py index d0407388..3efc11b8 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - P2P Anonymous Storage Network This file handles all incoming http requests to the client, using Flask ''' @@ -20,9 +20,9 @@ import flask from flask import request, Response, abort, send_from_directory from multiprocessing import Process -from gevent.wsgi import WSGIServer +from gevent.pywsgi import WSGIServer import sys, random, threading, hmac, hashlib, base64, time, math, os, json -from core import Core +import core from onionrblockapi import Block import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config @@ -37,6 +37,9 @@ class API: ''' 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 @@ -69,7 +72,7 @@ class API: logger.debug('%s not in %s' % (path, mimetypes)) return 'text/plain' - def __init__(self, debug): + def __init__(self, debug, API_VERSION): ''' Initialize the api server, preping variables for later use @@ -88,7 +91,7 @@ class API: self.debug = debug self._privateDelayTime = 3 - self._core = Core() + self._core = core.Core() self._crypto = onionrcrypto.OnionrCrypto(self._core) self._utils = onionrutils.OnionrUtils(self._core) app = flask.Flask(__name__) @@ -102,7 +105,7 @@ class API: self.mimeType = 'text/plain' self.overrideCSP = False - with open('data/time-bypass.txt', 'w') as bypass: + with open(self._core.dataDir + 'time-bypass.txt', 'w') as bypass: bypass.write(self.timeBypassToken) if not debug and not self._developmentMode: @@ -111,7 +114,7 @@ class API: else: self.host = '127.0.0.1' - with open('data/host.txt', 'w') as file: + with open(self._core.dataDir + 'host.txt', 'w') as file: file.write(self.host) @app.before_request @@ -133,14 +136,14 @@ class API: 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['server'] = 'Onionr' + resp.headers['api'] = API_VERSION # reset to text/plain to help prevent browser attacks self.mimeType = 'text/plain' self.overrideCSP = False return resp - + @app.route('/www/private/') def www_private(path): startTime = math.floor(time.time()) @@ -466,7 +469,7 @@ class API: elif action == 'getData': resp = '' if self._utils.validateHash(data): - if os.path.exists('data/blocks/' + data + '.dat'): + 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: @@ -515,7 +518,7 @@ class API: while len(self._core.hsAddress) == 0: self._core.refreshFirstStartVars() time.sleep(0.5) - self.http_server = WSGIServer((self.host, bindPort), app) + self.http_server = WSGIServer((self.host, bindPort), app, log=None) self.http_server.serve_forever() except KeyboardInterrupt: pass diff --git a/onionr/communicator2.py b/onionr/communicator2.py index 1c8329da..08806569 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 ''' - Onionr - P2P Microblogging Platform & Social network. + Onionr - P2P Anonymous Storage Network This file contains both the OnionrCommunicate class for communcating with peers and code to operate as a daemon, getting commands from the command queue database (see core.Core.daemonQueue) @@ -19,9 +19,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import sys, os, core, config, json, requests, time, logger, threading, base64, onionr +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 +import onionrdaemontools, onionrsockets, onionrchat +from dependencies import secrets from defusedxml import minidom class OnionrCommunicatorDaemon: @@ -51,6 +52,8 @@ class OnionrCommunicatorDaemon: # lists of connected peers and peers we know we can't reach currently self.onlinePeers = [] self.offlinePeers = [] + self.cooldownPeer = {} + self.connectTimes = {} self.peerProfiles = [] # list of peer's profiles (onionrpeers.PeerProfile instances) # amount of threads running by name, used to prevent too many @@ -77,29 +80,39 @@ class OnionrCommunicatorDaemon: #self.daemonTools = onionrdaemontools.DaemonTools(self) self.daemonTools = onionrdaemontools.DaemonTools(self) + self._chat = onionrchat.OnionrChat(self) + if debug or developmentMode: - OnionrCommunicatorTimers(self, self.heartbeat, 10) + OnionrCommunicatorTimers(self, self.heartbeat, 30) # Set timers, function reference, seconds # requiresPeer True means the timer function won't fire if we have no connected peers - # TODO: make some of these timer counts configurable OnionrCommunicatorTimers(self, self.daemonCommands, 5) OnionrCommunicatorTimers(self, self.detectAPICrash, 5) - peerPoolTimer = OnionrCommunicatorTimers(self, self.getOnlinePeers, 60) + peerPoolTimer = OnionrCommunicatorTimers(self, self.getOnlinePeers, 60, 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) OnionrCommunicatorTimers(self, self.clearOfflinePeer, 58) OnionrCommunicatorTimers(self, self.daemonTools.cleanOldBlocks, 65) - OnionrCommunicatorTimers(self, self.lookupKeys, 60, requiresPeer=True) OnionrCommunicatorTimers(self, self.lookupAdders, 60, requiresPeer=True) + OnionrCommunicatorTimers(self, self.daemonTools.cooldownPeer, 30, requiresPeer=True) netCheckTimer = OnionrCommunicatorTimers(self, self.daemonTools.netCheck, 600) announceTimer = OnionrCommunicatorTimers(self, self.daemonTools.announceNode, 305, requiresPeer=True, maxThreads=1) cleanupTimer = OnionrCommunicatorTimers(self, self.peerCleanup, 300, requiresPeer=True) + forwardSecrecyTimer = OnionrCommunicatorTimers(self, self.daemonTools.cleanKeys, 15) # set loop to execute instantly to load up peer pool (replaced old pool init wait) peerPoolTimer.count = (peerPoolTimer.frequency - 1) cleanupTimer.count = (cleanupTimer.frequency - 60) announceTimer.count = (cleanupTimer.frequency - 60) + #forwardSecrecyTimer.count = (forwardSecrecyTimer.frequency - 990) + + self.socketServer = threading.Thread(target=onionrsockets.OnionrSocketServer, args=(self._core,)) + 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: @@ -114,21 +127,10 @@ class OnionrCommunicatorDaemon: pass logger.info('Goodbye.') + self._core.killSockets = True self._core._utils.localCommand('shutdown') # shutdown the api time.sleep(0.5) - def lookupKeys(self): - '''Lookup new keys''' - logger.debug('Looking up new keys...') - tryAmount = 1 - for i in range(tryAmount): # amount of times to ask peers for new keys - # Download new key list from random online peers - peer = self.pickOnlinePeer() - newKeys = self.peerAction(peer, action='kex') - self._core._utils.mergeKeys(newKeys) - self.decrementThreadCount('lookupKeys') - return - def lookupAdders(self): '''Lookup new peer addresses''' logger.info('LOOKING UP NEW ADDRESSES') @@ -147,10 +149,13 @@ class OnionrCommunicatorDaemon: newBlocks = '' existingBlocks = self._core.getBlockList() triedPeers = [] # list of peers we've tried this time around + maxBacklog = 1560 # Max amount of *new* block hashes to have already in queue, to avoid memory exhaustion for i in range(tryAmount): - # check if disk allocation is used + if len(self.blockQueue) >= maxBacklog: + break if not self.isOnline: break + # check if disk allocation is used if self._core._utils.storageCounter.isFull(): logger.debug('Not looking up new blocks due to maximum amount of allowed disk space used') break @@ -180,6 +185,7 @@ class OnionrCommunicatorDaemon: 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): + # TODO ensure block starts with minimum difficulty before adding to queue self.blockQueue.append(i) # add blocks to download queue self.decrementThreadCount('lookupBlocks') return @@ -207,7 +213,7 @@ class OnionrCommunicatorDaemon: logger.info("Attempting to download %s..." % blockHash) peerUsed = self.pickOnlinePeer() content = self.peerAction(peerUsed, 'getData', data=blockHash) # block content from random peer (includes metadata) - if content != False: + if content != False and len(content) > 0: try: content = content.encode() except AttributeError: @@ -253,7 +259,10 @@ class OnionrCommunicatorDaemon: onionrpeers.PeerProfiles(peerUsed, self._core).addScore(-50) logger.warn('Block hash validation failed for ' + blockHash + ' got ' + tempHash) if removeFromQueue: - self.blockQueue.remove(blockHash) # remove from block queue both if success or false + try: + self.blockQueue.remove(blockHash) # remove from block queue both if success or false + except ValueError: + pass self.currentDownloading.remove(blockHash) self.decrementThreadCount('getBlocks') return @@ -296,7 +305,7 @@ class OnionrCommunicatorDaemon: '''Manages the self.onlinePeers attribute list, connects to more peers if we have none connected''' logger.info('Refreshing peer pool.') - maxPeers = 6 + maxPeers = int(config.get('peers.maxConnect')) needed = maxPeers - len(self.onlinePeers) for i in range(needed): @@ -339,7 +348,7 @@ class OnionrCommunicatorDaemon: for address in peerList: if not config.get('tor.v3onions') and len(address) == 62: continue - if len(address) == 0 or address in tried or address in self.onlinePeers: + if len(address) == 0 or address in tried or address in self.onlinePeers or address in self.cooldownPeer: continue if self.shutdown: return @@ -348,6 +357,7 @@ class OnionrCommunicatorDaemon: time.sleep(0.1) if address not in self.onlinePeers: self.onlinePeers.append(address) + self.connectTimes[address] = self._core._utils.getEpoch() retData = address # add peer to profile list if they're not in it @@ -362,6 +372,17 @@ class OnionrCommunicatorDaemon: logger.debug('Failed to connect to ' + address) return retData + def removeOnlinePeer(self, peer): + '''Remove an online peer''' + try: + del self.connectTimes[peer] + except KeyError: + pass + try: + self.onlinePeers.remove(peer) + except ValueError: + pass + def peerCleanup(self): '''This just calls onionrpeers.cleanupPeers, which removes dead or bad peers (offline too long, too slow)''' onionrpeers.peerCleanup(self._core) @@ -393,8 +414,9 @@ class OnionrCommunicatorDaemon: if retData == False: try: self.getPeerProfileInstance(peer).addScore(-10) - self.onlinePeers.remove(peer) - self.getOnlinePeers() # Will only add a new peer to pool if needed + self.removeOnlinePeer(peer) + if action != 'ping': + self.getOnlinePeers() # Will only add a new peer to pool if needed except ValueError: pass else: @@ -430,16 +452,15 @@ class OnionrCommunicatorDaemon: if cmd[0] == 'shutdown': self.shutdown = True elif cmd[0] == 'announceNode': - self.announce(cmd[1]) + if len(self.onlinePeers) > 0: + self.announce(cmd[1]) + else: + logger.warn("Not introducing, since I have no connected nodes.") elif cmd[0] == 'runCheck': logger.debug('Status check; looks good.') - open('data/.runcheck', 'w+').close() + open(self._core.dataDir + '.runcheck', 'w+').close() elif cmd[0] == 'connectedPeers': self.printOnlinePeers() - elif cmd[0] == 'kex': - for i in self.timers: - if i.timerFunction.__name__ == 'lookupKeys': - i.count = (i.frequency - 1) elif cmd[0] == 'pex': for i in self.timers: if i.timerFunction.__name__ == 'lookupAdders': @@ -447,6 +468,17 @@ class OnionrCommunicatorDaemon: elif cmd[0] == 'uploadBlock': self.blockToUpload = cmd[1] threading.Thread(target=self.uploadBlock).start() + elif cmd[0] == 'startSocket': + # Create our own socket server + socketInfo = json.loads(cmd[1]) + socketInfo['id'] = uuid.uuid4() + self._core.startSocket = socketInfo + elif cmd[0] == 'addSocket': + # Socket server was created for us + socketInfo = json.loads(cmd[1]) + peer = socketInfo['peer'] + reason = socketInfo['reason'] + threading.Thread(target=self.socketClient.startSocket, args=(peer, reason)).start() else: logger.info('Recieved daemonQueue command:' + cmd[0]) @@ -476,9 +508,7 @@ class OnionrCommunicatorDaemon: def announce(self, peer): '''Announce to peers our address''' - if self.daemonTools.announceNode(): - logger.info('Successfully introduced node to ' + peer) - else: + if self.daemonTools.announceNode() == False: logger.warn('Could not introduce node.') def detectAPICrash(self): diff --git a/onionr/config.py b/onionr/config.py index 880b4dba..1e782dbe 100644 --- a/onionr/config.py +++ b/onionr/config.py @@ -20,7 +20,14 @@ import os, json, logger -_configfile = os.path.abspath('data/config.json') +try: + dataDir = os.environ['ONIONR_HOME'] + if not dataDir.endswith('/'): + dataDir += '/' +except KeyError: + dataDir = 'data/' + +_configfile = os.path.abspath(dataDir + 'config.json') _config = {} def get(key, default = None): diff --git a/onionr/core.py b/onionr/core.py index 9d0b831d..fab7b04a 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - P2P Anonymous Storage Network Core Onionr library, useful for external programs. Handles peer & data processing ''' @@ -17,11 +17,11 @@ 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, getpass, simplecrypt, hashlib, nacl, logger, json, netcontroller, math, config +import sqlite3, os, sys, time, math, base64, tarfile, nacl, logger, json, netcontroller, math, config from onionrblockapi import Block import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions, onionrvalues -import onionrblacklist +import onionrblacklist, onionrchat, onionrusers import dbcreator if sys.version_info < (3, 6): try: @@ -35,34 +35,52 @@ class Core: ''' Initialize Core Onionr library ''' + try: - self.queueDB = 'data/queue.db' - self.peerDB = 'data/peers.db' - self.blockDB = 'data/blocks.db' - self.blockDataLocation = 'data/blocks/' - self.addressDB = 'data/address.db' + self.dataDir = os.environ['ONIONR_HOME'] + if not self.dataDir.endswith('/'): + self.dataDir += '/' + except KeyError: + self.dataDir = 'data/' + + try: + self.queueDB = self.dataDir + 'queue.db' + self.peerDB = self.dataDir + 'peers.db' + self.blockDB = self.dataDir + 'blocks.db' + self.blockDataLocation = self.dataDir + 'blocks/' + self.addressDB = self.dataDir + 'address.db' self.hsAddress = '' self.bootstrapFileLocation = 'static-data/bootstrap-nodes.txt' self.bootstrapList = [] self.requirements = onionrvalues.OnionrValues() self.torPort = torPort - self.dataNonceFile = 'data/block-nonces.dat' + self.dataNonceFile = self.dataDir + 'block-nonces.dat' self.dbCreate = dbcreator.DBCreator(self) + self.forwardKeysFile = self.dataDir + 'forward-keys.db' - self.usageFile = 'data/disk-usage.txt' + # Socket data, defined here because of multithreading constraints with gevent + self.killSockets = False + self.startSocket = {} + self.socketServerConnData = {} + self.socketReasons = {} + self.socketServerResponseData = {} + + self.usageFile = self.dataDir + 'disk-usage.txt' self.config = config self.maxBlockSize = 10000000 # max block size in bytes - if not os.path.exists('data/'): - os.mkdir('data/') - if not os.path.exists('data/blocks/'): - os.mkdir('data/blocks/') + if not os.path.exists(self.dataDir): + os.mkdir(self.dataDir) + if not os.path.exists(self.dataDir + 'blocks/'): + os.mkdir(self.dataDir + 'blocks/') if not os.path.exists(self.blockDB): self.createBlockDB() + if not os.path.exists(self.forwardKeysFile): + self.dbCreate.createForwardKeyDB() - if os.path.exists('data/hs/hostname'): - with open('data/hs/hostname', 'r') as hs: + if os.path.exists(self.dataDir + '/hs/hostname'): + with open(self.dataDir + '/hs/hostname', 'r') as hs: self.hsAddress = hs.read().strip() # Load bootstrap address list @@ -87,8 +105,8 @@ class Core: def refreshFirstStartVars(self): '''Hack to refresh some vars which may not be set on first start''' - if os.path.exists('data/hs/hostname'): - with open('data/hs/hostname', 'r') as hs: + if os.path.exists(self.dataDir + '/hs/hostname'): + with open(self.dataDir + '/hs/hostname', 'r') as hs: self.hsAddress = hs.read().strip() def addPeer(self, peerID, powID, name=''): @@ -102,10 +120,12 @@ class Core: logger.warn("POW token for pubkey base64 representation exceeded 120 bytes, is " + str(sys.getsizeof(powID))) return False - conn = sqlite3.connect(self.peerDB) + events.event('pubkey_add', data = {'key': peerID}, onionr = None) + + conn = sqlite3.connect(self.peerDB, timeout=10) hashID = self._crypto.pubKeyHashID(peerID) c = conn.cursor() - t = (peerID, name, 'unknown', hashID, powID) + t = (peerID, name, 'unknown', hashID, powID, 0) for i in c.execute("SELECT * FROM PEERS where id = '" + peerID + "';"): try: @@ -116,7 +136,7 @@ class Core: pass except IndexError: pass - c.execute('INSERT INTO peers (id, name, dateSeen, pow, hashID) VALUES(?, ?, ?, ?, ?);', t) + c.execute('INSERT INTO peers (id, name, dateSeen, pow, hashID, trust) VALUES(?, ?, ?, ?, ?, ?);', t) conn.commit() conn.close() @@ -126,11 +146,11 @@ class Core: ''' Add an address to the address database (only tor currently) ''' - if address == config.get('i2p.ownAddr', None): + if address == config.get('i2p.ownAddr', None) or address == self.hsAddress: return False if self._utils.validateID(address): - conn = sqlite3.connect(self.addressDB) + conn = sqlite3.connect(self.addressDB, timeout=10) 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 @@ -162,7 +182,7 @@ class Core: Remove an address from the address database ''' if self._utils.validateID(address): - conn = sqlite3.connect(self.addressDB) + conn = sqlite3.connect(self.addressDB, timeout=10) c = conn.cursor() t = (address,) c.execute('Delete from adders where address=?;', t) @@ -181,13 +201,13 @@ class Core: **You may want blacklist.addToDB(blockHash) ''' if self._utils.validateHash(block): - conn = sqlite3.connect(self.blockDB) + conn = sqlite3.connect(self.blockDB, timeout=10) c = conn.cursor() t = (block,) c.execute('Delete from hashes where hash=?;', t) conn.commit() conn.close() - blockFile = 'data/blocks/' + block + '.dat' + blockFile = self.dataDir + '/blocks/' + block + '.dat' dataSize = 0 try: ''' Get size of data when loaded as an object/var, rather than on disk, @@ -228,7 +248,7 @@ class Core: raise Exception('Block db does not exist') if self._utils.hasBlock(newHash): return - conn = sqlite3.connect(self.blockDB) + conn = sqlite3.connect(self.blockDB, timeout=10) c = conn.cursor() currentTime = self._utils.getEpoch() if selfInsert or dataSaved: @@ -256,14 +276,6 @@ class Core: return data - def _getSha3Hash(self, data): - hasher = hashlib.sha3_256() - if not type(data) is bytes: - data = data.encode() - hasher.update(data) - dataHash = hasher.hexdigest() - return dataHash - def setData(self, data): ''' Set the data assciated with a hash @@ -274,7 +286,7 @@ class Core: if not type(data) is bytes: data = data.encode() - dataHash = self._getSha3Hash(data) + dataHash = self._crypto.sha3Hash(data) if type(dataHash) is bytes: dataHash = dataHash.decode() @@ -287,7 +299,7 @@ class Core: blockFile = open(blockFileName, 'wb') blockFile.write(data) blockFile.close() - conn = sqlite3.connect(self.blockDB) + conn = sqlite3.connect(self.blockDB, timeout=10) c = conn.cursor() c.execute("UPDATE hashes SET dataSaved=1 WHERE hash = '" + dataHash + "';") conn.commit() @@ -299,42 +311,6 @@ class Core: return dataHash - def dataDirEncrypt(self, password): - ''' - Encrypt the data directory on Onionr shutdown - ''' - if os.path.exists('data.tar'): - os.remove('data.tar') - tar = tarfile.open("data.tar", "w") - for name in ['data']: - tar.add(name) - tar.close() - tarData = open('data.tar', 'r', encoding = "ISO-8859-1").read() - encrypted = simplecrypt.encrypt(password, tarData) - open('data-encrypted.dat', 'wb').write(encrypted) - os.remove('data.tar') - - return - - def dataDirDecrypt(self, password): - ''' - Decrypt the data directory on startup - ''' - if not os.path.exists('data-encrypted.dat'): - return (False, 'encrypted archive does not exist') - data = open('data-encrypted.dat', 'rb').read() - try: - decrypted = simplecrypt.decrypt(password, data) - except simplecrypt.DecryptionException: - return (False, 'wrong password (or corrupted archive)') - else: - open('data.tar', 'wb').write(decrypted) - tar = tarfile.open('data.tar') - tar.extractall() - tar.close() - - return (True, '') - def daemonQueue(self): ''' Gives commands to the communication proccess/daemon by reading an sqlite3 database @@ -343,16 +319,16 @@ class Core: ''' retData = False if not os.path.exists(self.queueDB): - self.makeDaemonDB() + self.dbCreate.createDaemonDB() else: - conn = sqlite3.connect(self.queueDB) + 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'): retData = row break except sqlite3.OperationalError: - self.makeDaemonDB() + self.dbCreate.createDaemonDB() else: if retData != False: c.execute('DELETE FROM commands WHERE id=?;', (retData[3],)) @@ -363,38 +339,32 @@ class Core: return retData - def makeDaemonDB(self): - '''generate the daemon queue db''' - conn = sqlite3.connect(self.queueDB) - c = conn.cursor() - # Create table - c.execute('''CREATE TABLE commands - (id integer primary key autoincrement, command text, data text, date text)''') - conn.commit() - conn.close() - def daemonQueueAdd(self, command, data=''): ''' 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) + conn = sqlite3.connect(self.queueDB, timeout=10) c = conn.cursor() t = (command, data, date) - c.execute('INSERT INTO commands (command, data, date) VALUES(?, ?, ?)', t) - conn.commit() - conn.close() - + try: + c.execute('INSERT INTO commands (command, data, date) VALUES(?, ?, ?)', t) + conn.commit() + conn.close() + except sqlite3.OperationalError: + retData = False + self.daemonQueue() events.event('queue_push', data = {'command': command, 'data': data}, onionr = None) - return + return retData def clearDaemonQueue(self): ''' Clear the daemon queue (somewhat dangerous) ''' - conn = sqlite3.connect(self.queueDB) + conn = sqlite3.connect(self.queueDB, timeout=10) c = conn.cursor() try: c.execute('DELETE FROM commands;') @@ -410,7 +380,7 @@ class Core: ''' Return a list of addresses ''' - conn = sqlite3.connect(self.addressDB) + conn = sqlite3.connect(self.addressDB, timeout=10) c = conn.cursor() if randomOrder: addresses = c.execute('SELECT * FROM adders ORDER BY RANDOM();') @@ -422,19 +392,23 @@ class Core: conn.close() return addressList - def listPeers(self, randomOrder=True, getPow=False): + def listPeers(self, randomOrder=True, getPow=False, trust=0): ''' Return a list of public keys (misleading function name) randomOrder determines if the list should be in a random order + trust sets the minimum trust to list ''' - conn = sqlite3.connect(self.peerDB) + conn = sqlite3.connect(self.peerDB, timeout=10) c = conn.cursor() payload = "" + if trust not in (0, 1, 2): + logger.error('Tried to select invalid trust.') + return if randomOrder: - payload = 'SELECT * FROM peers ORDER BY RANDOM();' + payload = 'SELECT * FROM peers where trust >= %s ORDER BY RANDOM();' % (trust,) else: - payload = 'SELECT * FROM peers;' + payload = 'SELECT * FROM peers where trust >= %s;' % (trust,) peerList = [] for i in c.execute(payload): try: @@ -462,18 +436,17 @@ class Core: id text 0 name text, 1 adders text, 2 - forwardKey text, 3 - dateSeen not null, 4 - bytesStored int, 5 - trust int 6 - pubkeyExchanged int 7 - hashID text 8 - pow text 9 + dateSeen not null, 3 + bytesStored int, 4 + trust int 5 + pubkeyExchanged int 6 + hashID text 7 + pow text 8 ''' - conn = sqlite3.connect(self.peerDB) + conn = sqlite3.connect(self.peerDB, timeout=10) c = conn.cursor() command = (peer,) - infoNumbers = {'id': 0, 'name': 1, 'adders': 2, 'forwardKey': 3, 'dateSeen': 4, 'bytesStored': 5, 'trust': 6, 'pubkeyExchanged': 7, 'hashID': 8} + infoNumbers = {'id': 0, 'name': 1, 'adders': 2, 'dateSeen': 3, 'bytesStored': 4, 'trust': 5, 'pubkeyExchanged': 6, 'hashID': 7} info = infoNumbers[info] iterCount = 0 retVal = '' @@ -492,7 +465,7 @@ class Core: ''' Update a peer for a key ''' - conn = sqlite3.connect(self.peerDB) + conn = sqlite3.connect(self.peerDB, timeout=10) c = conn.cursor() command = (data, peer) # TODO: validate key on whitelist @@ -516,7 +489,7 @@ class Core: failure int 6 lastConnect 7 ''' - conn = sqlite3.connect(self.addressDB) + 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} @@ -537,7 +510,7 @@ class Core: ''' Update an address for a key ''' - conn = sqlite3.connect(self.addressDB) + conn = sqlite3.connect(self.addressDB, timeout=10) c = conn.cursor() command = (data, address) # TODO: validate key on whitelist @@ -553,7 +526,7 @@ class Core: ''' Get list of our blocks ''' - conn = sqlite3.connect(self.blockDB) + conn = sqlite3.connect(self.blockDB, timeout=10) c = conn.cursor() if unsaved: execute = 'SELECT hash FROM hashes WHERE dataSaved != 1 ORDER BY RANDOM();' @@ -570,7 +543,7 @@ class Core: ''' Returns the date a block was received ''' - conn = sqlite3.connect(self.blockDB) + conn = sqlite3.connect(self.blockDB, timeout=10) c = conn.cursor() execute = 'SELECT dateReceived FROM hashes WHERE hash=?;' args = (blockHash,) @@ -584,7 +557,7 @@ class Core: ''' Returns a list of blocks by the type ''' - conn = sqlite3.connect(self.blockDB) + conn = sqlite3.connect(self.blockDB, timeout=10) c = conn.cursor() if orderDate: execute = 'SELECT hash FROM hashes WHERE dataType=? ORDER BY dateReceived;' @@ -595,15 +568,28 @@ class Core: for row in c.execute(execute, args): for i in row: rows.append(i) - return rows + def getExpiredBlocks(self): + '''Returns a list of expired blocks''' + conn = sqlite3.connect(self.blockDB, timeout=10) + c = conn.cursor() + date = int(self._utils.getEpoch()) + + execute = 'SELECT hash FROM hashes WHERE expire <= %s ORDER BY dateReceived;' % (date,) + + rows = list() + for row in c.execute(execute): + for i in row: + rows.append(i) + return rows + def setBlockType(self, hash, blockType): ''' Sets the type of block ''' - conn = sqlite3.connect(self.blockDB) + conn = sqlite3.connect(self.blockDB, timeout=10) c = conn.cursor() c.execute("UPDATE hashes SET dataType='" + blockType + "' WHERE hash = '" + hash + "';") conn.commit() @@ -623,12 +609,13 @@ class Core: sig - optional signature by the author (not optional if author is specified) author - multi-round partial sha3-256 hash of authors public key dateClaimed - timestamp claimed inside the block, only as trustworthy as the block author is + expire - expire date for a block ''' - if key not in ('dateReceived', 'decrypted', 'dataType', 'dataFound', 'dataSaved', 'sig', 'author', 'dateClaimed'): + if key not in ('dateReceived', 'decrypted', 'dataType', 'dataFound', 'dataSaved', 'sig', 'author', 'dateClaimed', 'expire'): return False - conn = sqlite3.connect(self.blockDB) + conn = sqlite3.connect(self.blockDB, timeout=10) c = conn.cursor() args = (data, hash) c.execute("UPDATE hashes SET " + key + " = ? where hash = ?;", args) @@ -636,7 +623,7 @@ class Core: conn.close() return True - def insertBlock(self, data, header='txt', sign=False, encryptType='', symKey='', asymPeer='', meta = None): + def insertBlock(self, data, header='txt', sign=False, encryptType='', symKey='', asymPeer='', meta = None, expire=None): ''' Inserts a block into the network encryptType must be specified to encrypt a block @@ -673,8 +660,6 @@ class Core: meta['type'] = header meta['type'] = str(meta['type']) - jsonMeta = json.dumps(meta) - if encryptType in ('asym', 'sym', ''): metadata['encryptType'] = encryptType else: @@ -684,7 +669,20 @@ class Core: data = data.encode() except AttributeError: pass - # sign before encrypt, as unauthenticated crypto should not be a problem here + + if encryptType == 'asym': + try: + forwardEncrypted = onionrusers.OnionrUser(self, asymPeer).forwardEncrypt(data) + data = forwardEncrypted[0] + meta['forwardEnc'] = True + except onionrexceptions.InvalidPubkey: + onionrusers.OnionrUser(self, asymPeer).generateForwardKey() + else: + logger.info(forwardEncrypted) + onionrusers.OnionrUser(self, asymPeer).generateForwardKey() + fsKey = onionrusers.OnionrUser(self, asymPeer).getGeneratedForwardKeys()[0] + meta['newFSKey'] = fsKey[0] + jsonMeta = json.dumps(meta) if sign: signature = self._crypto.edSign(jsonMeta.encode() + data, key=self._crypto.privKey, encodeResult=True) signer = self._crypto.pubKey @@ -692,8 +690,11 @@ class Core: if len(jsonMeta) > 1000: raise onionrexceptions.InvalidMetadata('meta in json encoded form must not exceed 1000 bytes') + user = onionrusers.OnionrUser(self, symKey) + # encrypt block metadata/sig/content if encryptType == 'sym': + if len(symKey) < self.requirements.passwordLength: raise onionrexceptions.SecurityError('Weak encryption key') jsonMeta = self._crypto.symmetricEncrypt(jsonMeta, key=symKey, returnEncoded=True).decode() @@ -702,6 +703,8 @@ class Core: signer = self._crypto.symmetricEncrypt(signer, key=symKey, returnEncoded=True).decode() elif encryptType == 'asym': 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() @@ -714,6 +717,11 @@ class Core: metadata['sig'] = signature metadata['signer'] = signer metadata['time'] = str(self._utils.getEpoch()) + + # ensure expire is integer and of sane length + if type(expire) is not type(None): + assert len(str(int(expire))) < 14 + metadata['expire'] = expire # send block data (and metadata) to POW module to get tokenized block data proof = onionrproofs.POW(metadata, data) @@ -721,7 +729,8 @@ class Core: if payload != False: retData = self.setData(payload) self.addToBlockDB(retData, selfInsert=True, dataSaved=True) - self.setBlockType(retData, meta['type']) + #self.setBlockType(retData, meta['type']) + self._utils.processBlockMetadata(retData) self.daemonQueueAdd('uploadBlock', retData) if retData != False: diff --git a/onionr/dbcreator.py b/onionr/dbcreator.py index 19a9a7bd..d11aa4fa 100644 --- a/onionr/dbcreator.py +++ b/onionr/dbcreator.py @@ -61,8 +61,6 @@ class DBCreator: ID text not null, name text, adders text, - blockDBHash text, - forwardKey text, dateSeen not null, bytesStored int, trust int, @@ -70,6 +68,12 @@ class DBCreator: hashID text, pow text not null); ''') + c.execute('''CREATE TABLE forwardKeys( + peerKey text not null, + forwardKey text not null, + date int not null, + expire int not null + );''') conn.commit() conn.close() return @@ -87,6 +91,7 @@ class DBCreator: sig - optional signature by the author (not optional if author is specified) author - multi-round partial sha3-256 hash of authors public key dateClaimed - timestamp claimed inside the block, only as trustworthy as the block author is + expire int - block expire date in epoch ''' if os.path.exists(self.core.blockDB): raise Exception("Block database already exists") @@ -101,9 +106,42 @@ class DBCreator: dataSaved int, sig text, author text, - dateClaimed int + dateClaimed int, + expire int ); ''') conn.commit() conn.close() - return \ No newline at end of file + return + + def createForwardKeyDB(self): + ''' + Create the forward secrecy key db (*for *OUR* keys*) + ''' + if os.path.exists(self.core.forwardKeysFile): + raise Exception("Block database already exists") + conn = sqlite3.connect(self.core.forwardKeysFile) + c = conn.cursor() + c.execute('''CREATE TABLE myForwardKeys( + peer text not null, + publickey text not null, + privatekey text not null, + date int not null, + expire int not null + ); + ''') + conn.commit() + conn.close() + return + + def createDaemonDB(self): + ''' + Create the daemon queue database + ''' + 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)''') + conn.commit() + conn.close() \ No newline at end of file diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py index 3749ce7a..97e73296 100644 --- a/onionr/netcontroller.py +++ b/onionr/netcontroller.py @@ -18,21 +18,37 @@ along with this program. If not, see . ''' -import subprocess, os, random, sys, logger, time, signal, config +import subprocess, os, random, sys, logger, time, signal, config, base64 +from stem.control import Controller from onionrblockapi import Block - +from dependencies import secrets class NetController: ''' This class handles hidden service setup on Tor and I2P ''' def __init__(self, hsPort): - self.torConfigLocation = 'data/torrc' + try: + self.dataDir = os.environ['ONIONR_HOME'] + if not self.dataDir.endswith('/'): + self.dataDir += '/' + except KeyError: + self.dataDir = 'data/' + + self.torConfigLocation = self.dataDir + 'torrc' self.readyState = False self.socksPort = random.randint(1024, 65535) self.hsPort = hsPort self._torInstnace = '' self.myID = '' + + if os.path.exists('./tor'): + self.torBinary = './tor' + elif os.path.exists('/usr/bin/tor'): + self.torBinary = '/usr/bin/tor' + else: + self.torBinary = 'tor' + config.reload() ''' if os.path.exists(self.torConfigLocation): @@ -52,13 +68,33 @@ class NetController: if config.get('tor.v3onions'): hsVer = 'HiddenServiceVersion 3' logger.info('Using v3 onions :)') + if os.path.exists(self.torConfigLocation): os.remove(self.torConfigLocation) + + # Set the Tor control password. Meant to make it harder to manipulate our Tor instance + plaintext = base64.b64encode(os.urandom(50)).decode() + config.set('tor.controlpassword', plaintext, savefile=True) + config.set('tor.socksport', self.socksPort, savefile=True) + + controlPort = random.randint(1025, 65535) + + config.set('tor.controlPort', controlPort, savefile=True) + + hashedPassword = subprocess.Popen([self.torBinary, '--hash-password', plaintext], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + for line in iter(hashedPassword.stdout.readline, b''): + password = line.decode() + if 'warn' not in password: + break + torrcData = '''SocksPort ''' + str(self.socksPort) + ''' -HiddenServiceDir data/hs/ +HiddenServiceDir ''' + self.dataDir + '''hs/ \n''' + hsVer + '''\n HiddenServicePort 80 127.0.0.1:''' + str(self.hsPort) + ''' -DataDirectory data/tordata/ +DataDirectory ''' + self.dataDir + '''tordata/ +CookieAuthentication 1 +ControlPort ''' + str(controlPort) + ''' +HashedControlPassword ''' + str(password) + ''' ''' torrc = open(self.torConfigLocation, 'w') torrc.write(torrcData) @@ -74,20 +110,20 @@ DataDirectory data/tordata/ self.generateTorrc() if os.path.exists('./tor'): - torBinary = './tor' + self.torBinary = './tor' elif os.path.exists('/usr/bin/tor'): - torBinary = '/usr/bin/tor' + self.torBinary = '/usr/bin/tor' else: - torBinary = 'tor' + self.torBinary = 'tor' try: - tor = subprocess.Popen([torBinary, '-f', self.torConfigLocation], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + tor = subprocess.Popen([self.torBinary, '-f', self.torConfigLocation], stdout=subprocess.PIPE, stderr=subprocess.PIPE) except FileNotFoundError: logger.fatal("Tor was not found in your path or the Onionr directory. Please install Tor and try again.") sys.exit(1) else: # Test Tor Version - torVersion = subprocess.Popen([torBinary, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + torVersion = subprocess.Popen([self.torBinary, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) for line in iter(torVersion.stdout.readline, b''): if 'Tor 0.2.' in line.decode(): logger.warn("Running 0.2.x Tor series, no support for v3 onion peers") @@ -111,11 +147,11 @@ DataDirectory data/tordata/ logger.debug('Finished starting Tor.', timestamp=True) self.readyState = True - myID = open('data/hs/hostname', 'r') + myID = open(self.dataDir + 'hs/hostname', 'r') self.myID = myID.read().replace('\n', '') myID.close() - torPidFile = open('data/torPid.txt', 'w') + torPidFile = open(self.dataDir + 'torPid.txt', 'w') torPidFile.write(str(tor.pid)) torPidFile.close() @@ -127,7 +163,7 @@ DataDirectory data/tordata/ ''' try: - pid = open('data/torPid.txt', 'r') + pid = open(self.dataDir + 'torPid.txt', 'r') pidN = pid.read() pid.close() except FileNotFoundError: @@ -140,7 +176,7 @@ DataDirectory data/tordata/ try: os.kill(int(pidN), signal.SIGTERM) - os.remove('data/torPid.txt') + os.remove(self.dataDir + 'torPid.txt') except ProcessLookupError: pass except FileNotFoundError: diff --git a/onionr/onionr.py b/onionr/onionr.py index 52b12e48..eceb7639 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 ''' - Onionr - P2P Microblogging Platform & Social network. + Onionr - P2P Anonymous Storage Network Onionr is the name for both the protocol and the original/reference software. @@ -26,13 +26,13 @@ if sys.version_info[0] == 2 or sys.version_info[1] < 5: print('Error, Onionr requires Python 3.4+') sys.exit(1) 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 onionrutils -from onionrutils import OnionrUtils from netcontroller import NetController from onionrblockapi import Block -import onionrproofs +import onionrproofs, onionrexceptions, onionrusers try: from urllib3.contrib.socks import SOCKSProxyManager @@ -40,9 +40,9 @@ 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_VERSION = '0.2.0' # for debugging and stuff +ONIONR_VERSION = '0.3.0' # for debugging and stuff ONIONR_VERSION_TUPLE = tuple(ONIONR_VERSION.split('.')) # (MAJOR, MINOR, VERSION) -API_VERSION = '4' # 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 fundemental 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): @@ -50,23 +50,31 @@ class Onionr: Main Onionr class. This is for the CLI program, and does not handle much of the logic. In general, external programs and plugins should not use this class. ''' + self.userRunDir = os.getcwd() # Directory user runs the program from try: os.chdir(sys.path[0]) except FileNotFoundError: pass + try: + self.dataDir = os.environ['ONIONR_HOME'] + if not self.dataDir.endswith('/'): + self.dataDir += '/' + except KeyError: + self.dataDir = 'data/' + # Load global configuration data - data_exists = os.path.exists('data/') + data_exists = os.path.exists(self.dataDir) if not data_exists: - os.mkdir('data/') + os.mkdir(self.dataDir) if os.path.exists('static-data/default_config.json'): config.set_config(json.loads(open('static-data/default_config.json').read())) # this is the default config, it will be overwritten if a config file already exists. Else, it saves it else: # the default config file doesn't exist, try hardcoded config - config.set_config({'dev_mode': True, 'log': {'file': {'output': True, 'path': 'data/output.log'}, 'console': {'output': True, 'color': True}}}) + config.set_config({'dev_mode': True, 'log': {'file': {'output': True, 'path': self.dataDir + 'output.log'}, 'console': {'output': True, 'color': True}}}) if not data_exists: config.save() config.reload() # this will read the configuration file into memory @@ -78,7 +86,7 @@ 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')) + logger.set_file(config.get('log.file.path', '/tmp/onionr.log').replace('data/', self.dataDir)) logger.set_settings(settings) if str(config.get('general.dev_mode', True)).lower() == 'true': @@ -89,37 +97,27 @@ class Onionr: logger.set_level(logger.LEVEL_INFO) self.onionrCore = core.Core() - self.onionrUtils = OnionrUtils(self.onionrCore) + self.onionrUtils = onionrutils.OnionrUtils(self.onionrCore) # Handle commands self.debug = False # Whole application debugging - if os.path.exists('data-encrypted.dat'): - while True: - print('Enter password to decrypt:') - password = getpass.getpass() - result = self.onionrCore.dataDirDecrypt(password) - if os.path.exists('data/'): - break - else: - logger.error('Failed to decrypt: ' + result[1], timestamp = False) - else: - # If data folder does not exist - if not data_exists: - if not os.path.exists('data/blocks/'): - os.mkdir('data/blocks/') + # If data folder does not exist + if not data_exists: + if not os.path.exists(self.dataDir + 'blocks/'): + os.mkdir(self.dataDir + 'blocks/') - # Copy default plugins into plugins folder - if not os.path.exists(plugins.get_plugins_folder()): - if os.path.exists('static-data/default-plugins/'): - names = [f for f in os.listdir("static-data/default-plugins/") if not os.path.isfile(f)] - shutil.copytree('static-data/default-plugins/', plugins.get_plugins_folder()) + # Copy default plugins into plugins folder + if not os.path.exists(plugins.get_plugins_folder()): + if os.path.exists('static-data/default-plugins/'): + names = [f for f in os.listdir("static-data/default-plugins/") if not os.path.isfile(f)] + shutil.copytree('static-data/default-plugins/', plugins.get_plugins_folder()) - # Enable plugins - for name in names: - if not name in plugins.get_enabled_plugins(): - plugins.enable(name, self) + # Enable plugins + for name in names: + if not name in plugins.get_enabled_plugins(): + plugins.enable(name, self) for name in plugins.get_enabled_plugins(): if not os.path.exists(plugins.get_plugin_data_folder(name)): @@ -190,6 +188,10 @@ class Onionr: 'add-file': self.addFile, 'addfile': self.addFile, + + 'get-file': self.getFile, + 'getfile': self.getFile, + 'listconn': self.listConn, 'import-blocks': self.onionrUtils.importNewBlocks, @@ -210,7 +212,11 @@ class Onionr: 'getpass': self.printWebPassword, 'get-pass': self.printWebPassword, 'getpasswd': self.printWebPassword, - 'get-passwd': self.printWebPassword + 'get-passwd': self.printWebPassword, + + 'chat': self.startChat, + + 'friend': self.friendCmd } self.cmdhelp = { @@ -228,12 +234,14 @@ class Onionr: 'add-peer': 'Adds a peer to database', 'list-peers': 'Displays a list of peers', 'add-file': 'Create an Onionr block from a file', + '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', + 'friend': '[add|remove] [public key/id]' } # initialize plugins @@ -247,19 +255,64 @@ class Onionr: finally: self.execute(command) - if not self._developmentMode: - encryptionPassword = self.onionrUtils.getPassword('Enter password to encrypt directory: ') - self.onionrCore.dataDirEncrypt(encryptionPassword) - shutil.rmtree('data/') - return ''' THIS SECTION HANDLES THE COMMANDS ''' + 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 + + 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') + if friend not in self.onionrCore.listPeers(): + raise onionrexceptions.KeyNotKnown + friend = onionrusers.OnionrUser(self.onionrCore, friend) + 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 banBlock(self): try: @@ -567,7 +620,7 @@ class Onionr: ''' communicatorDaemon = './communicator2.py' - apiThread = Thread(target=api.API, args=(self.debug,)) + apiThread = Thread(target=api.API, args=(self.debug,API_VERSION)) apiThread.start() try: time.sleep(3) @@ -587,7 +640,7 @@ class Onionr: logger.info('Our Public key: ' + self.onionrCore._crypto.pubKey) time.sleep(1) #TODO make runable on windows - subprocess.Popen([communicatorDaemon, "run", str(net.socksPort)]) + communicatorProc = subprocess.Popen([communicatorDaemon, "run", str(net.socksPort)]) # Print nice header thing :) if config.get('general.display_header', True): self.header() @@ -596,6 +649,9 @@ class Onionr: 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') @@ -630,26 +686,23 @@ class Onionr: # define stats messages here totalBlocks = len(Block.getBlocks()) signedBlocks = len(Block.getBlocks(signed = True)) - powToken = self.onionrCore._crypto.pubKeyPowToken messages = { # info about local client - 'Onionr Daemon Status' : ((logger.colors.fg.green + 'Online') if self.onionrUtils.isCommunicatorRunning(timeout = 2) else logger.colors.fg.red + 'Offline'), + 'Onionr Daemon Status' : ((logger.colors.fg.green + 'Online') if self.onionrUtils.isCommunicatorRunning(timeout = 9) else logger.colors.fg.red + 'Offline'), 'Public Key' : self.onionrCore._crypto.pubKey, - 'POW Token' : powToken, - 'Combined' : self.onionrCore._crypto.pubKey + '-' + powToken, 'Human readable public key' : self.onionrCore._utils.getHumanReadableID(), 'Node Address' : self.get_hostname(), # file and folder size stats 'div1' : True, # this creates a solid line across the screen, a div - 'Total Block Size' : onionrutils.humanSize(onionrutils.size('data/blocks/')), - 'Total Plugin Size' : onionrutils.humanSize(onionrutils.size('data/plugins/')), - 'Log File Size' : onionrutils.humanSize(onionrutils.size('data/output.log')), + 'Total Block Size' : onionrutils.humanSize(onionrutils.size(self.dataDir + 'blocks/')), + 'Total Plugin Size' : onionrutils.humanSize(onionrutils.size(self.dataDir + 'plugins/')), + 'Log File Size' : onionrutils.humanSize(onionrutils.size(self.dataDir + 'output.log')), # count stats 'div2' : True, 'Known Peers Count' : str(len(self.onionrCore.listPeers()) - 1), - 'Enabled Plugins Count' : str(len(config.get('plugins.enabled', list()))) + ' / ' + str(len(os.listdir('data/plugins/'))), + 'Enabled Plugins Count' : str(len(config.get('plugins.enabled', list()))) + ' / ' + str(len(os.listdir(self.dataDir + 'plugins/'))), 'Known Blocks Count' : str(totalBlocks), 'Percent Blocks Signed' : str(round(100 * signedBlocks / max(totalBlocks, 1), 2)) + '%' } @@ -715,7 +768,7 @@ class Onionr: def get_hostname(self): try: - with open('./data/hs/hostname', 'r') as hostname: + with open('./' + self.dataDir + 'hs/hostname', 'r') as hostname: return hostname.read().strip() except Exception: return None @@ -735,6 +788,27 @@ class Onionr: return columns + def getFile(self): + ''' + Get a file from onionr blocks + ''' + try: + fileName = sys.argv[2] + bHash = sys.argv[3] + except IndexError: + logger.error("Syntax %s %s" % (sys.argv[0], '/path/to/filename ')) + else: + print(fileName) + contents = None + if os.path.exists(fileName): + logger.error("File already exists") + return + if not self.onionrUtils.validateHash(bHash): + logger.error('Block hash is invalid') + return + Block.mergeChain(bHash, fileName) + return + def addFile(self): ''' Adds a file to the onionr network @@ -745,8 +819,9 @@ class Onionr: contents = None if not os.path.exists(filename): - logger.warn('That file does not exist. Improper path?') - + logger.error('That file does not exist. Improper path (specify full path)?') + return + logger.info('Adding file... this might take a long time.') try: blockhash = Block.createChain(file = filename) logger.info('File %s saved in block %s.' % (filename, blockhash)) @@ -756,7 +831,6 @@ class Onionr: logger.error('%s add-file ' % sys.argv[0], timestamp = False) def openUI(self): - import webbrowser url = 'http://127.0.0.1:%s/ui/index.html?timingToken=%s' % (config.get('client.port', 59496), self.onionrUtils.getTimeBypassToken()) print('Opening %s ...' % url) diff --git a/onionr/onionrblacklist.py b/onionr/onionrblacklist.py index 86823283..1d7e83f7 100644 --- a/onionr/onionrblacklist.py +++ b/onionr/onionrblacklist.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network. + Onionr - P2P Anonymous Storage Network This file handles maintenence of a blacklist database, for blocks and peers ''' @@ -20,7 +20,7 @@ import sqlite3, os, logger class OnionrBlackList: def __init__(self, coreInst): - self.blacklistDB = 'data/blacklist.db' + self.blacklistDB = coreInst.dataDir + 'blacklist.db' self._core = coreInst if not os.path.exists(self.blacklistDB): @@ -32,7 +32,8 @@ class OnionrBlackList: retData = False if not hashed.isalnum(): raise Exception("Hashed data is not alpha numeric") - + if len(hashed) > 64: + raise Exception("Hashed data is too large") for i in self._dbExecute("select * from blacklist where hash='%s'" % (hashed,)): retData = True # this only executes if an entry is present by that hash break @@ -95,9 +96,8 @@ class OnionrBlackList: ''' # we hash the data so we can remove data entirely from our node's disk hashed = self._core._utils.bytesToStr(self._core._crypto.sha3Hash(data)) - - if self.inBlacklist(hashed): - return + if len(hashed) > 64: + raise Exception("Hashed data is too large") if not hashed.isalnum(): raise Exception("Hashed data is not alpha numeric") @@ -109,7 +109,8 @@ class OnionrBlackList: int(expire) except ValueError: raise Exception("expire is not int") - #TODO check for length sanity + if self.inBlacklist(hashed): + return insert = (hashed,) blacklistDate = self._core._utils.getEpoch() self._dbExecute("insert into blacklist (hash, dataType, blacklistDate, expire) VALUES('%s', %s, %s, %s);" % (hashed, dataType, blacklistDate, expire)) diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py index e255c0ae..dbc05a0a 100644 --- a/onionr/onionrblockapi.py +++ b/onionr/onionrblockapi.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network. + Onionr - P2P Anonymous Storage Network This class contains the OnionrBlocks class which is a class for working with Onionr blocks ''' @@ -18,14 +18,14 @@ along with this program. If not, see . ''' -import core as onionrcore, logger, config, onionrexceptions, nacl.exceptions +import core as onionrcore, logger, config, onionrexceptions, nacl.exceptions, onionrusers import json, os, sys, datetime, base64 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): + def __init__(self, hash = None, core = None, type = None, content = None, expire=None): # take from arguments # sometimes people input a bytes object instead of str in `hash` if (not hash is None) and isinstance(hash, bytes): @@ -35,6 +35,7 @@ class Block: self.core = core self.btype = type self.bcontent = content + self.expire = expire # initialize variables self.valid = True @@ -90,9 +91,18 @@ class Block: self.signature = core._crypto.pubKeyDecrypt(self.signature, anonymous=anonymous, encodedData=encodedData) self.signer = core._crypto.pubKeyDecrypt(self.signer, anonymous=anonymous, encodedData=encodedData) self.signedData = json.dumps(self.bmetadata) + self.bcontent.decode() + try: + assert self.bmetadata['forwardEnc'] is True + except (AssertionError, KeyError) as e: + pass + else: + try: + self.bcontent = onionrusers.OnionrUser(self.getCore(), self.signer).forwardDecrypt(self.bcontent) + except (onionrexceptions.DecryptionError, nacl.exceptions.CryptoError) as e: + logger.error(str(e)) + pass except nacl.exceptions.CryptoError: - pass - #logger.debug('Could not decrypt block. Either invalid key or corrupted data') + logger.debug('Could not decrypt block. Either invalid key or corrupted data') else: retData = True self.decrypted = True @@ -149,7 +159,7 @@ class Block: # read from file if it's still None if blockdata is None: - filelocation = 'data/blocks/%s.dat' % self.getHash() + filelocation = self.core.dataDir + 'blocks/%s.dat' % self.getHash() if readfile: with open(filelocation, 'rb') as f: @@ -177,6 +187,7 @@ class Block: # signed data is jsonMeta + block content (no linebreak) self.signedData = (None if not self.isSigned() else self.getHeader('meta') + self.getContent()) self.date = self.getCore().getBlockDate(self.getHash()) + self.claimedTime = self.getHeader('time', None) if not self.getDate() is None: self.date = datetime.datetime.fromtimestamp(self.getDate()) @@ -226,7 +237,7 @@ class Block: blockFile.write(self.getRaw().encode()) self.update() else: - self.hash = self.getCore().insertBlock(self.getContent(), header = self.getType(), sign = sign) + self.hash = self.getCore().insertBlock(self.getContent(), header = self.getType(), sign = sign, expire=self.getExpire()) self.update() return self.getHash() @@ -239,6 +250,15 @@ class Block: # getters + def getExpire(self): + ''' + Returns the expire time for a block + + Outputs: + - (int): the expire time for a block, or None + ''' + return self.expire + def getHash(self): ''' Returns the hash of the block if saved to file @@ -726,7 +746,7 @@ class Block: if type(hash) == Block: blockfile = hash.getBlockFile() else: - blockfile = 'data/blocks/%s.dat' % hash + blockfile = onionrcore.Core().dataDir + 'blocks/%s.dat' % hash return os.path.exists(blockfile) and os.path.isfile(blockfile) diff --git a/onionr/onionrchat.py b/onionr/onionrchat.py new file mode 100644 index 00000000..80569e6e --- /dev/null +++ b/onionr/onionrchat.py @@ -0,0 +1,49 @@ +''' + 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) \ No newline at end of file diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py index 00c5a604..fe13ae01 100644 --- a/onionr/onionrcrypto.py +++ b/onionr/onionrcrypto.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - P2P Anonymous Storage Network This file handles Onionr's cryptography. ''' @@ -24,51 +24,32 @@ if sys.version_info[0] == 3 and sys.version_info[1] < 6: from dependencies import secrets elif sys.version_info[0] == 3 and sys.version_info[1] >= 6: import secrets +import config class OnionrCrypto: def __init__(self, coreInstance): + config.reload() self._core = coreInstance - self._keyFile = 'data/keys.txt' - self.keyPowFile = 'data/keyPow.txt' + self._keyFile = self._core.dataDir + 'keys.txt' self.pubKey = None self.privKey = None self.secrets = secrets - - self.pubKeyPowToken = None - #self.pubKeyPowHash = None - + self.HASH_ID_ROUNDS = 2000 # Load our own pub/priv Ed25519 keys, gen & save them if they don't exist if os.path.exists(self._keyFile): - with open('data/keys.txt', 'r') as keys: + with open(self._core.dataDir + 'keys.txt', 'r') as keys: keys = keys.read().split(',') self.pubKey = keys[0] self.privKey = keys[1] - try: - with open(self.keyPowFile, 'r') as powFile: - data = powFile.read() - self.pubKeyPowToken = data - except (FileNotFoundError, IndexError): - pass else: keys = self.generatePubKey() self.pubKey = keys[0] self.privKey = keys[1] with open(self._keyFile, 'w') as keyfile: keyfile.write(self.pubKey + ',' + self.privKey) - with open(self.keyPowFile, 'w') as keyPowFile: - proof = onionrproofs.DataPOW(self.pubKey) - logger.info('Doing necessary work to insert our public key') - while True: - time.sleep(0.2) - powToken = proof.getResult() - if powToken != False: - break - keyPowFile.write(base64.b64encode(powToken[1]).decode()) - self.pubKeyPowToken = powToken[1] - self.pubKeyPowHash = powToken[0] return def edVerify(self, data, key, sig, encodedData=True): @@ -76,7 +57,10 @@ class OnionrCrypto: try: key = nacl.signing.VerifyKey(key=key, encoder=nacl.encoding.Base32Encoder) except nacl.exceptions.ValueError: - logger.warn('Signature by unknown key (cannot reverse hash)') + #logger.debug('Signature by unknown key (cannot reverse hash)') + return False + except binascii.Error: + logger.warn('Could not load key for verification, invalid padding') return False retData = False sig = base64.b64decode(sig) @@ -125,7 +109,7 @@ class OnionrCrypto: encoding = nacl.encoding.RawEncoder if self.privKey != None and not anonymous: - ownKey = nacl.signing.SigningKey(seed=self.privKey, encoder=nacl.encoding.Base32Encoder) + 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) @@ -139,9 +123,9 @@ class OnionrCrypto: retVal = anonBox.encrypt(data, encoder=encoding) return retVal - def pubKeyDecrypt(self, data, pubkey='', anonymous=False, encodedData=False): + def pubKeyDecrypt(self, data, pubkey='', privkey='', anonymous=False, encodedData=False): '''pubkey decrypt (Curve25519, taken from Ed25519 pubkey)''' - retVal = False + decrypted = False if encodedData: encoding = nacl.encoding.Base64Encoder else: @@ -151,30 +135,14 @@ class OnionrCrypto: ourBox = nacl.public.Box(ownKey, pubkey) decrypted = ourBox.decrypt(data, encoder=encoding) elif anonymous: - anonBox = nacl.public.SealedBox(ownKey) + 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 symmetricPeerEncrypt(self, data, peer): - '''Salsa20 encrypt data to peer (with mac) - this function does not accept a key, it is a wrapper for encryption with a peer - ''' - key = self._core.getPeerInfo(4) - if type(key) != bytes: - key = self._core.getPeerInfo(2) - encrypted = self.symmetricEncrypt(data, key, encodedKey=True) - return encrypted - - def symmetricPeerDecrypt(self, data, peer): - '''Salsa20 decrypt data from peer (with mac) - this function does not accept a key, it is a wrapper for encryption with a peer - ''' - key = self._core.getPeerInfo(4) - if type(key) != bytes: - key = self._core.getPeerInfo(2) - decrypted = self.symmetricDecrypt(data, key, encodedKey=True) - return decrypted - def symmetricEncrypt(self, data, key, encodedKey=False, returnEncoded=True): '''Encrypt data to a 32-byte key (Salsa20-Poly1305 MAC)''' if encodedKey: @@ -282,7 +250,8 @@ class OnionrCrypto: pass difficulty = math.floor(dataLen / 1000000) - + 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() puzzle = mainHash[:difficulty] diff --git a/onionr/onionrdaemontools.py b/onionr/onionrdaemontools.py index 7509d421..cac06f9f 100644 --- a/onionr/onionrdaemontools.py +++ b/onionr/onionrdaemontools.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network. + Onionr - P2P Anonymous Storage Network Contains the CommunicatorUtils class which contains useful functions for the communicator daemon ''' @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import onionrexceptions, onionrpeers, onionrproofs, base64, logger +import onionrexceptions, onionrpeers, onionrproofs, base64, logger, onionrusers, sqlite3 +from dependencies import secrets class DaemonTools: def __init__(self, daemon): self.daemon = daemon @@ -51,23 +52,80 @@ class DaemonTools: 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.decrementThreadCount('announceNode') return retData def netCheck(self): '''Check if we are connected to the internet or not when we can't connect to any peers''' - if len(self.daemon.onlinePeers) != 0: + if len(self.daemon.onlinePeers) == 0: if not self.daemon._core._utils.checkNetwork(torPort=self.daemon.proxyPort): logger.warn('Network check failed, are you connected to the internet?') self.daemon.isOnline = False self.daemon.decrementThreadCount('netCheck') def cleanOldBlocks(self): - '''Delete old blocks if our disk allocation is full/near full''' + '''Delete old blocks if our disk allocation is full/near full, and also expired blocks''' + while self.daemon._core._utils.storageCounter.isFull(): oldest = self.daemon._core.getBlockList()[0] self.daemon._core._blacklist.addToDB(oldest) self.daemon._core.removeBlock(oldest) - logger.info('Deleted block: %s' % (oldest,)) - self.daemon.decrementThreadCount('cleanOldBlocks') \ No newline at end of file + logger.info('Deleted block: %s' % (oldest,)) + # Delete expired blocks + for bHash in self.daemon._core.getExpiredBlocks(): + self.daemon._core._blacklist.addToDB(bHash) + self.daemon._core.removeBlock(bHash) + self.daemon.decrementThreadCount('cleanOldBlocks') + + def cleanKeys(self): + '''Delete expired forward secrecy keys''' + conn = sqlite3.connect(self.daemon._core.peerDB, timeout=10) + c = conn.cursor() + time = self.daemon._core._utils.getEpoch() + deleteKeys = [] + for entry in c.execute("SELECT * FROM forwardKeys where expire <= ?", (time,)): + logger.info(entry[1]) + deleteKeys.append(entry[1]) + + for key in deleteKeys: + logger.info('Deleting forward key '+ key) + c.execute("DELETE from forwardKeys where forwardKey = ?", (key,)) + conn.commit() + conn.close() + + onionrusers.deleteExpiredKeys(self.daemon._core) + + self.daemon.decrementThreadCount('cleanKeys') + + def cooldownPeer(self): + '''Randomly add an online peer to cooldown, so we can connect a new one''' + onlinePeerAmount = len(self.daemon.onlinePeers) + minTime = 300 + cooldownTime = 600 + toCool = '' + tempConnectTimes = dict(self.daemon.connectTimes) + + # Remove peers from cooldown that have been there long enough + tempCooldown = dict(self.daemon.cooldownPeer) + for peer in tempCooldown: + if (self.daemon._core._utils.getEpoch() - tempCooldown[peer]) >= cooldownTime: + del self.daemon.cooldownPeer[peer] + + # Cool down a peer, if we have max connections alive for long enough + if onlinePeerAmount >= self.daemon._core.config.get('peers.maxConnect'): + finding = True + while finding: + try: + toCool = min(tempConnectTimes, key=tempConnectTimes.get) + if (self.daemon._core._utils.getEpoch() - tempConnectTimes[toCool]) < minTime: + del tempConnectTimes[toCool] + else: + finding = False + except ValueError: + break + else: + self.daemon.removeOnlinePeer(toCool) + self.daemon.cooldownPeer[toCool] = self.daemon._core._utils.getEpoch() + self.daemon.decrementThreadCount('cooldownPeer') \ No newline at end of file diff --git a/onionr/onionrexceptions.py b/onionr/onionrexceptions.py index 8044508f..d450e3ae 100644 --- a/onionr/onionrexceptions.py +++ b/onionr/onionrexceptions.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network. + Onionr - P2P Anonymous Storage Network This file contains exceptions for onionr ''' @@ -34,6 +34,12 @@ class OnlinePeerNeeded(Exception): class InvalidPubkey(Exception): pass +class KeyNotKnown(Exception): + pass + +class DecryptionError(Exception): + pass + # block exceptions class InvalidMetadata(Exception): pass @@ -59,7 +65,15 @@ class MissingPort(Exception): class InvalidAddress(Exception): pass +class InvalidAPIVersion(Exception): + pass + # file exceptions class DiskAllocationReached(Exception): + pass + +# onionrsocket exceptions + +class MissingAddress(Exception): pass \ No newline at end of file diff --git a/onionr/onionri2p.py b/onionr/onionri2p.py deleted file mode 100644 index baeb2977..00000000 --- a/onionr/onionri2p.py +++ /dev/null @@ -1,19 +0,0 @@ -''' - Onionr - P2P Microblogging Platform & Social network - - Funcitons for talking to I2P -''' -''' - 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 . -''' diff --git a/onionr/onionrpeers.py b/onionr/onionrpeers.py index a1f4aa8b..322db9ba 100644 --- a/onionr/onionrpeers.py +++ b/onionr/onionrpeers.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network. + Onionr - P2P Anonymous Storage Network This file contains both the PeerProfiles class for network profiling of Onionr nodes ''' @@ -90,13 +90,15 @@ def peerCleanup(coreInst): if PeerProfiles(address, coreInst).score < minScore: coreInst.removeAddress(address) try: - if (coreInst._utils.getEpoch() - coreInst.getPeerInfo(address, 4)) >= 600: + 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 diff --git a/onionr/onionrplugins.py b/onionr/onionrplugins.py index a699433c..ce038856 100644 --- a/onionr/onionrplugins.py +++ b/onionr/onionrplugins.py @@ -21,7 +21,14 @@ import os, re, importlib, config, logger import onionrevents as events -_pluginsfolder = 'data/plugins/' +try: + dataDir = os.environ['ONIONR_HOME'] + if not dataDir.endswith('/'): + dataDir += '/' +except KeyError: + dataDir = 'data/' + +_pluginsfolder = dataDir + 'plugins/' _instances = dict() def reload(onionr = None, stop_event = True): @@ -217,7 +224,7 @@ def get_plugin_data_folder(name, absolute = True): Returns the location of a plugin's data folder ''' - return get_plugins_folder(name, absolute) + 'data/' + return get_plugins_folder(name, absolute) + dataDir def check(): ''' diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index b93d5724..30ca4b6c 100644 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - P2P Anonymous Storage Network Proof of work module ''' @@ -19,7 +19,30 @@ ''' import nacl.encoding, nacl.hash, nacl.utils, time, math, threading, binascii, logger, sys, base64, json -import core +import core, config + +def getHashDifficulty(h): + ''' + Return the amount of leading zeroes in a hex hash string (h) + ''' + difficulty = 0 + assert type(h) is str + for character in h: + if character == '0': + difficulty += 1 + return difficulty + +def hashMeetsDifficulty(h): + ''' + Return bool for a hash string to see if it meets pow difficulty defined in config + ''' + config.reload() + hashDifficulty = getHashDifficulty(h) + expected = int(config.get('minimum_block_pow')) + if hashDifficulty >= expected: + return True + else: + return False class DataPOW: def __init__(self, data, forceDifficulty=0, threadCount = 5): @@ -27,6 +50,7 @@ class DataPOW: self.difficulty = 0 self.data = data self.threadCount = threadCount + config.reload() if forceDifficulty == 0: dataLen = sys.getsizeof(data) @@ -77,7 +101,6 @@ 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('Random value was: %s' % base64.b64encode(rand).decode()) self.result = (token, rand) def shutdown(self): @@ -128,7 +151,7 @@ class POW: dataLen = len(data) + len(json.dumps(metadata)) self.difficulty = math.floor(dataLen / 1000000) if self.difficulty <= 2: - self.difficulty = 4 + self.difficulty = int(config.get('general.minimum_block_pow')) try: self.data = self.data.encode() @@ -144,7 +167,7 @@ class POW: for i in range(max(1, threadCount)): t = threading.Thread(name = 'thread%s' % i, target = self.pow, args = (True,myCore)) t.start() - + self.myCore = myCore return def pow(self, reporting = False, myCore = None): @@ -177,7 +200,6 @@ class POW: endTime = math.floor(time.time()) if self.reporting: logger.debug('Found token after %s seconds: %s' % (endTime - startTime, token), timestamp=True) - logger.debug('Random value was: %s' % base64.b64encode(rand).decode()) def shutdown(self): self.hashing = False diff --git a/onionr/onionrsockets.py b/onionr/onionrsockets.py new file mode 100644 index 00000000..f20ad4f8 --- /dev/null +++ b/onionr/onionrsockets.py @@ -0,0 +1,171 @@ +''' + Onionr - P2P Anonymous Storage Network + + Onionr Socket interface +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import stem.control +import threading +import socks, config, uuid +import onionrexceptions, time, requests, onionrblockapi, logger +from dependencies import secrets +from gevent.pywsgi import WSGIServer +from flask import request, Response, abort +import flask +class OnionrSocketServer: + def __init__(self, coreInst): + self._core = coreInst + app = flask.Flask(__name__) + self._core.socketServerConnData = {} + self.bindPort = 0 + + self.sockets = {} + + while self.bindPort < 1024: + self.bindPort = secrets.randbelow(65535) + + self.responseData = {} + + threading.Thread(target=self.detectShutdown).start() + threading.Thread(target=self.socketStarter).start() + app = flask.Flask(__name__) + self.http_server = WSGIServer(('127.0.0.1', self.bindPort), app) + self.http_server.serve_forever() + + @app.route('/dc/', methods=['POST']) + def acceptConn(self): + data = request.form['data'] + data = self._core._utils.bytesTorStr(data) + data = {'date': self._core._utils.getEpoch(), 'data': data} + myPeer = '' + retData = '' + for peer in self.sockets: + if self.sockets[peer] == request.host: + myPeer = peer + break + else: + return "" + + if request.host in self.sockets: + self._core.socketServerConnData[myPeer].append(data) + else: + self._core.socketServerConnData[myPeer] = [data] + + try: + retData = self._core.socketServerResponseData[myPeer] + except KeyError: + pass + else: + self._core.socketServerResponseData[myPeer] = '' + + return retData + + def socketStarter(self): + while not self._core.killSockets: + try: + self.addSocket(self._core.startSocket['peer'], reason=self._core.startSocket['reason']) + except KeyError: + pass + else: + logger.info('%s socket started with %s' % (self._core.startSocket['reason'], self._core.startSocket['peer'])) + self._core.startSocket = {} + time.sleep(1) + + def detectShutdown(self): + while not self._core.killSockets: + time.sleep(5) + logger.info('Killing socket server') + self.http_server.stop() + + def addSocket(self, peer, reason=''): + bindPort = 1337 + + assert len(reason) <= 12 + + with stem.control.Controller.from_port(port=config.get('tor.controlPort')) as controller: + controller.authenticate(config.get('tor.controlpassword')) + + socket = controller.create_ephemeral_hidden_service({80: bindPort}, await_publication = True) + self.sockets[peer] = socket.service_id + '.onion' + + self.responseData[socket.service_id + '.onion'] = '' + + self._core.insertBlock(str(uuid.uuid4()), header='socket', sign=True, encryptType='asym', asymPeer=peer, meta={'reason': reason, 'address': socket.service_id + '.onion'}) + self._core.socketReasons[peer] = reason + return + +class OnionrSocketClient: + def __init__(self, coreInst): + self.sockets = {} # pubkey: tor address + self.connPool = {} + self.sendData = {} + self._core = coreInst + self.response = '' + self.request = '' + self.connected = False + self.killSocket = False + + def startSocket(self, peer, reason): + address = '' + logger.info('Trying to find socket server for %s' % (peer,)) + # Find the newest open socket for a given peer + for block in self._core.getBlocksByType('socket'): + block = onionrblockapi.Block(block, core=self._core) + if block.decrypt(): + theSigner = block.signer + try: + theSigner = theSigner.decode() + except AttributeError: + pass + if block.verifySig() and theSigner == peer: + address = block.getMetadata('address') + if self._core._utils.validateID(address): + # If we got their address, it is valid, and verified, we can break out + if block.getMetadata('reason') == reason: + break + else: + logger.error('The socket the peer opened is not for %s' % (reason,)) + else: + logger.error('Peer transport id is invalid for socket: %s' % (address,)) + address = '' + else: + logger.warn('Block has invalid sig or id, was for %s' % (theSigner,)) + if address != '': + logger.info('%s socket client started with %s' % (reason, peer)) + self.sockets[peer] = address + data = 'hey' + while not self.killSocket: + try: + data = self.sendData[peer] + logger.info('Sending %s to %s' % (data, peer)) + except KeyError: + pass + else: + self.sendData[peer] = '' + postData = {'data': data} + self.connPool[peer] = {'date': self._core._utils.getEpoch(), 'data': self._core._utils.doPostRequest('http://' + address + '/dc/', data=postData)} + time.sleep(2) + + def getResponse(self, peer): + retData = '' + try: + retData = self.connPool[peer] + except KeyError: + pass + return + + def sendData(self, peer, data): + self.sendData[peer] = data \ No newline at end of file diff --git a/onionr/onionrusers.py b/onionr/onionrusers.py new file mode 100644 index 00000000..93ecd6b0 --- /dev/null +++ b/onionr/onionrusers.py @@ -0,0 +1,189 @@ +''' + Onionr - P2P Anonymous Storage Network + + Contains abstractions for interacting with users of Onionr +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import onionrblockapi, logger, onionrexceptions, json, sqlite3 +import nacl.exceptions + +def deleteExpiredKeys(coreInst): + # Fetch the keys we generated for the peer, that are still around + conn = sqlite3.connect(coreInst.forwardKeysFile, timeout=10) + c = conn.cursor() + + curTime = coreInst._utils.getEpoch() + c.execute("DELETE from myForwardKeys where expire <= ?", (curTime,)) + conn.commit() + conn.execute("VACUUM") + conn.close() + return + +class OnionrUser: + def __init__(self, coreInst, publicKey): + self.trust = 0 + self._core = coreInst + self.publicKey = publicKey + + self.trust = self._core.getPeerInfo(self.publicKey, 'trust') + return + + def setTrust(self, newTrust): + '''Set the peers trust. 0 = not trusted, 1 = friend, 2 = ultimate''' + self._core.setPeerInfo(self.publicKey, 'trust', newTrust) + + def isFriend(self): + if self._core.getPeerInfo(self.publicKey, 'trust') == 1: + return True + return False + + def getName(self): + retData = 'anonymous' + name = self._core.getPeerInfo(self.publicKey, 'name') + try: + if len(name) > 0: + retData = name + except ValueError: + pass + return retData + + def encrypt(self, data): + encrypted = coreInst._crypto.pubKeyEncrypt(data, self.publicKey, encodedData=True) + return encrypted + + def decrypt(self, data, anonymous=True): + decrypted = coreInst._crypto.pubKeyDecrypt(data, self.publicKey, encodedData=True) + return decrypted + + def forwardEncrypt(self, data): + retData = '' + forwardKey = self._getLatestForwardKey() + #logger.info('using ' + forwardKey) + 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") + #self.generateForwardKey() + return (retData, forwardKey) + + def forwardDecrypt(self, encrypted): + retData = "" + #logger.error(self.publicKey) + #logger.error(self.getGeneratedForwardKeys(False)) + for key in self.getGeneratedForwardKeys(False): + logger.info(encrypted) + try: + retData = self._core._crypto.pubKeyDecrypt(encrypted, privkey=key[1], anonymous=True, encodedData=True) + except nacl.exceptions.CryptoError: + retData = False + else: + break + else: + raise onionrexceptions.DecryptionError("Could not decrypt forward secrecy content") + return retData + + def _getLatestForwardKey(self): + # Get the latest forward secrecy key for a peer + key = "" + conn = sqlite3.connect(self._core.peerDB, timeout=10) + c = conn.cursor() + + for row in c.execute("SELECT forwardKey FROM forwardKeys WHERE peerKey = ? order by date desc", (self.publicKey,)): + key = row[0] + break + + conn.commit() + conn.close() + return key + + def _getForwardKeys(self): + conn = sqlite3.connect(self._core.peerDB, timeout=10) + c = conn.cursor() + keyList = [] + for row in c.execute("SELECT forwardKey FROM forwardKeys WHERE peerKey = ? order by date desc", (self.publicKey,)): + key = row[0] + keyList.append(key) + + conn.commit() + conn.close() + + return list(keyList) + + def generateForwardKey(self, expire=604800): + + # Generate a forward secrecy key for the peer + conn = sqlite3.connect(self._core.forwardKeysFile, timeout=10) + c = conn.cursor() + # Prepare the insert + time = self._core._utils.getEpoch() + newKeys = self._core._crypto.generatePubKey() + newPub = self._core._utils.bytesToStr(newKeys[0]) + newPriv = self._core._utils.bytesToStr(newKeys[1]) + + time = self._core._utils.getEpoch() + command = (self.publicKey, newPub, newPriv, time, expire + time) + + c.execute("INSERT INTO myForwardKeys VALUES(?, ?, ?, ?, ?);", command) + + conn.commit() + conn.close() + return newPub + + def getGeneratedForwardKeys(self, genNew=True): + # Fetch the keys we generated for the peer, that are still around + conn = sqlite3.connect(self._core.forwardKeysFile, timeout=10) + c = conn.cursor() + pubkey = self.publicKey + pubkey = self._core._utils.bytesToStr(pubkey) + command = (pubkey,) + keyList = [] # list of tuples containing pub, private for peer + for result in c.execute("SELECT * FROM myForwardKeys where peer=?", command): + keyList.append((result[1], result[2])) + if len(keyList) == 0: + if genNew: + self.generateForwardKey() + keyList = self.getGeneratedForwardKeys() + return list(keyList) + + def addForwardKey(self, newKey, expire=604800): + if not self._core._utils.validatePubKey(newKey): + raise onionrexceptions.InvalidPubkey + # Add a forward secrecy key for the peer + conn = sqlite3.connect(self._core.peerDB, timeout=10) + c = conn.cursor() + # Prepare the insert + time = self._core._utils.getEpoch() + command = (self.publicKey, newKey, time, time + expire) + + c.execute("INSERT INTO forwardKeys VALUES(?, ?, ?, ?);", command) + + conn.commit() + conn.close() + return + + def findAndSetID(self): + '''Find any info about the user from existing blocks and cache it to their DB entry''' + infoBlocks = [] + for bHash in self._core.getBlocksByType('userInfo'): + block = onionrblockapi.Block(bHash, core=self._core) + if block.signer == self.publicKey: + if block.verifySig(): + newName = block.getMetadata('name') + if newName.isalnum(): + logger.info('%s is now using the name %s.' % (self.publicKey, self._core._utils.escapeAnsi(newName))) + self._core.setPeerInfo(self.publicKey, 'name', newName) + else: + raise onionrexceptions.InvalidPubkey \ No newline at end of file diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 2a47aaba..558a6b20 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -22,8 +22,10 @@ import getpass, sys, requests, os, socket, hashlib, logger, sqlite3, config, bin import nacl.signing, nacl.encoding from onionrblockapi import Block import onionrexceptions +from onionr import API_VERSION from defusedxml import minidom -import pgpwords, storagecounter +import onionrevents +import pgpwords, onionrusers, storagecounter if sys.version_info < (3, 6): try: import sha3 @@ -36,20 +38,23 @@ class OnionrUtils: Various useful functions for validating things, etc functions, connectivity ''' def __init__(self, coreInstance): - self.fingerprintFile = 'data/own-fingerprint.txt' - self._core = coreInstance + #self.fingerprintFile = 'data/own-fingerprint.txt' #TODO Remove since probably not needed + self._core = coreInstance # onionr core instance - self.timingToken = '' + self.timingToken = '' # for when we make local connections to our http api, to bypass timing attack defense mechanism self.avoidDupe = [] # list used to prevent duplicate requests per peer for certain actions self.peerProcessing = {} # dict of current peer actions: peer, actionList - self.storageCounter = storagecounter.StorageCounter(self._core) - config.reload() + self.storageCounter = storagecounter.StorageCounter(self._core) # used to keep track of how much data onionr is using on disk + config.reload() # onionr config return def getTimeBypassToken(self): + ''' + Load our timingToken from disk for faster local HTTP API + ''' try: - if os.path.exists('data/time-bypass.txt'): - with open('data/time-bypass.txt', 'r') as bypass: + if os.path.exists(self._core.dataDir + 'time-bypass.txt'): + with open(self._core.dataDir + 'time-bypass.txt', 'r') as bypass: self.timingToken = bypass.read() except Exception as error: logger.error('Failed to fetch time bypass token.', error = error) @@ -63,22 +68,6 @@ class OnionrUtils: epoch = self.getEpoch() return epoch - (epoch % roundS) - def incrementAddressSuccess(self, address): - ''' - Increase the recorded sucesses for an address - ''' - increment = self._core.getAddressInfo(address, 'success') + 1 - self._core.setAddressInfo(address, 'success', increment) - return - - def decrementAddressSuccess(self, address): - ''' - Decrease the recorded sucesses for an address - ''' - increment = self._core.getAddressInfo(address, 'success') - 1 - self._core.setAddressInfo(address, 'success', increment) - return - def mergeKeys(self, newKeyList): ''' Merge ed25519 key list to our database, comma seperated string @@ -88,6 +77,7 @@ class OnionrUtils: 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]) @@ -95,17 +85,24 @@ class OnionrUtils: except IndexError: logger.warn('No pow token') continue - #powHash = self._core._crypto.blake2bHash(base64.b64decode(key[1]) + self._core._crypto.blake2bHash(key[0].encode())) - value = base64.b64decode(key[1]) + 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") @@ -147,7 +144,7 @@ class OnionrUtils: def getMyAddress(self): try: - with open('./data/hs/hostname', 'r') as hostname: + with open('./' + self._core.dataDir + 'hs/hostname', 'r') as hostname: return hostname.read().strip() except Exception as error: logger.error('Failed to read my address.', error = error) @@ -162,7 +159,7 @@ 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('data/host.txt', 'r') as host: + with open(self._core.dataDir + 'host.txt', 'r') as host: hostname = host.read() except FileNotFoundError: return False @@ -266,13 +263,38 @@ class OnionrUtils: ''' myBlock = Block(blockHash, self._core) if myBlock.isEncrypted: - myBlock.decrypt() - blockType = myBlock.getMetadata('type') # we would use myBlock.getType() here, but it is bugged with encrypted blocks - try: - if len(blockType) <= 10: - self._core.updateBlockInfo(blockHash, 'dataType', blockType) - except TypeError: - pass + #pass + logger.warn(myBlock.decrypt()) + if (myBlock.isEncrypted and myBlock.decrypted) or (not myBlock.isEncrypted): + blockType = myBlock.getMetadata('type') # we would use myBlock.getType() here, but it is bugged with encrypted blocks + signer = self.bytesToStr(myBlock.signer) + valid = myBlock.verifySig() + + logger.info('Checking for fs key') + if myBlock.getMetadata('newFSKey') is not None: + onionrusers.OnionrUser(self._core, signer).addForwardKey(myBlock.getMetadata('newFSKey')) + else: + logger.warn('FS not used for this encrypted block') + logger.info(myBlock.bmetadata) + + try: + if len(blockType) <= 10: + self._core.updateBlockInfo(blockHash, 'dataType', blockType) + onionrevents.event('processblocks', data = {'block': myBlock, 'type': blockType, 'signer': signer, 'validSig': valid}, onionr = None) + except TypeError: + logger.warn("Missing block information") + pass + # Set block expire time if specified + try: + expireTime = myBlock.getHeader('expire') + assert len(str(int(expireTime))) < 20 # test that expire time is an integer of sane length (for epoch) + except (AssertionError, ValueError, TypeError) as e: + pass + else: + self._core.updateBlockInfo(blockHash, 'expire', expireTime) + else: + logger.info(myBlock.isEncrypted) + logger.debug('Not processing metadata on encrypted block we cannot decrypt.') def escapeAnsi(self, line): ''' @@ -361,15 +383,26 @@ class OnionrUtils: logger.warn('Block has invalid metadata key ' + i) break else: - if self._core.requirements.blockMetadataLengths[i] < len(metadata[i]): + testData = metadata[i] + try: + testData = len(testData) + except (TypeError, AttributeError) as e: + testData = len(str(testData)) + if self._core.requirements.blockMetadataLengths[i] < testData: logger.warn('Block metadata key ' + i + ' exceeded maximum size') break if i == 'time': if not self.isIntegerString(metadata[i]): logger.warn('Block metadata time stamp is not integer string') break + elif i == 'expire': + try: + assert int(metadata[i]) > self.getEpoch() + except AssertionError: + logger.warn('Block is expired') + break else: - # if metadata loop gets no errors, it does not break, therefore metadata is valid + # if metadata loop gets no errors, it does not break, therefore metadata is valid # make sure we do not have another block with the same data content (prevent data duplication and replay attacks) nonce = self._core._utils.bytesToStr(self._core._crypto.sha3Hash(blockData)) try: @@ -455,6 +488,12 @@ class OnionrUtils: retVal = False if not idNoDomain.isalnum(): retVal = False + + # Validate address is valid base32 (when capitalized and minus extension); v2/v3 onions and .b32.i2p use base32 + try: + base64.b32decode(idNoDomain.upper().encode()) + except binascii.Error: + retVal = False return retVal except: @@ -478,7 +517,7 @@ class OnionrUtils: def isCommunicatorRunning(self, timeout = 5, interval = 0.1): try: - runcheck_file = 'data/.runcheck' + runcheck_file = self._core.dataDir + '.runcheck' if os.path.isfile(runcheck_file): os.remove(runcheck_file) @@ -521,6 +560,7 @@ class OnionrUtils: if self._core._crypto.sha3Hash(newBlock.read()) == block.replace('.dat', ''): self._core.addToBlockDB(block.replace('.dat', ''), dataSaved=True) logger.info('Imported block %s.' % block) + self._core._utils.processBlockMetadata(block) else: logger.warn('Failed to verify hash for %s' % block) @@ -586,13 +626,22 @@ class OnionrUtils: try: proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} r = requests.get(url, headers=headers, proxies=proxies, allow_redirects=False, timeout=(15, 30)) + # Check server is using same API version as us + try: + if r.headers['api'] != str(API_VERSION): + raise onionrexceptions.InvalidAPIVersion + except KeyError: + raise onionrexceptions.InvalidAPIVersion retData = r.text except KeyboardInterrupt: raise KeyboardInterrupt except ValueError as e: logger.debug('Failed to make request', error = e) + except onionrexceptions.InvalidAPIVersion: + logger.debug("Node is using different API version :(") except requests.exceptions.RequestException as e: - logger.debug('Error: %s' % str(e)) + if not 'ConnectTimeoutError' in str(e) and not 'Request rejected or failed' in str(e): + logger.debug('Error: %s' % str(e)) retData = False return retData diff --git a/onionr/onionrvalues.py b/onionr/onionrvalues.py index 3f806702..84203d88 100644 --- a/onionr/onionrvalues.py +++ b/onionr/onionrvalues.py @@ -21,4 +21,4 @@ class OnionrValues: def __init__(self): self.passwordLength = 20 - self.blockMetadataLengths = {'meta': 1000, 'sig': 200, 'signer': 200, 'time': 10, 'powRandomToken': 1000, 'encryptType': 4} #TODO properly refine values to minimum needed \ No newline at end of file + self.blockMetadataLengths = {'meta': 1000, 'sig': 200, 'signer': 200, 'time': 10, 'powRandomToken': 1000, 'encryptType': 4, 'expire': 14} #TODO properly refine values to minimum needed \ No newline at end of file diff --git a/onionr/static-data/bootstrap-nodes.txt b/onionr/static-data/bootstrap-nodes.txt index 5473550f..e69de29b 100644 --- a/onionr/static-data/bootstrap-nodes.txt +++ b/onionr/static-data/bootstrap-nodes.txt @@ -1,2 +0,0 @@ -onionragxuddecmg.onion -dgyllprmtmym4gbk.onion diff --git a/onionr/static-data/default-plugins/cliui/info.json b/onionr/static-data/default-plugins/cliui/info.json new file mode 100644 index 00000000..e06855f6 --- /dev/null +++ b/onionr/static-data/default-plugins/cliui/info.json @@ -0,0 +1,5 @@ +{ + "name" : "cliui", + "version" : "1.0", + "author" : "onionr" +} diff --git a/onionr/static-data/default-plugins/cliui/main.py b/onionr/static-data/default-plugins/cliui/main.py new file mode 100644 index 00000000..646aa7c2 --- /dev/null +++ b/onionr/static-data/default-plugins/cliui/main.py @@ -0,0 +1,133 @@ +''' + Onionr - P2P Anonymous Storage Network + + This is an interactive menu-driven CLI interface for Onionr +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' + +# Imports some useful libraries +import logger, config, threading, time, uuid, subprocess +from onionrblockapi import Block + +plugin_name = 'cliui' +PLUGIN_VERSION = '0.0.1' + +class OnionrCLIUI: + def __init__(self, apiInst): + self.api = apiInst + self.myCore = apiInst.get_core() + return + + def subCommand(self, command): + try: + subprocess.run(["./onionr.py", command]) + except KeyboardInterrupt: + pass + + def refresh(self): + for i in range(100): + print('') + + def start(self): + '''Main CLI UI interface menu''' + showMenu = True + isOnline = "No" + firstRun = True + choice = '' + + if self.myCore._utils.localCommand('ping') == 'pong': + 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" + + 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) + ''') + try: + choice = input(">").strip().lower() + except (KeyboardInterrupt, EOFError): + choice = "quit" + + if choice in ("flow", "1"): + self.subCommand("flow") + 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", "daemon"): + if isOnline == "Yes": + print("Onionr daemon will shutdown...") + self.myCore.daemonQueueAdd('shutdown') + try: + daemon.kill() + except UnboundLocalError: + pass + else: + print("Starting Daemon...") + daemon = subprocess.Popen(["./onionr.py", "start"], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + elif choice in ("6", "quit"): + showMenu = False + elif choice == "": + pass + else: + print("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): + ''' + This event is called after Onionr is initialized, but before the command + inputted is executed. Could be called when daemon is starting or when + just the client is running. + ''' + + # Doing this makes it so that the other functions can access the api object + # by simply referencing the variable `pluginapi`. + pluginapi = api + ui = OnionrCLIUI(api) + api.commands.register('interactive', ui.start) + api.commands.register_help('interactive', 'Open the CLI interface') + return diff --git a/onionr/static-data/default-plugins/encrypt/info.json b/onionr/static-data/default-plugins/encrypt/info.json new file mode 100644 index 00000000..d675197a --- /dev/null +++ b/onionr/static-data/default-plugins/encrypt/info.json @@ -0,0 +1,5 @@ +{ + "name" : "encrypt", + "version" : "1.0", + "author" : "onionr" +} diff --git a/onionr/static-data/default-plugins/encrypt/main.py b/onionr/static-data/default-plugins/encrypt/main.py new file mode 100644 index 00000000..2aa87dcb --- /dev/null +++ b/onionr/static-data/default-plugins/encrypt/main.py @@ -0,0 +1,117 @@ +''' + Onionr - P2P Microblogging Platform & Social network + + This default plugin allows users to encrypt/decrypt messages without using blocks +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' + +# Imports some useful libraries +import logger, config, threading, time, readline, datetime, sys, json +from onionrblockapi import Block +import onionrexceptions, onionrusers +import locale +locale.setlocale(locale.LC_ALL, '') + +class PlainEncryption: + def __init__(self, api): + self.api = api + return + def encrypt(self): + # peer, data + plaintext = "" + encrypted = "" + # detect if signing is enabled + sign = True + try: + if sys.argv[3].lower() == 'false': + sign = False + except IndexError: + pass + + try: + if not self.api.get_core()._utils.validatePubKey(sys.argv[2]): + raise onionrexceptions.InvalidPubkey + except (ValueError, IndexError) as e: + logger.error("Peer public key not specified") + except onionrexceptions.InvalidPubkey: + logger.error("Invalid public key") + else: + pubkey = sys.argv[2] + # Encrypt if public key is valid + logger.info("Please enter your message (ctrl-d or -q to stop):") + try: + for line in sys.stdin: + if line == '-q\n': + break + plaintext += line + except KeyboardInterrupt: + sys.exit(1) + # Build Message to encrypt + data = {} + myPub = self.api.get_core()._crypto.pubKey + if sign: + data['sig'] = self.api.get_core()._crypto.edSign(plaintext, key=self.api.get_core()._crypto.privKey, encodeResult=True) + data['sig'] = self.api.get_core()._utils.bytesToStr(data['sig']) + data['signer'] = myPub + 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()._utils.bytesToStr(encrypted) + print('ONIONR ENCRYPTED DATA %s END ENCRYPTED DATA' % (encrypted,)) + def decrypt(self): + plaintext = "" + data = "" + logger.info("Please enter your message (ctrl-d or -q to stop):") + try: + for line in sys.stdin: + if line == '-q\n': + break + data += line + except KeyboardInterrupt: + sys.exit(1) + if len(data) <= 1: + return + encrypted = data.replace('ONIONR ENCRYPTED DATA ', '').replace('END ENCRYPTED DATA', '') + myPub = self.api.get_core()._crypto.pubKey + decrypted = self.api.get_core()._crypto.pubKeyDecrypt(encrypted, privkey=self.api.get_core()._crypto.privKey, anonymous=True, encodedData=True) + if decrypted == False: + print("Decryption failed") + else: + data = json.loads(decrypted) + print(data['data']) + try: + logger.info("Signing public key: %s" % (data['signer'],)) + assert self.api.get_core()._crypto.edVerify(data['data'], data['signer'], data['sig']) != False + except (AssertionError, KeyError) as e: + logger.warn("WARNING: THIS MESSAGE HAS A MISSING OR INVALID SIGNATURE") + else: + logger.info("Message has good signature.") + return + + +def on_init(api, data = None): + ''' + This event is called after Onionr is initialized, but before the command + inputted is executed. Could be called when daemon is starting or when + just the client is running. + ''' + + pluginapi = api + encrypt = PlainEncryption(pluginapi) + api.commands.register(['encrypt'], encrypt.encrypt) + api.commands.register(['decrypt'], encrypt.decrypt) + return \ 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 b2fb1dfa..a7f8842b 100644 --- a/onionr/static-data/default-plugins/flow/main.py +++ b/onionr/static-data/default-plugins/flow/main.py @@ -45,9 +45,9 @@ class OnionrFlow: self.flowRunning = False if message == "q": self.flowRunning = False - + expireTime = self.myCore._utils.getEpoch() + 43200 if len(message) > 0: - Block(content = message, type = 'txt', core = self.myCore).save() + Block(content = message, type = 'txt', expire=expireTime, core = self.myCore).save() logger.info("Flow is exiting, goodbye") return diff --git a/onionr/static-data/default-plugins/metadataprocessor/info.json b/onionr/static-data/default-plugins/metadataprocessor/info.json new file mode 100644 index 00000000..355d98f1 --- /dev/null +++ b/onionr/static-data/default-plugins/metadataprocessor/info.json @@ -0,0 +1,5 @@ +{ + "name" : "metadataprocessor", + "version" : "1.0", + "author" : "onionr" +} diff --git a/onionr/static-data/default-plugins/metadataprocessor/main.py b/onionr/static-data/default-plugins/metadataprocessor/main.py new file mode 100644 index 00000000..6b0b5a28 --- /dev/null +++ b/onionr/static-data/default-plugins/metadataprocessor/main.py @@ -0,0 +1,103 @@ +''' + Onionr - P2P Anonymous Storage Network + + This processes metadata for Onionr blocks +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' + +# useful libraries +import logger, config +import os, sys, json, time, random, shutil, base64, getpass, datetime, re +from onionrblockapi import Block +import onionrusers, onionrexceptions + +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: + 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 + ''' + peer = onionrusers.OnionrUser(api.get_core(), myBlock.signer) + key = myBlock.getMetadata('newFSKey') + + # We don't need to validate here probably, but it helps + if api.get_utils().validatePubKey(key): + peer.addForwardKey(key) + else: + raise onionrexceptions.InvalidPubkey("%s is nota valid pubkey key" % (key,)) + +def on_processblocks(api): + # Generally fired by utils. + myBlock = api.data['block'] + blockType = api.data['type'] + logger.info('blockType is ' + blockType) + + # 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 api.data['validSig'] == True: + _processForwardKey(api, myBlock) + # socket blocks + elif blockType == 'socket': + if api.data['validSig'] == True and myBlock.decrypted: # we check if it is decrypted as a way of seeing if it was for us + logger.info('Detected socket advertised to us...') + try: + address = myBlock.getMetadata('address') + except KeyError: + raise onionrexceptions.MissingAddress("Missing address for new socket") + try: + port = myBlock.getMetadata('port') + except KeyError: + raise ValueError("Missing port for new socket") + try: + reason = myBlock.getMetadata('reason') + except KeyError: + raise ValueError("Missing socket reason") + + socketInfo = json.dumps({'peer': api.data['signer'], 'address': address, 'port': port, 'create': False, 'reason': reason}) + api.get_core().daemonQueueAdd('addSocket', socketInfo) + else: + logger.warn("socket is not for us or is invalid") + +def on_init(api, data = None): + + pluginapi = api + + return diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py index 27f56438..d05ff36f 100644 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - P2P Anonymous Storage Network This default plugin handles private messages in an email like fashion ''' @@ -21,10 +21,14 @@ # Imports some useful libraries import logger, config, threading, time, readline, datetime from onionrblockapi import Block -import onionrexceptions -import locale +import onionrexceptions, onionrusers +import locale, sys, os + locale.setlocale(locale.LC_ALL, '') +sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) +import sentboxdb # import after path insert + plugin_name = 'pms' PLUGIN_VERSION = '0.0.1' @@ -44,22 +48,23 @@ class MailStrings: self.mailInstance = mailInstance self.programTag = 'OnionrMail v%s' % (PLUGIN_VERSION) - choices = ['view inbox', 'view sentbox', 'send message', 'help', 'quit'] + choices = ['view inbox', 'view sentbox', 'send message', 'quit'] self.mainMenuChoices = choices self.mainMenu = '''\n ----------------- 1. %s 2. %s 3. %s -4. %s -5. %s''' % (choices[0], choices[1], choices[2], choices[3], choices[4]) +4. %s''' % (choices[0], choices[1], choices[2], choices[3]) class OnionrMail: def __init__(self, pluginapi): self.myCore = pluginapi.get_core() - #self.dataFolder = pluginapi.get_data_folder() self.strings = MailStrings(self) + self.sentboxTools = sentboxdb.SentBox(self.myCore) + self.sentboxList = [] + self.sentMessages = {} return def inbox(self): @@ -68,6 +73,7 @@ class OnionrMail: pmBlocks = {} logger.info('Decrypting messages...') choice = '' + displayList = [] # this could use a lot of memory if someone has recieved a lot of messages for blockHash in self.myCore.getBlocksByType('pm'): @@ -81,9 +87,22 @@ class OnionrMail: continue blockCount += 1 pmBlockMap[blockCount] = blockHash - blockDate = pmBlocks[blockHash].getDate().strftime("%m/%d %H:%M") - print('%s. %s: %s' % (blockCount, blockDate, blockHash)) + + block = pmBlocks[blockHash] + senderKey = block.signer + try: + senderKey = senderKey.decode() + except AttributeError: + pass + senderDisplay = onionrusers.OnionrUser(self.myCore, senderKey).getName() + if senderDisplay == 'anonymous': + senderDisplay = senderKey + blockDate = pmBlocks[blockHash].getDate().strftime("%m/%d %H:%M") + displayList.append('%s. %s - %s: %s' % (blockCount, blockDate, senderDisplay[:12], blockHash)) + #displayList.reverse() + for i in displayList: + print(i) try: choice = logger.readline('Enter a block number, -r to refresh, or -q to stop: ').strip().lower() except (EOFError, KeyboardInterrupt): @@ -110,15 +129,52 @@ class OnionrMail: else: cancel = '' readBlock.verifySig() - print('Message recieved from %s' % (readBlock.signer,)) + print('Message recieved from %s' % (self.myCore._utils.bytesToStr(readBlock.signer,))) print('Valid signature:', readBlock.validSig) if not readBlock.validSig: 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()))) + input("Press enter to continue") return + def sentbox(self): + ''' + Display sent mail messages + ''' + entering = True + while entering: + self.getSentList() + print('Enter block number or -q to return') + try: + choice = input('>') + except (EOFError, KeyboardInterrupt) as e: + entering = False + else: + if choice == '-q': + entering = False + else: + try: + self.sentboxList[int(choice) - 1] + except IndexError: + print('Invalid block') + else: + logger.info('Sent to: ' + self.sentMessages[self.sentboxList[int(choice) - 1]][1]) + # Print ansi escaped sent message + print(self.myCore._utils.escapeAnsi(self.sentMessages[self.sentboxList[int(choice) - 1]][0])) + input('Press enter to continue...') + + return + + def getSentList(self): + count = 1 + for i in self.sentboxTools.listSent(): + self.sentboxList.append(i['hash']) + self.sentMessages[i['hash']] = (i['message'], i['peer']) + print('%s. %s - %s - %s' % (count, i['hash'], i['peer'][:12], i['date'])) + count += 1 + def draftMessage(self): message = '' newLine = '' @@ -155,8 +211,8 @@ class OnionrMail: print('Inserting encrypted message as Onionr block....') - 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) + self.sentboxTools.addToSent(blockID, recip, message) def menu(self): choice = '' while True: @@ -171,12 +227,10 @@ class OnionrMail: if choice in (self.strings.mainMenuChoices[0], '1'): self.inbox() elif choice in (self.strings.mainMenuChoices[1], '2'): - logger.warn('not implemented yet') + self.sentbox() elif choice in (self.strings.mainMenuChoices[2], '3'): self.draftMessage() elif choice in (self.strings.mainMenuChoices[3], '4'): - logger.warn('not implemented yet') - elif choice in (self.strings.mainMenuChoices[4], '5'): logger.info('Goodbye.') break elif choice == '': diff --git a/onionr/static-data/default-plugins/pms/sentboxdb.py b/onionr/static-data/default-plugins/pms/sentboxdb.py new file mode 100644 index 00000000..f2328ccb --- /dev/null +++ b/onionr/static-data/default-plugins/pms/sentboxdb.py @@ -0,0 +1,62 @@ +''' + Onionr - P2P Microblogging Platform & Social network + + This file handles the sentbox for the mail plugin +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sqlite3, os +import core +class SentBox: + def __init__(self, mycore): + assert isinstance(mycore, core.Core) + self.dbLocation = mycore.dataDir + 'sentbox.db' + if not os.path.exists(self.dbLocation): + self.createDB() + self.conn = sqlite3.connect(self.dbLocation) + self.cursor = self.conn.cursor() + self.core = mycore + return + + def createDB(self): + conn = sqlite3.connect(self.dbLocation) + cursor = conn.cursor() + cursor.execute('''CREATE TABLE sent( + hash id not null, + peer text not null, + message text not null, + date int not null + ); + ''') + conn.commit() + return + + 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]}) + return retData + + def addToSent(self, blockID, peer, message): + args = (blockID, peer, message, self.core._utils.getEpoch()) + self.cursor.execute('INSERT INTO sent VALUES(?, ?, ?, ?)', args) + self.conn.commit() + return + + def removeSent(self, blockID): + args = (blockID,) + self.cursor.execute('DELETE FROM sent where hash=?', args) + self.conn.commit() + return \ No newline at end of file diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index 6e35245e..8ce16361 100644 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -2,6 +2,8 @@ "general" : { "dev_mode": true, "display_header" : true, + "minimum_block_pow": 5, + "minimum_send_pow": 5, "direct_connect" : { "respond" : true, @@ -30,7 +32,7 @@ "log": { "file": { - "output": true, + "output": false, "path": "data/output.log" }, @@ -59,7 +61,7 @@ "peers":{ "minimumScore": -100, "maxStoredPeers": 5000, - "maxConnect": 5 + "maxConnect": 10 }, "timers":{ "lookupBlocks": 25, diff --git a/onionr/static-data/index.html b/onionr/static-data/index.html index 93e48beb..157f1586 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 with the owner's knowledge.

+

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.

-

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

+

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

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

diff --git a/onionr/tests.py b/onionr/tests.py index 008d2bed..c23db1fa 100755 --- a/onionr/tests.py +++ b/onionr/tests.py @@ -14,7 +14,7 @@ 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, simplecrypt, logger +import unittest, sys, os, base64, tarfile, shutil, logger class OnionrTests(unittest.TestCase): def testPython3(self): @@ -61,36 +61,6 @@ class OnionrTests(unittest.TestCase): else: self.assertTrue(False) - def testData_b_Encrypt(self): - self.assertTrue(True) - return - - logger.debug('-'*26 + '\n') - logger.info('Running data dir encrypt test...') - - import core - myCore = core.Core() - myCore.dataDirEncrypt('password') - if os.path.exists('data-encrypted.dat'): - self.assertTrue(True) - else: - self.assertTrue(False) - - def testData_a_Decrypt(self): - self.assertTrue(True) - return - - logger.debug('-'*26 + '\n') - logger.info('Running data dir decrypt test...') - - import core - myCore = core.Core() - myCore.dataDirDecrypt('password') - if os.path.exists('data/'): - self.assertTrue(True) - else: - self.assertTrue(False) - def testConfig(self): logger.debug('-'*26 + '\n') logger.info('Running simple configuration test...') diff --git a/requirements.txt b/requirements.txt index 4653eaf0..2375324d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ urllib3==1.23 -requests==2.18.4 +requests==2.20.0 PyNaCl==1.2.1 -gevent==1.2.2 +gevent==1.3.6 sha3==0.2.1 defusedxml==0.5.0 -simple_crypt==4.1.7 Flask==1.0.2 PySocks==1.6.8 +stem==1.6.0 diff --git a/setprofile.sh b/setprofile.sh new file mode 100755 index 00000000..3541a652 --- /dev/null +++ b/setprofile.sh @@ -0,0 +1,7 @@ +#!/bin/bash +ONIONR_HOME=. +if [ $# -gt 0 ]; then + ONIONR_HOME=$1 +export ONIONR_HOME +echo "set ONIONR_HOME to $ONIONR_HOME" +fi