diff --git a/onionr/api.py b/onionr/api.py index 2be268c8..adaa3cd7 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -25,10 +25,11 @@ from flask import request, Response, abort, send_from_directory import core import onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config, onionrblockapi import httpapi -from httpapi import friendsapi, profilesapi, configapi, miscpublicapi, insertblock +from httpapi import friendsapi, profilesapi, configapi, miscpublicapi, miscclientapi, insertblock, onionrsitesapi from onionrservices import httpheaders import onionr from onionrutils import bytesconverter, stringvalidators, epoch, mnemonickeys +from httpapi import apiutils config.reload() class FDSafeHandler(WSGIHandler): @@ -194,11 +195,6 @@ class API: bindPort = int(config.get('client.client.port', 59496)) self.bindPort = bindPort - # Be extremely mindful of this. These are endpoints available without a password - self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'homedata', 'board', 'profiles', 'profilesindex', - 'boardContent', 'sharedContent', 'mail', 'mailindex', 'friends', 'friendsindex', - 'clandestine', 'clandestineIndex') - self.clientToken = config.get('client.webpassword') self.timeBypassToken = base64.b16encode(os.urandom(32)).decode() @@ -214,67 +210,12 @@ class API: app.register_blueprint(profilesapi.profile_BP) app.register_blueprint(configapi.config_BP) app.register_blueprint(insertblock.ib) + app.register_blueprint(miscclientapi.getblocks.client_get_blocks) + app.register_blueprint(miscclientapi.staticfiles.static_files_bp) + app.register_blueprint(onionrsitesapi.site_api) + app.register_blueprint(apiutils.shutdown.shutdown_bp) httpapi.load_plugin_blueprints(app) - - @app.before_request - def validateRequest(): - '''Validate request has set password and is the correct hostname''' - # For the purpose of preventing DNS rebinding attacks - if request.host != '%s:%s' % (self.host, self.bindPort): - abort(403) - if request.endpoint in self.whitelistEndpoints: - return - try: - if not hmac.compare_digest(request.headers['token'], self.clientToken): - if not hmac.compare_digest(request.form['token'], self.clientToken): - abort(403) - except KeyError: - if not hmac.compare_digest(request.form['token'], self.clientToken): - abort(403) - - @app.after_request - def afterReq(resp): - # Security headers - resp = httpheaders.set_default_onionr_http_headers(resp) - if request.endpoint == 'site': - resp.headers['Content-Security-Policy'] = "default-src 'none'; style-src data: 'unsafe-inline'; img-src data:" - else: - resp.headers['Content-Security-Policy'] = "default-src 'none'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'self'" - return resp - - @app.route('/board/', endpoint='board') - def loadBoard(): - return send_from_directory('static-data/www/board/', "index.html") - - @app.route('/mail/', endpoint='mail') - def loadMail(path): - return send_from_directory('static-data/www/mail/', path) - @app.route('/mail/', endpoint='mailindex') - def loadMailIndex(): - return send_from_directory('static-data/www/mail/', 'index.html') - - @app.route('/clandestine/', endpoint='clandestine') - def loadClandestine(path): - return send_from_directory('static-data/www/clandestine/', path) - @app.route('/clandestine/', endpoint='clandestineIndex') - def loadClandestineIndex(): - return send_from_directory('static-data/www/clandestine/', 'index.html') - - @app.route('/friends/', endpoint='friends') - def loadContacts(path): - return send_from_directory('static-data/www/friends/', path) - - @app.route('/friends/', endpoint='friendsindex') - def loadContacts(): - return send_from_directory('static-data/www/friends/', 'index.html') - - @app.route('/profiles/', endpoint='profiles') - def loadContacts(path): - return send_from_directory('static-data/www/profiles/', path) - - @app.route('/profiles/', endpoint='profilesindex') - def loadContacts(): - return send_from_directory('static-data/www/profiles/', 'index.html') + self.get_block_data = apiutils.GetBlockData(self) @app.route('/serviceactive/') def serviceActive(pubkey): @@ -285,22 +226,6 @@ class API: pass return Response('false') - @app.route('/board/', endpoint='boardContent') - def boardContent(path): - return send_from_directory('static-data/www/board/', path) - @app.route('/shared/', endpoint='sharedContent') - def sharedContent(path): - return send_from_directory('static-data/www/shared/', path) - - @app.route('/', endpoint='onionrhome') - def hello(): - # ui home - return send_from_directory('static-data/www/private/', 'index.html') - - @app.route('/private/', endpoint='homedata') - def homedata(path): - return send_from_directory('static-data/www/private/', path) - @app.route('/www/', endpoint='www') def wwwPublic(path): if not config.get("www.private.run", True): @@ -336,67 +261,11 @@ class API: def ping(): # Used to check if client api is working return Response("pong!") - - @app.route('/getblocksbytype/') - def getBlocksByType(name): - blocks = self._core.getBlocksByType(name) - return Response(','.join(blocks)) - - @app.route('/getblockbody/') - def getBlockBodyData(name): - resp = '' - if stringvalidators.validate_hash(name): - try: - resp = onionrblockapi.Block(name, decrypt=True).bcontent - except TypeError: - pass - else: - abort(404) - return Response(resp) - - @app.route('/getblockdata/') - def getData(name): - resp = "" - if stringvalidators.validate_hash(name): - if name in self._core.getBlockList(): - try: - resp = self.getBlockData(name, decrypt=True) - except ValueError: - pass - else: - abort(404) - else: - abort(404) - return Response(resp) - - @app.route('/getblockheader/') - def getBlockHeader(name): - resp = self.getBlockData(name, decrypt=True, headerOnly=True) - return Response(resp) @app.route('/lastconnect') def lastConnect(): return Response(str(self.publicAPI.lastRequest)) - @app.route('/site/', endpoint='site') - def site(name): - bHash = name - resp = 'Not Found' - if stringvalidators.validate_hash(bHash): - try: - resp = onionrblockapi.Block(bHash).bcontent - except onionrexceptions.NoDataAvailable: - abort(404) - except TypeError: - pass - try: - resp = base64.b64decode(resp) - except: - pass - if resp == 'Not Found' or not resp: - abort(404) - return Response(resp) - @app.route('/waitforshare/', methods=['post']) def waitforshare(name): '''Used to prevent the **public** api from sharing blocks we just created''' @@ -410,18 +279,7 @@ class API: @app.route('/shutdown') def shutdown(): - try: - self.publicAPI.httpServer.stop() - self.httpServer.stop() - except AttributeError: - pass - return Response("bye") - - @app.route('/shutdownclean') - def shutdownClean(): - # good for calling from other clients - self._core.daemonQueueAdd('shutdown') - return Response("bye") + return apiutils.shutdown.shutdown(self) @app.route('/getstats') def getStats(): @@ -474,31 +332,6 @@ class API: except (AttributeError, NameError): # Don't error on race condition with startup pass - - def getBlockData(self, bHash, decrypt=False, raw=False, headerOnly=False): - assert stringvalidators.validate_hash(bHash) - bl = onionrblockapi.Block(bHash, core=self._core) - if decrypt: - bl.decrypt() - if bl.isEncrypted and not bl.decrypted: - raise ValueError - if not raw: - if not headerOnly: - retData = {'meta':bl.bheader, 'metadata': bl.bmetadata, 'content': bl.bcontent} - for x in list(retData.keys()): - try: - retData[x] = retData[x].decode() - except AttributeError: - pass - else: - validSig = False - signer = bytesconverter.bytes_to_str(bl.signer) - if bl.isSigned() and stringvalidators.validate_pub_key(signer) and bl.isSigner(signer): - validSig = True - bl.bheader['validSig'] = validSig - bl.bheader['meta'] = '' - retData = {'meta': bl.bheader, 'metadata': bl.bmetadata} - return json.dumps(retData) - else: - return bl.raw + def getBlockData(self, bHash, decrypt=False, raw=False, headerOnly=False): + return self.get_block_data.get_block_data(self, bHash, decrypt, raw, headerOnly) diff --git a/onionr/httpapi/apiutils/__init__.py b/onionr/httpapi/apiutils/__init__.py new file mode 100644 index 00000000..29b0c778 --- /dev/null +++ b/onionr/httpapi/apiutils/__init__.py @@ -0,0 +1,44 @@ +import json +import core, onionrblockapi +from onionrutils import bytesconverter, stringvalidators +from . import shutdown + +class GetBlockData: + def __init__(self, client_api_inst=None): + if client_api_inst is None: + self.client_api_inst = None + self.c = core.Core() + elif isinstance(client_api_inst, core.Core): + self.client_api_inst = None + self.c = client_api_inst + else: + self.client_api_Inst = client_api_inst + self.c = core.Core() + + def get_block_data(self, bHash, decrypt=False, raw=False, headerOnly=False): + assert stringvalidators.validate_hash(bHash) + bl = onionrblockapi.Block(bHash, core=self.c) + if decrypt: + bl.decrypt() + if bl.isEncrypted and not bl.decrypted: + raise ValueError + + if not raw: + if not headerOnly: + retData = {'meta':bl.bheader, 'metadata': bl.bmetadata, 'content': bl.bcontent} + for x in list(retData.keys()): + try: + retData[x] = retData[x].decode() + except AttributeError: + pass + else: + validSig = False + signer = bytesconverter.bytes_to_str(bl.signer) + if bl.isSigned() and stringvalidators.validate_pub_key(signer) and bl.isSigner(signer): + validSig = True + bl.bheader['validSig'] = validSig + bl.bheader['meta'] = '' + retData = {'meta': bl.bheader, 'metadata': bl.bmetadata} + return json.dumps(retData) + else: + return bl.raw \ No newline at end of file diff --git a/onionr/httpapi/apiutils/shutdown.py b/onionr/httpapi/apiutils/shutdown.py new file mode 100644 index 00000000..90cd33cb --- /dev/null +++ b/onionr/httpapi/apiutils/shutdown.py @@ -0,0 +1,38 @@ +''' + Onionr - Private P2P Communication + + Shutdown the node either hard or cleanly +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +from flask import Blueprint, Response +import core, onionrblockapi, onionrexceptions +from onionrutils import stringvalidators + +shutdown_bp = Blueprint('shutdown', __name__) + +def shutdown(client_api_inst): + try: + client_api_inst.publicAPI.httpServer.stop() + client_api_inst.httpServer.stop() + except AttributeError: + pass + return Response("bye") + +@shutdown_bp.route('/shutdownclean') +def shutdown_clean(): + # good for calling from other clients + core.Core().daemonQueueAdd('shutdown') + return Response("bye") \ No newline at end of file diff --git a/onionr/httpapi/miscclientapi/__init__.py b/onionr/httpapi/miscclientapi/__init__.py new file mode 100644 index 00000000..161fe6d5 --- /dev/null +++ b/onionr/httpapi/miscclientapi/__init__.py @@ -0,0 +1 @@ +from . import getblocks, staticfiles \ No newline at end of file diff --git a/onionr/httpapi/miscclientapi/getblocks.py b/onionr/httpapi/miscclientapi/getblocks.py new file mode 100644 index 00000000..f9a6aa90 --- /dev/null +++ b/onionr/httpapi/miscclientapi/getblocks.py @@ -0,0 +1,66 @@ +''' + Onionr - Private P2P Communication + + Create blocks with the client api server +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +from flask import Blueprint, Response, abort +import core, onionrblockapi +from httpapi import apiutils +from onionrutils import stringvalidators + +c = core.Core() + +client_get_block = apiutils.GetBlockData(c) + +client_get_blocks = Blueprint('miscclient', __name__) + +@client_get_blocks.route('/getblocksbytype/') +def getBlocksByType(name): + blocks = c.getBlocksByType(name) + return Response(','.join(blocks)) + +@client_get_blocks.route('/getblockbody/') +def getBlockBodyData(name): + resp = '' + if stringvalidators.validate_hash(name): + try: + resp = onionrblockapi.Block(name, decrypt=True, core=c).bcontent + except TypeError: + pass + else: + abort(404) + return Response(resp) + +@client_get_blocks.route('/getblockdata/') +def getData(name): + resp = "" + if stringvalidators.validate_hash(name): + if name in c.getBlockList(): + try: + resp = client_get_block.get_block_data(name, decrypt=True) + except ValueError: + pass + else: + abort(404) + else: + abort(404) + return Response(resp) + +@client_get_blocks.route('/getblockheader/') +def getBlockHeader(name): + resp = client_get_block.get_block_data(name, decrypt=True, headerOnly=True) + return Response(resp) \ No newline at end of file diff --git a/onionr/httpapi/miscclientapi/staticfiles.py b/onionr/httpapi/miscclientapi/staticfiles.py new file mode 100644 index 00000000..f943d6f1 --- /dev/null +++ b/onionr/httpapi/miscclientapi/staticfiles.py @@ -0,0 +1,75 @@ +''' + Onionr - Private P2P Communication + + Register static file routes +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +from flask import Blueprint, send_from_directory + +static_files_bp = Blueprint('staticfiles', __name__) + +@static_files_bp.route('/board/', endpoint='board') +def loadBoard(): + return send_from_directory('static-data/www/board/', "index.html") + +@static_files_bp.route('/mail/', endpoint='mail') +def loadMail(path): + return send_from_directory('static-data/www/mail/', path) + +@static_files_bp.route('/mail/', endpoint='mailindex') +def loadMailIndex(): + return send_from_directory('static-data/www/mail/', 'index.html') + +@static_files_bp.route('/clandestine/', endpoint='clandestine') +def loadClandestine(path): + return send_from_directory('static-data/www/clandestine/', path) + +@static_files_bp.route('/clandestine/', endpoint='clandestineIndex') +def loadClandestineIndex(): + return send_from_directory('static-data/www/clandestine/', 'index.html') + +@static_files_bp.route('/friends/', endpoint='friends') +def loadContacts(path): + return send_from_directory('static-data/www/friends/', path) + +@static_files_bp.route('/friends/', endpoint='friendsindex') +def loadContacts(): + return send_from_directory('static-data/www/friends/', 'index.html') + +@static_files_bp.route('/profiles/', endpoint='profiles') +def loadContacts(path): + return send_from_directory('static-data/www/profiles/', path) + +@static_files_bp.route('/profiles/', endpoint='profilesindex') +def loadContacts(): + return send_from_directory('static-data/www/profiles/', 'index.html') + +@static_files_bp.route('/board/', endpoint='boardContent') +def boardContent(path): + return send_from_directory('static-data/www/board/', path) + +@static_files_bp.route('/shared/', endpoint='sharedContent') +def sharedContent(path): + return send_from_directory('static-data/www/shared/', path) + +@static_files_bp.route('/', endpoint='onionrhome') +def hello(): + # ui home + return send_from_directory('static-data/www/private/', 'index.html') + +@static_files_bp.route('/private/', endpoint='homedata') +def homedata(path): + return send_from_directory('static-data/www/private/', path) \ No newline at end of file diff --git a/onionr/httpapi/miscpublicapi/getblocks.py b/onionr/httpapi/miscpublicapi/getblocks.py index 08d9f678..8fe5cbf4 100755 --- a/onionr/httpapi/miscpublicapi/getblocks.py +++ b/onionr/httpapi/miscpublicapi/getblocks.py @@ -20,6 +20,7 @@ from flask import Response, abort import config from onionrutils import bytesconverter, stringvalidators + def get_public_block_list(clientAPI, publicAPI, request): # Provide a list of our blocks, with a date offset dateAdjust = request.args.get('date') diff --git a/onionr/httpapi/onionrsitesapi/__init__.py b/onionr/httpapi/onionrsitesapi/__init__.py new file mode 100644 index 00000000..82805192 --- /dev/null +++ b/onionr/httpapi/onionrsitesapi/__init__.py @@ -0,0 +1,44 @@ +''' + Onionr - Private P2P Communication + + view and interact with onionr sites +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import base64 +from flask import Blueprint, Response, request, abort +import core, onionrblockapi, onionrexceptions +from onionrutils import stringvalidators + +site_api = Blueprint('siteapi', __name__) + +@site_api.route('/site/', endpoint='site') +def site(name): + bHash = name + resp = 'Not Found' + if stringvalidators.validate_hash(bHash): + try: + resp = onionrblockapi.Block(bHash).bcontent + except onionrexceptions.NoDataAvailable: + abort(404) + except TypeError: + pass + try: + resp = base64.b64decode(resp) + except: + pass + if resp == 'Not Found' or not resp: + abort(404) + return Response(resp) \ No newline at end of file diff --git a/onionr/httpapi/security/__init__.py b/onionr/httpapi/security/__init__.py new file mode 100644 index 00000000..653f6af4 --- /dev/null +++ b/onionr/httpapi/security/__init__.py @@ -0,0 +1 @@ +from . import client, public \ No newline at end of file diff --git a/onionr/httpapi/security/client.py b/onionr/httpapi/security/client.py new file mode 100644 index 00000000..65798ad7 --- /dev/null +++ b/onionr/httpapi/security/client.py @@ -0,0 +1,36 @@ +import hmac +from flask import Blueprint, request, abort + +# Be extremely mindful of this. These are endpoints available without a password +whitelist_endpoints = ('siteapi.site', 'www', 'staticfiles.onionrhome', 'staticfiles.homedata', +'staticfiles.board', 'staticfiles.profiles', +'staticfiles.profilesindex', +'staticfiles.boardContent', 'staticfiles.sharedContent', +'staticfiles.mail', 'staticfiles.mailindex', 'staticfiles.friends', 'staticfiles.friendsindex', +'staticfiles.clandestine', 'staticfiles.clandestineIndex') + +@app.before_request +def validateRequest(): + '''Validate request has set password and is the correct hostname''' + # For the purpose of preventing DNS rebinding attacks + if request.host != '%s:%s' % (self.host, self.bindPort): + abort(403) + if request.endpoint in whitelist_endpoints: + return + try: + if not hmac.compare_digest(request.headers['token'], self.clientToken): + if not hmac.compare_digest(request.form['token'], self.clientToken): + abort(403) + except KeyError: + if not hmac.compare_digest(request.form['token'], self.clientToken): + abort(403) + +@app.after_request +def afterReq(resp): + # Security headers + resp = httpheaders.set_default_onionr_http_headers(resp) + if request.endpoint == 'site': + resp.headers['Content-Security-Policy'] = "default-src 'none'; style-src data: 'unsafe-inline'; img-src data:" + else: + resp.headers['Content-Security-Policy'] = "default-src 'none'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'self'" + return resp \ No newline at end of file diff --git a/onionr/httpapi/security/public.py b/onionr/httpapi/security/public.py new file mode 100644 index 00000000..e69de29b diff --git a/onionr/static-data/www/board/index.html b/onionr/static-data/www/board/index.html index 8101b55c..e4fc7db2 100755 --- a/onionr/static-data/www/board/index.html +++ b/onionr/static-data/www/board/index.html @@ -17,7 +17,7 @@