Refactors the API to run using dispatch instead of decorate

The API was never made an object since I couldn't find a way or working around the Flask decorators.

By using dispatch we can get around the issues in #14 and will be able to create better mocks for the API
This commit is contained in:
Sergi Delgado Segura
2019-12-17 17:04:57 +01:00
parent 6129ae9b25
commit 531523c534
3 changed files with 138 additions and 142 deletions

View File

@@ -14,189 +14,185 @@ from common.constants import HTTP_OK, HTTP_BAD_REQUEST, HTTP_SERVICE_UNAVAILABLE
# ToDo: #5-add-async-to-api # ToDo: #5-add-async-to-api
app = Flask(__name__) app = Flask(__name__)
logger = Logger("API") logger = Logger("API")
watcher = None
@app.route("/", methods=["POST"]) class API:
def add_appointment(): def __init__(self, watcher):
""" self.watcher = watcher
Add appointment endpoint, it is used as the main endpoint of the Watchtower.
The client sends requests (appointments) to this endpoint to request a job to the Watchtower. Requests must be json def add_appointment(self):
encoded and contain an ``appointment`` field and optionally a ``signature`` and ``public_key`` fields. """
Main endpoint of the Watchtower.
Returns: The client sends requests (appointments) to this endpoint to request a job to the Watchtower. Requests must be json
:obj:`tuple`: A tuple containing the response (``json``) and response code (``int``). For accepted appointments, encoded and contain an ``appointment`` field and optionally a ``signature`` and ``public_key`` fields.
the ``rcode`` is always 0 and the response contains the signed receipt. For rejected appointments, the ``rcode``
is a negative value and the response contains the error message. Error messages can be found at
:mod:`Errors <pisa.errors>`.
"""
remote_addr = request.environ.get("REMOTE_ADDR") Returns:
remote_port = request.environ.get("REMOTE_PORT") :obj:`tuple`: A tuple containing the response (``json``) and response code (``int``). For accepted appointments,
the ``rcode`` is always 0 and the response contains the signed receipt. For rejected appointments, the ``rcode``
is a negative value and the response contains the error message. Error messages can be found at
:mod:`Errors <pisa.errors>`.
"""
logger.info("Connection accepted", from_addr_port="{}:{}".format(remote_addr, remote_port)) remote_addr = request.environ.get("REMOTE_ADDR")
remote_port = request.environ.get("REMOTE_PORT")
# Check content type once if properly defined logger.info("Connection accepted", from_addr_port="{}:{}".format(remote_addr, remote_port))
request_data = json.loads(request.get_json())
inspector = Inspector()
appointment = inspector.inspect(
request_data.get("appointment"), request_data.get("signature"), request_data.get("public_key")
)
error = None # Check content type once if properly defined
response = None request_data = json.loads(request.get_json())
inspector = Inspector()
appointment = inspector.inspect(
request_data.get("appointment"), request_data.get("signature"), request_data.get("public_key")
)
if type(appointment) == Appointment: error = None
appointment_added, signature = watcher.add_appointment(appointment) response = None
if appointment_added: if type(appointment) == Appointment:
rcode = HTTP_OK appointment_added, signature = self.watcher.add_appointment(appointment)
response = {"locator": appointment.locator, "signature": signature}
if appointment_added:
rcode = HTTP_OK
response = {"locator": appointment.locator, "signature": signature}
else:
rcode = HTTP_SERVICE_UNAVAILABLE
error = "appointment rejected"
elif type(appointment) == tuple:
rcode = HTTP_BAD_REQUEST
error = "appointment rejected. Error {}: {}".format(appointment[0], appointment[1])
else: else:
rcode = HTTP_SERVICE_UNAVAILABLE # We should never end up here, since inspect only returns appointments or tuples. Just in case.
error = "appointment rejected" rcode = HTTP_BAD_REQUEST
error = "appointment rejected. Request does not match the standard"
elif type(appointment) == tuple: logger.info(
rcode = HTTP_BAD_REQUEST "Sending response and disconnecting",
error = "appointment rejected. Error {}: {}".format(appointment[0], appointment[1]) from_addr_port="{}:{}".format(remote_addr, remote_port),
response=response,
error=error,
)
else: if error is None:
# We should never end up here, since inspect only returns appointments or tuples. Just in case. return jsonify(response), rcode
rcode = HTTP_BAD_REQUEST else:
error = "appointment rejected. Request does not match the standard" return jsonify({"error": error}), rcode
logger.info( # FIXME: THE NEXT THREE API ENDPOINTS ARE FOR TESTING AND SHOULD BE REMOVED / PROPERLY MANAGED BEFORE PRODUCTION!
"Sending response and disconnecting", # ToDo: #17-add-api-keys
from_addr_port="{}:{}".format(remote_addr, remote_port), def get_appointment(self):
response=response, """
error=error, Gives information about a given appointment state in the Watchtower.
)
if error is None: The information is requested by ``locator``.
return jsonify(response), rcode
else:
return jsonify({"error": error}), rcode
Returns:
:obj:`dict`: A json formatted dictionary containing information about the requested appointment.
# FIXME: THE NEXT THREE API ENDPOINTS ARE FOR TESTING AND SHOULD BE REMOVED / PROPERLY MANAGED BEFORE PRODUCTION! A ``status`` flag is added to the data provided by either the :obj:`Watcher <pisa.watcher.Watcher>` or the
# ToDo: #17-add-api-keys :obj:`Responder <pisa.responder.Responder>` that signals the status of the appointment.
@app.route("/get_appointment", methods=["GET"])
def get_appointment():
"""
Get appointment endpoint, it gives information about a given appointment state in the Watchtower.
The information is requested by ``locator``. - Appointments hold by the :obj:`Watcher <pisa.watcher.Watcher>` are flagged as ``being_watched``.
- Appointments hold by the :obj:`Responder <pisa.responder.Responder>` are flagged as ``dispute_triggered``.
- Unknown appointments are flagged as ``not_found``.
"""
Returns: locator = request.args.get("locator")
:obj:`dict`: A json formatted dictionary containing information about the requested appointment. response = []
A ``status`` flag is added to the data provided by either the :obj:`Watcher <pisa.watcher.Watcher>` or the # ToDo: #15-add-system-monitor
:obj:`Responder <pisa.responder.Responder>` that signals the status of the appointment. if not isinstance(locator, str) or len(locator) != LOCATOR_LEN_HEX:
response.append({"locator": locator, "status": "not_found"})
return jsonify(response)
- Appointments hold by the :obj:`Watcher <pisa.watcher.Watcher>` are flagged as ``being_watched``. locator_map = self.watcher.db_manager.load_locator_map(locator)
- Appointments hold by the :obj:`Responder <pisa.responder.Responder>` are flagged as ``dispute_triggered``.
- Unknown appointments are flagged as ``not_found``.
"""
locator = request.args.get("locator") if locator_map is not None:
response = [] for uuid in locator_map:
appointment_data = self.watcher.db_manager.load_watcher_appointment(uuid)
# ToDo: #15-add-system-monitor if appointment_data is not None and appointment_data["triggered"] is False:
if not isinstance(locator, str) or len(locator) != LOCATOR_LEN_HEX: # Triggered is an internal flag
response.append({"locator": locator, "status": "not_found"}) del appointment_data["triggered"]
return jsonify(response)
locator_map = watcher.db_manager.load_locator_map(locator) appointment_data["status"] = "being_watched"
response.append(appointment_data)
if locator_map is not None: tracker_data = self.watcher.db_manager.load_responder_tracker(uuid)
for uuid in locator_map:
appointment_data = watcher.db_manager.load_watcher_appointment(uuid)
if appointment_data is not None and appointment_data["triggered"] is False: if tracker_data is not None:
# Triggered is an internal flag tracker_data["status"] = "dispute_responded"
del appointment_data["triggered"] response.append(tracker_data)
appointment_data["status"] = "being_watched" else:
response.append(appointment_data) response.append({"locator": locator, "status": "not_found"})
tracker_data = watcher.db_manager.load_responder_tracker(uuid) response = jsonify(response)
if tracker_data is not None: return response
tracker_data["status"] = "dispute_responded"
response.append(tracker_data)
else: def get_all_appointments(self):
response.append({"locator": locator, "status": "not_found"}) """
Gives information about all the appointments in the Watchtower.
response = jsonify(response) This endpoint should only be accessible by the administrator. Requests are only allowed from localhost.
return response Returns:
:obj:`dict`: A json formatted dictionary containing all the appointments hold by the
:obj:`Watcher <pisa.watcher.Watcher>` (``watcher_appointments``) and by the
:obj:`Responder <pisa.responder.Responder>` (``responder_trackers``).
"""
@app.route("/get_all_appointments", methods=["GET"]) # ToDo: #15-add-system-monitor
def get_all_appointments(): response = None
"""
Get all appointments endpoint, it gives information about all the appointments in the Watchtower.
This endpoint should only be accessible by the administrator. Requests are only allowed from localhost. if request.remote_addr in request.host or request.remote_addr == "127.0.0.1":
watcher_appointments = self.watcher.db_manager.load_watcher_appointments()
responder_trackers = self.watcher.db_manager.load_responder_trackers()
Returns: response = jsonify({"watcher_appointments": watcher_appointments, "responder_trackers": responder_trackers})
:obj:`dict`: A json formatted dictionary containing all the appointments hold by the
:obj:`Watcher <pisa.watcher.Watcher>` (``watcher_appointments``) and by the
:obj:`Responder <pisa.responder.Responder>` (``responder_trackers``).
""" else:
abort(404)
# ToDo: #15-add-system-monitor return response
response = None
if request.remote_addr in request.host or request.remote_addr == "127.0.0.1": @staticmethod
watcher_appointments = watcher.db_manager.load_watcher_appointments() def get_block_count():
responder_trackers = watcher.db_manager.load_responder_trackers() """
Provides the block height of the Watchtower.
response = jsonify({"watcher_appointments": watcher_appointments, "responder_trackers": responder_trackers}) This is a testing endpoint that (most likely) will be removed in production. Its purpose is to give information to
testers about the current block so they can define a dummy appointment without having to run a bitcoin node.
else: Returns:
abort(404) :obj:`dict`: A json encoded dictionary containing the block height.
return response """
return jsonify({"block_count": BlockProcessor.get_block_count()})
@app.route("/get_block_count", methods=["GET"]) def start(self):
def get_block_count(): """
""" This function starts the Flask server used to run the API. Adds all the routes to the functions listed above.
Get block count endpoint, it provides the block height of the Watchtower. """
This is a testing endpoint that (most likely) will be removed in production. Its purpose is to give information to routes = {
testers about the current block so they can define a dummy appointment without having to run a bitcoin node. "/": (self.add_appointment, ["POST"]),
"/get_appointment": (self.get_appointment, ["GET"]),
"/get_all_appointments": (self.get_all_appointments, ["GET"]),
"/get_block_count": (self.get_block_count, ["GET"]),
}
Returns: for url, params in routes.items():
:obj:`dict`: A json encoded dictionary containing the block height. app.add_url_rule(url, view_func=params[0], methods=params[1])
""" # Setting Flask log to ERROR only so it does not mess with out logging. Also disabling flask initial messages
logging.getLogger("werkzeug").setLevel(logging.ERROR)
os.environ["WERKZEUG_RUN_MAIN"] = "true"
return jsonify({"block_count": BlockProcessor.get_block_count()}) app.run(host=HOST, port=PORT)
def start_api(w):
"""
This function starts the Flask server used to run the API.
Args:
w (:obj:`Watcher <pisa.watcher.Watcher>`): A ``Watcher`` object.
"""
# FIXME: Pretty ugly but I haven't found a proper way to pass it to add_appointment
global watcher
# ToDo: #18-separate-api-from-watcher
watcher = w
# Setting Flask log to ERROR only so it does not mess with out logging. Also disabling flask initial messages
logging.getLogger("werkzeug").setLevel(logging.ERROR)
os.environ["WERKZEUG_RUN_MAIN"] = "true"
app.run(host=HOST, port=PORT)

View File

@@ -4,7 +4,7 @@ from signal import signal, SIGINT, SIGQUIT, SIGTERM
from pisa.conf import DB_PATH from pisa.conf import DB_PATH
from common.logger import Logger from common.logger import Logger
from pisa.api import start_api from pisa.api import API
from pisa.watcher import Watcher from pisa.watcher import Watcher
from pisa.builder import Builder from pisa.builder import Builder
from pisa.conf import BTC_NETWORK, PISA_SECRET_KEY from pisa.conf import BTC_NETWORK, PISA_SECRET_KEY
@@ -79,8 +79,8 @@ if __name__ == "__main__":
watcher.appointments, watcher.locator_uuid_map = Builder.build_appointments(watcher_appointments_data) watcher.appointments, watcher.locator_uuid_map = Builder.build_appointments(watcher_appointments_data)
watcher.block_queue = Builder.build_block_queue(missed_blocks_watcher) watcher.block_queue = Builder.build_block_queue(missed_blocks_watcher)
# Create an instance of the Watcher and fire the API # Fire the API
start_api(watcher) API(watcher).start()
except Exception as e: except Exception as e:
logger.error("An error occurred: {}. Shutting down".format(e)) logger.error("An error occurred: {}. Shutting down".format(e))

View File

@@ -5,7 +5,7 @@ from time import sleep
from threading import Thread from threading import Thread
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from pisa.api import start_api from pisa.api import API
from pisa.watcher import Watcher from pisa.watcher import Watcher
from pisa.tools import bitcoin_cli from pisa.tools import bitcoin_cli
from pisa import HOST, PORT, c_logger from pisa import HOST, PORT, c_logger
@@ -40,7 +40,7 @@ def run_api(db_manager):
) )
watcher = Watcher(db_manager, sk_der) watcher = Watcher(db_manager, sk_der)
api_thread = Thread(target=start_api, args=[watcher]) api_thread = Thread(target=API(watcher).start)
api_thread.daemon = True api_thread.daemon = True
api_thread.start() api_thread.start()