renamed onionr dir and bugfixes/linting progress

This commit is contained in:
Kevin Froman 2019-11-20 04:52:50 -06:00
parent 2b996da17f
commit 720efe4fca
226 changed files with 179 additions and 142 deletions

13
src/httpapi/README.md Executable file
View file

@ -0,0 +1,13 @@
# httpapi
The httpapi contains collections of endpoints for the client and public API servers.
## Files:
configapi: manage onionr configuration from the client http api
friendsapi: add, remove and list friends from the client http api
miscpublicapi: misculanious onionr network interaction from the **public** httpapi, such as announcements, block fetching and uploading.
profilesapi: work in progress in returning a profile page for an Onionr user

29
src/httpapi/__init__.py Executable file
View file

@ -0,0 +1,29 @@
"""
Onionr - Private P2P Communication
This file registers plugin's flask blueprints for the client http 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 <https://www.gnu.org/licenses/>.
"""
import onionrplugins
def load_plugin_blueprints(flaskapp, blueprint: str = 'flask_blueprint'):
"""Iterate enabled plugins and load any http endpoints they have"""
for plugin in onionrplugins.get_enabled_plugins():
plugin = onionrplugins.get_plugin(plugin)
try:
flaskapp.register_blueprint(getattr(plugin, blueprint))
except AttributeError:
pass

View file

@ -0,0 +1,3 @@
from . import shutdown, setbindip, getblockdata
GetBlockData = getblockdata.GetBlockData

View file

@ -0,0 +1,35 @@
import json
from onionrblocks import onionrblockapi
from onionrutils import bytesconverter, stringvalidators
import onionrexceptions
class GetBlockData:
def __init__(self, client_api_inst=None):
return
def get_block_data(self, bHash, decrypt=False, raw=False, headerOnly=False):
if not stringvalidators.validate_hash(bHash): raise onionrexceptions.InvalidHexHash("block hash not valid hash format")
bl = onionrblockapi.Block(bHash)
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

View file

@ -0,0 +1,42 @@
import gevent
from gevent import socket, sleep
import secrets, random
import config, logger
import os
# Hacky monkey patch so we can bind random localhosts without gevent trying to switch with an empty hub
socket.getfqdn = lambda n: n
def _get_acceptable_random_number()->int:
"""Return a cryptographically random number in the inclusive range (1, 255)"""
number = 0
while number == 0:
number = secrets.randbelow(0xFF)
return number
def set_bind_IP(filePath=''):
'''Set a random localhost IP to a specified file (intended for private or public API localhost IPs)'''
if config.get('general.random_bind_ip', True):
hostOctets = []
# Build the random localhost address
for i in range(3):
hostOctets.append(str(_get_acceptable_random_number()))
hostOctets = ['127'] + hostOctets
# Convert the localhost address to a normal string address
data = '.'.join(hostOctets)
# Try to bind IP. Some platforms like Mac block non normal 127.x.x.x
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.bind((data, 0))
except OSError:
# 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()
else:
data = '127.0.0.1'
if filePath != '':
with open(filePath, 'w') as bindFile:
bindFile.write(data)
return data

View file

@ -0,0 +1,39 @@
'''
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 <https://www.gnu.org/licenses/>.
'''
from flask import Blueprint, Response
from onionrblocks import onionrblockapi
import onionrexceptions
from onionrutils import stringvalidators
from coredb import daemonqueue
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
daemonqueue.daemon_queue_add('shutdown')
return Response("bye")

View file

@ -0,0 +1,62 @@
'''
Onionr - Private P2P Communication
This file handles configuration setting and getting from the HTTP API
'''
'''
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
import json
from flask import Blueprint, request, Response, abort
import config, onionrutils
config.reload()
config_BP = Blueprint('config_BP', __name__)
@config_BP.route('/config/get')
def get_all_config():
'''Simply return all configuration as JSON string'''
return Response(json.dumps(config.get_config(), indent=4, sort_keys=True))
@config_BP.route('/config/get/<key>')
def get_by_key(key):
'''Return a config setting by key'''
return Response(json.dumps(config.get(key)))
@config_BP.route('/config/setall', methods=['POST'])
def set_all_config():
'''Overwrite existing JSON config with new JSON string'''
try:
new_config = request.get_json(force=True)
except json.JSONDecodeError:
abort(400)
else:
config.set_config(new_config)
config.save()
return Response('success')
@config_BP.route('/config/set/<key>', methods=['POST'])
def set_by_key(key):
'''Overwrite/set only 1 config key'''
'''
{
'data': data
}
'''
try:
data = json.loads(onionrutils.OnionrUtils.bytesToStr(request.data))['data']
except (json.JSONDecodeError, KeyError):
abort(400)
config.set(key, data, True)
return Response('success')

View file

@ -0,0 +1,69 @@
'''
Onionr - Private P2P Communication
Misc client API endpoints too small to need their own file and that need access to the client api inst
'''
'''
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 <https://www.gnu.org/licenses/>.
'''
import threading # For the client creation thread
from flask import Response # For direct connection management HTTP endpoints
from flask import Blueprint # To make the direct connection management blueprint in the webUI
from flask import g # Mainly to access the shared toomanyobjs object
import deadsimplekv
import filepaths
import onionrservices
from onionrservices import pool
def _get_communicator(g):
while True:
try:
return g.too_many.get_by_string("OnionrCommunicatorDaemon")
except KeyError:
pass
class DirectConnectionManagement:
def __init__(self, client_api):
direct_conn_management_bp = Blueprint('direct_conn_management', __name__)
self.direct_conn_management_bp = direct_conn_management_bp
cache = deadsimplekv.DeadSimpleKV(filepaths.cached_storage)
@direct_conn_management_bp.route('/dc-client/isconnected/<pubkey>')
def is_connected(pubkey):
communicator = _get_communicator(g)
resp = ""
if pubkey in communicator.direct_connection_clients:
resp = communicator.direct_connection_clients[pubkey]
return Response(resp)
@direct_conn_management_bp.route('/dc-client/connect/<pubkey>')
def make_new_connection(pubkey):
communicator = _get_communicator(g)
resp = "pending"
if pubkey in communicator.shared_state.get(pool.ServicePool).bootstrap_pending:
return Response(resp)
if pubkey in communicator.direct_connection_clients:
resp = communicator.direct_connection_clients[pubkey]
else:
"""Spawn a thread that will create the client and eventually add it to the
communicator.active_services
"""
threading.Thread(target=onionrservices.OnionrServices().create_client,
args=[pubkey, communicator], daemon=True).start()
return Response(resp)

View file

@ -0,0 +1,14 @@
from gevent.pywsgi import WSGIServer, WSGIHandler
from gevent import Timeout
class FDSafeHandler(WSGIHandler):
'''Our WSGI handler. Doesn't do much non-default except timeouts'''
def handle(self):
self.timeout = Timeout(120, Exception)
self.timeout.start()
try:
WSGIHandler.handle(self)
except Timeout as ex:
if ex is self.timeout:
pass
else:
raise

View file

@ -0,0 +1,59 @@
'''
Onionr - Private P2P Communication
This file creates http endpoints for friend management
'''
'''
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 <https://www.gnu.org/licenses/>.
'''
import json
from onionrusers import contactmanager
from flask import Blueprint, Response, request, abort, redirect
from coredb import keydb
friends = Blueprint('friends', __name__)
@friends.route('/friends/list')
def list_friends():
pubkey_list = {}
friend_list = contactmanager.ContactManager.list_friends()
for friend in friend_list:
pubkey_list[friend.publicKey] = {'name': friend.get_info('name')}
return json.dumps(pubkey_list)
@friends.route('/friends/add/<pubkey>', methods=['POST'])
def add_friend(pubkey):
contactmanager.ContactManager(pubkey, saveUser=True).setTrust(1)
return redirect(request.referrer + '#' + request.form['token'])
@friends.route('/friends/remove/<pubkey>', methods=['POST'])
def remove_friend(pubkey):
contactmanager.ContactManager(pubkey).setTrust(0)
contactmanager.ContactManager(pubkey).delete_contact()
keydb.removekeys.remove_user(pubkey)
return redirect(request.referrer + '#' + request.form['token'])
@friends.route('/friends/setinfo/<pubkey>/<key>', methods=['POST'])
def set_info(pubkey, key):
data = request.form['data']
contactmanager.ContactManager(pubkey).set_info(key, data)
return redirect(request.referrer + '#' + request.form['token'])
@friends.route('/friends/getinfo/<pubkey>/<key>')
def get_info(pubkey, key):
retData = contactmanager.ContactManager(pubkey).get_info(key)
if retData is None:
abort(404)
else:
return retData

View file

@ -0,0 +1,73 @@
'''
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 <https://www.gnu.org/licenses/>.
'''
import json, threading
from flask import Blueprint, Response, request, g
import onionrblocks
from onionrcrypto import hashers
from onionrutils import bytesconverter
from onionrutils import mnemonickeys
ib = Blueprint('insertblock', __name__)
@ib.route('/insertblock', methods=['POST'])
def client_api_insert_block():
encrypt = False
bData = request.get_json(force=True)
message = bData['message']
message_hash = bytesconverter.bytes_to_str(hashers.sha3_hash(message))
# Detect if message (block body) is not specified
if type(message) is None:
return 'failure due to unspecified message', 400
# Detect if block with same message is already being inserted
if message_hash in g.too_many.get_by_string("OnionrCommunicatorDaemon").generating_blocks:
return 'failure due to duplicate insert', 400
else:
g.too_many.get_by_string("OnionrCommunicatorDaemon").generating_blocks.append(message_hash)
subject = 'temp'
encryptType = ''
sign = True
meta = {}
to = ''
try:
if bData['encrypt']:
to = bData['to'].strip()
if "-" in to:
to = mnemonickeys.get_base32(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=onionrblocks.insert, args=(message,), kwargs={'header': bType, 'encryptType': encryptType, 'sign':sign, 'asymPeer': to, 'meta': meta}).start()
return Response('success')

View file

@ -0,0 +1 @@
from . import getblocks, staticfiles, endpoints, motd

View file

@ -0,0 +1,138 @@
'''
Onionr - Private P2P Communication
Misc client API endpoints too small to need their own file and that need access to the client api inst
'''
'''
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 <https://www.gnu.org/licenses/>.
'''
import os
import subprocess
from flask import Response, Blueprint, request, send_from_directory, abort
import unpaddedbase32
from httpapi import apiutils
import onionrcrypto, config
from netcontroller import NetController
from serializeddata import SerializedData
from onionrutils import mnemonickeys
from onionrutils import bytesconverter
from etc import onionrvalues
from utils import reconstructhash
from onionrcommands import restartonionr
pub_key = onionrcrypto.pub_key.replace('=', '')
SCRIPT_NAME = os.path.dirname(os.path.realpath(__file__)) + f'/../../../{onionrvalues.SCRIPT_NAME}'
class PrivateEndpoints:
def __init__(self, client_api):
private_endpoints_bp = Blueprint('privateendpoints', __name__)
self.private_endpoints_bp = private_endpoints_bp
@private_endpoints_bp.route('/www/<path:path>', endpoint='www')
def wwwPublic(path):
if not config.get("www.private.run", True):
abort(403)
return send_from_directory(config.get('www.private.path', 'static-data/www/private/'), path)
@private_endpoints_bp.route('/hitcount')
def get_hit_count():
return Response(str(client_api.publicAPI.hitCount))
@private_endpoints_bp.route('/queueResponseAdd/<name>', methods=['post'])
def queueResponseAdd(name):
# Responses from the daemon. TODO: change to direct var access instead of http endpoint
client_api.queueResponse[name] = request.form['data']
return Response('success')
@private_endpoints_bp.route('/queueResponse/<name>')
def queueResponse(name):
# Fetch a daemon queue response
resp = 'failure'
try:
resp = client_api.queueResponse[name]
except KeyError:
pass
else:
del client_api.queueResponse[name]
if resp == 'failure':
return resp, 404
else:
return resp
@private_endpoints_bp.route('/ping')
def ping():
# Used to check if client api is working
return Response("pong!")
@private_endpoints_bp.route('/lastconnect')
def lastConnect():
return Response(str(client_api.publicAPI.lastRequest))
@private_endpoints_bp.route('/waitforshare/<name>', methods=['post'])
def waitforshare(name):
'''Used to prevent the **public** api from sharing blocks we just created'''
if not name.isalnum(): raise ValueError('block hash needs to be alpha numeric')
name = reconstructhash.reconstruct_hash(name)
if name in client_api.publicAPI.hideBlocks:
client_api.publicAPI.hideBlocks.remove(name)
return Response("removed")
else:
client_api.publicAPI.hideBlocks.append(name)
return Response("added")
@private_endpoints_bp.route('/shutdown')
def shutdown():
return apiutils.shutdown.shutdown(client_api)
@private_endpoints_bp.route('/restartclean')
def restart_clean():
subprocess.Popen([SCRIPT_NAME, 'restart'])
return Response("bye")
@private_endpoints_bp.route('/getstats')
def getStats():
# returns node stats
while True:
try:
return Response(client_api._too_many.get(SerializedData).get_stats())
except AttributeError as e:
pass
@private_endpoints_bp.route('/getuptime')
def showUptime():
return Response(str(client_api.getUptime()))
@private_endpoints_bp.route('/getActivePubkey')
def getActivePubkey():
return Response(pub_key)
@private_endpoints_bp.route('/getHumanReadable')
def getHumanReadableDefault():
return Response(mnemonickeys.get_human_readable_ID())
@private_endpoints_bp.route('/getHumanReadable/<name>')
def getHumanReadable(name):
name = unpaddedbase32.repad(bytesconverter.str_to_bytes(name))
return Response(mnemonickeys.get_human_readable_ID(name))
@private_endpoints_bp.route('/getBase32FromHumanReadable/<words>')
def get_base32_from_human_readable(words):
return Response(bytesconverter.bytes_to_str(mnemonickeys.get_base32(words)))
@private_endpoints_bp.route('/gettorsocks')
def get_tor_socks():
return Response(str(client_api._too_many.get(NetController).socksPort))

View file

@ -0,0 +1,65 @@
'''
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 <https://www.gnu.org/licenses/>.
'''
from flask import Blueprint, Response, abort
from onionrblocks import onionrblockapi
from .. import apiutils
from onionrutils import stringvalidators
from coredb import blockmetadb
client_get_block = apiutils.GetBlockData()
client_get_blocks = Blueprint('miscclient', __name__)
@client_get_blocks.route('/getblocksbytype/<name>')
def get_blocks_by_type_endpoint(name):
blocks = blockmetadb.get_blocks_by_type(name)
return Response(','.join(blocks))
@client_get_blocks.route('/getblockbody/<name>')
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)
@client_get_blocks.route('/getblockdata/<name>')
def getData(name):
resp = ""
if stringvalidators.validate_hash(name):
if name in blockmetadb.get_block_list():
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/<name>')
def getBlockHeader(name):
resp = client_get_block.get_block_data(name, decrypt=True, headerOnly=True)
return Response(resp)

View file

@ -0,0 +1,27 @@
from flask import Blueprint
from flask import Response
import unpaddedbase32
from coredb import blockmetadb
import onionrblocks
from etc import onionrvalues
import config
from onionrutils import bytesconverter
bp = Blueprint('motd', __name__)
signer = config.get("motd.motd_key", onionrvalues.MOTD_SIGN_KEY)
@bp.route('/getmotd')
def get_motd()->Response:
motds = blockmetadb.get_blocks_by_type("motd")
newest_time = 0
message = "No MOTD currently present."
for x in motds:
bl = onionrblocks.onionrblockapi.Block(x)
if not bl.verifySig() or bl.signer != bytesconverter.bytes_to_str(unpaddedbase32.repad(bytesconverter.str_to_bytes(signer))): continue
if not bl.isSigner(signer): continue
if bl.claimedTime > newest_time:
newest_time = bl.claimedTime
message = bl.bcontent
return Response(message, headers={"Content-Type": "text/plain"})

View file

@ -0,0 +1,92 @@
'''
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 <https://www.gnu.org/licenses/>.
'''
import os
import mimetypes
from flask import Blueprint, send_from_directory
# Was having some mime type issues on windows, this appeared to fix it.
# we have no-sniff set, so if the mime types are invalid sripts can't load.
mimetypes.add_type('application/javascript', '.js')
mimetypes.add_type('text/css', '.css')
static_files_bp = Blueprint('staticfiles', __name__)
root = os.path.dirname(os.path.realpath(__file__)) + '/../../../static-data/www/' # should be set to onionr install directory from onionr startup
@static_files_bp.route('/onboarding/', endpoint='onboardingIndex')
def onboard():
return send_from_directory(f'{root}onboarding/', "index.html")
@static_files_bp.route('/onboarding/<path:path>', endpoint='onboarding')
def onboard_files(path):
return send_from_directory(f'{root}onboarding/', path)
@static_files_bp.route('/chat/', endpoint='chatIndex')
def chat_index():
return send_from_directory(root + 'chat/', "index.html")
@static_files_bp.route('/chat/<path:path>', endpoint='chat')
def load_chat(path):
return send_from_directory(root + 'chat/', path)
@static_files_bp.route('/board/', endpoint='board')
def loadBoard():
return send_from_directory(root + 'board/', "index.html")
@static_files_bp.route('/mail/<path:path>', endpoint='mail')
def loadMail(path):
return send_from_directory(root + 'mail/', path)
@static_files_bp.route('/mail/', endpoint='mailindex')
def loadMailIndex():
return send_from_directory(root + 'mail/', 'index.html')
@static_files_bp.route('/friends/<path:path>', endpoint='friends')
def loadContacts(path):
return send_from_directory(root + 'friends/', path)
@static_files_bp.route('/friends/', endpoint='friendsindex')
def loadContacts():
return send_from_directory(root + 'friends/', 'index.html')
@static_files_bp.route('/profiles/<path:path>', endpoint='profiles')
def loadContacts(path):
return send_from_directory(root + 'profiles/', path)
@static_files_bp.route('/profiles/', endpoint='profilesindex')
def loadContacts():
return send_from_directory(root + 'profiles/', 'index.html')
@static_files_bp.route('/board/<path:path>', endpoint='boardContent')
def boardContent(path):
return send_from_directory(root + 'board/', path)
@static_files_bp.route('/shared/<path:path>', endpoint='sharedContent')
def sharedContent(path):
return send_from_directory(root + 'shared/', path)
@static_files_bp.route('/', endpoint='onionrhome')
def hello():
# ui home
return send_from_directory(root + 'private/', 'index.html')
@static_files_bp.route('/private/<path:path>', endpoint='homedata')
def homedata(path):
return send_from_directory(root + 'private/', path)

View file

@ -0,0 +1,6 @@
from . import announce, upload, getblocks, endpoints
announce = announce.handle_announce # endpoint handler for accepting peer announcements
upload = upload.accept_upload # endpoint handler for accepting public uploads
public_block_list = getblocks.get_public_block_list # endpoint handler for getting block lists
public_get_block_data = getblocks.get_block_data # endpoint handler for responding to peers requests for block data

View file

@ -0,0 +1,76 @@
'''
Onionr - Private P2P Communication
Handle announcements to the public API server
'''
'''
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
import base64
from flask import Response, g
import deadsimplekv
import logger
from etc import onionrvalues
from onionrutils import stringvalidators, bytesconverter
from utils import gettransports
import onionrcrypto as crypto, filepaths
from communicator import OnionrCommunicatorDaemon
def handle_announce(request):
'''
accept announcement posts, validating POW
clientAPI should be an instance of the clientAPI server running, request is a instance of a flask request
'''
resp = 'failure'
powHash = ''
randomData = ''
newNode = ''
try:
newNode = request.form['node'].encode()
except KeyError:
logger.warn('No node specified for upload')
pass
else:
try:
randomData = request.form['random']
randomData = base64.b64decode(randomData)
except KeyError:
logger.warn('No random data specified for upload')
else:
nodes = newNode + bytesconverter.str_to_bytes(gettransports.get()[0])
nodes = crypto.hashers.blake2b_hash(nodes)
powHash = crypto.hashers.blake2b_hash(randomData + nodes)
try:
powHash = powHash.decode()
except AttributeError:
pass
if powHash.startswith('0' * onionrvalues.ANNOUNCE_POW):
newNode = bytesconverter.bytes_to_str(newNode)
announce_queue = deadsimplekv.DeadSimpleKV(filepaths.announce_cache)
announce_queue_list = announce_queue.get('new_peers')
if announce_queue_list is None:
announce_queue_list = []
if stringvalidators.validate_transport(newNode) and not newNode in announce_queue_list:
#clientAPI.onionrInst.communicatorInst.newPeers.append(newNode)
g.shared_state.get(OnionrCommunicatorDaemon).newPeers.append(newNode)
announce_queue.put('new_peers', announce_queue_list.append(newNode))
announce_queue.flush()
resp = 'Success'
else:
logger.warn(newNode.decode() + ' failed to meet POW: ' + powHash)
resp = Response(resp)
if resp == 'failure':
return resp, 406
return resp

View file

@ -0,0 +1,81 @@
'''
Onionr - Private P2P Communication
Misc public API endpoints too small to need their own file and that need access to the public api inst
'''
'''
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 <https://www.gnu.org/licenses/>.
'''
from flask import Response, Blueprint, request, send_from_directory, abort, g
from . import getblocks, upload, announce
from coredb import keydb
import config
class PublicEndpoints:
def __init__(self, public_api):
public_endpoints_bp = Blueprint('publicendpoints', __name__)
self.public_endpoints_bp = public_endpoints_bp
@public_endpoints_bp.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')
except FileNotFoundError:
resp = Response("")
return resp
@public_endpoints_bp.route('/getblocklist')
def get_block_list():
'''Get a list of blocks, optionally filtered by epoch time stamp, excluding those hidden'''
return getblocks.get_public_block_list(public_api, request)
@public_endpoints_bp.route('/getdata/<name>')
def get_block_data(name):
# Share data for a block if we have it and it isn't hidden
return getblocks.get_block_data(public_api, name)
@public_endpoints_bp.route('/www/<path:path>')
def www_public(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)
@public_endpoints_bp.route('/ping')
def ping():
# Endpoint to test if nodes are up
return Response("pong!")
@public_endpoints_bp.route('/pex')
def peer_exchange():
response = ','.join(keydb.listkeys.list_adders(recent=3600))
if len(response) == 0:
response = ''
return Response(response)
@public_endpoints_bp.route('/announce', methods=['post'])
def accept_announce():
'''Accept announcements with pow token to prevent spam'''
g.shared_state = public_api._too_many
resp = announce.handle_announce(request)
return resp
@public_endpoints_bp.route('/upload', methods=['post'])
def upload_endpoint():
'''Accept file uploads. In the future this will be done more often than on creation
to speed up block sync
'''
return upload.accept_upload(request)

View file

@ -0,0 +1,58 @@
'''
Onionr - Private P2P Communication
Public endpoints to get block data and lists
'''
'''
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
from flask import Response, abort
import config
from onionrutils import bytesconverter, stringvalidators
from coredb import blockmetadb
from utils import reconstructhash
from .. import apiutils
def get_public_block_list(publicAPI, request):
# Provide a list of our blocks, with a date offset
dateAdjust = request.args.get('date')
bList = blockmetadb.get_block_list(dateRec=dateAdjust)
share_list = ''
if config.get('general.hide_created_blocks', True):
for b in publicAPI.hideBlocks:
if b in bList:
# Don't share blocks we created if they haven't been *uploaded* yet, makes it harder to find who created a block
bList.remove(b)
for b in bList:
share_list += '%s\n' % (reconstructhash.deconstruct_hash(b),)
return Response(share_list)
def get_block_data(publicAPI, data):
'''data is the block hash in hex'''
resp = ''
if stringvalidators.validate_hash(data):
if not config.get('general.hide_created_blocks', True) or data not in publicAPI.hideBlocks:
if data in blockmetadb.get_block_list():
block = apiutils.GetBlockData().get_block_data(data, raw=True)
try:
block = block.encode() # Encode in case data is binary
except AttributeError:
if len(block) == 0:
abort(404)
block = bytesconverter.str_to_bytes(block)
resp = block
if len(resp) == 0:
abort(404)
resp = ""
# Has to be octet stream, otherwise binary data fails hash check
return Response(resp, mimetype='application/octet-stream')

View file

@ -0,0 +1,49 @@
'''
Onionr - Private P2P Communication
Accept block uploads to the public API server
'''
'''
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
import sys
from flask import Response, abort
from onionrblocks import blockimporter
import onionrexceptions, logger
def accept_upload(request):
resp = 'failure'
data = request.get_data()
if sys.getsizeof(data) < 100000000:
try:
if blockimporter.importBlockFromData(data):
resp = 'success'
else:
resp = 'failure'
logger.warn('Error encountered importing uploaded block')
except onionrexceptions.BlacklistedBlock:
logger.debug('uploaded block is blacklisted')
resp = 'failure'
except onionrexceptions.InvalidProof:
resp = 'proof'
except onionrexceptions.DataExists:
resp = 'exists'
if resp == 'failure':
abort(400)
elif resp == 'proof':
resp = Response(resp, 400)
else:
resp = Response(resp)
return resp

View file

@ -0,0 +1,95 @@
"""
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 <https://www.gnu.org/licenses/>.
"""
import base64
import binascii
import mimetypes
import unpaddedbase32
from flask import Blueprint, Response, request, abort
from onionrblocks import onionrblockapi
import onionrexceptions
from onionrutils import stringvalidators
from utils import safezip
from onionrutils import mnemonickeys
from . import sitefiles
site_api = Blueprint('siteapi', __name__)
@site_api.route('/site/<name>/', endpoint='site')
def site(name: str)->Response:
"""Accept a site 'name', if pubkey then show multi-page site, if hash show single page site"""
resp: str = 'Not Found'
mime_type = 'text/html'
# If necessary convert the name to base32 from mnemonic
if mnemonickeys.DELIMITER in name:
name = mnemonickeys.get_base32(name)
# Now make sure the key is regardless a valid base32 format ed25519 key (readding padding if necessary)
if stringvalidators.validate_pub_key(name):
name = unpaddedbase32.repad(name)
resp = sitefiles.get_file(name, 'index.html')
elif stringvalidators.validate_hash(name):
try:
resp = onionrblockapi.Block(name).bcontent
except onionrexceptions.NoDataAvailable:
abort(404)
except TypeError:
pass
try:
resp = base64.b64decode(resp)
except binascii.Error:
pass
if resp == 'Not Found' or not resp:
abort(404)
return Response(resp)
@site_api.route('/site/<name>/<path:file>', endpoint='siteFile')
def site_file(name: str, file: str)->Response:
"""Accept a site 'name', if pubkey then show multi-page site, if hash show single page site"""
resp: str = 'Not Found'
mime_type = mimetypes.MimeTypes().guess_type(file)[0]
# If necessary convert the name to base32 from mnemonic
if mnemonickeys.DELIMITER in name:
name = mnemonickeys.get_base32(name)
# Now make sure the key is regardless a valid base32 format ed25519 key (readding padding if necessary)
if stringvalidators.validate_pub_key(name):
name = unpaddedbase32.repad(name)
resp = sitefiles.get_file(name, file)
elif stringvalidators.validate_hash(name):
try:
resp = onionrblockapi.Block(name).bcontent
except onionrexceptions.NoDataAvailable:
abort(404)
except TypeError:
pass
try:
resp = base64.b64decode(resp)
except binascii.Error:
pass
if resp == 'Not Found' or not resp:
abort(404)
return Response(resp, mimetype=mime_type)

View file

@ -0,0 +1,49 @@
"""
Onionr - Private P2P Communication
view and interact with onionr sites
"""
from typing import Union
import onionrexceptions
from onionrutils import mnemonickeys
from onionrutils import stringvalidators
from coredb import blockmetadb
from onionrblocks.onionrblockapi import Block
from onionrtypes import BlockHash
"""
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 <https://www.gnu.org/licenses/>.
"""
def find_site(user_id: str) -> Union[BlockHash, None]:
"""Returns block hash str for latest block for a site by a given user id"""
# If mnemonic delim in key, convert to base32 version
if mnemonickeys.DELIMITER in user_id:
user_id = mnemonickeys.get_base32(user_id)
if not stringvalidators.validate_pub_key(user_id):
raise onionrexceptions.InvalidPubkey
found_site = None
sites = blockmetadb.get_blocks_by_type('zsite')
# Find site by searching all site blocks. eww O(N) ☹️, TODO: event based
for site in sites:
site = Block(site)
if site.isSigner(user_id) and site.verifySig():
found_site = site.hash
return found_site

View file

@ -0,0 +1,72 @@
"""
Onionr - Private P2P Communication
Read onionr site files
"""
"""
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 <https://www.gnu.org/licenses/>.
"""
from typing import Union, Tuple
import tarfile
import io
import os
import unpaddedbase32
from coredb import blockmetadb
from onionrblocks import onionrblockapi
from onionrblocks import insert
# Import types. Just for type hiting
from onionrtypes import UserID, DeterministicKeyPassphrase, BlockHash
from onionrcrypto import generate_deterministic
def find_site_gzip(user_id: str)->tarfile.TarFile:
"""Return verified site tar object"""
sites = blockmetadb.get_blocks_by_type('osite')
user_site = None
user_id = unpaddedbase32.repad(user_id)
for site in sites:
block = onionrblockapi.Block(site)
if block.isSigner(user_id):
user_site = block
if not user_site is None:
return tarfile.open(fileobj=io.BytesIO(user_site.bcontent), mode='r')
return None
def get_file(user_id, file)->Union[bytes, None]:
"""Get a site file content"""
ret_data = ""
site = find_site_gzip(user_id)
if site is None: return None
for t_file in site.getmembers():
if t_file.name.replace('./', '') == file:
return site.extractfile(t_file)
return None
def create_site(admin_pass: DeterministicKeyPassphrase, directory:str='.')->Tuple[UserID, BlockHash]:
public_key, private_key = generate_deterministic(admin_pass)
raw_tar = io.BytesIO()
tar = tarfile.open(mode='x:gz', fileobj=raw_tar)
tar.add(directory)
tar.close()
raw_tar.seek(0)
block_hash = insert(raw_tar.read(), header='osite', signing_key=private_key, sign=True)
return (public_key, block_hash)

View file

@ -0,0 +1,27 @@
'''
Onionr - Private P2P Communication
This file creates http endpoints for user profile pages
'''
'''
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
from flask import Blueprint, Response, request, abort
from . import profiles
profile_BP = Blueprint('profile_BP', __name__)
@profile_BP.route('/profile/get/<pubkey>', endpoint='profiles')
def get_profile_page(pubkey):
return Response(pubkey)

View file

@ -0,0 +1,2 @@
def get_latest_user_profile(pubkey):
return ''

View file

@ -0,0 +1 @@
from . import client, public

View file

@ -0,0 +1,69 @@
'''
Onionr - Private P2P Communication
Process incoming requests to the client api server to validate they are legitimate
'''
'''
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 <https://www.gnu.org/licenses/>.
'''
import hmac
from flask import Blueprint, request, abort, g
from onionrservices import httpheaders
from . import pluginwhitelist
# Be extremely mindful of this. These are endpoints available without a password
whitelist_endpoints = ['www', 'staticfiles.homedata', 'staticfiles.sharedContent',
'staticfiles.friends', 'staticfiles.friendsindex', 'siteapi.site', 'siteapi.siteFile', 'staticfiles.onionrhome',
'themes.getTheme', 'staticfiles.onboarding', 'staticfiles.onboardingIndex']
class ClientAPISecurity:
def __init__(self, client_api):
client_api_security_bp = Blueprint('clientapisecurity', __name__)
self.client_api_security_bp = client_api_security_bp
self.client_api = client_api
pluginwhitelist.load_plugin_security_whitelist_endpoints(whitelist_endpoints)
@client_api_security_bp.before_app_request
def validate_request():
'''Validate request has set password and is the correct hostname'''
# For the purpose of preventing DNS rebinding attacks
if request.host != '%s:%s' % (client_api.host, client_api.bindPort):
abort(403)
# Add shared objects
try:
g.too_many = self.client_api._too_many
except KeyError:
g.too_many = None
if request.endpoint in whitelist_endpoints:
return
if request.path.startswith('/site/'): return
try:
if not hmac.compare_digest(request.headers['token'], client_api.clientToken):
if not hmac.compare_digest(request.form['token'], client_api.clientToken):
abort(403)
except KeyError:
if not hmac.compare_digest(request.form['token'], client_api.clientToken):
abort(403)
@client_api_security_bp.after_app_request
def after_req(resp):
# Security headers
resp = httpheaders.set_default_onionr_http_headers(resp)
if request.endpoint in ('siteapi.site', 'siteapi.siteFile'):
resp.headers['Content-Security-Policy'] = "default-src 'none'; style-src 'self' data: 'unsafe-inline'; img-src 'self' data:; media-src 'self' data:"
else:
resp.headers['Content-Security-Policy'] = "default-src 'none'; script-src 'self'; object-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; media-src 'none'; frame-src 'none'; font-src 'self'; connect-src 'self'"
return resp

View file

@ -0,0 +1,32 @@
"""
Onionr - Private P2P Communication
Load web UI client endpoints into the whitelist from plugins
"""
"""
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 <https://www.gnu.org/licenses/>.
"""
import onionrplugins
def load_plugin_security_whitelist_endpoints(whitelist: list):
"""Accept a list reference of whitelist endpoints from security/client.py and
append plugin's specified endpoints to them by attribute"""
for plugin in onionrplugins.get_enabled_plugins():
try:
plugin = onionrplugins.get_plugin(plugin)
except FileNotFoundError:
continue
try:
whitelist.extend(getattr(plugin, "security_whitelist"))
except AttributeError:
pass

View file

@ -0,0 +1,60 @@
'''
Onionr - Private P2P Communication
Process incoming requests to the public api server for certain attacks
'''
'''
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 <https://www.gnu.org/licenses/>.
'''
from flask import Blueprint, request, abort, g
from onionrservices import httpheaders
from onionrutils import epoch
from utils import gettransports
class PublicAPISecurity:
def __init__(self, public_api):
public_api_security_bp = Blueprint('publicapisecurity', __name__)
self.public_api_security_bp = public_api_security_bp
@public_api_security_bp.before_app_request
def validate_request():
'''Validate request has the correct hostname'''
# If high security level, deny requests to public (HS should be disabled anyway for Tor, but might not be for I2P)
transports = gettransports.get()
if public_api.config.get('general.security_level', default=1) > 0:
abort(403)
if request.host not in transports:
# Disallow connection if wrong HTTP hostname, in order to prevent DNS rebinding attacks
abort(403)
public_api.hitCount += 1 # raise hit count for valid requests
try:
if 'onionr' in request.headers['User-Agent'].lower():
g.is_onionr_client = True
else:
g.is_onionr_client = False
except KeyError:
g.is_onionr_client = False
@public_api_security_bp.after_app_request
def send_headers(resp):
'''Send api, access control headers'''
resp = httpheaders.set_default_onionr_http_headers(resp)
# Network API version
resp.headers['X-API'] = public_api.API_VERSION
# Delete some HTTP headers for Onionr user agents
NON_NETWORK_HEADERS = ('Content-Security-Policy', 'X-Frame-Options',
'X-Content-Type-Options', 'Feature-Policy', 'Clear-Site-Data', 'Referrer-Policy')
if g.is_onionr_client:
for header in NON_NETWORK_HEADERS: del resp.headers[header]
public_api.lastRequest = epoch.get_rounded_epoch(roundS=5)
return resp

View file

@ -0,0 +1,46 @@
"""
Onionr - Private P2P Communication
API to get current CSS theme for the client web UI
"""
"""
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 <https://www.gnu.org/licenses/>.
"""
from flask import Blueprint, Response
import config
from utils import readstatic
theme_blueprint = Blueprint('themes', __name__)
LIGHT_THEME_FILES = ['bulma-light.min.css', 'styles-light.css']
DARK_THEME_FILES = ['bulma-dark.min.css', 'styles-dark.css']
def _load_from_files(file_list: list)->str:
"""Loads multiple static dir files and returns them in combined string format (non-binary)"""
combo_data = ''
for f in file_list:
combo_data += readstatic.read_static('www/shared/main/themes/' + f)
return combo_data
@theme_blueprint.route('/gettheme', endpoint='getTheme')
def get_theme_file()->Response:
"""Returns the css theme data"""
css: str
theme = config.get('ui.theme', 'dark').lower()
if theme == 'dark':
css = _load_from_files(DARK_THEME_FILES)
elif theme == 'light':
css = _load_from_files(LIGHT_THEME_FILES)
return Response(css, mimetype='text/css')