diff --git a/teos/api.py b/teos/api.py index 6aecb10..8abf790 100644 --- a/teos/api.py +++ b/teos/api.py @@ -2,10 +2,11 @@ import os import logging from flask import Flask, request, abort, jsonify +import teos.errors as errors from teos import HOST, PORT, LOG_PREFIX + from common.logger import Logger from common.appointment import Appointment - from common.constants import HTTP_OK, HTTP_BAD_REQUEST, HTTP_SERVICE_UNAVAILABLE, LOCATOR_LEN_HEX @@ -14,6 +15,24 @@ app = Flask(__name__) logger = Logger(actor="API", log_name_prefix=LOG_PREFIX) +# TODO: UNITTEST +def get_remote_addr(): + """ + Gets the remote client ip address. The HTTP_X_REAL_IP field is tried first in case the server is behind a reverse + proxy. + + Returns: + :obj:`str`: the IP address of the client. + """ + + # Getting the real IP if the server is behind a reverse proxy + remote_addr = request.environ.get("HTTP_X_REAL_IP") + if not remote_addr: + remote_addr = request.environ.get("REMOTE_ADDR") + + return remote_addr + + class API: """ The :class:`API` is in charge of the interface between the user and the tower. It handles and server user requests. @@ -24,9 +43,48 @@ class API: watcher (:obj:`Watcher `): a ``Watcher`` instance to pass the requests to. """ - def __init__(self, inspector, watcher): + # TODO: UNITTEST + def __init__(self, inspector, watcher, gatekeeper): self.inspector = inspector self.watcher = watcher + self.gatekeeper = gatekeeper + + # TODO: UNITTEST + def register(self): + remote_addr = get_remote_addr() + + logger.info("Received register request", from_addr="{}".format(remote_addr)) + + if request.is_json: + request_data = request.get_json() + client_pk = request_data.get("public_key") + + if client_pk: + try: + rcode = HTTP_OK + available_slots = self.gatekeeper.add_update_user(client_pk) + response = {"public_key": client_pk, "available_slots": available_slots} + + except ValueError as e: + rcode = HTTP_BAD_REQUEST + error = "Error {}: {}".format(errors.REGISTRATION_MISSING_FIELD, str(e)) + response = {"error": error} + + else: + rcode = HTTP_BAD_REQUEST + error = "Error {}: public_key not found in register message".format( + errors.REGISTRATION_WRONG_FIELD_FORMAT + ) + response = {"error": error} + + else: + rcode = HTTP_BAD_REQUEST + error = "appointment rejected. Request is not json encoded" + response = {"error": error} + + logger.info("Sending response and disconnecting", from_addr="{}".format(remote_addr), response=response) + + return jsonify(response), rcode def add_appointment(self): """ @@ -43,15 +101,10 @@ class API: """ # Getting the real IP if the server is behind a reverse proxy - remote_addr = request.environ.get("HTTP_X_REAL_IP") - if not remote_addr: - remote_addr = request.environ.get("REMOTE_ADDR") + remote_addr = get_remote_addr() logger.info("Received add_appointment request", from_addr="{}".format(remote_addr)) - # FIXME: Logging every request so we can get better understanding of bugs in the alpha - logger.debug("Request details", data="{}".format(request.data)) - if request.is_json: # Check content type once if properly defined request_data = request.get_json() @@ -59,9 +112,6 @@ class API: request_data.get("appointment"), request_data.get("signature"), request_data.get("public_key") ) - error = None - response = None - if type(appointment) == Appointment: appointment_added, signature = self.watcher.add_appointment(appointment) @@ -72,29 +122,26 @@ class API: else: rcode = HTTP_SERVICE_UNAVAILABLE error = "appointment rejected" + response = {"error": error} elif type(appointment) == tuple: rcode = HTTP_BAD_REQUEST error = "appointment rejected. Error {}: {}".format(appointment[0], appointment[1]) + response = {"error": error} else: # We should never end up here, since inspect only returns appointments or tuples. Just in case. rcode = HTTP_BAD_REQUEST error = "appointment rejected. Request does not match the standard" + response = {"error": error} else: rcode = HTTP_BAD_REQUEST error = "appointment rejected. Request is not json encoded" - response = None + response = {"error": error} - logger.info( - "Sending response and disconnecting", from_addr="{}".format(remote_addr), response=response, error=error - ) - - if error is None: - return jsonify(response), rcode - else: - return jsonify({"error": error}), rcode + logger.info("Sending response and disconnecting", from_addr="{}".format(remote_addr), response=response) + return jsonify(response), rcode # FIXME: THE NEXT TWO API ENDPOINTS ARE FOR TESTING AND SHOULD BE REMOVED / PROPERLY MANAGED BEFORE PRODUCTION! # ToDo: #17-add-api-keys @@ -116,9 +163,7 @@ class API: """ # Getting the real IP if the server is behind a reverse proxy - remote_addr = request.environ.get("HTTP_X_REAL_IP") - if not remote_addr: - remote_addr = request.environ.get("REMOTE_ADDR") + remote_addr = get_remote_addr() locator = request.args.get("locator") response = [] @@ -182,12 +227,14 @@ class API: return response + # TODO: UNITTEST def start(self): """ This function starts the Flask server used to run the API. Adds all the routes to the functions listed above. """ routes = { + "/register": (self.register, ["POST"]), "/add_appointment": (self.add_appointment, ["POST"]), "/get_appointment": (self.get_appointment, ["GET"]), "/get_all_appointments": (self.get_all_appointments, ["GET"]), diff --git a/teos/errors.py b/teos/errors.py index 747103b..c4b5b25 100644 --- a/teos/errors.py +++ b/teos/errors.py @@ -1,4 +1,4 @@ -# Appointment errors +# Appointment errors [-1, -64] APPOINTMENT_EMPTY_FIELD = -1 APPOINTMENT_WRONG_FIELD_TYPE = -2 APPOINTMENT_WRONG_FIELD_SIZE = -3 @@ -8,6 +8,10 @@ APPOINTMENT_FIELD_TOO_BIG = -6 APPOINTMENT_WRONG_FIELD = -7 APPOINTMENT_INVALID_SIGNATURE = -8 +# Registration errors [-65, -128] +REGISTRATION_MISSING_FIELD = -65 +REGISTRATION_WRONG_FIELD_FORMAT = -66 + # Custom RPC errors RPC_TX_REORGED_AFTER_BROADCAST = -98 diff --git a/teos/gatekeeper.py b/teos/gatekeeper.py new file mode 100644 index 0000000..75d71f7 --- /dev/null +++ b/teos/gatekeeper.py @@ -0,0 +1,32 @@ +import re + +SUBSCRIPTION_SLOTS = 100 + +# TODO: UNITTEST +class Gatekeeper: + def __init__(self): + self.registered_users = {} + + @staticmethod + def check_user_pk(user_pk): + """ + Checks if a given value is a 33-byte hex encoded string. + + Args: + user_pk(:mod:`str`): the value to be checked. + + Returns: + :obj:`bool`: Whether or not the value matches the format. + """ + return isinstance(user_pk, str) and re.match(r"^[0-9A-Fa-f]{66}$", user_pk) is not None + + def add_update_user(self, user_pk): + if not self.check_user_pk(user_pk): + raise ValueError("provided public key does not match expected format (33-byte hex string)") + + if user_pk not in self.registered_users: + self.registered_users[user_pk] = SUBSCRIPTION_SLOTS + else: + self.registered_users[user_pk] += SUBSCRIPTION_SLOTS + + return self.registered_users[user_pk] diff --git a/teos/teosd.py b/teos/teosd.py index dd63bd6..2f46341 100644 --- a/teos/teosd.py +++ b/teos/teosd.py @@ -17,6 +17,7 @@ from teos.carrier import Carrier from teos.inspector import Inspector from teos.responder import Responder from teos.db_manager import DBManager +from teos.gatekeeper import Gatekeeper from teos.chain_monitor import ChainMonitor from teos.block_processor import BlockProcessor from teos.tools import can_connect_to_bitcoind, in_correct_network @@ -150,7 +151,7 @@ def main(command_line_conf): # Fire the API and the ChainMonitor # FIXME: 92-block-data-during-bootstrap-db chain_monitor.monitor_chain() - API(Inspector(block_processor, config.get("MIN_TO_SELF_DELAY")), watcher).start() + API(Inspector(block_processor, config.get("MIN_TO_SELF_DELAY")), watcher, Gatekeeper()).start() except Exception as e: logger.error("An error occurred: {}. Shutting down".format(e)) exit(1) diff --git a/test/teos/unit/test_api.py b/test/teos/unit/test_api.py index 6cbf15f..c1d4786 100644 --- a/test/teos/unit/test_api.py +++ b/test/teos/unit/test_api.py @@ -10,6 +10,7 @@ from teos.watcher import Watcher from teos.tools import bitcoin_cli from teos.inspector import Inspector from teos.responder import Responder +from teos.gatekeeper import Gatekeeper from teos.chain_monitor import ChainMonitor from test.teos.unit.conftest import ( @@ -55,7 +56,9 @@ def run_api(db_manager, carrier, block_processor): watcher.awake() chain_monitor.monitor_chain() - api_thread = Thread(target=API(Inspector(block_processor, config.get("MIN_TO_SELF_DELAY")), watcher).start) + api_thread = Thread( + target=API(Inspector(block_processor, config.get("MIN_TO_SELF_DELAY")), watcher, Gatekeeper()).start + ) api_thread.daemon = True api_thread.start()