2017-12-26 07:25:29 +00:00
|
|
|
'''
|
2019-06-12 00:05:15 +00:00
|
|
|
Onionr - Private P2P Communication
|
2018-01-14 08:48:23 +00:00
|
|
|
|
|
|
|
This file handles all incoming http requests to the client, using Flask
|
|
|
|
'''
|
|
|
|
'''
|
2017-12-26 07:25:29 +00:00
|
|
|
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/>.
|
|
|
|
'''
|
2019-06-15 18:56:57 +00:00
|
|
|
import random, threading, hmac, base64, time, os, json, socket
|
2019-01-20 02:23:26 +00:00
|
|
|
from gevent.pywsgi import WSGIServer, WSGIHandler
|
|
|
|
from gevent import Timeout
|
2019-06-15 18:56:57 +00:00
|
|
|
import flask
|
2018-07-27 05:48:22 +00:00
|
|
|
from flask import request, Response, abort, send_from_directory
|
2018-10-27 03:29:25 +00:00
|
|
|
import core
|
2019-06-25 23:07:35 +00:00
|
|
|
import onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config, onionrblockapi
|
2019-03-02 06:22:59 +00:00
|
|
|
import httpapi
|
2019-07-01 20:38:55 +00:00
|
|
|
from httpapi import friendsapi, profilesapi, configapi, miscpublicapi, miscclientapi, insertblock, onionrsitesapi
|
2019-03-26 04:25:46 +00:00
|
|
|
from onionrservices import httpheaders
|
2019-02-21 20:25:45 +00:00
|
|
|
import onionr
|
2019-06-25 23:07:35 +00:00
|
|
|
from onionrutils import bytesconverter, stringvalidators, epoch, mnemonickeys
|
2019-07-01 20:38:55 +00:00
|
|
|
from httpapi import apiutils
|
2019-06-15 18:56:57 +00:00
|
|
|
|
2019-06-15 01:31:01 +00:00
|
|
|
config.reload()
|
2019-01-20 02:23:26 +00:00
|
|
|
class FDSafeHandler(WSGIHandler):
|
2019-02-11 22:36:43 +00:00
|
|
|
'''Our WSGI handler. Doesn't do much non-default except timeouts'''
|
2019-01-20 02:23:26 +00:00
|
|
|
def handle(self):
|
2019-06-29 20:17:48 +00:00
|
|
|
self.timeout = Timeout(120, Exception)
|
|
|
|
self.timeout.start()
|
|
|
|
try:
|
|
|
|
WSGIHandler.handle(self)
|
|
|
|
except Exception:
|
|
|
|
self.handle_error()
|
|
|
|
finally:
|
|
|
|
self.timeout.close()
|
|
|
|
|
|
|
|
def handle_error(self):
|
|
|
|
if v is self.timeout:
|
|
|
|
self.result = [b"Timeout"]
|
|
|
|
self.start_response("200 OK", [])
|
|
|
|
self.process_result()
|
|
|
|
else:
|
|
|
|
WSGIHandler.handle_error(self)
|
2019-01-20 02:23:26 +00:00
|
|
|
|
2019-03-25 23:46:25 +00:00
|
|
|
def setBindIP(filePath=''):
|
2019-01-08 05:51:39 +00:00
|
|
|
'''Set a random localhost IP to a specified file (intended for private or public API localhost IPs)'''
|
2019-02-24 00:11:43 +00:00
|
|
|
if config.get('general.random_bind_ip', True):
|
|
|
|
hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))]
|
|
|
|
data = '.'.join(hostOctets)
|
|
|
|
# Try to bind IP. Some platforms like Mac block non normal 127.x.x.x
|
|
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
|
try:
|
|
|
|
s.bind((data, 0))
|
|
|
|
except OSError:
|
|
|
|
# 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:
|
2019-01-08 07:25:56 +00:00
|
|
|
data = '127.0.0.1'
|
2019-03-25 23:46:25 +00:00
|
|
|
if filePath != '':
|
2019-03-25 03:32:17 +00:00
|
|
|
with open(filePath, 'w') as bindFile:
|
|
|
|
bindFile.write(data)
|
2019-01-08 05:51:39 +00:00
|
|
|
return data
|
|
|
|
|
|
|
|
class PublicAPI:
|
|
|
|
'''
|
|
|
|
The new client api server, isolated from the public api
|
|
|
|
'''
|
|
|
|
def __init__(self, clientAPI):
|
|
|
|
assert isinstance(clientAPI, API)
|
|
|
|
app = flask.Flask('PublicAPI')
|
2018-06-14 04:17:58 +00:00
|
|
|
self.i2pEnabled = config.get('i2p.host', False)
|
2018-11-13 17:07:46 +00:00
|
|
|
self.hideBlocks = [] # Blocks to be denied sharing
|
2019-01-08 05:51:39 +00:00
|
|
|
self.host = setBindIP(clientAPI._core.publicApiHostFile)
|
|
|
|
self.torAdder = clientAPI._core.hsAddress
|
|
|
|
self.i2pAdder = clientAPI._core.i2pAddress
|
|
|
|
self.bindPort = config.get('client.public.port')
|
2019-02-14 23:48:41 +00:00
|
|
|
self.lastRequest = 0
|
2019-06-16 22:34:43 +00:00
|
|
|
self.hitCount = 0 # total rec requests to public api since server started
|
2019-01-08 05:51:39 +00:00
|
|
|
logger.info('Running public api on %s:%s' % (self.host, self.bindPort))
|
2017-12-27 01:13:19 +00:00
|
|
|
|
2017-12-28 00:18:00 +00:00
|
|
|
@app.before_request
|
2019-01-08 05:51:39 +00:00
|
|
|
def validateRequest():
|
|
|
|
'''Validate request has the correct hostname'''
|
2019-02-12 05:30:56 +00:00
|
|
|
# If high security level, deny requests to public (HS should be disabled anyway for Tor, but might not be for I2P)
|
2019-05-15 23:25:36 +00:00
|
|
|
if config.get('general.security_level', default=1) > 0:
|
2019-01-17 05:31:56 +00:00
|
|
|
abort(403)
|
2019-01-08 05:51:39 +00:00
|
|
|
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):
|
2019-02-12 05:30:56 +00:00
|
|
|
# Disallow connection if wrong HTTP hostname, in order to prevent DNS rebinding attacks
|
2019-01-08 05:51:39 +00:00
|
|
|
abort(403)
|
2019-06-16 22:34:43 +00:00
|
|
|
self.hitCount += 1 # raise hit count for valid requests
|
2017-12-28 00:18:00 +00:00
|
|
|
|
2017-12-27 05:00:02 +00:00
|
|
|
@app.after_request
|
2019-01-08 05:51:39 +00:00
|
|
|
def sendHeaders(resp):
|
|
|
|
'''Send api, access control headers'''
|
2019-03-26 04:25:46 +00:00
|
|
|
resp = httpheaders.set_default_onionr_http_headers(resp)
|
2019-02-12 05:30:56 +00:00
|
|
|
# Network API version
|
2019-01-08 05:51:39 +00:00
|
|
|
resp.headers['X-API'] = onionr.API_VERSION
|
2019-06-25 23:07:35 +00:00
|
|
|
self.lastRequest = epoch.get_rounded_epoch(roundS=5)
|
2017-12-27 05:00:02 +00:00
|
|
|
return resp
|
2018-05-02 06:50:29 +00:00
|
|
|
|
2018-04-25 06:56:40 +00:00
|
|
|
@app.route('/')
|
|
|
|
def banner():
|
2019-02-12 05:30:56 +00:00
|
|
|
# Display a bit of information to people who visit a node address in their browser
|
2018-04-25 06:56:40 +00:00
|
|
|
try:
|
|
|
|
with open('static-data/index.html', 'r') as html:
|
2018-12-09 17:29:39 +00:00
|
|
|
resp = Response(html.read(), mimetype='text/html')
|
2018-04-25 06:56:40 +00:00
|
|
|
except FileNotFoundError:
|
|
|
|
resp = Response("")
|
|
|
|
return resp
|
2017-12-27 05:00:02 +00:00
|
|
|
|
2019-01-08 05:51:39 +00:00
|
|
|
@app.route('/getblocklist')
|
|
|
|
def getBlockList():
|
2019-05-07 17:56:20 +00:00
|
|
|
return httpapi.miscpublicapi.public_block_list(clientAPI, self, request)
|
2019-01-08 05:51:39 +00:00
|
|
|
|
|
|
|
@app.route('/getdata/<name>')
|
|
|
|
def getBlockData(name):
|
2019-02-12 05:30:56 +00:00
|
|
|
# Share data for a block if we have it
|
2019-05-07 17:56:20 +00:00
|
|
|
return httpapi.miscpublicapi.public_get_block_data(clientAPI, self, name)
|
2018-07-23 07:43:10 +00:00
|
|
|
|
2019-01-08 05:51:39 +00:00
|
|
|
@app.route('/www/<path:path>')
|
|
|
|
def wwwPublic(path):
|
2019-02-12 05:30:56 +00:00
|
|
|
# A way to share files directly over your .onion
|
2019-01-08 05:51:39 +00:00
|
|
|
if not config.get("www.public.run", True):
|
|
|
|
abort(403)
|
|
|
|
return send_from_directory(config.get('www.public.path', 'static-data/www/public/'), path)
|
2018-08-08 19:26:02 +00:00
|
|
|
|
2019-01-08 05:51:39 +00:00
|
|
|
@app.route('/ping')
|
|
|
|
def ping():
|
2019-02-12 05:30:56 +00:00
|
|
|
# Endpoint to test if nodes are up
|
2019-01-08 05:51:39 +00:00
|
|
|
return Response("pong!")
|
|
|
|
|
|
|
|
@app.route('/pex')
|
|
|
|
def peerExchange():
|
2019-01-11 22:59:21 +00:00
|
|
|
response = ','.join(clientAPI._core.listAdders(recent=3600))
|
2019-01-08 05:51:39 +00:00
|
|
|
if len(response) == 0:
|
2019-01-11 22:59:21 +00:00
|
|
|
response = ''
|
2019-01-08 05:51:39 +00:00
|
|
|
return Response(response)
|
|
|
|
|
|
|
|
@app.route('/announce', methods=['post'])
|
2018-08-08 19:26:02 +00:00
|
|
|
def acceptAnnounce():
|
2019-05-07 17:56:20 +00:00
|
|
|
resp = httpapi.miscpublicapi.announce(clientAPI, request)
|
2018-09-08 06:45:33 +00:00
|
|
|
return resp
|
2018-08-08 19:26:02 +00:00
|
|
|
|
2019-01-08 05:51:39 +00:00
|
|
|
@app.route('/upload', methods=['post'])
|
|
|
|
def upload():
|
2019-02-12 05:30:56 +00:00
|
|
|
'''Accept file uploads. In the future this will be done more often than on creation
|
|
|
|
to speed up block sync
|
|
|
|
'''
|
2019-05-07 17:56:20 +00:00
|
|
|
return httpapi.miscpublicapi.upload(clientAPI, request)
|
2018-02-04 03:44:29 +00:00
|
|
|
|
2019-02-12 05:30:56 +00:00
|
|
|
# Set instances, then startup our public api server
|
2019-01-08 05:51:39 +00:00
|
|
|
clientAPI.setPublicAPIInstance(self)
|
|
|
|
while self.torAdder == '':
|
|
|
|
clientAPI._core.refreshFirstStartVars()
|
|
|
|
self.torAdder = clientAPI._core.hsAddress
|
2019-02-20 23:12:11 +00:00
|
|
|
time.sleep(0.1)
|
2019-01-20 02:23:26 +00:00
|
|
|
self.httpServer = WSGIServer((self.host, self.bindPort), app, log=None, handler_class=FDSafeHandler)
|
2019-01-08 05:51:39 +00:00
|
|
|
self.httpServer.serve_forever()
|
2018-02-04 03:44:29 +00:00
|
|
|
|
2019-01-08 05:51:39 +00:00
|
|
|
class API:
|
|
|
|
'''
|
|
|
|
Client HTTP api
|
|
|
|
'''
|
2018-02-04 03:44:29 +00:00
|
|
|
|
2019-01-08 05:51:39 +00:00
|
|
|
callbacks = {'public' : {}, 'private' : {}}
|
2018-12-09 17:29:39 +00:00
|
|
|
|
2019-01-08 05:51:39 +00:00
|
|
|
def __init__(self, onionrInst, debug, API_VERSION):
|
|
|
|
'''
|
|
|
|
Initialize the api server, preping variables for later use
|
2017-12-27 01:13:19 +00:00
|
|
|
|
2019-04-23 02:02:09 +00:00
|
|
|
This initialization defines all of the API entry points and handlers for the endpoints and errors
|
2019-01-08 05:51:39 +00:00
|
|
|
This also saves the used host (random localhost IP address) to the data folder in host.txt
|
2018-02-04 03:44:29 +00:00
|
|
|
'''
|
2018-01-26 07:22:48 +00:00
|
|
|
|
2019-01-08 05:51:39 +00:00
|
|
|
self.debug = debug
|
2019-01-20 22:54:04 +00:00
|
|
|
self._core = onionrInst.onionrCore
|
2019-06-25 23:07:35 +00:00
|
|
|
self.startTime = epoch.get_epoch()
|
2019-01-08 05:51:39 +00:00
|
|
|
self._crypto = onionrcrypto.OnionrCrypto(self._core)
|
|
|
|
app = flask.Flask(__name__)
|
|
|
|
bindPort = int(config.get('client.client.port', 59496))
|
|
|
|
self.bindPort = bindPort
|
2018-05-19 21:32:21 +00:00
|
|
|
|
2019-01-08 05:51:39 +00:00
|
|
|
self.clientToken = config.get('client.webpassword')
|
|
|
|
self.timeBypassToken = base64.b16encode(os.urandom(32)).decode()
|
2018-05-19 21:32:21 +00:00
|
|
|
|
2019-01-08 05:51:39 +00:00
|
|
|
self.publicAPI = None # gets set when the thread calls our setter... bad hack but kinda necessary with flask
|
|
|
|
#threading.Thread(target=PublicAPI, args=(self,)).start()
|
|
|
|
self.host = setBindIP(self._core.privateApiHostFile)
|
|
|
|
logger.info('Running api on %s:%s' % (self.host, self.bindPort))
|
|
|
|
self.httpServer = ''
|
2019-01-07 05:50:20 +00:00
|
|
|
|
|
|
|
self.queueResponse = {}
|
2019-01-08 05:51:39 +00:00
|
|
|
onionrInst.setClientAPIInst(self)
|
2019-02-21 20:25:45 +00:00
|
|
|
app.register_blueprint(friendsapi.friends)
|
2019-04-09 17:30:54 +00:00
|
|
|
app.register_blueprint(profilesapi.profile_BP)
|
2019-04-27 15:43:16 +00:00
|
|
|
app.register_blueprint(configapi.config_BP)
|
2019-06-29 20:17:48 +00:00
|
|
|
app.register_blueprint(insertblock.ib)
|
2019-07-01 20:38:55 +00:00
|
|
|
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)
|
2019-03-02 06:22:59 +00:00
|
|
|
httpapi.load_plugin_blueprints(app)
|
2019-07-01 20:38:55 +00:00
|
|
|
self.get_block_data = apiutils.GetBlockData(self)
|
2019-04-02 16:50:09 +00:00
|
|
|
|
|
|
|
@app.route('/serviceactive/<pubkey>')
|
|
|
|
def serviceActive(pubkey):
|
|
|
|
try:
|
|
|
|
if pubkey in self._core.onionrInst.communicatorInst.active_services:
|
|
|
|
return Response('true')
|
|
|
|
except AttributeError as e:
|
|
|
|
pass
|
|
|
|
return Response('false')
|
2019-01-30 06:10:29 +00:00
|
|
|
|
2018-12-26 06:14:05 +00:00
|
|
|
@app.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)
|
|
|
|
|
2019-06-16 22:34:43 +00:00
|
|
|
@app.route('/hitcount')
|
|
|
|
def get_hit_count():
|
|
|
|
return Response(str(self.publicAPI.hitCount))
|
|
|
|
|
2019-01-07 05:50:20 +00:00
|
|
|
@app.route('/queueResponseAdd/<name>', methods=['post'])
|
|
|
|
def queueResponseAdd(name):
|
2019-02-12 05:30:56 +00:00
|
|
|
# Responses from the daemon. TODO: change to direct var access instead of http endpoint
|
2019-01-07 05:50:20 +00:00
|
|
|
self.queueResponse[name] = request.form['data']
|
|
|
|
return Response('success')
|
|
|
|
|
|
|
|
@app.route('/queueResponse/<name>')
|
|
|
|
def queueResponse(name):
|
2019-02-12 05:30:56 +00:00
|
|
|
# Fetch a daemon queue response
|
2019-01-13 22:20:10 +00:00
|
|
|
resp = 'failure'
|
2019-01-07 05:50:20 +00:00
|
|
|
try:
|
2019-01-07 21:09:58 +00:00
|
|
|
resp = self.queueResponse[name]
|
2019-01-07 05:50:20 +00:00
|
|
|
except KeyError:
|
2019-01-07 21:09:58 +00:00
|
|
|
pass
|
|
|
|
else:
|
|
|
|
del self.queueResponse[name]
|
2019-04-12 17:14:16 +00:00
|
|
|
if resp == 'failure':
|
|
|
|
return resp, 404
|
|
|
|
else:
|
|
|
|
return resp
|
2019-01-07 05:50:20 +00:00
|
|
|
|
2019-01-08 05:51:39 +00:00
|
|
|
@app.route('/ping')
|
|
|
|
def ping():
|
2019-02-12 05:30:56 +00:00
|
|
|
# Used to check if client api is working
|
2019-01-08 05:51:39 +00:00
|
|
|
return Response("pong!")
|
2019-02-04 23:48:21 +00:00
|
|
|
|
2019-02-14 23:48:41 +00:00
|
|
|
@app.route('/lastconnect')
|
|
|
|
def lastConnect():
|
|
|
|
return Response(str(self.publicAPI.lastRequest))
|
|
|
|
|
2019-01-08 05:51:39 +00:00
|
|
|
@app.route('/waitforshare/<name>', methods=['post'])
|
2019-02-08 06:19:05 +00:00
|
|
|
def waitforshare(name):
|
2019-02-12 05:30:56 +00:00
|
|
|
'''Used to prevent the **public** api from sharing blocks we just created'''
|
2019-01-08 05:51:39 +00:00
|
|
|
assert name.isalnum()
|
|
|
|
if name in self.publicAPI.hideBlocks:
|
|
|
|
self.publicAPI.hideBlocks.remove(name)
|
|
|
|
return Response("removed")
|
|
|
|
else:
|
|
|
|
self.publicAPI.hideBlocks.append(name)
|
|
|
|
return Response("added")
|
2018-07-30 00:37:12 +00:00
|
|
|
|
2019-01-08 05:51:39 +00:00
|
|
|
@app.route('/shutdown')
|
|
|
|
def shutdown():
|
2019-07-01 20:38:55 +00:00
|
|
|
return apiutils.shutdown.shutdown(self)
|
2019-01-13 22:20:10 +00:00
|
|
|
|
|
|
|
@app.route('/getstats')
|
|
|
|
def getStats():
|
2019-02-12 05:30:56 +00:00
|
|
|
# returns node stats
|
2019-01-20 18:09:53 +00:00
|
|
|
#return Response("disabled")
|
2019-01-30 06:10:29 +00:00
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
return Response(self._core.serializer.getStats())
|
|
|
|
except AttributeError:
|
|
|
|
pass
|
2019-01-13 22:20:10 +00:00
|
|
|
|
|
|
|
@app.route('/getuptime')
|
|
|
|
def showUptime():
|
|
|
|
return Response(str(self.getUptime()))
|
2019-02-04 00:31:03 +00:00
|
|
|
|
|
|
|
@app.route('/getActivePubkey')
|
|
|
|
def getActivePubkey():
|
|
|
|
return Response(self._core._crypto.pubKey)
|
|
|
|
|
|
|
|
@app.route('/getHumanReadable/<name>')
|
|
|
|
def getHumanReadable(name):
|
2019-06-25 23:07:35 +00:00
|
|
|
return Response(mnemonickeys.get_human_readable_ID(name))
|
2019-02-10 02:21:36 +00:00
|
|
|
|
2019-01-20 02:23:26 +00:00
|
|
|
self.httpServer = WSGIServer((self.host, bindPort), app, log=None, handler_class=FDSafeHandler)
|
2019-01-08 05:51:39 +00:00
|
|
|
self.httpServer.serve_forever()
|
2018-07-30 00:37:12 +00:00
|
|
|
|
2019-01-08 05:51:39 +00:00
|
|
|
def setPublicAPIInstance(self, inst):
|
|
|
|
assert isinstance(inst, PublicAPI)
|
|
|
|
self.publicAPI = inst
|
2018-07-30 00:37:12 +00:00
|
|
|
|
2019-01-08 05:51:39 +00:00
|
|
|
def validateToken(self, token):
|
|
|
|
'''
|
2019-02-12 05:30:56 +00:00
|
|
|
Validate that the client token matches the given token. Used to prevent CSRF and data exfiltration
|
2019-01-08 05:51:39 +00:00
|
|
|
'''
|
|
|
|
if len(self.clientToken) == 0:
|
|
|
|
logger.error("client password needs to be set")
|
|
|
|
return False
|
|
|
|
try:
|
|
|
|
if not hmac.compare_digest(self.clientToken, token):
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
return True
|
|
|
|
except TypeError:
|
|
|
|
return False
|
2019-01-13 22:20:10 +00:00
|
|
|
|
|
|
|
def getUptime(self):
|
2019-01-28 22:49:04 +00:00
|
|
|
while True:
|
|
|
|
try:
|
2019-06-25 23:07:35 +00:00
|
|
|
return epoch.get_epoch() - self.startTime
|
2019-04-12 17:14:16 +00:00
|
|
|
except (AttributeError, NameError):
|
2019-01-28 22:49:04 +00:00
|
|
|
# Don't error on race condition with startup
|
2019-02-01 06:38:12 +00:00
|
|
|
pass
|
2019-07-01 20:38:55 +00:00
|
|
|
|
2019-02-04 23:48:21 +00:00
|
|
|
def getBlockData(self, bHash, decrypt=False, raw=False, headerOnly=False):
|
2019-07-01 20:38:55 +00:00
|
|
|
return self.get_block_data.get_block_data(self, bHash, decrypt, raw, headerOnly)
|