diff --git a/.dockerignore b/.dockerignore old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..7c0d5dc2 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,7 @@ +test: + image: ubuntu:bionic + script: + - apt-get update -qy + - apt-get install -y python3-pip tor + - pip3 install -r requirements.txt + - make test diff --git a/.gitmodules b/.gitmodules old mode 100644 new mode 100755 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md old mode 100644 new mode 100755 index 052ac851..ccd047ea --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at beardog@firemail.cc. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at beardog at mailbox.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md old mode 100644 new mode 100755 diff --git a/Dockerfile b/Dockerfile old mode 100644 new mode 100755 diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md old mode 100644 new mode 100755 diff --git a/LICENSE.txt b/LICENSE.txt old mode 100644 new mode 100755 diff --git a/Makefile b/Makefile old mode 100644 new mode 100755 index c3616e3d..4721e97c --- a/Makefile +++ b/Makefile @@ -18,26 +18,20 @@ uninstall: rm -f $(DESTDIR)$(PREFIX)/bin/onionr test: - @./onionr.sh stop - @sleep 1 - @rm -rf onionr/data-backup - @mv onionr/data onionr/data-backup | true > /dev/null 2>&1 - -@cd onionr; ./tests.py; - @rm -rf onionr/data - @mv onionr/data-backup onionr/data | true > /dev/null 2>&1 + ./run_tests.sh soft-reset: @echo "Soft-resetting Onionr..." - rm -f onionr/data/blocks/*.dat onionr/data/*.db onionr/data/block-nonces.dat | true > /dev/null 2>&1 + rm -f onionr/$(ONIONR_HOME)/blocks/*.dat onionr/data/*.db onionr/$(ONIONR_HOME)/block-nonces.dat | true > /dev/null 2>&1 @./onionr.sh version | grep -v "Failed" --color=always reset: @echo "Hard-resetting Onionr..." - rm -rf onionr/data/ | true > /dev/null 2>&1 + rm -rf onionr/$(ONIONR_HOME)/ | true > /dev/null 2>&1 cd onionr/static-data/www/ui/; rm -rf ./dist; python compile.py #@./onionr.sh.sh version | grep -v "Failed" --color=always plugins-reset: @echo "Resetting plugins..." - rm -rf onionr/data/plugins/ | true > /dev/null 2>&1 + rm -rf onionr/$(ONIONR_HOME)/plugins/ | true > /dev/null 2>&1 @./onionr.sh version | grep -v "Failed" --color=always diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 5a346e5d..7cc1a597 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ (***pre-alpha & experimental, not well tested or easy to use yet***) [![Open Source Love](https://badges.frapsoft.com/os/v3/open-source.png?v=103)](https://github.com/ellerbrock/open-source-badges/) +
@@ -60,6 +61,7 @@ Everyone is welcome to help out. Help is wanted for the following: * Testing * Running stable nodes * Security review/audit +* Automatic I2P setup Bitcoin: [1onion55FXzm6h8KQw3zFw2igpHcV7LPq](bitcoin:1onion55FXzm6h8KQw3zFw2igpHcV7LPq) USD: [Ko-Fi](https://www.ko-fi.com/beardogkf) diff --git a/docs/api.md b/docs/api.md old mode 100644 new mode 100755 diff --git a/docs/onionr-logo.png b/docs/onionr-logo.png old mode 100644 new mode 100755 diff --git a/docs/onionr-logo.png~ b/docs/onionr-logo.png~ deleted file mode 100644 index 5e15d42f..00000000 Binary files a/docs/onionr-logo.png~ and /dev/null differ diff --git a/docs/onionr-web.png b/docs/onionr-web.png old mode 100644 new mode 100755 diff --git a/docs/whitepaper.md b/docs/whitepaper.md old mode 100644 new mode 100755 diff --git a/onionr/__init__.py b/onionr/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/onionr/api.py b/onionr/api.py index 57f34ac0..2e6ff46d 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -19,9 +19,7 @@ ''' from gevent.pywsgi import WSGIServer, WSGIHandler from gevent import Timeout -#import gevent.monkey -#gevent.monkey.patch_socket() -import flask, cgi +import flask, cgi, uuid from flask import request, Response, abort, send_from_directory import sys, random, threading, hmac, hashlib, base64, time, math, os, json, socket import core @@ -29,34 +27,15 @@ from onionrblockapi import Block import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config, onionr class FDSafeHandler(WSGIHandler): + '''Our WSGI handler. Doesn't do much non-default except timeouts''' def handle(self): timeout = Timeout(60, exception=Exception) timeout.start() - - #timeout = gevent.Timeout.start_new(3) try: WSGIHandler.handle(self) except Timeout as ex: raise -def guessMime(path): - ''' - Guesses the mime type of a file from the input filename - ''' - mimetypes = { - 'html' : 'text/html', - 'js' : 'application/javascript', - 'css' : 'text/css', - 'png' : 'image/png', - 'jpg' : 'image/jpeg' - } - - for mimetype in mimetypes: - if path.endswith('.%s' % mimetype): - return mimetypes[mimetype] - - return 'text/plain' - def setBindIP(filePath): '''Set a random localhost IP to a specified file (intended for private or public API localhost IPs)''' hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))] @@ -67,6 +46,7 @@ def setBindIP(filePath): try: s.bind((data, 0)) except OSError: + # if mac/non-bindable, show warning and default to 127.0.0.1 logger.warn('Your platform appears to not support random local host addresses 127.x.x.x. Falling back to 127.0.0.1.') data = '127.0.0.1' s.close() @@ -89,33 +69,42 @@ class PublicAPI: self.torAdder = clientAPI._core.hsAddress self.i2pAdder = clientAPI._core.i2pAddress self.bindPort = config.get('client.public.port') + self.lastRequest = 0 logger.info('Running public api on %s:%s' % (self.host, self.bindPort)) @app.before_request def validateRequest(): '''Validate request has the correct hostname''' - # If high security level, deny requests to public + # If high security level, deny requests to public (HS should be disabled anyway for Tor, but might not be for I2P) if config.get('general.security_level', default=0) > 0: abort(403) if type(self.torAdder) is None and type(self.i2pAdder) is None: # abort if our hs addresses are not known abort(403) if request.host not in (self.i2pAdder, self.torAdder): + # Disallow connection if wrong HTTP hostname, in order to prevent DNS rebinding attacks abort(403) @app.after_request def sendHeaders(resp): '''Send api, access control headers''' - resp.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch. + resp.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch, since we can't fully remove the header. + # CSP to prevent XSS. Mainly for client side attacks (if hostname protection could somehow be bypassed) resp.headers["Content-Security-Policy"] = "default-src 'none'; script-src 'none'; object-src 'none'; style-src data: 'unsafe-inline'; img-src data:; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'none'" + # Prevent click jacking resp.headers['X-Frame-Options'] = 'deny' + # No sniff is possibly not needed resp.headers['X-Content-Type-Options'] = "nosniff" + # Network API version resp.headers['X-API'] = onionr.API_VERSION + # Close connections to limit FD use resp.headers['Connection'] = "close" + self.lastRequest = clientAPI._core._utils.getRoundedEpoch(roundS=5) return resp @app.route('/') def banner(): + # Display a bit of information to people who visit a node address in their browser try: with open('static-data/index.html', 'r') as html: resp = Response(html.read(), mimetype='text/html') @@ -125,41 +114,48 @@ class PublicAPI: @app.route('/getblocklist') def getBlockList(): + # Provide a list of our blocks, with a date offset dateAdjust = request.args.get('date') bList = clientAPI._core.getBlockList(dateRec=dateAdjust) for b in self.hideBlocks: if b in bList: + # Don't share blocks we created if they haven't been *uploaded* yet, makes it harder to find who created a block bList.remove(b) return Response('\n'.join(bList)) @app.route('/getdata/') def getBlockData(name): + # Share data for a block if we have it resp = '' data = name if clientAPI._utils.validateHash(data): if data not in self.hideBlocks: if data in clientAPI._core.getBlockList(): - block = clientAPI.getBlockData(data, raw=True).encode() - resp = base64.b64encode(block).decode() + block = clientAPI.getBlockData(data, raw=True) + try: + block = block.encode() + except AttributeError: + abort(404) + block = clientAPI._core._utils.strToBytes(block) + resp = block + #resp = base64.b64encode(block).decode() if len(resp) == 0: abort(404) resp = "" - return Response(resp) + return Response(resp, mimetype='application/octet-stream') @app.route('/www/') def wwwPublic(path): + # A way to share files directly over your .onion if not config.get("www.public.run", True): abort(403) return send_from_directory(config.get('www.public.path', 'static-data/www/public/'), path) @app.route('/ping') def ping(): + # Endpoint to test if nodes are up return Response("pong!") - @app.route('/getdbhash') - def getDBHash(): - return Response(clientAPI._utils.getBlockDBHash()) - @app.route('/pex') def peerExchange(): response = ','.join(clientAPI._core.listAdders(recent=3600)) @@ -194,11 +190,9 @@ class PublicAPI: except AttributeError: pass if powHash.startswith('0000'): - try: - newNode = newNode.decode() - except AttributeError: - pass - if clientAPI._core.addAddress(newNode): + newNode = clientAPI._core._utils.bytesToStr(newNode) + if clientAPI._core._utils.validateID(newNode) and not newNode in clientAPI._core.onionrInst.communicatorInst.newPeers: + clientAPI._core.onionrInst.communicatorInst.newPeers.append(newNode) resp = 'Success' else: logger.warn(newNode.decode() + ' failed to meet POW: ' + powHash) @@ -207,6 +201,9 @@ class PublicAPI: @app.route('/upload', methods=['post']) def upload(): + '''Accept file uploads. In the future this will be done more often than on creation + to speed up block sync + ''' resp = 'failure' try: data = request.form['block'] @@ -228,6 +225,7 @@ class PublicAPI: resp = Response(resp) return resp + # Set instances, then startup our public api server clientAPI.setPublicAPIInstance(self) while self.torAdder == '': clientAPI._core.refreshFirstStartVars() @@ -255,7 +253,6 @@ class API: onionr.Onionr.setupConfig('data/', self = self) self.debug = debug - self._privateDelayTime = 3 self._core = onionrInst.onionrCore self.startTime = self._core._utils.getEpoch() self._crypto = onionrcrypto.OnionrCrypto(self._core) @@ -264,7 +261,7 @@ class API: bindPort = int(config.get('client.client.port', 59496)) self.bindPort = bindPort - # Be extremely mindful of this + # Be extremely mindful of this. These are endpoints available without a password self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'board', 'boardContent', 'sharedContent', 'mail', 'mailindex') self.clientToken = config.get('client.webpassword') @@ -276,12 +273,14 @@ class API: logger.info('Running api on %s:%s' % (self.host, self.bindPort)) self.httpServer = '' + self.pluginResponses = {} # Responses for plugin endpoints self.queueResponse = {} onionrInst.setClientAPIInst(self) @app.before_request def validateRequest(): '''Validate request has set password and is the correct hostname''' + # For the purpose of preventing DNS rebinding attacks if request.host != '%s:%s' % (self.host, self.bindPort): abort(403) if request.endpoint in self.whitelistEndpoints: @@ -294,11 +293,13 @@ class API: @app.after_request def afterReq(resp): - #resp.headers["Content-Security-Policy"] = "default-src 'none'; script-src 'none'; object-src 'none'; style-src data: 'unsafe-inline'; img-src data:; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'none'" - resp.headers['Content-Security-Policy'] = "default-src 'none'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'self'" + # Security headers + if request.endpoint == 'site': + resp.headers['Content-Security-Policy'] = "default-src 'none'; style-src data: 'unsafe-inline'; img-src data:" + else: + resp.headers['Content-Security-Policy'] = "default-src 'none'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'self'" resp.headers['X-Frame-Options'] = 'deny' resp.headers['X-Content-Type-Options'] = "nosniff" - resp.headers['X-API'] = onionr.API_VERSION resp.headers['Server'] = '' resp.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch. resp.headers['Connection'] = "close" @@ -330,11 +331,13 @@ class API: @app.route('/queueResponseAdd/', methods=['post']) def queueResponseAdd(name): + # Responses from the daemon. TODO: change to direct var access instead of http endpoint self.queueResponse[name] = request.form['data'] return Response('success') @app.route('/queueResponse/') def queueResponse(name): + # Fetch a daemon queue response resp = 'failure' try: resp = self.queueResponse[name] @@ -346,10 +349,12 @@ class API: @app.route('/ping') def ping(): + # Used to check if client api is working return Response("pong!") @app.route('/', endpoint='onionrhome') def hello(): + # ui home return send_from_directory('static-data/www/private/', 'index.html') @app.route('/getblocksbytype/') @@ -357,12 +362,13 @@ class API: blocks = self._core.getBlocksByType(name) return Response(','.join(blocks)) - @app.route('/gethtmlsafeblockdata/') - def getSafeData(name): + @app.route('/getblockbody/') + def getBlockBodyData(name): resp = '' if self._core._utils.validateHash(name): try: - resp = cgi.escape(Block(name).bcontent, quote=True) + resp = Block(name, decrypt=True).bcontent + #resp = cgi.escape(Block(name, decrypt=True).bcontent, quote=True) except TypeError: pass else: @@ -384,6 +390,15 @@ class API: abort(404) return Response(resp) + @app.route('/getblockheader/') + def getBlockHeader(name): + resp = self.getBlockData(name, decrypt=True, headerOnly=True) + return Response(resp) + + @app.route('/lastconnect') + def lastConnect(): + return Response(str(self.publicAPI.lastRequest)) + @app.route('/site/', endpoint='site') def site(name): bHash = name @@ -402,7 +417,8 @@ class API: return Response(resp) @app.route('/waitforshare/', methods=['post']) - def waitforshare(): + def waitforshare(name): + '''Used to prevent the **public** api from sharing blocks we just created''' assert name.isalnum() if name in self.publicAPI.hideBlocks: self.publicAPI.hideBlocks.remove(name) @@ -428,6 +444,7 @@ class API: @app.route('/getstats') def getStats(): + # returns node stats #return Response("disabled") while True: try: @@ -438,6 +455,78 @@ class API: @app.route('/getuptime') def showUptime(): return Response(str(self.getUptime())) + + @app.route('/getActivePubkey') + def getActivePubkey(): + return Response(self._core._crypto.pubKey) + + @app.route('/getHumanReadable/') + def getHumanReadable(name): + return Response(self._core._utils.getHumanReadableID(name)) + + @app.route('/insertblock', methods=['POST']) + def insertBlock(): + encrypt = False + bData = request.get_json(force=True) + message = bData['message'] + subject = 'temp' + encryptType = '' + sign = True + meta = {} + to = '' + try: + if bData['encrypt']: + to = bData['to'] + encrypt = True + encryptType = 'asym' + except KeyError: + pass + try: + if not bData['sign']: + sign = False + except KeyError: + pass + try: + bType = bData['type'] + except KeyError: + bType = 'bin' + try: + meta = json.loads(bData['meta']) + except KeyError: + pass + threading.Thread(target=self._core.insertBlock, args=(message,), kwargs={'header': bType, 'encryptType': encryptType, 'sign':sign, 'asymPeer': to, 'meta': meta}).start() + return Response('success') + + @app.route('/apipoints/', methods=['POST', 'GET']) + def pluginEndpoints(subpath=''): + '''Send data to plugins''' + # TODO have a variable for the plugin to set data to that we can use for the response + pluginResponseCode = str(uuid.uuid4()) + resp = 'success' + responseTimeout = 20 + startTime = self._core._utils.getEpoch() + postData = {} + if request.method == 'POST': + postData = request.form['postData'] + if len(subpath) > 1: + data = subpath.split('/') + if len(data) > 1: + plName = data[0] + + events.event('pluginRequest', {'name': plName, 'path': subpath, 'pluginResponse': pluginResponseCode, 'postData': postData}, onionr=onionrInst) + while True: + try: + resp = self.pluginResponses[pluginResponseCode] + except KeyError: + time.sleep(0.2) + if self._core._utils.getEpoch() - startTime > responseTimeout: + abort(504) + break + else: + break + else: + abort(404) + return Response(resp) self.httpServer = WSGIServer((self.host, bindPort), app, log=None, handler_class=FDSafeHandler) self.httpServer.serve_forever() @@ -448,7 +537,7 @@ class API: def validateToken(self, token): ''' - Validate that the client token matches the given token + Validate that the client token matches the given token. Used to prevent CSRF and data exfiltration ''' if len(self.clientToken) == 0: logger.error("client password needs to be set") @@ -469,7 +558,8 @@ class API: # Don't error on race condition with startup pass - def getBlockData(self, bHash, decrypt=False, raw=False): + def getBlockData(self, bHash, decrypt=False, raw=False, headerOnly=False): + assert self._core._utils.validateHash(bHash) bl = Block(bHash, core=self._core) if decrypt: bl.decrypt() @@ -477,12 +567,22 @@ class API: raise ValueError if not raw: - retData = {'meta':bl.bheader, 'metadata': bl.bmetadata, 'content': bl.bcontent} - for x in list(retData.keys()): - try: - retData[x] = retData[x].decode() - except AttributeError: - pass + if not headerOnly: + retData = {'meta':bl.bheader, 'metadata': bl.bmetadata, 'content': bl.bcontent} + for x in list(retData.keys()): + try: + retData[x] = retData[x].decode() + except AttributeError: + pass + else: + validSig = False + signer = self._core._utils.bytesToStr(bl.signer) + #print(signer, bl.isSigned(), self._core._utils.validatePubKey(signer), bl.isSigner(signer)) + if bl.isSigned() and self._core._utils.validatePubKey(signer) and bl.isSigner(signer): + validSig = True + bl.bheader['validSig'] = validSig + bl.bheader['meta'] = '' + retData = {'meta': bl.bheader, 'metadata': bl.bmetadata} return json.dumps(retData) else: return bl.raw diff --git a/onionr/apimanager.py b/onionr/apimanager.py deleted file mode 100644 index 44737097..00000000 --- a/onionr/apimanager.py +++ /dev/null @@ -1,75 +0,0 @@ -''' - Onionr - P2P Anonymous Storage Network - - Handles api data exchange, interfaced by both public and client http api -''' -''' - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' -import config, apipublic, apiprivate, core, socket, random, threading, time -config.reload() - -PRIVATE_API_VERSION = 0 -PUBLIC_API_VERSION = 1 - -DEV_MODE = config.get('general.dev_mode') - -def getOpenPort(): - '''Get a random open port''' - p = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - p.bind(("127.0.0.1",0)) - p.listen(1) - port = p.getsockname()[1] - p.close() - return port - -def getRandomLocalIP(): - '''Get a random local ip address''' - hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))] - host = '.'.join(hostOctets) - return host - -class APIManager: - def __init__(self, coreInst): - assert isinstance(coreInst, core.Core) - self.core = coreInst - self.utils = coreInst._utils - self.crypto = coreInst._crypto - - # if this gets set to true, both the public and private apis will shutdown - self.shutdown = False - - publicIP = '127.0.0.1' - privateIP = '127.0.0.1' - if DEV_MODE: - # set private and local api servers bind IPs to random localhost (127.x.x.x), make sure not the same - privateIP = getRandomLocalIP() - while True: - publicIP = getRandomLocalIP() - if publicIP != privateIP: - break - - # Make official the IPs and Ports - self.publicIP = publicIP - self.privateIP = privateIP - self.publicPort = config.get('client.port', 59496) - self.privatePort = config.get('client.port', 59496) - - # Run the API servers in new threads - self.publicAPI = apipublic.APIPublic(self) - self.privateAPI = apiprivate.APIPrivate(self) - threading.Thread(target=self.publicAPI.run).start() - threading.Thread(target=self.privateAPI.run).start() - while not self.shutdown: - time.sleep(1) \ No newline at end of file diff --git a/onionr/apipublic.py b/onionr/apipublic.py deleted file mode 100644 index 9308f50a..00000000 --- a/onionr/apipublic.py +++ /dev/null @@ -1,41 +0,0 @@ -''' - Onionr - P2P Anonymous Storage Network - - Handle incoming commands from other Onionr nodes, over HTTP -''' -''' - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' -import flask, apimanager -from flask import request, Response, abort, send_from_directory -from gevent.pywsgi import WSGIServer - - -class APIPublic: - def __init__(self, managerInst): - assert isinstance(managerInst, apimanager.APIManager) - app = flask.Flask(__name__) - @app.route('/') - def banner(): - try: - with open('static-data/index.html', 'r') as html: - resp = Response(html.read(), mimetype='text/html') - except FileNotFoundError: - resp = Response("") - return resp - self.httpServer = WSGIServer((managerInst.publicIP, managerInst.publicPort), app) - - def run(self): - self.httpServer.serve_forever() - return diff --git a/onionr/blockimporter.py b/onionr/blockimporter.py old mode 100644 new mode 100755 diff --git a/onionr/communicator.py b/onionr/communicator.py index 1b8dc7fd..39c75c96 100755 --- a/onionr/communicator.py +++ b/onionr/communicator.py @@ -21,10 +21,12 @@ ''' import sys, os, core, config, json, requests, time, logger, threading, base64, onionr, uuid import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block -import onionrdaemontools, onionrsockets, onionr, onionrproofs, proofofmemory +import onionrdaemontools, onionrsockets, onionr, onionrproofs import binascii from dependencies import secrets from defusedxml import minidom +from utils import networkmerger + config.reload() class OnionrCommunicatorDaemon: def __init__(self, onionrInst, proxyPort, developmentMode=config.get('general.dev_mode', False)): @@ -38,11 +40,11 @@ class OnionrCommunicatorDaemon: # list of timer instances self.timers = [] - # initalize core with Tor socks port being 3rd argument + # initialize core with Tor socks port being 3rd argument self.proxyPort = proxyPort self._core = onionrInst.onionrCore - # intalize NIST beacon salt and time + # initialize NIST beacon salt and time self.nistSaltTimestamp = 0 self.powSalt = 0 @@ -57,11 +59,12 @@ class OnionrCommunicatorDaemon: self.cooldownPeer = {} self.connectTimes = {} self.peerProfiles = [] # list of peer's profiles (onionrpeers.PeerProfile instances) + self.newPeers = [] # Peers merged to us. Don't add to db until we know they're reachable # amount of threads running by name, used to prevent too many self.threadCounts = {} - # set true when shutdown command recieved + # set true when shutdown command received self.shutdown = False # list of new blocks to download, added to when new block lists are fetched from peers @@ -130,7 +133,6 @@ class OnionrCommunicatorDaemon: self.socketServer.start() self.socketClient = onionrsockets.OnionrSocketClient(self._core) - # Main daemon loop, mainly for calling timers, don't do any complex operations here to avoid locking try: while not self.shutdown: @@ -155,11 +157,27 @@ class OnionrCommunicatorDaemon: '''Lookup new peer addresses''' logger.info('Looking up new addresses...') tryAmount = 1 + newPeers = [] for i in range(tryAmount): # Download new peer address list from random online peers + if len(newPeers) > 10000: + # Dont get new peers if we have too many queued up + break peer = self.pickOnlinePeer() newAdders = self.peerAction(peer, action='pex') - self._core._utils.mergeAdders(newAdders) + try: + newPeers = newAdders.split(',') + except AttributeError: + pass + else: + # Validate new peers are good format and not already in queue + invalid = [] + for x in newPeers: + if not self._core._utils.validateID(x) or x in self.newPeers: + invalid.append(x) + for x in invalid: + newPeers.remove(x) + self.newPeers.extend(newPeers) self.decrementThreadCount('lookupAdders') def lookupBlocks(self): @@ -188,40 +206,37 @@ class OnionrCommunicatorDaemon: break else: continue - newDBHash = self.peerAction(peer, 'getdbhash') # get their db hash - if newDBHash == False or not self._core._utils.validateHash(newDBHash): - continue # if request failed, restart loop (peer is added to offline peers automatically) triedPeers.append(peer) - if newDBHash != self._core.getAddressInfo(peer, 'DBHash'): - self._core.setAddressInfo(peer, 'DBHash', newDBHash) - # Get the last time we looked up a peer's stamp to only fetch blocks since then. - # Saved in memory only for privacy reasons - try: - lastLookupTime = self.dbTimestamps[peer] - except KeyError: - lastLookupTime = 0 - else: - listLookupCommand += '?date=%s' % (lastLookupTime,) - try: - newBlocks = self.peerAction(peer, listLookupCommand) # get list of new block hashes - except Exception as error: - logger.warn('Could not get new blocks from %s.' % peer, error = error) - newBlocks = False - else: - self.dbTimestamps[peer] = self._core._utils.getRoundedEpoch(roundS=60) - if newBlocks != False: - # if request was a success - for i in newBlocks.split('\n'): - if self._core._utils.validateHash(i): - # if newline seperated string is valid hash - if not i in existingBlocks: - # if block does not exist on disk and is not already in block queue - if i not in self.blockQueue: - if onionrproofs.hashMeetsDifficulty(i) and not self._core._blacklist.inBlacklist(i): - if len(self.blockQueue) <= 1000000: - self.blockQueue[i] = [peer] # add blocks to download queue - else: - if peer not in self.blockQueue[i]: + + # Get the last time we looked up a peer's stamp to only fetch blocks since then. + # Saved in memory only for privacy reasons + try: + lastLookupTime = self.dbTimestamps[peer] + except KeyError: + lastLookupTime = 0 + else: + listLookupCommand += '?date=%s' % (lastLookupTime,) + try: + newBlocks = self.peerAction(peer, listLookupCommand) # get list of new block hashes + except Exception as error: + logger.warn('Could not get new blocks from %s.' % peer, error = error) + newBlocks = False + else: + self.dbTimestamps[peer] = self._core._utils.getRoundedEpoch(roundS=60) + if newBlocks != False: + # if request was a success + for i in newBlocks.split('\n'): + if self._core._utils.validateHash(i): + # if newline seperated string is valid hash + if not i in existingBlocks: + # if block does not exist on disk and is not already in block queue + if i not in self.blockQueue: + if onionrproofs.hashMeetsDifficulty(i) and not self._core._blacklist.inBlacklist(i): + if len(self.blockQueue) <= 1000000: + self.blockQueue[i] = [peer] # add blocks to download queue + else: + if peer not in self.blockQueue[i]: + if len(self.blockQueue[i]) < 10: self.blockQueue[i].append(peer) self.decrementThreadCount('lookupBlocks') return @@ -240,10 +255,10 @@ class OnionrCommunicatorDaemon: break # Do not download blocks being downloaded or that are already saved (edge cases) if blockHash in self.currentDownloading: - logger.debug('Already downloading block %s...' % blockHash) + #logger.debug('Already downloading block %s...' % blockHash) continue if blockHash in self._core.getBlockList(): - logger.debug('Block %s is already saved.' % (blockHash,)) + #logger.debug('Block %s is already saved.' % (blockHash,)) try: del self.blockQueue[blockHash] except KeyError: @@ -268,10 +283,7 @@ class OnionrCommunicatorDaemon: content = content.encode() except AttributeError: pass - try: - content = base64.b64decode(content) # content is base64 encoded in transport - except binascii.Error: - pass + realHash = self._core._crypto.sha3Hash(content) try: realHash = realHash.decode() # bytes on some versions for some reason @@ -402,8 +414,18 @@ class OnionrCommunicatorDaemon: else: peerList = self._core.listAdders() + mainPeerList = self._core.listAdders() peerList = onionrpeers.getScoreSortedPeerList(self._core) + if len(peerList) < 8 or secrets.randbelow(4) == 3: + tryingNew = [] + for x in self.newPeers: + if x not in peerList: + peerList.append(x) + tryingNew.append(x) + for i in tryingNew: + self.newPeers.remove(i) + if len(peerList) == 0 or useBootstrap: # Avoid duplicating bootstrap addresses in peerList self.addBootstrapListToPeerList(peerList) @@ -418,6 +440,8 @@ class OnionrCommunicatorDaemon: if self.peerAction(address, 'ping') == 'pong!': logger.info('Connected to ' + address) time.sleep(0.1) + if address not in mainPeerList: + networkmerger.mergeAdders(address, self._core) if address not in self.onlinePeers: self.onlinePeers.append(address) self.connectTimes[address] = self._core._utils.getEpoch() @@ -588,7 +612,7 @@ class OnionrCommunicatorDaemon: proxyType = 'i2p' logger.info("Uploading block to " + peer) if not self._core._utils.doPostRequest(url, data=data, proxyType=proxyType) == False: - self._core._utils.localCommand('waitforshare/' + bl) + self._core._utils.localCommand('waitforshare/' + bl, post=True) finishedUploads.append(bl) for x in finishedUploads: try: @@ -605,8 +629,8 @@ class OnionrCommunicatorDaemon: def detectAPICrash(self): '''exit if the api server crashes/stops''' if self._core._utils.localCommand('ping', silent=False) not in ('pong', 'pong!'): - for i in range(5): - if self._core._utils.localCommand('ping') in ('pong', 'pong!'): + for i in range(8): + if self._core._utils.localCommand('ping') in ('pong', 'pong!') or self.shutdown: break # break for loop time.sleep(1) else: diff --git a/onionr/config.py b/onionr/config.py old mode 100644 new mode 100755 diff --git a/onionr/core.py b/onionr/core.py old mode 100644 new mode 100755 index 8634ab1f..c1b743e4 --- a/onionr/core.py +++ b/onionr/core.py @@ -21,7 +21,8 @@ import sqlite3, os, sys, time, math, base64, tarfile, nacl, logger, json, netcon from onionrblockapi import Block import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions -import onionrblacklist, onionrusers +import onionrblacklist +from onionrusers import onionrusers import dbcreator, onionrstorage, serializeddata from etc import onionrvalues @@ -56,7 +57,7 @@ class Core: self.privateApiHostFile = self.dataDir + 'private-host.txt' self.addressDB = self.dataDir + 'address.db' self.hsAddress = '' - self.i2pAddress = config.get('i2p.ownAddr', None) + self.i2pAddress = config.get('i2p.own_addr', None) self.bootstrapFileLocation = 'static-data/bootstrap-nodes.txt' self.bootstrapList = [] self.requirements = onionrvalues.OnionrValues() @@ -85,6 +86,10 @@ class Core: self.createBlockDB() if not os.path.exists(self.forwardKeysFile): self.dbCreate.createForwardKeyDB() + if not os.path.exists(self.peerDB): + self.createPeerDB() + if not os.path.exists(self.addressDB): + self.createAddressDB() if os.path.exists(self.dataDir + '/hs/hostname'): with open(self.dataDir + '/hs/hostname', 'r') as hs: @@ -125,6 +130,7 @@ class Core: ''' Adds a public key to the key database (misleading function name) ''' + assert peerID not in self.listPeers() # This function simply adds a peer to the DB if not self._utils.validatePubKey(peerID): @@ -221,18 +227,8 @@ class Core: c.execute('Delete from hashes where hash=?;', t) conn.commit() conn.close() - blockFile = self.dataDir + '/blocks/%s.dat' % block - dataSize = 0 - try: - ''' Get size of data when loaded as an object/var, rather than on disk, - to avoid conflict with getsizeof when saving blocks - ''' - with open(blockFile, 'r') as data: - dataSize = sys.getsizeof(data.read()) - self._utils.storageCounter.removeBytes(dataSize) - os.remove(blockFile) - except FileNotFoundError: - pass + dataSize = sys.getsizeof(onionrstorage.getData(self, block)) + self._utils.storageCounter.removeBytes(dataSize) def createAddressDB(self): ''' @@ -282,15 +278,6 @@ class Core: Simply return the data associated to a hash ''' - ''' - try: - # logger.debug('Opening %s' % (str(self.blockDataLocation) + str(hash) + '.dat')) - dataFile = open(self.blockDataLocation + hash + '.dat', 'rb') - data = dataFile.read() - dataFile.close() - except FileNotFoundError: - data = False - ''' data = onionrstorage.getData(self, hash) return data @@ -316,9 +303,6 @@ class Core: #raise Exception("Data is already set for " + dataHash) else: if self._utils.storageCounter.addBytes(dataSize) != False: - #blockFile = open(blockFileName, 'wb') - #blockFile.write(data) - #blockFile.close() onionrstorage.store(self, data, blockHash=dataHash) conn = sqlite3.connect(self.blockDB, timeout=30) c = conn.cursor() @@ -557,19 +541,18 @@ class Core: knownPeer text, 2 speed int, 3 success int, 4 - DBHash text, 5 - powValue 6 - failure int 7 - lastConnect 8 - trust 9 - introduced 10 + powValue 5 + failure int 6 + lastConnect 7 + trust 8 + introduced 9 ''' conn = sqlite3.connect(self.addressDB, timeout=30) c = conn.cursor() command = (address,) - infoNumbers = {'address': 0, 'type': 1, 'knownPeer': 2, 'speed': 3, 'success': 4, 'DBHash': 5, 'powValue': 6, 'failure': 7, 'lastConnect': 8, 'trust': 9, 'introduced': 10} + infoNumbers = {'address': 0, 'type': 1, 'knownPeer': 2, 'speed': 3, 'success': 4, 'powValue': 5, 'failure': 6, 'lastConnect': 7, 'trust': 8, 'introduced': 9} info = infoNumbers[info] iterCount = 0 retVal = '' @@ -595,7 +578,7 @@ class Core: command = (data, address) - if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'DBHash', 'failure', 'powValue', 'lastConnect', 'lastConnectAttempt', 'trust', 'introduced'): + if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'failure', 'powValue', 'lastConnect', 'lastConnectAttempt', 'trust', 'introduced'): raise Exception("Got invalid database key when setting address info") else: c.execute('UPDATE adders SET ' + key + ' = ? WHERE address=?', command) @@ -680,19 +663,6 @@ class Core: conn.close() return rows - def setBlockType(self, hash, blockType): - ''' - Sets the type of block - ''' - - conn = sqlite3.connect(self.blockDB, timeout=30) - c = conn.cursor() - c.execute("UPDATE hashes SET dataType = ? WHERE hash = ?;", (blockType, hash)) - conn.commit() - conn.close() - - return - def updateBlockInfo(self, hash, key, data): ''' sets info associated with a block @@ -731,6 +701,7 @@ class Core: logger.error(allocationReachedMessage) return False retData = False + # check nonce dataNonce = self._utils.bytesToStr(self._crypto.sha3Hash(data)) try: @@ -746,6 +717,12 @@ class Core: if type(data) is bytes: data = data.decode() data = str(data) + plaintext = data + plaintextMeta = {} + + # Convert asym peer human readable key to base32 if set + if ' ' in asymPeer.strip(): + asymPeer = self._utils.convertHumanReadableID(asymPeer) retData = '' signature = '' @@ -768,7 +745,7 @@ class Core: pass if encryptType == 'asym': - if not disableForward and asymPeer != self._crypto.pubKey: + if not disableForward and sign and asymPeer != self._crypto.pubKey: try: forwardEncrypted = onionrusers.OnionrUser(self, asymPeer).forwardEncrypt(data) data = forwardEncrypted[0] @@ -780,6 +757,7 @@ class Core: #fsKey = onionrusers.OnionrUser(self, asymPeer).getGeneratedForwardKeys().reverse() meta['newFSKey'] = fsKey jsonMeta = json.dumps(meta) + plaintextMeta = jsonMeta if sign: signature = self._crypto.edSign(jsonMeta.encode() + data, key=self._crypto.privKey, encodeResult=True) signer = self._crypto.pubKey @@ -802,10 +780,10 @@ class Core: if self._utils.validatePubKey(asymPeer): # Encrypt block data with forward secrecy key first, but not meta jsonMeta = json.dumps(meta) - jsonMeta = self._crypto.pubKeyEncrypt(jsonMeta, asymPeer, encodedData=True, anonymous=True).decode() - data = self._crypto.pubKeyEncrypt(data, asymPeer, encodedData=True, anonymous=True).decode() - signature = self._crypto.pubKeyEncrypt(signature, asymPeer, encodedData=True, anonymous=True).decode() - signer = self._crypto.pubKeyEncrypt(signer, asymPeer, encodedData=True, anonymous=True).decode() + jsonMeta = self._crypto.pubKeyEncrypt(jsonMeta, asymPeer, encodedData=True).decode() + data = self._crypto.pubKeyEncrypt(data, asymPeer, encodedData=True).decode() + signature = self._crypto.pubKeyEncrypt(signature, asymPeer, encodedData=True).decode() + signer = self._crypto.pubKeyEncrypt(signer, asymPeer, encodedData=True).decode() onionrusers.OnionrUser(self, asymPeer, saveUser=True) else: raise onionrexceptions.InvalidPubkey(asymPeer + ' is not a valid base32 encoded ed25519 key') @@ -832,14 +810,14 @@ class Core: retData = False else: # Tell the api server through localCommand to wait for the daemon to upload this block to make stastical analysis more difficult - self._utils.localCommand('waitforshare/' + retData) + self._utils.localCommand('/waitforshare/' + retData, post=True) self.addToBlockDB(retData, selfInsert=True, dataSaved=True) #self.setBlockType(retData, meta['type']) self._utils.processBlockMetadata(retData) self.daemonQueueAdd('uploadBlock', retData) if retData != False: - events.event('insertBlock', onionr = None, threaded = False) + events.event('insertblock', {'content': plaintext, 'meta': plaintextMeta, 'hash': retData, 'peer': self._utils.bytesToStr(asymPeer)}, onionr = self.onionrInst, threaded = True) return retData def introduceNode(self): diff --git a/onionr/cryptotests.py b/onionr/cryptotests.py deleted file mode 100755 index f7927fb8..00000000 --- a/onionr/cryptotests.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -''' - Onionr - P2P Microblogging Platform & Social network - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' -import unittest, sys, os, core, onionrcrypto, logger - -class OnionrCryptoTests(unittest.TestCase): - def testSymmetric(self): - dataString = "this is a secret message" - dataBytes = dataString.encode() - myCore = core.Core() - crypto = onionrcrypto.OnionrCrypto(myCore) - key = key = b"tttttttttttttttttttttttttttttttt" - - logger.info("Encrypting: " + dataString, timestamp=True) - encrypted = crypto.symmetricEncrypt(dataString, key, returnEncoded=True) - logger.info(encrypted, timestamp=True) - logger.info('Decrypting encrypted string:', timestamp=True) - decrypted = crypto.symmetricDecrypt(encrypted, key, encodedMessage=True) - logger.info(decrypted, timestamp=True) - self.assertTrue(True) -if __name__ == "__main__": - unittest.main() diff --git a/onionr/dbcreator.py b/onionr/dbcreator.py old mode 100644 new mode 100755 index f728254a..b84794d3 --- a/onionr/dbcreator.py +++ b/onionr/dbcreator.py @@ -39,7 +39,6 @@ class DBCreator: knownPeer text, speed int, success int, - DBHash text, powValue text, failure int, lastConnect int, diff --git a/onionr/dependencies/secrets.py b/onionr/dependencies/secrets.py old mode 100644 new mode 100755 diff --git a/onionr/etc/onionrvalues.py b/onionr/etc/onionrvalues.py old mode 100644 new mode 100755 diff --git a/onionr/etc/pgpwords.py b/onionr/etc/pgpwords.py old mode 100644 new mode 100755 index 6183eba9..d9738f3f --- a/onionr/etc/pgpwords.py +++ b/onionr/etc/pgpwords.py @@ -1,7 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- (because 0xFF, even : "Yucatán") -import os, re, sys +'''This file is adapted from https://github.com/thblt/pgp-words by github user 'thblt' ('Thibault Polge), GPL v3 license''' + +import os, re, sys, binascii _words = [ ["aardvark", "adroitness"], @@ -259,7 +261,7 @@ _words = [ ["wayside", "Wilmington"], ["willow", "Wyoming"], ["woodlark", "yesteryear"], - ["Zulu", "Yucatán"]] + ["Zulu", "Yucatan"]] hexre = re.compile("[a-fA-F0-9]+") @@ -275,41 +277,21 @@ def wordify(seq): ret = [] for i in range(0, len(seq), 2): - ret.append(_words[int(seq[i:i+2], 16)][(i//2)%2]) + ret.append(_words[int(seq[i:i+2], 16)][(i//2)%2].lower()) return ret -def usage(): - print("Usage:") - print(" {0} [fingerprint...]".format(os.path.basename(sys.argv[0]))) - print("") - print("If called with multiple arguments, they will be concatenated") - print("and treated as a single fingerprint.") - print("") - print("If called with no arguments, input is read from stdin,") - print("and each line is treated as a single fingerprint. In this") - print("mode, invalid values are silently ignored.") - exit(1) - -if __name__ == '__main__': - if 1 == len(sys.argv): - fps = sys.stdin.readlines() - else: - fps = [" ".join(sys.argv[1:])] - for fp in fps: - try: - words = wordify(fp) - print("\n{0}: ".format(fp.strip())) - sys.stdout.write("\t") - for i in range(0, len(words)): - sys.stdout.write(words[i] + " ") - if (not (i+1) % 4) and not i == len(words)-1: - sys.stdout.write("\n\t") - print("") - - except Exception as e: - if len(fps) == 1: - print (e) - usage() - - print("") - +def hexify(seq, delim=' '): + ret = b'' + sentence = seq + try: + sentence = seq.split(delim) + except AttributeError: + pass + count = 0 + for word in sentence: + count = 0 + for wordPair in _words: + if word in wordPair: + ret += bytes([(count)]) + count += 1 + return binascii.hexlify(ret) \ No newline at end of file diff --git a/onionr/keymanager.py b/onionr/keymanager.py old mode 100644 new mode 100755 diff --git a/onionr/logger.py b/onionr/logger.py old mode 100644 new mode 100755 index 7cb409a1..18dfdf1d --- a/onionr/logger.py +++ b/onionr/logger.py @@ -18,7 +18,7 @@ along with this program. If not, see . ''' -import re, sys, time, traceback +import re, sys, time, traceback, os class colors: ''' @@ -64,7 +64,10 @@ class colors: ''' Use the bitwise operators to merge these settings ''' -USE_ANSI = 0b100 +if os.name == 'nt': + USE_ANSI = 0b000 +else: + USE_ANSI = 0b100 OUTPUT_TO_CONSOLE = 0b010 OUTPUT_TO_FILE = 0b001 diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py old mode 100644 new mode 100755 diff --git a/onionr/onionr.py b/onionr/onionr.py index 110aa6f2..c79b6488 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -25,7 +25,7 @@ MIN_PY_VERSION = 6 if sys.version_info[0] == 2 or sys.version_info[1] < MIN_PY_VERSION: print('Error, Onionr requires Python 3.%s+' % (MIN_PY_VERSION,)) sys.exit(1) -import os, base64, random, getpass, shutil, subprocess, requests, time, platform, datetime, re, json, getpass, sqlite3 +import os, base64, random, getpass, shutil, time, platform, datetime, re, json, getpass, sqlite3 import webbrowser, uuid, signal from threading import Thread import api, core, config, logger, onionrplugins as plugins, onionrevents as events @@ -33,17 +33,18 @@ import onionrutils import netcontroller, onionrstorage from netcontroller import NetController from onionrblockapi import Block -import onionrproofs, onionrexceptions, onionrusers, communicator +import onionrproofs, onionrexceptions, communicator +from onionrusers import onionrusers try: from urllib3.contrib.socks import SOCKSProxyManager except ImportError: raise Exception("You need the PySocks module (for use with socks5 proxy to use Tor)") -ONIONR_TAGLINE = 'Anonymous P2P Platform - GPLv3 - https://Onionr.VoidNet.Tech' +ONIONR_TAGLINE = 'Anonymous P2P Platform - GPLv3 - https://Onionr.net' ONIONR_VERSION = '0.5.0' # for debugging and stuff ONIONR_VERSION_TUPLE = tuple(ONIONR_VERSION.split('.')) # (MAJOR, MINOR, VERSION) -API_VERSION = '5' # increments of 1; only change when something fundemental about how the API works changes. This way other nodes know how to communicate without learning too much information about you. +API_VERSION = '5' # increments of 1; only change when something fundamental about how the API works changes. This way other nodes know how to communicate without learning too much information about you. class Onionr: def __init__(self): @@ -110,12 +111,6 @@ class Onionr: except: plugins.disable(name, onionr = self, stop_event = False) - if not os.path.exists(self.onionrCore.peerDB): - self.onionrCore.createPeerDB() - pass - if not os.path.exists(self.onionrCore.addressDB): - self.onionrCore.createAddressDB() - # Get configuration if type(config.get('client.webpassword')) is type(None): config.set('client.webpassword', base64.b16encode(os.urandom(32)).decode('utf-8'), savefile=True) @@ -124,6 +119,7 @@ class Onionr: config.set('client.client.port', randomPort, savefile=True) if type(config.get('client.public.port')) is type(None): randomPort = netcontroller.getOpenPort() + print(randomPort) config.set('client.public.port', randomPort, savefile=True) if type(config.get('client.participate')) is type(None): config.set('client.participate', True, savefile=True) @@ -206,9 +202,6 @@ class Onionr: 'connect': self.addAddress, 'pex': self.doPEX, - 'ui' : self.openUI, - 'gui' : self.openUI, - 'getpassword': self.printWebPassword, 'get-password': self.printWebPassword, 'getpwd': self.printWebPassword, @@ -297,8 +290,6 @@ class Onionr: with open('%s/%s.dat' % (exportDir, bHash), 'wb') as exportFile: exportFile.write(data) - - def showDetails(self): details = { 'Node Address' : self.get_hostname(), @@ -562,24 +553,12 @@ class Onionr: if self.onionrUtils.hasKey(newPeer): logger.info('We already have that key') return - if not '-' in newPeer: - logger.info('Since no POW token was supplied for that key, one is being generated') - proof = onionrproofs.DataPOW(newPeer) - while True: - result = proof.getResult() - if result == False: - time.sleep(0.5) - else: - break - newPeer += '-' + base64.b64encode(result[1]).decode() - logger.info(newPeer) - logger.info("Adding peer: " + logger.colors.underline + newPeer) - if self.onionrUtils.mergeKeys(newPeer): - logger.info('Successfully added key') - else: + try: + if self.onionrCore.addPeer(newPeer): + logger.info('Successfully added key') + except AssertionError: logger.error('Failed to add key') - return def addAddress(self): @@ -598,7 +577,6 @@ class Onionr: logger.info("Successfully added address.") else: logger.warn("Unable to add address.") - return def addMessage(self, header="txt"): @@ -774,7 +752,7 @@ class Onionr: Onionr.setupConfig('data/', self = self) if self._developmentMode: - logger.warn('DEVELOPMENT MODE ENABLED (LESS SECURE)', timestamp = False) + logger.warn('DEVELOPMENT MODE ENABLED (NOT RECOMMENDED)', timestamp = False) net = NetController(config.get('client.public.port', 59497), apiServerIP=apiHost) logger.debug('Tor is starting...') if not net.startTor(): @@ -985,7 +963,7 @@ class Onionr: ''' self.addFile(singleBlock=True, blockType='html') - def addFile(self, singleBlock=False, blockType='txt'): + def addFile(self, singleBlock=False, blockType='bin'): ''' Adds a file to the onionr network ''' @@ -1001,7 +979,8 @@ class Onionr: try: with open(filename, 'rb') as singleFile: blockhash = self.onionrCore.insertBlock(base64.b64encode(singleFile.read()), header=blockType) - logger.info('File %s saved in block %s' % (filename, blockhash)) + if len(blockhash) > 0: + logger.info('File %s saved in block %s' % (filename, blockhash)) except: logger.error('Failed to save file in block.', timestamp = False) else: @@ -1030,7 +1009,6 @@ class Onionr: settings = settings | logger.OUTPUT_TO_CONSOLE if config.get('log.file.output', True): settings = settings | logger.OUTPUT_TO_FILE - logger.set_file(config.get('log.file.path', '/tmp/onionr.log').replace('data/', dataDir)) logger.set_settings(settings) if not self is None: @@ -1073,12 +1051,6 @@ class Onionr: return data_exists - def openUI(self): - url = 'http://127.0.0.1:%s/ui/index.html?timingToken=%s' % (config.get('client.port', 59496), self.onionrUtils.getTimeBypassToken()) - - logger.info('Opening %s ...' % url) - webbrowser.open(url, new = 1, autoraise = True) - def header(self, message = logger.colors.fg.pink + logger.colors.bold + 'Onionr' + logger.colors.reset + logger.colors.fg.pink + ' has started.'): if os.path.exists('static-data/header.txt') and logger.get_level() <= logger.LEVEL_INFO: with open('static-data/header.txt', 'rb') as file: diff --git a/onionr/onionrblacklist.py b/onionr/onionrblacklist.py old mode 100644 new mode 100755 index a87163f3..63ecbb6b --- a/onionr/onionrblacklist.py +++ b/onionr/onionrblacklist.py @@ -116,4 +116,7 @@ class OnionrBlackList: return insert = (hashed,) blacklistDate = self._core._utils.getEpoch() - self._dbExecute("INSERT INTO blacklist (hash, dataType, blacklistDate, expire) VALUES(?, ?, ?, ?);", (str(hashed), dataType, blacklistDate, expire)) + try: + self._dbExecute("INSERT INTO blacklist (hash, dataType, blacklistDate, expire) VALUES(?, ?, ?, ?);", (str(hashed), dataType, blacklistDate, expire)) + except sqlite3.IntegrityError: + pass diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py old mode 100644 new mode 100755 index 09dd77ea..e9724b03 --- a/onionr/onionrblockapi.py +++ b/onionr/onionrblockapi.py @@ -18,8 +18,9 @@ along with this program. If not, see . ''' -import core as onionrcore, logger, config, onionrexceptions, nacl.exceptions, onionrusers +import core as onionrcore, logger, config, onionrexceptions, nacl.exceptions import json, os, sys, datetime, base64, onionrstorage +from onionrusers import onionrusers class Block: blockCacheOrder = list() # NEVER write your own code that writes to this! @@ -59,7 +60,7 @@ class Block: self.update() - def decrypt(self, anonymous = True, encodedData = True): + def decrypt(self, encodedData = True): ''' Decrypt a block, loading decrypted data into their vars ''' @@ -71,16 +72,16 @@ class Block: # decrypt data if self.getHeader('encryptType') == 'asym': try: - self.bcontent = core._crypto.pubKeyDecrypt(self.bcontent, anonymous=anonymous, encodedData=encodedData) - bmeta = core._crypto.pubKeyDecrypt(self.bmetadata, anonymous=anonymous, encodedData=encodedData) + self.bcontent = core._crypto.pubKeyDecrypt(self.bcontent, encodedData=encodedData) + bmeta = core._crypto.pubKeyDecrypt(self.bmetadata, encodedData=encodedData) try: bmeta = bmeta.decode() except AttributeError: # yet another bytes fix pass self.bmetadata = json.loads(bmeta) - self.signature = core._crypto.pubKeyDecrypt(self.signature, anonymous=anonymous, encodedData=encodedData) - self.signer = core._crypto.pubKeyDecrypt(self.signer, anonymous=anonymous, encodedData=encodedData) + self.signature = core._crypto.pubKeyDecrypt(self.signature, encodedData=encodedData) + self.signer = core._crypto.pubKeyDecrypt(self.signer, encodedData=encodedData) self.bheader['signer'] = self.signer.decode() self.signedData = json.dumps(self.bmetadata) + self.bcontent.decode() try: @@ -94,7 +95,8 @@ class Block: logger.error(str(e)) pass except nacl.exceptions.CryptoError: - logger.debug('Could not decrypt block. Either invalid key or corrupted data') + pass + #logger.debug('Could not decrypt block. Either invalid key or corrupted data') else: retData = True self.decrypted = True diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py old mode 100644 new mode 100755 index 074c8aa1..8f6f8e7a --- a/onionr/onionrcrypto.py +++ b/onionr/onionrcrypto.py @@ -18,7 +18,7 @@ along with this program. If not, see . ''' import nacl.signing, nacl.encoding, nacl.public, nacl.hash, nacl.pwhash, nacl.utils, nacl.secret, os, binascii, base64, hashlib, logger, onionrproofs, time, math, sys, hmac -import onionrexceptions, keymanager +import onionrexceptions, keymanager, core # secrets module was added into standard lib in 3.6+ if sys.version_info[0] == 3 and sys.version_info[1] < 6: from dependencies import secrets @@ -94,52 +94,41 @@ class OnionrCrypto: retData = key.sign(data).signature return retData - def pubKeyEncrypt(self, data, pubkey, anonymous=True, encodedData=False): + def pubKeyEncrypt(self, data, pubkey, encodedData=False): '''Encrypt to a public key (Curve25519, taken from base32 Ed25519 pubkey)''' retVal = '' - try: - pubkey = pubkey.encode() - except AttributeError: - pass + box = None + data = self._core._utils.strToBytes(data) + + pubkey = nacl.signing.VerifyKey(pubkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_public_key() if encodedData: encoding = nacl.encoding.Base64Encoder else: encoding = nacl.encoding.RawEncoder + + box = nacl.public.SealedBox(pubkey) + retVal = box.encrypt(data, encoder=encoding) - if self.privKey != None and not anonymous: - ownKey = nacl.signing.SigningKey(seed=self.privKey, encoder=nacl.encoding.Base32Encoder).to_curve25519_private_key() - key = nacl.signing.VerifyKey(key=pubkey, encoder=nacl.encoding.Base32Encoder).to_curve25519_public_key() - ourBox = nacl.public.Box(ownKey, key) - retVal = ourBox.encrypt(data.encode(), encoder=encoding) - elif anonymous: - key = nacl.signing.VerifyKey(key=pubkey, encoder=nacl.encoding.Base32Encoder).to_curve25519_public_key() - anonBox = nacl.public.SealedBox(key) - try: - data = data.encode() - except AttributeError: - pass - retVal = anonBox.encrypt(data, encoder=encoding) return retVal - def pubKeyDecrypt(self, data, pubkey='', privkey='', anonymous=False, encodedData=False): + def pubKeyDecrypt(self, data, pubkey='', privkey='', encodedData=False): '''pubkey decrypt (Curve25519, taken from Ed25519 pubkey)''' decrypted = False if encodedData: encoding = nacl.encoding.Base64Encoder else: encoding = nacl.encoding.RawEncoder - ownKey = nacl.signing.SigningKey(seed=self.privKey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key() - if self.privKey != None and not anonymous: - ourBox = nacl.public.Box(ownKey, pubkey) - decrypted = ourBox.decrypt(data, encoder=encoding) - elif anonymous: - if self._core._utils.validatePubKey(privkey): - privkey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key() - anonBox = nacl.public.SealedBox(privkey) - else: - anonBox = nacl.public.SealedBox(ownKey) - decrypted = anonBox.decrypt(data, encoder=encoding) + if privkey == '': + privkey = self.privKey + ownKey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key() + + if self._core._utils.validatePubKey(privkey): + privkey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key() + anonBox = nacl.public.SealedBox(privkey) + else: + anonBox = nacl.public.SealedBox(ownKey) + decrypted = anonBox.decrypt(data, encoder=encoding) return decrypted def symmetricEncrypt(self, data, key, encodedKey=False, returnEncoded=True): @@ -180,12 +169,6 @@ class OnionrCrypto: decrypted = base64.b64encode(decrypted) return decrypted - def generateSymmetricPeer(self, peer): - '''Generate symmetric key for a peer and save it to the peer database''' - key = self.generateSymmetric() - self._core.setPeerInfo(peer, 'forwardKey', key) - return - def generateSymmetric(self): '''Generate a symmetric key (bytes) and return it''' return binascii.hexlify(nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE)) @@ -283,6 +266,15 @@ class OnionrCrypto: @staticmethod def safeCompare(one, two): + # Do encode here to avoid spawning core + try: + one = one.encode() + except AttributeError: + pass + try: + two = two.encode() + except AttributeError: + pass return hmac.compare_digest(one, two) @staticmethod diff --git a/onionr/onionrdaemontools.py b/onionr/onionrdaemontools.py old mode 100644 new mode 100755 index faea0070..dace5b06 --- a/onionr/onionrdaemontools.py +++ b/onionr/onionrdaemontools.py @@ -18,9 +18,11 @@ along with this program. If not, see . ''' -import onionrexceptions, onionrpeers, onionrproofs, logger, onionrusers +import onionrexceptions, onionrpeers, onionrproofs, logger import base64, sqlite3, os from dependencies import secrets +from utils import netutils +from onionrusers import onionrusers class DaemonTools: ''' @@ -43,44 +45,50 @@ class DaemonTools: else: peer = self.daemon.pickOnlinePeer() - ourID = self.daemon._core.hsAddress.strip() - - url = 'http://' + peer + '/announce' - data = {'node': ourID} - - combinedNodes = ourID + peer - existingRand = self.daemon._core.getAddressInfo(peer, 'powValue') - if type(existingRand) is type(None): - existingRand = '' - - if peer in self.announceCache: - data['random'] = self.announceCache[peer] - elif len(existingRand) > 0: - data['random'] = existingRand - else: - proof = onionrproofs.DataPOW(combinedNodes, forceDifficulty=4) - try: - data['random'] = base64.b64encode(proof.waitForResult()[1]) - except TypeError: - # Happens when we failed to produce a proof - logger.error("Failed to produce a pow for announcing to " + peer) - announceFail = True + for x in range(1): + if x == 1 and self.daemon._core.config.get('i2p.host'): + ourID = self.daemon._core.config.get('i2p.own_addr').strip() else: - self.announceCache[peer] = data['random'] - if not announceFail: - logger.info('Announcing node to ' + url) - if self.daemon._core._utils.doPostRequest(url, data) == 'Success': - logger.info('Successfully introduced node to ' + peer) - retData = True - self.daemon._core.setAddressInfo(peer, 'introduced', 1) - self.daemon._core.setAddressInfo(peer, 'powValue', data['random']) + ourID = self.daemon._core.hsAddress.strip() + + url = 'http://' + peer + '/announce' + data = {'node': ourID} + + combinedNodes = ourID + peer + if ourID != 1: + #TODO: Extend existingRand for i2p + existingRand = self.daemon._core.getAddressInfo(peer, 'powValue') + if type(existingRand) is type(None): + existingRand = '' + + if peer in self.announceCache: + data['random'] = self.announceCache[peer] + elif len(existingRand) > 0: + data['random'] = existingRand + else: + proof = onionrproofs.DataPOW(combinedNodes, forceDifficulty=4) + try: + data['random'] = base64.b64encode(proof.waitForResult()[1]) + except TypeError: + # Happens when we failed to produce a proof + logger.error("Failed to produce a pow for announcing to " + peer) + announceFail = True + else: + self.announceCache[peer] = data['random'] + if not announceFail: + logger.info('Announcing node to ' + url) + if self.daemon._core._utils.doPostRequest(url, data) == 'Success': + logger.info('Successfully introduced node to ' + peer) + retData = True + self.daemon._core.setAddressInfo(peer, 'introduced', 1) + self.daemon._core.setAddressInfo(peer, 'powValue', data['random']) self.daemon.decrementThreadCount('announceNode') return retData def netCheck(self): '''Check if we are connected to the internet or not when we can't connect to any peers''' if len(self.daemon.onlinePeers) == 0: - if not self.daemon._core._utils.checkNetwork(torPort=self.daemon.proxyPort): + if not netutils.checkNetwork(self.daemon._core._utils, torPort=self.daemon.proxyPort): logger.warn('Network check failed, are you connected to the internet?') self.daemon.isOnline = False else: @@ -186,10 +194,11 @@ class DaemonTools: def insertDeniableBlock(self): '''Insert a fake block in order to make it more difficult to track real blocks''' - fakePeer = self.daemon._core._crypto.generatePubKey()[0] + fakePeer = '' chance = 10 if secrets.randbelow(chance) == (chance - 1): + fakePeer = self.daemon._core._crypto.generatePubKey()[0] data = secrets.token_hex(secrets.randbelow(500) + 1) - self.daemon._core.insertBlock(data, header='pm', encryptType='asym', asymPeer=fakePeer) + self.daemon._core.insertBlock(data, header='pm', encryptType='asym', asymPeer=fakePeer, meta={'subject': 'foo'}) self.daemon.decrementThreadCount('insertDeniableBlock') return \ No newline at end of file diff --git a/onionr/onionrevents.py b/onionr/onionrevents.py old mode 100644 new mode 100755 index 26fdc093..0a2c48f1 --- a/onionr/onionrevents.py +++ b/onionr/onionrevents.py @@ -61,10 +61,9 @@ def call(plugin, event_name, data = None, pluginapi = None): try: attribute = 'on_' + str(event_name).lower() - # TODO: Use multithreading perhaps? if hasattr(plugin, attribute): #logger.debug('Calling event ' + str(event_name)) - getattr(plugin, attribute)(pluginapi) + getattr(plugin, attribute)(pluginapi, data) return True except Exception as e: diff --git a/onionr/onionrexceptions.py b/onionr/onionrexceptions.py old mode 100644 new mode 100755 index 6763a172..757091db --- a/onionr/onionrexceptions.py +++ b/onionr/onionrexceptions.py @@ -44,6 +44,10 @@ class PasswordStrengthError(Exception): pass # block exceptions + +class DifficultyTooLarge(Exception): + pass + class InvalidMetadata(Exception): pass @@ -82,4 +86,9 @@ class DiskAllocationReached(Exception): # onionrsocket exceptions class MissingAddress(Exception): + pass + +# Contact exceptions + +class ContactDeleted(Exception): pass \ No newline at end of file diff --git a/onionr/onionrpeers.py b/onionr/onionrpeers.py old mode 100644 new mode 100755 diff --git a/onionr/onionrpluginapi.py b/onionr/onionrpluginapi.py old mode 100644 new mode 100755 diff --git a/onionr/onionrplugins.py b/onionr/onionrplugins.py old mode 100644 new mode 100755 diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py old mode 100644 new mode 100755 index f1645a49..81a189a9 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -41,9 +41,9 @@ def getDifficultyModifier(coreOrUtilsInst=None): if percentUse >= 0.50: retData += 1 elif percentUse >= 0.75: - retData += 2 + retData += 2 elif percentUse >= 0.95: - retData += 3 + retData += 3 return retData @@ -64,11 +64,12 @@ def getDifficultyForNewBlock(data, ourBlock=True): else: raise ValueError('not Block, str, or int') if ourBlock: - minDifficulty = config.get('general.minimum_send_pow') + minDifficulty = config.get('general.minimum_send_pow', 4) else: - minDifficulty = config.get('general.minimum_block_pow') + minDifficulty = config.get('general.minimum_block_pow', 4) + + retData = max(minDifficulty, math.floor(dataSize / 100000)) + getDifficultyModifier() - retData = max(minDifficulty, math.floor(dataSize / 1000000)) + getDifficultyModifier() return retData def getHashDifficulty(h): @@ -192,7 +193,7 @@ class DataPOW: if not self.hashing: break else: - time.sleep(2) + time.sleep(1) except KeyboardInterrupt: self.shutdown() logger.warn('Got keyboard interrupt while waiting for POW result, stopping') @@ -243,13 +244,11 @@ class POW: self.reporting = reporting iFound = False # if current thread is the one that found the answer answer = '' - heartbeat = 200000 hbCount = 0 - + nonce = int(binascii.hexlify(nacl.utils.random(2)), 16) while self.hashing: - rand = nacl.utils.random() #token = nacl.hash.blake2b(rand + self.data).decode() - self.metadata['powRandomToken'] = base64.b64encode(rand).decode() + self.metadata['powRandomToken'] = nonce payload = json.dumps(self.metadata).encode() + b'\n' + self.data token = myCore._crypto.sha3Hash(payload) try: @@ -262,6 +261,7 @@ class POW: iFound = True self.result = payload break + nonce += 1 if iFound: endTime = math.floor(time.time()) @@ -299,7 +299,7 @@ class POW: if not self.hashing: break else: - time.sleep(2) + time.sleep(1) except KeyboardInterrupt: self.shutdown() logger.warn('Got keyboard interrupt while waiting for POW result, stopping') diff --git a/onionr/onionrsockets.py b/onionr/onionrsockets.py old mode 100644 new mode 100755 diff --git a/onionr/onionrstorage.py b/onionr/onionrstorage.py index 8e2aae41..63aa150d 100644 --- a/onionr/onionrstorage.py +++ b/onionr/onionrstorage.py @@ -55,6 +55,21 @@ def _dbFetch(coreInst, blockHash): conn.close() return None +def deleteBlock(coreInst, blockHash): + # You should call core.removeBlock if you automatically want to remove storage byte count + assert isinstance(coreInst, core.Core) + if os.path.exists('%s/%s.dat' % (coreInst.blockDataLocation, blockHash)): + os.remove('%s/%s.dat' % (coreInst.blockDataLocation, blockHash)) + return True + dbCreate(coreInst) + conn = sqlite3.connect(coreInst.blockDataDB, timeout=10) + c = conn.cursor() + data = (blockHash,) + c.execute('DELETE FROM blockData where hash = ?', data) + conn.commit() + conn.close() + return True + def store(coreInst, data, blockHash=''): assert isinstance(coreInst, core.Core) assert coreInst._utils.validateHash(blockHash) diff --git a/onionr/onionrusers/contactmanager.py b/onionr/onionrusers/contactmanager.py new file mode 100644 index 00000000..c47bbe45 --- /dev/null +++ b/onionr/onionrusers/contactmanager.py @@ -0,0 +1,72 @@ +''' + Onionr - P2P Anonymous Storage Network + + Sets more abstract information related to a peer. Can be thought of as traditional 'contact' system +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import os, json, onionrexceptions +from onionrusers import onionrusers + +class ContactManager(onionrusers.OnionrUser): + def __init__(self, coreInst, publicKey, saveUser=False, recordExpireSeconds=5): + super(ContactManager, self).__init__(coreInst, publicKey, saveUser=saveUser) + self.dataDir = coreInst.dataDir + '/contacts/' + self.dataFile = '%s/contacts/%s.json' % (coreInst.dataDir, publicKey) + self.lastRead = 0 + self.recordExpire = recordExpireSeconds + self.data = self._loadData() + self.deleted = False + + if not os.path.exists(self.dataDir): + os.mkdir(self.dataDir) + + def _writeData(self): + data = json.dumps(self.data) + with open(self.dataFile, 'w') as dataFile: + dataFile.write(data) + + def _loadData(self): + self.lastRead = self._core._utils.getEpoch() + retData = {} + if os.path.exists(self.dataFile): + with open(self.dataFile, 'r') as dataFile: + retData = json.loads(dataFile.read()) + return retData + + def set_info(self, key, value, autoWrite=True): + if self.deleted: + raise onionrexceptions.ContactDeleted + + self.data[key] = value + if autoWrite: + self._writeData() + return + + def get_info(self, key, forceReload=False): + if self.deleted: + raise onionrexceptions.ContactDeleted + + if (self._core._utils.getEpoch() - self.lastRead >= self.recordExpire) or forceReload: + self.data = self._loadData() + try: + return self.data[key] + except KeyError: + return None + + def delete_contact(self): + self.deleted = True + if os.path.exists(self.dataFile): + os.remove(self.dataFile) \ No newline at end of file diff --git a/onionr/onionrusers.py b/onionr/onionrusers/onionrusers.py old mode 100644 new mode 100755 similarity index 95% rename from onionr/onionrusers.py rename to onionr/onionrusers/onionrusers.py index c5bf41d1..ab14a5a0 --- a/onionr/onionrusers.py +++ b/onionr/onionrusers/onionrusers.py @@ -40,12 +40,18 @@ class OnionrUser: Takes an instance of onionr core, a base32 encoded ed25519 public key, and a bool saveUser saveUser determines if we should add a user to our peer database or not. ''' + if ' ' in coreInst._utils.bytesToStr(publicKey).strip(): + publicKey = coreInst._utils.convertHumanReadableID(publicKey) + self.trust = 0 self._core = coreInst self.publicKey = publicKey if saveUser: - self._core.addPeer(publicKey) + try: + self._core.addPeer(publicKey) + except AssertionError: + pass self.trust = self._core.getPeerInfo(self.publicKey, 'trust') return @@ -73,7 +79,7 @@ class OnionrUser: encrypted = coreInst._crypto.pubKeyEncrypt(data, self.publicKey, encodedData=True) return encrypted - def decrypt(self, data, anonymous=True): + def decrypt(self, data): decrypted = coreInst._crypto.pubKeyDecrypt(data, self.publicKey, encodedData=True) return decrypted @@ -81,7 +87,7 @@ class OnionrUser: retData = '' forwardKey = self._getLatestForwardKey() if self._core._utils.validatePubKey(forwardKey): - retData = self._core._crypto.pubKeyEncrypt(data, forwardKey, encodedData=True, anonymous=True) + retData = self._core._crypto.pubKeyEncrypt(data, forwardKey, encodedData=True) else: raise onionrexceptions.InvalidPubkey("No valid forward secrecy key available for this user") #self.generateForwardKey() @@ -91,7 +97,7 @@ class OnionrUser: retData = "" for key in self.getGeneratedForwardKeys(False): try: - retData = self._core._crypto.pubKeyDecrypt(encrypted, privkey=key[1], anonymous=True, encodedData=True) + retData = self._core._crypto.pubKeyDecrypt(encrypted, privkey=key[1], encodedData=True) except nacl.exceptions.CryptoError: retData = False else: diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py old mode 100644 new mode 100755 index b34eb948..afe834e8 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -18,14 +18,15 @@ along with this program. If not, see . ''' # Misc functions that do not fit in the main api, but are useful -import getpass, sys, requests, os, socket, hashlib, logger, sqlite3, config, binascii, time, base64, json, glob, shutil, math, json, re, urllib.parse +import getpass, sys, requests, os, socket, hashlib, logger, sqlite3, config, binascii, time, base64, json, glob, shutil, math, json, re, urllib.parse, string import nacl.signing, nacl.encoding from onionrblockapi import Block import onionrexceptions from onionr import API_VERSION import onionrevents -import onionrusers, storagecounter -from etc import pgpwords +import storagecounter +from etc import pgpwords +from onionrusers import onionrusers if sys.version_info < (3, 6): try: import sha3 @@ -67,90 +68,6 @@ class OnionrUtils: ''' epoch = self.getEpoch() return epoch - (epoch % roundS) - - def mergeKeys(self, newKeyList): - ''' - Merge ed25519 key list to our database, comma seperated string - ''' - try: - retVal = False - if newKeyList != False: - for key in newKeyList.split(','): - key = key.split('-') - # Test if key is valid - try: - if len(key[0]) > 60 or len(key[1]) > 1000: - logger.warn('%s or its pow value is too large.' % key[0]) - continue - except IndexError: - logger.warn('No pow token') - continue - try: - value = base64.b64decode(key[1]) - except binascii.Error: - continue - # Load the pow token - hashedKey = self._core._crypto.blake2bHash(key[0]) - powHash = self._core._crypto.blake2bHash(value + hashedKey) - try: - powHash = powHash.encode() - except AttributeError: - pass - # if POW meets required difficulty, TODO make configurable/dynamic - if powHash.startswith(b'0000'): - # if we don't already have the key and its not our key, add it. - if not key[0] in self._core.listPeers(randomOrder=False) and type(key) != None and key[0] != self._core._crypto.pubKey: - if self._core.addPeer(key[0], key[1]): - # Check if the peer has a set username already - onionrusers.OnionrUser(self._core, key[0]).findAndSetID() - retVal = True - else: - logger.warn("Failed to add key") - else: - pass - #logger.debug('%s pow failed' % key[0]) - return retVal - except Exception as error: - logger.error('Failed to merge keys.', error=error) - return False - - - def mergeAdders(self, newAdderList): - ''' - Merge peer adders list to our database - ''' - try: - retVal = False - if newAdderList != False: - for adder in newAdderList.split(','): - adder = adder.strip() - if not adder in self._core.listAdders(randomOrder = False) and adder != self.getMyAddress() and not self._core._blacklist.inBlacklist(adder): - if not config.get('tor.v3onions') and len(adder) == 62: - continue - if self._core.addAddress(adder): - # Check if we have the maxmium amount of allowed stored peers - if config.get('peers.max_stored_peers') > len(self._core.listAdders()): - logger.info('Added %s to db.' % adder, timestamp = True) - retVal = True - else: - logger.warn('Reached the maximum amount of peers in the net database as allowed by your config.') - else: - pass - #logger.debug('%s is either our address or already in our DB' % adder) - return retVal - except Exception as error: - logger.error('Failed to merge adders.', error = error) - return False - - def getMyAddress(self): - try: - with open('./' + self._core.dataDir + 'hs/hostname', 'r') as hostname: - return hostname.read().strip() - except FileNotFoundError: - return "" - except Exception as error: - logger.error('Failed to read my address.', error = error) - return None def getClientAPIServer(self): retData = '' @@ -163,7 +80,7 @@ class OnionrUtils: retData += '%s:%s' % (hostname, config.get('client.client.port')) return retData - def localCommand(self, command, data='', silent = True, post=False, postData = {}, maxWait=10): + def localCommand(self, command, data='', silent = True, post=False, postData = {}, maxWait=20): ''' Send a command to the local http API server, securely. Intended for local clients, DO NOT USE for remote peers. ''' @@ -185,9 +102,9 @@ class OnionrUtils: payload = 'http://%s/%s%s' % (hostname, command, data) try: if post: - retData = requests.post(payload, data=postData, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, 30)).text + retData = requests.post(payload, data=postData, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, maxWait)).text else: - retData = requests.get(payload, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, 30)).text + retData = requests.get(payload, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, maxWait)).text except Exception as error: if not silent: logger.error('Failed to make local request (command: %s):%s' % (command, error)) @@ -195,38 +112,23 @@ class OnionrUtils: return retData - def getPassword(self, message='Enter password: ', confirm = True): - ''' - Get a password without showing the users typing and confirm the input - ''' - # Get a password safely with confirmation and return it - while True: - print(message) - pass1 = getpass.getpass() - if confirm: - print('Confirm password: ') - pass2 = getpass.getpass() - if pass1 != pass2: - logger.error("Passwords do not match.") - logger.readline() - else: - break - else: - break - - return pass1 - def getHumanReadableID(self, pub=''): '''gets a human readable ID from a public key''' if pub == '': pub = self._core._crypto.pubKey pub = base64.b16encode(base64.b32decode(pub)).decode() - return '-'.join(pgpwords.wordify(pub)) + return ' '.join(pgpwords.wordify(pub)) + + def convertHumanReadableID(self, pub): + '''Convert a human readable pubkey id to base32''' + pub = pub.lower() + return self.bytesToStr(base64.b32encode(binascii.unhexlify(pgpwords.hexify(pub.strip())))) def getBlockMetadataFromData(self, blockData): ''' - accepts block contents as string, returns a tuple of metadata, meta (meta being internal metadata, which will be returned as an encrypted base64 string if it is encrypted, dict if not). - + accepts block contents as string, returns a tuple of + metadata, meta (meta being internal metadata, which will be + returned as an encrypted base64 string if it is encrypted, dict if not). ''' meta = {} metadata = {} @@ -251,34 +153,6 @@ class OnionrUtils: meta = metadata['meta'] return (metadata, meta, data) - def checkPort(self, port, host=''): - ''' - Checks if a port is available, returns bool - ''' - # inspired by https://www.reddit.com/r/learnpython/comments/2i4qrj/how_to_write_a_python_script_that_checks_to_see/ckzarux/ - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - retVal = False - try: - sock.bind((host, port)) - except OSError as e: - if e.errno is 98: - retVal = True - finally: - sock.close() - - return retVal - - def checkIsIP(self, ip): - ''' - Check if a string is a valid IPv4 address - ''' - try: - socket.inet_aton(ip) - except: - return False - else: - return True - def processBlockMetadata(self, blockHash): ''' Read metadata from a block and cache it to the block database @@ -309,7 +183,8 @@ class OnionrUtils: else: self._core.updateBlockInfo(blockHash, 'expire', expireTime) else: - logger.debug('Not processing metadata on encrypted block we cannot decrypt.') + pass + #logger.debug('Not processing metadata on encrypted block we cannot decrypt.') def escapeAnsi(self, line): ''' @@ -320,21 +195,6 @@ class OnionrUtils: ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]') return ansi_escape.sub('', line) - def getBlockDBHash(self): - ''' - Return a sha3_256 hash of the blocks DB - ''' - try: - with open(self._core.blockDB, 'rb') as data: - data = data.read() - hasher = hashlib.sha3_256() - hasher.update(data) - dataHash = hasher.hexdigest() - - return dataHash - except Exception as error: - logger.error('Failed to get block DB hash.', error=error) - def hasBlock(self, hash): ''' Check for new block in the list @@ -361,7 +221,7 @@ class OnionrUtils: def validateHash(self, data, length=64): ''' - Validate if a string is a valid hex formatted hash + Validate if a string is a valid hash hex digest (does not compare, just checks length and charset) ''' retVal = True if data == False or data == True: @@ -450,6 +310,8 @@ class OnionrUtils: Validate if a string is a valid base32 encoded Ed25519 key ''' retVal = False + if type(key) is type(None): + return False try: nacl.signing.SigningKey(seed=key, encoder=nacl.encoding.Base32Encoder) except nacl.exceptions.ValueError: @@ -460,15 +322,6 @@ class OnionrUtils: retVal = True return retVal - def isIntegerString(self, data): - '''Check if a string is a valid base10 integer (also returns true if already an int)''' - try: - int(data) - except ValueError: - return False - else: - return True - def validateID(self, id): ''' Validate if an address is a valid tor or i2p hidden service @@ -513,36 +366,22 @@ class OnionrUtils: retVal = False # Validate address is valid base32 (when capitalized and minus extension); v2/v3 onions and .b32.i2p use base32 - try: - base64.b32decode(idNoDomain.upper().encode()) - except binascii.Error: - retVal = False - - # Validate address is valid base32 (when capitalized and minus extension); v2/v3 onions and .b32.i2p use base32 - try: - base64.b32decode(idNoDomain.upper().encode()) - except binascii.Error: - retVal = False + for x in idNoDomain.upper(): + if x not in string.ascii_uppercase and x not in '234567': + retVal = False return retVal except: return False - def getPeerByHashId(self, hash): - ''' - Return the pubkey of the user if known from the hash - ''' - if self._core._crypto.pubKeyHashID() == hash: - retData = self._core._crypto.pubKey - return retData - conn = sqlite3.connect(self._core.peerDB) - c = conn.cursor() - command = (hash,) - retData = '' - for row in c.execute('SELECT id FROM peers WHERE hashID = ?', command): - if row[0] != '': - retData = row[0] - return retData + def isIntegerString(self, data): + '''Check if a string is a valid base10 integer (also returns true if already an int)''' + try: + int(data) + except (ValueError, TypeError) as e: + return False + else: + return True def isCommunicatorRunning(self, timeout = 5, interval = 0.1): try: @@ -564,13 +403,6 @@ class OnionrUtils: except: return False - def token(self, size = 32): - ''' - Generates a secure random hex encoded token - ''' - - return binascii.hexlify(os.urandom(size)) - def importNewBlocks(self, scanDir=''): ''' This function is intended to scan for new blocks ON THE DISK and import them @@ -692,22 +524,6 @@ class OnionrUtils: pass return data - def checkNetwork(self, torPort=0): - '''Check if we are connected to the internet (through Tor)''' - retData = False - connectURLs = [] - try: - with open('static-data/connect-check.txt', 'r') as connectTest: - connectURLs = connectTest.read().split(',') - - for url in connectURLs: - if self.doGetRequest(url, port=torPort, ignoreAPI=True) != False: - retData = True - break - except FileNotFoundError: - pass - return retData - def size(path='.'): ''' Returns the size of a folder's contents in bytes @@ -732,4 +548,4 @@ def humanSize(num, suffix='B'): if abs(num) < 1024.0: return "%.1f %s%s" % (num, unit, suffix) num /= 1024.0 - return "%.1f %s%s" % (num, 'Yi', suffix) + return "%.1f %s%s" % (num, 'Yi', suffix) \ No newline at end of file diff --git a/onionr/static-data/bootstrap-nodes.txt b/onionr/static-data/bootstrap-nodes.txt old mode 100644 new mode 100755 index 93dec7ce..e35198a3 --- a/onionr/static-data/bootstrap-nodes.txt +++ b/onionr/static-data/bootstrap-nodes.txt @@ -1 +1 @@ -dd3llxdp5q6ak3zmmicoy3jnodmroouv2xr7whkygiwp3rl7nf23gdad.onion \ No newline at end of file +dd3llxdp5q6ak3zmmicoy3jnodmroouv2xr7whkygiwp3rl7nf23gdad.onion diff --git a/onionr/static-data/connect-check.txt b/onionr/static-data/connect-check.txt old mode 100644 new mode 100755 index 009a2a9a..7be85aa0 --- a/onionr/static-data/connect-check.txt +++ b/onionr/static-data/connect-check.txt @@ -1 +1 @@ -https://3g2upl4pq6kufc4m.onion/robots.txt,http://expyuzz4wqqyqhjn.onion/robots.txt,https://onionr.voidnet.tech/ +https://3g2upl4pq6kufc4m.onion/robots.txt,http://expyuzz4wqqyqhjn.onion/robots.txt,http://archivecaslytosk.onion/robots.txt diff --git a/onionr/static-data/default-plugins/cliui/info.json b/onionr/static-data/default-plugins/cliui/info.json old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/cliui/main.py b/onionr/static-data/default-plugins/cliui/main.py old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/encrypt/info.json b/onionr/static-data/default-plugins/encrypt/info.json old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/encrypt/main.py b/onionr/static-data/default-plugins/encrypt/main.py old mode 100644 new mode 100755 index 01eabd81..7ea5b446 --- a/onionr/static-data/default-plugins/encrypt/main.py +++ b/onionr/static-data/default-plugins/encrypt/main.py @@ -69,7 +69,7 @@ class PlainEncryption: data['data'] = plaintext data = json.dumps(data) plaintext = data - encrypted = self.api.get_core()._crypto.pubKeyEncrypt(plaintext, pubkey, anonymous=True, encodedData=True) + encrypted = self.api.get_core()._crypto.pubKeyEncrypt(plaintext, pubkey, encodedData=True) encrypted = self.api.get_core()._utils.bytesToStr(encrypted) logger.info('Encrypted Message: \n\nONIONR ENCRYPTED DATA %s END ENCRYPTED DATA' % (encrypted,)) @@ -88,7 +88,7 @@ class PlainEncryption: 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) + decrypted = self.api.get_core()._crypto.pubKeyDecrypt(encrypted, privkey=self.api.get_core()._crypto.privKey, encodedData=True) if decrypted == False: logger.error("Decryption failed") else: diff --git a/onionr/static-data/default-plugins/flow/info.json b/onionr/static-data/default-plugins/flow/info.json old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/flow/main.py b/onionr/static-data/default-plugins/flow/main.py old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/metadataprocessor/info.json b/onionr/static-data/default-plugins/metadataprocessor/info.json old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/metadataprocessor/main.py b/onionr/static-data/default-plugins/metadataprocessor/main.py old mode 100644 new mode 100755 index 166249be..e7277c7d --- a/onionr/static-data/default-plugins/metadataprocessor/main.py +++ b/onionr/static-data/default-plugins/metadataprocessor/main.py @@ -41,7 +41,7 @@ def _processForwardKey(api, myBlock): else: raise onionrexceptions.InvalidPubkey("%s is nota valid pubkey key" % (key,)) -def on_processblocks(api): +def on_processblocks(api, data=None): # Generally fired by utils. myBlock = api.data['block'] blockType = api.data['type'] diff --git a/onionr/static-data/default-plugins/pluginmanager/.gitignore b/onionr/static-data/default-plugins/pluginmanager/.gitignore old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/pluginmanager/LICENSE b/onionr/static-data/default-plugins/pluginmanager/LICENSE old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/pluginmanager/README.md b/onionr/static-data/default-plugins/pluginmanager/README.md old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/pluginmanager/info.json b/onionr/static-data/default-plugins/pluginmanager/info.json old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/pluginmanager/main.py b/onionr/static-data/default-plugins/pluginmanager/main.py old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/pms/info.json b/onionr/static-data/default-plugins/pms/info.json old mode 100644 new mode 100755 diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py old mode 100644 new mode 100755 index 35b85519..a5efab12 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -21,8 +21,9 @@ # Imports some useful libraries import logger, config, threading, time, readline, datetime from onionrblockapi import Block -import onionrexceptions, onionrusers -import locale, sys, os +import onionrexceptions +from onionrusers import onionrusers +import locale, sys, os, json locale.setlocale(locale.LC_ALL, '') @@ -48,14 +49,14 @@ class MailStrings: self.mailInstance = mailInstance self.programTag = 'OnionrMail v%s' % (PLUGIN_VERSION) - choices = ['view inbox', 'view sentbox', 'send message', 'quit'] + choices = ['view inbox', 'view sentbox', 'send message', 'toggle pseudonymity', 'quit'] self.mainMenuChoices = choices - self.mainMenu = '''\n ------------------ -1. %s -2. %s -3. %s -4. %s''' % (choices[0], choices[1], choices[2], choices[3]) + self.mainMenu = '''----------------- + 1. %s + 2. %s + 3. %s + 4. %s + 5. %s''' % (choices[0], choices[1], choices[2], choices[3], choices[4]) class OnionrMail: def __init__(self, pluginapi): @@ -65,6 +66,7 @@ class OnionrMail: self.sentboxTools = sentboxdb.SentBox(self.myCore) self.sentboxList = [] self.sentMessages = {} + self.doSigs = True return def inbox(self): @@ -133,18 +135,30 @@ class OnionrMail: else: cancel = '' readBlock.verifySig() - - logger.info('Message recieved from %s' % (self.myCore._utils.bytesToStr(readBlock.signer,))) + senderDisplay = self.myCore._utils.bytesToStr(readBlock.signer) + if len(senderDisplay.strip()) == 0: + senderDisplay = 'Anonymous' + logger.info('Message received from %s' % (senderDisplay,)) logger.info('Valid signature: %s' % readBlock.validSig) if not readBlock.validSig: - logger.warn('This message has an INVALID signature. ANYONE could have sent this message.') + logger.warn('This message has an INVALID/NO signature. ANYONE could have sent this message.') cancel = logger.readline('Press enter to continue to message, or -q to not open the message (recommended).') + print('') if cancel != '-q': - print(draw_border(self.myCore._utils.escapeAnsi(readBlock.bcontent.decode().strip()))) - reply = logger.readline("Press enter to continue, or enter %s to reply" % ("-r",)) - if reply == "-r": - self.draftMessage(self.myCore._utils.bytesToStr(readBlock.signer,)) + try: + print(draw_border(self.myCore._utils.escapeAnsi(readBlock.bcontent.decode().strip()))) + except ValueError: + logger.warn('Error presenting message. This is usually due to a malformed or blank message.') + pass + if readBlock.validSig: + reply = logger.readline("Press enter to continue, or enter %s to reply" % ("-r",)) + print('') + if reply == "-r": + self.draft_message(self.myCore._utils.bytesToStr(readBlock.signer,)) + else: + logger.readline("Press enter to continue") + print('') return def sentbox(self): @@ -153,7 +167,7 @@ class OnionrMail: ''' entering = True while entering: - self.getSentList() + self.get_sent_list() logger.info('Enter a block number or -q to return') try: choice = input('>') @@ -180,18 +194,19 @@ class OnionrMail: return - def getSentList(self): + def get_sent_list(self, display=True): count = 1 self.sentboxList = [] self.sentMessages = {} for i in self.sentboxTools.listSent(): self.sentboxList.append(i['hash']) - self.sentMessages[i['hash']] = (i['message'], i['peer']) - - logger.info('%s. %s - %s - %s' % (count, i['hash'], i['peer'][:12], i['date'])) + self.sentMessages[i['hash']] = (self.myCore._utils.bytesToStr(i['message']), i['peer'], i['subject']) + if display: + logger.info('%s. %s - %s - (%s) - %s' % (count, i['hash'], i['peer'][:12], i['subject'], i['date'])) count += 1 + return json.dumps(self.sentMessages) - def draftMessage(self, recip=''): + def draft_message(self, recip=''): message = '' newLine = '' subject = '' @@ -237,14 +252,26 @@ class OnionrMail: if not cancelEnter: logger.info('Inserting encrypted message as Onionr block....') - blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=True, meta={'subject': subject}) - self.sentboxTools.addToSent(blockID, recip, message) + blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=self.doSigs, meta={'subject': subject}) + + def toggle_signing(self): + self.doSigs = not self.doSigs + def menu(self): choice = '' while True: + sigMsg = 'Message Signing: %s' - logger.info(self.strings.programTag + '\n\nOur ID: ' + self.myCore._crypto.pubKey + self.strings.mainMenu.title()) # print out main menu - + logger.info(self.strings.programTag + '\n\nUser ID: ' + self.myCore._crypto.pubKey) + if self.doSigs: + sigMsg = sigMsg % ('enabled',) + else: + sigMsg = sigMsg % ('disabled (Your messages cannot be trusted)',) + if self.doSigs: + logger.info(sigMsg) + else: + logger.warn(sigMsg) + logger.info(self.strings.mainMenu.title()) # print out main menu try: choice = logger.readline('Enter 1-%s:\n' % (len(self.strings.mainMenuChoices))).lower().strip() except (KeyboardInterrupt, EOFError): @@ -255,8 +282,10 @@ class OnionrMail: elif choice in (self.strings.mainMenuChoices[1], '2'): self.sentbox() elif choice in (self.strings.mainMenuChoices[2], '3'): - self.draftMessage() + self.draft_message() elif choice in (self.strings.mainMenuChoices[3], '4'): + self.toggle_signing() + elif choice in (self.strings.mainMenuChoices[4], '5'): logger.info('Goodbye.') break elif choice == '': @@ -265,6 +294,26 @@ class OnionrMail: logger.warn('Invalid choice.') return +def on_insertblock(api, data={}): + sentboxTools = sentboxdb.SentBox(api.get_core()) + meta = json.loads(data['meta']) + sentboxTools.addToSent(data['hash'], data['peer'], data['content'], meta['subject']) + +def on_pluginrequest(api, data=None): + resp = '' + subject = '' + recip = '' + message = '' + postData = {} + blockID = '' + sentboxTools = sentboxdb.SentBox(api.get_core()) + if data['name'] == 'mail': + path = data['path'] + cmd = path.split('/')[1] + if cmd == 'sentbox': + resp = OnionrMail(api).get_sent_list(display=False) + if resp != '': + api.get_onionr().clientAPIInst.pluginResponses[data['pluginResponse']] = resp def on_init(api, data = None): ''' diff --git a/onionr/static-data/default-plugins/pms/sentboxdb.py b/onionr/static-data/default-plugins/pms/sentboxdb.py old mode 100644 new mode 100755 index 2d6207e6..f00accb3 --- a/onionr/static-data/default-plugins/pms/sentboxdb.py +++ b/onionr/static-data/default-plugins/pms/sentboxdb.py @@ -37,6 +37,7 @@ class SentBox: hash id not null, peer text not null, message text not null, + subject text not null, date int not null ); ''') @@ -46,12 +47,12 @@ class SentBox: def listSent(self): retData = [] for entry in self.cursor.execute('SELECT * FROM sent;'): - retData.append({'hash': entry[0], 'peer': entry[1], 'message': entry[2], 'date': entry[3]}) + retData.append({'hash': entry[0], 'peer': entry[1], 'message': entry[2], 'subject': entry[3], 'date': entry[4]}) return retData - def addToSent(self, blockID, peer, message): - args = (blockID, peer, message, self.core._utils.getEpoch()) - self.cursor.execute('INSERT INTO sent VALUES(?, ?, ?, ?)', args) + def addToSent(self, blockID, peer, message, subject=''): + args = (blockID, peer, message, subject, self.core._utils.getEpoch()) + self.cursor.execute('INSERT INTO sent VALUES(?, ?, ?, ?, ?)', args) self.conn.commit() return diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json old mode 100644 new mode 100755 index b6cb3145..ba5e5566 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -2,8 +2,8 @@ "general" : { "dev_mode" : true, "display_header" : false, - "minimum_block_pow": 1, - "minimum_send_pow": 1, + "minimum_block_pow": 4, + "minimum_send_pow": 4, "socket_servers": false, "security_level": 0, "max_block_age": 2678400, @@ -21,8 +21,7 @@ "private" : { "run" : true, "path" : "static-data/www/private/", - "guess_mime" : true, - "timing_protection" : true + "guess_mime" : true }, "ui" : { diff --git a/onionr/static-data/default_plugin.py b/onionr/static-data/default_plugin.py old mode 100644 new mode 100755 diff --git a/onionr/static-data/header.txt b/onionr/static-data/header.txt old mode 100644 new mode 100755 diff --git a/onionr/static-data/index.html b/onionr/static-data/index.html old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html index 6e8ce037..67a67ba6 100644 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -5,6 +5,10 @@ Onionr Mail +<<<<<<< HEAD +======= + +>>>>>>> contacts @@ -12,6 +16,7 @@
+<<<<<<< HEAD Onionr Mail
@@ -20,5 +25,52 @@
+======= +
+ + Onionr Mail ✉️ +
Current Used Identity:
+

+
+ +
+
+
Nothing here yet 😞
+
+
+
+ +
+ From: Signature: +
+
+
+
+
+
+
+
+ + To: +
+
+
+
+
+
+
+ + To: + Subject: + + +
+
+
+
+ + + +>>>>>>> contacts \ No newline at end of file diff --git a/onionr/static-data/www/mail/mail.css b/onionr/static-data/www/mail/mail.css index 7a807d3e..116395ca 100644 --- a/onionr/static-data/www/mail/mail.css +++ b/onionr/static-data/www/mail/mail.css @@ -2,6 +2,101 @@ padding-top: 1em; } .threads div span{ +<<<<<<< HEAD padding-left: 0.5em; padding-right: 0.5em; +======= + padding-left: 0.2em; + padding-right: 0.2em; +} + +#threadPlaceholder{ + display: none; + margin-top: 1em; + font-size: 2em; +} + +input{ + background-color: white; + color: black; +} + +.btn-group button { + border: 1px solid black; + padding: 10px 24px; /* Some padding */ + cursor: pointer; /* Pointer/hand icon */ + float: left; /* Float the buttons side by side */ + } + + .btn-group button:hover { + background-color: darkgray; + } + + .btn-group { + margin-bottom: 2em; + } + +#tabBtns{ + margin-bottom: 3em; + display: block; +} + + .activeTab{ + color: black; + background-color: gray; + } + + .overlayContent{ + background-color: lightgray; + border: 3px solid black; + border-radius: 3px; + color: black; + font-family: Verdana, Geneva, Tahoma, sans-serif; + min-height: 100%; + padding: 1em; + margin: 1em; + } + +.danger{ + color: red; +} + +.warn{ + color: orange; +} + +.good{ + color: greenyellow; +} + +.pre{ + padding-top: 1em; + word-wrap: break-word; + font-family: monospace; + white-space: pre; +} +.messageContent{ + font-size: 1.5em; +} + +#draftText{ + margin-top: 1em; + margin-bottom: 1em; + display: block; + width: 50%; + height: 75%; + min-width: 2%; + min-height: 5%; + background: white; + color: black; +} + +.successBtn{ + background-color: #28a745; + border-radius: 3px; + padding: 5px; + color: black; + font-size: 1.5em; + width: 10%; +>>>>>>> contacts } \ No newline at end of file diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index f392f569..43d89335 100644 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -1,3 +1,4 @@ +<<<<<<< HEAD pms = '' threadPart = document.getElementById('threads') function getInbox(){ @@ -38,6 +39,183 @@ function getInbox(){ }.bind([pms, i])) } +======= +/* + Onionr - P2P Anonymous Storage Network + + This file handles the mail interface + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +pms = '' +sentbox = '' +threadPart = document.getElementById('threads') +threadPlaceholder = document.getElementById('threadPlaceholder') +tabBtns = document.getElementById('tabBtns') +threadContent = {} +myPub = httpGet('/getActivePubkey') + +function openThread(bHash, sender, date, sigBool){ + var messageDisplay = document.getElementById('threadDisplay') + var blockContent = httpGet('/getblockbody/' + bHash) + document.getElementById('fromUser').value = sender + messageDisplay.innerText = blockContent + var sigEl = document.getElementById('sigValid') + var sigMsg = 'signature' + + if (sigBool){ + sigMsg = 'Good ' + sigMsg + sigEl.classList.remove('danger') + } + else{ + sigMsg = 'Bad/no ' + sigMsg + ' (message could be fake)' + sigEl.classList.add('danger') + } + sigEl.innerText = sigMsg + overlay('messageDisplay') +} + +function setActiveTab(tabName){ + threadPart.innerHTML = "" + switch(tabName){ + case 'inbox': + getInbox() + break + case 'sentbox': + getSentbox() + break + case 'send message': + overlay('sendMessage') + break + } +} + +function loadInboxEntrys(bHash){ + fetch('/getblockheader/' + bHash, { + headers: { + "token": webpass + }}) + .then((resp) => resp.json()) // Transform the data into json + .then(function(resp) { + //console.log(resp) + var entry = document.createElement('div') + var bHashDisplay = document.createElement('span') + var senderInput = document.createElement('input') + var subjectLine = document.createElement('span') + var dateStr = document.createElement('span') + var validSig = document.createElement('span') + var humanDate = new Date(0) + var metadata = resp['metadata'] + humanDate.setUTCSeconds(resp['meta']['time']) + if (resp['meta']['signer'] != ''){ + senderInput.value = httpGet('/getHumanReadable/' + resp['meta']['signer']) + } + if (resp['meta']['validSig']){ + validSig.innerText = 'Signature Validity: Good' + } + else{ + validSig.innerText = 'Signature Validity: Bad' + validSig.style.color = 'red' + } + if (senderInput.value == ''){ + senderInput.value = 'Anonymous' + } + bHashDisplay.innerText = bHash.substring(0, 10) + entry.setAttribute('hash', bHash) + senderInput.readOnly = true + dateStr.innerText = humanDate.toString() + if (metadata['subject'] === undefined || metadata['subject'] === null) { + subjectLine.innerText = '()' + } + else{ + subjectLine.innerText = '(' + metadata['subject'] + ')' + } + //entry.innerHTML = 'sender ' + resp['meta']['signer'] + ' - ' + resp['meta']['time'] + threadPart.appendChild(entry) + entry.appendChild(bHashDisplay) + entry.appendChild(senderInput) + entry.appendChild(validSig) + entry.appendChild(subjectLine) + entry.appendChild(dateStr) + entry.classList.add('threadEntry') + + entry.onclick = function(){ + openThread(entry.getAttribute('hash'), senderInput.value, dateStr.innerText, resp['meta']['validSig']) + } + + }.bind(bHash)) +} + +function getInbox(){ + var showed = false + var requested = '' + for(var i = 0; i < pms.length; i++) { + if (pms[i].trim().length == 0){ + continue + } + else{ + threadPlaceholder.style.display = 'none' + showed = true + } + loadInboxEntrys(pms[i]) + } + if (! showed){ + threadPlaceholder.style.display = 'block' + } +} + +function getSentbox(){ + fetch('/apipoints/mail/sentbox', { + headers: { + "token": webpass + }}) + .then((resp) => resp.json()) // Transform the data into json + .then(function(resp) { + var keys = []; + var entry = document.createElement('div') + var entryUsed; + for(var k in resp) keys.push(k); + for (var i = 0; i < keys.length; i++){ + var entry = document.createElement('div') + var obj = resp[i]; + var toLabel = document.createElement('span') + toLabel.innerText = 'To: ' + var toEl = document.createElement('input') + var preview = document.createElement('span') + toEl.readOnly = true + toEl.value = resp[keys[i]][1] + preview.innerText = '(' + resp[keys[i]][2] + ')' + entry.appendChild(toLabel) + entry.appendChild(toEl) + entry.appendChild(preview) + entryUsed = resp[keys[i]] + entry.onclick = function(){ + console.log(resp) + showSentboxWindow(toEl.value, entryUsed[0]) + } + threadPart.appendChild(entry) + } + threadPart.appendChild(entry) + }.bind(threadPart)) +} + +function showSentboxWindow(to, content){ + document.getElementById('toID').value = to + document.getElementById('sentboxDisplayText').innerText = content + overlay('sentboxDisplay') +>>>>>>> contacts } fetch('/getblocksbytype/pm', { @@ -47,6 +225,43 @@ fetch('/getblocksbytype/pm', { .then((resp) => resp.text()) // Transform the data into json .then(function(data) { pms = data.split(',') +<<<<<<< HEAD getInbox(pms) }) +======= + setActiveTab('inbox') + }) + +tabBtns.onclick = function(event){ + var children = tabBtns.children + for (var i = 0; i < children.length; i++) { + var btn = children[i] + btn.classList.remove('activeTab') + } + event.target.classList.add('activeTab') + setActiveTab(event.target.innerText.toLowerCase()) +} + +var idStrings = document.getElementsByClassName('myPub') +var myHumanReadable = httpGet('/getHumanReadable/' + myPub) +for (var i = 0; i < idStrings.length; i++){ + if (idStrings[i].tagName.toLowerCase() == 'input'){ + idStrings[i].value = myHumanReadable + } + else{ + idStrings[i].innerText = myHumanReadable + } +} + +for (var i = 0; i < document.getElementsByClassName('refresh').length; i++){ + document.getElementsByClassName('refresh')[i].style.float = 'right' +} + +for (var i = 0; i < document.getElementsByClassName('closeOverlay').length; i++){ + document.getElementsByClassName('closeOverlay')[i].onclick = function(e){ + document.getElementById(e.target.getAttribute('overlay')).style.visibility = 'hidden' + } +} + +>>>>>>> contacts diff --git a/onionr/static-data/www/mail/sendmail.js b/onionr/static-data/www/mail/sendmail.js new file mode 100644 index 00000000..945c7792 --- /dev/null +++ b/onionr/static-data/www/mail/sendmail.js @@ -0,0 +1,45 @@ +/* + Onionr - P2P Anonymous Storage Network + + This file handles the mail interface + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +var sendbutton = document.getElementById('sendMail') + +function sendMail(to, message, subject){ + //postData = {"postData": '{"to": "' + to + '", "message": "' + message + '"}'} // galaxy brain + postData = {'message': message, 'to': to, 'type': 'pm', 'encrypt': true, 'meta': JSON.stringify({'subject': subject})} + postData = JSON.stringify(postData) + fetch('/insertblock', { + method: 'POST', + body: postData, + headers: { + "content-type": "application/json", + "token": webpass + }}) + .then((resp) => resp.text()) // Transform the data into json + .then(function(data) { + }) +} + +sendForm.onsubmit = function(){ + var messageContent = document.getElementById('draftText') + var to = document.getElementById('draftID') + var subject = document.getElementById('draftSubject') + + sendMail(to.value, messageContent.value, subject.value) + return false; +} diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index 3e239eb0..80da5cdc 100644 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -21,6 +21,7 @@

Mail

Stats

Uptime:

+

Last Received Connection: Unknown

Stored Blocks:

Blocks in queue:

Connected nodes:

diff --git a/onionr/static-data/www/shared/main/stats.js b/onionr/static-data/www/shared/main/stats.js index b7d05776..6d56ed72 100644 --- a/onionr/static-data/www/shared/main/stats.js +++ b/onionr/static-data/www/shared/main/stats.js @@ -21,6 +21,10 @@ uptimeDisplay = document.getElementById('uptime') connectedDisplay = document.getElementById('connectedNodes') storedBlockDisplay = document.getElementById('storedBlocks') queuedBlockDisplay = document.getElementById('blockQueue') +<<<<<<< HEAD +======= +lastIncoming = document.getElementById('lastIncoming') +>>>>>>> contacts function getStats(){ stats = JSON.parse(httpGet('getstats', webpass)) @@ -28,5 +32,18 @@ function getStats(){ connectedDisplay.innerText = stats['connectedNodes'] storedBlockDisplay.innerText = stats['blockCount'] queuedBlockDisplay.innerText = stats['blockQueueCount'] +<<<<<<< HEAD +======= + var lastConnect = httpGet('/lastconnect') + if (lastConnect > 0){ + var humanDate = new Date(0) + humanDate.setUTCSeconds(httpGet('/lastconnect')) + lastConnect = humanDate.toString() + } + else{ + lastConnect = 'Unknown' + } + lastIncoming.innerText = lastConnect +>>>>>>> contacts } getStats() \ No newline at end of file diff --git a/onionr/static-data/www/shared/main/style.css b/onionr/static-data/www/shared/main/style.css index 69e1a407..78340541 100644 --- a/onionr/static-data/www/shared/main/style.css +++ b/onionr/static-data/www/shared/main/style.css @@ -37,7 +37,11 @@ body{ align-items:center; } .logo{ +<<<<<<< HEAD max-width: 25%; +======= + max-width: 20%; +>>>>>>> contacts vertical-align: middle; } .logoText{ @@ -132,9 +136,31 @@ body{ left: 0px; top: 0px; width:100%; +<<<<<<< HEAD opacity: 0.9; height:100%; text-align:center; z-index: 1000; background-color: black; } +======= + height:100%; + text-align:left; + z-index: 1000; + background-color: #2c2b3f; + color: white; + } + + .closeOverlay{ + background-color: white; + color: black; + border: 1px solid red; + border-radius: 5px; + float: right; + font-family: sans-serif; + } + .closeOverlay:after{ + content: '❌'; + padding: 5px; + } +>>>>>>> contacts diff --git a/onionr/static-data/www/shared/misc.js b/onionr/static-data/www/shared/misc.js index c9b3790e..464a0802 100644 --- a/onionr/static-data/www/shared/misc.js +++ b/onionr/static-data/www/shared/misc.js @@ -1,5 +1,30 @@ +<<<<<<< HEAD webpass = document.location.hash.replace('#', '') nowebpass = false +======= +/* + Onionr - P2P Anonymous Storage Network + + This file handles the mail interface + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +webpass = document.location.hash.replace('#', '') +nowebpass = false + +>>>>>>> contacts if (typeof webpass == "undefined"){ webpass = localStorage['webpass'] } @@ -12,6 +37,13 @@ if (typeof webpass == "undefined" || webpass == ""){ nowebpass = true } +<<<<<<< HEAD +======= +function arrayContains(needle, arrhaystack) { + return (arrhaystack.indexOf(needle) > -1); +} + +>>>>>>> contacts function httpGet(theUrl) { var xmlHttp = new XMLHttpRequest() xmlHttp.open( "GET", theUrl, false ) // false for synchronous request @@ -27,6 +59,10 @@ function httpGet(theUrl) { function overlay(overlayID) { el = document.getElementById(overlayID) el.style.visibility = (el.style.visibility == "visible") ? "hidden" : "visible" +<<<<<<< HEAD +======= + scroll(0,0) +>>>>>>> contacts } var passLinks = document.getElementsByClassName("idLink") diff --git a/onionr/static-data/www/ui/README.md b/onionr/static-data/www/ui/README.md deleted file mode 100644 index 451b08ed..00000000 --- a/onionr/static-data/www/ui/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Onionr UI - -## About - -The default GUI for Onionr - -## Setup - -To compile the application, simply execute the following: - -``` -python3 compile.py -``` - -If you are wanting to compile Onionr UI for another language, execute the following, replacing `[lang]` with the target language (supported languages include `eng` for English, `spa` para español, and `zho`为中国人): - -``` -python3 compile.py [lang] -``` - -## FAQ -### Why "compile" anyway? -This web application is compiled for a few reasons: -1. To make it easier to update; this way, we do not have to update the header in every file if we want to change something about it. -2. To make the application smaller in size; there is less duplicated code when the code like the header and footer can be stored in an individual file rather than every file. -3. For multi-language support; with the Python "tags" feature, we can reference strings by variable name, and based on a language file, they can be dynamically inserted into the page on compilation. -4. For compile-time customizations. - -### What exactly happens when you compile? -Upon compilation, files from the `src/` directory will be copied to `dist/` directory, header and footers will be injected in the proper places, and Python "tags" will be interpreted. - - -### How do Python "tags" work? -There are two types of Python "tags": -1. Logic tags (`<$ logic $>`): These tags allow you to perform logic at compile time. Example: `<$ import datetime; lastUpdate = datetime.datetime.now() $>`: This gets the current time while compiling, then stores it in `lastUpdate`. -2. Data tags (`<$= data $>`): These tags take whatever the return value of the statement in the tags is, and write it directly to the page. Example: `<$= 'This application was compiled at %s.' % lastUpdate $>`: This will write the message in the string in the tags to the page. - -**Note:** Logic tags take a higher priority and will always be interpreted first. - -### How does the language feature work? -When you use a data tag to write a string to the page (e.g. `<$= LANG.HELLO_WORLD $>`), the language feature simply takes dictionary of the language that is currently being used from the language map file (`lang.json`), then searches for the key (being the variable name after the characters `LANG.` in the data tag, like `HELLO_WORLD` from the example before). It then writes that string to the page. Language variables are always prefixed with `LANG.` and should always be uppercase (as they are a constant). - -### I changed a few things in the application and tried to view the updates in my browser, but nothing changed! -You most likely forgot to compile. Try running `python3 compile.py` and check again. If you are still having issues, [open up an issue](https://gitlab.com/beardog/Onionr/issues/new?issue[title]=Onionr UI not updating after compiling). \ No newline at end of file diff --git a/onionr/static-data/www/ui/common/default-icon.html b/onionr/static-data/www/ui/common/default-icon.html deleted file mode 100644 index 86ccf773..00000000 --- a/onionr/static-data/www/ui/common/default-icon.html +++ /dev/null @@ -1 +0,0 @@ -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 diff --git a/onionr/static-data/www/ui/common/footer.html b/onionr/static-data/www/ui/common/footer.html deleted file mode 100644 index 0143c2d8..00000000 --- a/onionr/static-data/www/ui/common/footer.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - diff --git a/onionr/static-data/www/ui/common/header.html b/onionr/static-data/www/ui/common/header.html deleted file mode 100644 index 2a2b4f56..00000000 --- a/onionr/static-data/www/ui/common/header.html +++ /dev/null @@ -1,30 +0,0 @@ -<$= LANG.ONIONR_TITLE $> - - - - - - - - - - diff --git a/onionr/static-data/www/ui/common/onionr-reply-creator.html b/onionr/static-data/www/ui/common/onionr-reply-creator.html deleted file mode 100644 index aafc8557..00000000 --- a/onionr/static-data/www/ui/common/onionr-reply-creator.html +++ /dev/null @@ -1,31 +0,0 @@ - -
-
-
-
-
- -
-
- - - - -
- - -
-
-
-
-
- -
-
-
- diff --git a/onionr/static-data/www/ui/common/onionr-timeline-post.html b/onionr/static-data/www/ui/common/onionr-timeline-post.html deleted file mode 100644 index 67ec158c..00000000 --- a/onionr/static-data/www/ui/common/onionr-timeline-post.html +++ /dev/null @@ -1,32 +0,0 @@ - -
-
-
-
- -
-
-
- - -
- -
-
- -
- $content -
- -
- $liked - <$= LANG.POST_REPLY $> -
-
-
-
-
- diff --git a/onionr/static-data/www/ui/common/onionr-timeline-reply-creator.html b/onionr/static-data/www/ui/common/onionr-timeline-reply-creator.html deleted file mode 100644 index 4cb95b02..00000000 --- a/onionr/static-data/www/ui/common/onionr-timeline-reply-creator.html +++ /dev/null @@ -1,30 +0,0 @@ - -
-
-
-
-
-
- -
-
- - - - -
- - -
-
-
- -
-
-
- diff --git a/onionr/static-data/www/ui/common/onionr-timeline-reply.html b/onionr/static-data/www/ui/common/onionr-timeline-reply.html deleted file mode 100644 index cc8a312e..00000000 --- a/onionr/static-data/www/ui/common/onionr-timeline-reply.html +++ /dev/null @@ -1,31 +0,0 @@ - -
-
-
-
- -
-
-
- - -
- -
-
- -
- $content -
- -
- $liked - <$= LANG.POST_REPLY $> -
-
-
-
-
- diff --git a/onionr/static-data/www/ui/compile.py b/onionr/static-data/www/ui/compile.py deleted file mode 100755 index e991af08..00000000 --- a/onionr/static-data/www/ui/compile.py +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/python3 - -import shutil, os, re, json, traceback - -# get user's config -settings = {} -with open('config.json', 'r') as file: - settings = json.loads(file.read()) - -# "hardcoded" config, not for user to mess with -HEADER_FILE = 'common/header.html' -FOOTER_FILE = 'common/footer.html' -SRC_DIR = 'src/' -DST_DIR = 'dist/' -HEADER_STRING = '
' -FOOTER_STRING = '